mirror of
https://github.com/avinal/avinal.github.io.git
synced 2026-07-04 07:40:09 +05:30
feat: redesign my webiste from scratch
- remove hugo and paper box theme - inspiration https://jay.fish - use astro based system Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
---
|
||||
/**
|
||||
* Combined activity card: graph (left) + stats (right) in one row.
|
||||
* Mirrors jay.fish's "Commit Carnage" + "Kill Count" layout.
|
||||
*/
|
||||
import type { ActivityData } from "@/lib/activity";
|
||||
import type { GitHubUser } from "@/lib/github";
|
||||
|
||||
interface Props {
|
||||
activity: ActivityData;
|
||||
user: GitHubUser | null;
|
||||
}
|
||||
|
||||
const { activity, user } = Astro.props;
|
||||
const hasWaka = activity.wakatime.available;
|
||||
---
|
||||
|
||||
<div class="activity-card card">
|
||||
<div class="activity-header">
|
||||
<h3 class="widget-title">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
||||
Activity
|
||||
</h3>
|
||||
<span class="text-muted text-xs">past year</span>
|
||||
</div>
|
||||
|
||||
{activity.weeks.length > 0 ? (
|
||||
<div class="graph-scroll">
|
||||
<div class="graph-grid" role="img" aria-label="Activity graph">
|
||||
{activity.weeks.map((week) => (
|
||||
<div class="graph-col">
|
||||
{week.map((day) => {
|
||||
const ghLevel = day.githubLevel;
|
||||
const wakaLvl = hasWaka && day.wakaSeconds > 0
|
||||
? Math.min(Math.max(1, Math.ceil(day.wakaSeconds / 1800)), 4)
|
||||
: 0;
|
||||
|
||||
const d = new Date(day.date);
|
||||
const fmtDate = `${String(d.getDate()).padStart(2, "0")} ${d.toLocaleString("en-US", { month: "short" })} ${d.getFullYear()}`;
|
||||
const tipParts = [fmtDate, `${day.githubCount} contribution${day.githubCount !== 1 ? "s" : ""}`];
|
||||
if (hasWaka && day.wakaSeconds > 0) tipParts.push(day.wakaText);
|
||||
const tip = tipParts.join(" · ");
|
||||
|
||||
const hasGh = ghLevel > 0;
|
||||
const hasWk = wakaLvl > 0;
|
||||
const isSplit = hasGh && hasWk;
|
||||
|
||||
if (isSplit) {
|
||||
return (
|
||||
<div class="graph-cell split-cell" title={tip}>
|
||||
<div class="cell-gh" style={`background-color: var(--graph-${ghLevel})`}></div>
|
||||
<div class="cell-waka" style={`background-color: var(--waka-${wakaLvl})`}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const color = hasWk ? `var(--waka-${wakaLvl})` : `var(--graph-${ghLevel})`;
|
||||
return <div class="graph-cell" style={`background-color: ${color}`} title={tip}></div>;
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div class="graph-legend">
|
||||
<span class="legend-group">
|
||||
<span class="text-xs text-muted">GitHub</span>
|
||||
<div class="legend-cell" style="background-color: var(--graph-1)"></div>
|
||||
<div class="legend-cell" style="background-color: var(--graph-2)"></div>
|
||||
<div class="legend-cell" style="background-color: var(--graph-3)"></div>
|
||||
<div class="legend-cell" style="background-color: var(--graph-4)"></div>
|
||||
</span>
|
||||
{hasWaka && (
|
||||
<span class="legend-group">
|
||||
<div class="legend-cell" style="background-color: var(--waka-1)"></div>
|
||||
<div class="legend-cell" style="background-color: var(--waka-2)"></div>
|
||||
<div class="legend-cell" style="background-color: var(--waka-3)"></div>
|
||||
<div class="legend-cell" style="background-color: var(--waka-4)"></div>
|
||||
<span class="text-xs text-muted">WakaTime</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p class="text-muted text-sm">Activity data unavailable.</p>
|
||||
)}
|
||||
|
||||
<!-- Stats below the graph -->
|
||||
<div class="activity-stats">
|
||||
<dl class="stats-row">
|
||||
<div class="stat-item gh">
|
||||
<dt>Contributions</dt>
|
||||
<dd>{activity.github.total.toLocaleString()}</dd>
|
||||
</div>
|
||||
<div class="stat-item gh">
|
||||
<dt>Public Repos</dt>
|
||||
<dd>{user?.public_repos ?? "—"}</dd>
|
||||
</div>
|
||||
<div class="stat-item gh">
|
||||
<dt>Followers</dt>
|
||||
<dd>{user?.followers ?? "—"}</dd>
|
||||
</div>
|
||||
{hasWaka && (
|
||||
<Fragment>
|
||||
<div class="stat-item waka">
|
||||
<dt>Coded (year)</dt>
|
||||
<dd>{activity.wakatime.totalText}</dd>
|
||||
</div>
|
||||
<div class="stat-item waka">
|
||||
<dt>Daily Avg</dt>
|
||||
<dd>{activity.wakatime.dailyAvgText}</dd>
|
||||
</div>
|
||||
<div class="stat-item waka">
|
||||
<dt>Best Day</dt>
|
||||
<dd>{activity.wakatime.bestDayText}</dd>
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.activity-card {
|
||||
padding: var(--space-5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.activity-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.graph-scroll {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.graph-scroll {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.graph-grid {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.graph-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.graph-cell {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: 2px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.split-cell {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.cell-gh { width: 50%; height: 11px; border-radius: 2px 0 0 2px; }
|
||||
.cell-waka { width: 50%; height: 11px; border-radius: 0 2px 2px 0; }
|
||||
|
||||
.graph-legend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.legend-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.legend-cell {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Stats — horizontal row below the graph */
|
||||
.activity-stats {
|
||||
padding-top: var(--space-4);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: var(--space-4) var(--space-6);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.stat-item dt {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stat-item dd {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.stat-item.gh dd {
|
||||
color: var(--graph-3);
|
||||
}
|
||||
|
||||
.stat-item.waka dd {
|
||||
color: var(--waka-3);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
const year = new Date().getFullYear();
|
||||
---
|
||||
|
||||
<footer class="footer">
|
||||
<div class="footer-inner">
|
||||
<p class="footer-copy">
|
||||
© {year} Avinal Kumar
|
||||
</p>
|
||||
<div class="footer-links">
|
||||
<a href="https://github.com/avinal" target="_blank" rel="noopener noreferrer">GitHub</a>
|
||||
<span class="footer-sep" aria-hidden="true">·</span>
|
||||
<a href="https://linkedin.com/in/avinal" target="_blank" rel="noopener noreferrer">LinkedIn</a>
|
||||
<span class="footer-sep" aria-hidden="true">·</span>
|
||||
<a href="/posts">Posts</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.footer {
|
||||
border-top: 1px solid var(--border);
|
||||
padding-block: var(--space-8);
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-4);
|
||||
max-width: var(--max-w-page);
|
||||
margin-inline: auto;
|
||||
padding-inline: var(--space-6);
|
||||
}
|
||||
|
||||
.footer-copy {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.footer-links a {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: color var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
.footer-links a:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.footer-sep {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,209 @@
|
||||
---
|
||||
/**
|
||||
* Conway's Game of Life widget.
|
||||
* Runs continuously; hovering over the canvas brings cells to life.
|
||||
* Resumes simulation after hover interaction.
|
||||
*/
|
||||
---
|
||||
|
||||
<div class="gol-card card">
|
||||
<h3 class="widget-title">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
|
||||
Game of Life
|
||||
</h3>
|
||||
<canvas id="gol-canvas"></canvas>
|
||||
<p class="gol-hint text-muted text-xs">hover to bring cells alive</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.gol-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
height: 100%;
|
||||
min-height: 220px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#gol-canvas {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 180px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: crosshair;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.gol-card {
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.gol-hint {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const canvas = document.getElementById("gol-canvas") as HTMLCanvasElement;
|
||||
if (canvas) {
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
const CELL = 8;
|
||||
const GAP = 1;
|
||||
const STEP = CELL + GAP;
|
||||
|
||||
let cols = 0;
|
||||
let rows = 0;
|
||||
let grid: Uint8Array;
|
||||
let next: Uint8Array;
|
||||
|
||||
function resize() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = rect.width * dpr;
|
||||
canvas.height = rect.height * dpr;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
|
||||
const newCols = Math.floor(rect.width / STEP);
|
||||
const newRows = Math.floor(rect.height / STEP);
|
||||
|
||||
if (newCols !== cols || newRows !== rows) {
|
||||
const oldGrid = grid;
|
||||
const oldCols = cols;
|
||||
const oldRows = rows;
|
||||
cols = newCols;
|
||||
rows = newRows;
|
||||
grid = new Uint8Array(cols * rows);
|
||||
next = new Uint8Array(cols * rows);
|
||||
|
||||
if (oldGrid) {
|
||||
const mc = Math.min(oldCols, cols);
|
||||
const mr = Math.min(oldRows, rows);
|
||||
for (let r = 0; r < mr; r++) {
|
||||
for (let c = 0; c < mc; c++) {
|
||||
grid[r * cols + c] = oldGrid[r * oldCols + c];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
seed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function seed() {
|
||||
for (let i = 0; i < grid.length; i++) {
|
||||
grid[i] = Math.random() < 0.3 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
function idx(r: number, c: number) {
|
||||
return ((r + rows) % rows) * cols + ((c + cols) % cols);
|
||||
}
|
||||
|
||||
function step() {
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
let neighbors = 0;
|
||||
for (let dr = -1; dr <= 1; dr++) {
|
||||
for (let dc = -1; dc <= 1; dc++) {
|
||||
if (dr === 0 && dc === 0) continue;
|
||||
neighbors += grid[idx(r + dr, c + dc)];
|
||||
}
|
||||
}
|
||||
const alive = grid[r * cols + c];
|
||||
next[r * cols + c] =
|
||||
alive ? (neighbors === 2 || neighbors === 3 ? 1 : 0) : (neighbors === 3 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
[grid, next] = [next, grid];
|
||||
}
|
||||
|
||||
function getColors() {
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
return {
|
||||
bg: style.getPropertyValue("--bg-surface").trim() || "#f8f9fa",
|
||||
alive: style.getPropertyValue("--accent").trim() || "#2563eb",
|
||||
dim: style.getPropertyValue("--border").trim() || "#e5e7eb",
|
||||
};
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const colors = getColors();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
ctx.clearRect(0, 0, rect.width, rect.height);
|
||||
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const x = c * STEP;
|
||||
const y = r * STEP;
|
||||
ctx.fillStyle = grid[r * cols + c] ? colors.alive : colors.dim;
|
||||
ctx.globalAlpha = grid[r * cols + c] ? 0.85 : 0.15;
|
||||
ctx.fillRect(x, y, CELL, CELL);
|
||||
}
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
let animId: number;
|
||||
let lastTick = 0;
|
||||
const TICK_MS = 300;
|
||||
|
||||
function loop(ts: number) {
|
||||
if (ts - lastTick >= TICK_MS) {
|
||||
step();
|
||||
draw();
|
||||
lastTick = ts;
|
||||
}
|
||||
animId = requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
function paintCells(e: MouseEvent | TouchEvent) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
let clientX: number, clientY: number;
|
||||
if ("touches" in e) {
|
||||
clientX = e.touches[0].clientX;
|
||||
clientY = e.touches[0].clientY;
|
||||
} else {
|
||||
clientX = e.clientX;
|
||||
clientY = e.clientY;
|
||||
}
|
||||
const x = clientX - rect.left;
|
||||
const y = clientY - rect.top;
|
||||
const c = Math.floor(x / STEP);
|
||||
const r = Math.floor(y / STEP);
|
||||
|
||||
for (let dr = -1; dr <= 1; dr++) {
|
||||
for (let dc = -1; dc <= 1; dc++) {
|
||||
const nr = r + dr;
|
||||
const nc = c + dc;
|
||||
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
|
||||
grid[nr * cols + nc] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let isDrawing = false;
|
||||
canvas.addEventListener("mouseenter", () => { isDrawing = true; });
|
||||
canvas.addEventListener("mouseleave", () => { isDrawing = false; });
|
||||
canvas.addEventListener("mousemove", (e) => {
|
||||
if (isDrawing) paintCells(e);
|
||||
});
|
||||
canvas.addEventListener("touchmove", (e) => {
|
||||
e.preventDefault();
|
||||
paintCells(e);
|
||||
}, { passive: false });
|
||||
canvas.addEventListener("click", (e) => paintCells(e));
|
||||
|
||||
const resizeObs = new ResizeObserver(() => {
|
||||
resize();
|
||||
draw();
|
||||
});
|
||||
resizeObs.observe(canvas);
|
||||
|
||||
resize();
|
||||
animId = requestAnimationFrame(loop);
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,242 @@
|
||||
---
|
||||
/**
|
||||
* Combined hero card: profile + about/skills + social links.
|
||||
* Mirrors jay.fish's left hero section — everything about the person in one card.
|
||||
*/
|
||||
|
||||
interface Skill {
|
||||
icon: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
svgPath: string;
|
||||
}
|
||||
|
||||
interface SocialLink {
|
||||
label: string;
|
||||
href: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
role: string;
|
||||
bio: string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
const { name, role, bio, avatarUrl } = Astro.props;
|
||||
|
||||
const about: Skill[] = [
|
||||
{ icon: "cloud", title: "Hybrid Cloud", desc: "Building infrastructure at Red Hat", svgPath: '<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/>' },
|
||||
{ icon: "monitor", title: "Linux Enthusiast", desc: "Fedora daily driver, Arch tinkerer", svgPath: '<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>' },
|
||||
{ icon: "code", title: "Open Source", desc: "GSoC/GSoD mentor and contributor", svgPath: '<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>' },
|
||||
{ icon: "server", title: "Homelab", desc: "Self-hosting on Raspberry Pi", svgPath: '<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/>' },
|
||||
];
|
||||
|
||||
const tools: Skill[] = [
|
||||
{ icon: "terminal", title: "Languages", desc: "Go, Python, Elm, C/C++", svgPath: '<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>' },
|
||||
{ icon: "pen-tool", title: "Editors", desc: "Neovim, VS Code", svgPath: '<path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/>' },
|
||||
{ icon: "database", title: "Infra", desc: "Kubernetes, OpenShift, Tekton", svgPath: '<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>' },
|
||||
{ icon: "flask", title: "Learning", desc: "Elm, functional programming", svgPath: '<path d="M9 3h6v7l5 8a2 2 0 0 1-1.7 3H5.7a2 2 0 0 1-1.7-3l5-8V3z"/><line x1="8" y1="3" x2="16" y2="3"/>' },
|
||||
];
|
||||
|
||||
const links: SocialLink[] = [
|
||||
{ label: "GitHub", href: "https://github.com/avinal", icon: "github" },
|
||||
{ label: "LinkedIn", href: "https://linkedin.com/in/avinal", icon: "linkedin" },
|
||||
{ label: "Twitter", href: "https://twitter.com/Avinal_", icon: "twitter" },
|
||||
{ label: "WakaTime", href: "https://wakatime.com/@avinal", icon: "wakatime" },
|
||||
{ label: "RSS", href: "/rss.xml", icon: "rss" },
|
||||
];
|
||||
---
|
||||
|
||||
<div class="hero-card card">
|
||||
<!-- Identity -->
|
||||
<div class="hero-identity">
|
||||
{avatarUrl ? (
|
||||
<img src={avatarUrl} alt={name} class="hero-avatar" width="64" height="64" />
|
||||
) : (
|
||||
<div class="hero-avatar hero-avatar-fallback" aria-hidden="true">{name.charAt(0)}</div>
|
||||
)}
|
||||
<div>
|
||||
<h1 class="hero-name">{name}</h1>
|
||||
<p class="hero-role">{role}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="hero-bio">{bio}</p>
|
||||
|
||||
<!-- Skills columns -->
|
||||
<div class="hero-skills">
|
||||
<div class="skills-col">
|
||||
<h3 class="col-heading">About</h3>
|
||||
<ul class="skill-list">
|
||||
{about.map(({ svgPath, title, desc }) => (
|
||||
<li class="skill-item">
|
||||
<svg class="skill-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><Fragment set:html={svgPath} /></svg>
|
||||
<div>
|
||||
<strong>{title}</strong>
|
||||
<span class="skill-desc">{desc}</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="skills-col">
|
||||
<h3 class="col-heading">Tools & Stack</h3>
|
||||
<ul class="skill-list">
|
||||
{tools.map(({ svgPath, title, desc }) => (
|
||||
<li class="skill-item">
|
||||
<svg class="skill-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><Fragment set:html={svgPath} /></svg>
|
||||
<div>
|
||||
<strong>{title}</strong>
|
||||
<span class="skill-desc">{desc}</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social links -->
|
||||
<div class="hero-links">
|
||||
{links.map(({ label, href, icon }) => (
|
||||
<a href={href} class="link-btn" target={href.startsWith("http") ? "_blank" : undefined} rel={href.startsWith("http") ? "noopener noreferrer" : undefined} aria-label={label}>
|
||||
<svg class="link-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
{icon === "github" && <path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/>}
|
||||
{icon === "linkedin" && <Fragment><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"/><rect x="2" y="9" width="4" height="12"/><circle cx="4" cy="4" r="2"/></Fragment>}
|
||||
{icon === "twitter" && <path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"/>}
|
||||
{icon === "wakatime" && <Fragment><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" fill="none"/><path d="M7.5 14.5l2-4 2 3 2-5 2.5 6" stroke-linejoin="round"/></Fragment>}
|
||||
{icon === "rss" && <Fragment><path d="M4 11a9 9 0 0 1 9 9"/><path d="M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1"/></Fragment>}
|
||||
</svg>
|
||||
{label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hero-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
|
||||
.hero-identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.hero-avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: var(--radius-full);
|
||||
object-fit: cover;
|
||||
border: 2px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hero-avatar-fallback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--accent);
|
||||
color: white;
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hero-name {
|
||||
font-size: var(--text-xl);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.hero-role {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.hero-bio {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--leading-relaxed);
|
||||
}
|
||||
|
||||
.hero-skills {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-6);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.hero-skills { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.col-heading {
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--accent);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.skill-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.skill-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-2);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.skill-icon {
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.skill-item strong {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin-right: var(--space-1);
|
||||
}
|
||||
|
||||
.skill-desc {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.hero-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
padding-top: var(--space-2);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.link-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-1) var(--space-3);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
text-decoration: none;
|
||||
transition: all var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
.link-btn:hover {
|
||||
color: var(--text);
|
||||
border-color: var(--border-strong);
|
||||
background-color: var(--bg-surface-hover);
|
||||
}
|
||||
|
||||
.link-icon { flex-shrink: 0; }
|
||||
</style>
|
||||
@@ -0,0 +1,227 @@
|
||||
---
|
||||
const navLinks: { href: string; label: string; external?: boolean }[] = [
|
||||
{ href: "/", label: "Home" },
|
||||
{ href: "/posts", label: "Posts" },
|
||||
{ href: "/resume", label: "Resume" },
|
||||
{ href: "/meeting", label: "Meet" },
|
||||
{ href: "https://todo.avinal.space/explore", label: "Memos", external: true },
|
||||
{ href: "/setup", label: "Setup" },
|
||||
];
|
||||
|
||||
const currentPath = Astro.url.pathname;
|
||||
|
||||
function isActive(href: string): boolean {
|
||||
if (href === "/") return currentPath === "/";
|
||||
return currentPath.startsWith(href);
|
||||
}
|
||||
---
|
||||
|
||||
<header class="nav-header">
|
||||
<nav class="nav" aria-label="Main navigation">
|
||||
<a href="/" class="nav-logo" aria-label="avinal.space home">
|
||||
avinal<span class="nav-logo-dot">.</span>space
|
||||
</a>
|
||||
|
||||
<button
|
||||
class="nav-toggle"
|
||||
aria-label="Toggle menu"
|
||||
aria-expanded="false"
|
||||
aria-controls="nav-menu"
|
||||
>
|
||||
<span class="nav-toggle-bar"></span>
|
||||
<span class="nav-toggle-bar"></span>
|
||||
<span class="nav-toggle-bar"></span>
|
||||
</button>
|
||||
|
||||
<ul id="nav-menu" class="nav-links" role="list">
|
||||
{navLinks.map(({ href, label, external }) => (
|
||||
<li>
|
||||
<a
|
||||
href={href}
|
||||
class:list={["nav-link", { active: !external && isActive(href) }]}
|
||||
aria-current={!external && isActive(href) ? "page" : undefined}
|
||||
target={external ? "_blank" : undefined}
|
||||
rel={external ? "noopener noreferrer" : undefined}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<button class="theme-toggle" aria-label="Toggle dark mode" type="button">
|
||||
<svg class="icon-sun" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
||||
<svg class="icon-moon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.nav-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background-color: color-mix(in srgb, var(--bg) 85%, transparent);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: var(--max-w-page);
|
||||
margin-inline: auto;
|
||||
padding-inline: var(--space-6);
|
||||
height: var(--nav-height);
|
||||
}
|
||||
|
||||
.nav-logo {
|
||||
font-weight: 700;
|
||||
font-size: var(--text-lg);
|
||||
letter-spacing: var(--tracking-tight);
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-logo-dot {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: block;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
text-decoration: none;
|
||||
transition: color var(--duration-fast) var(--ease-out),
|
||||
background-color var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--text);
|
||||
background-color: var(--bg-surface-hover);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
color: var(--text);
|
||||
background-color: var(--bg-surface-hover);
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: color var(--duration-fast) var(--ease-out),
|
||||
background-color var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
color: var(--text);
|
||||
background-color: var(--bg-surface-hover);
|
||||
}
|
||||
|
||||
.icon-moon { display: none; }
|
||||
|
||||
:global([data-theme="dark"]) .icon-sun { display: none; }
|
||||
:global([data-theme="dark"]) .icon-moon { display: block; }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:global(:root:not([data-theme="light"])) .icon-sun { display: none; }
|
||||
:global(:root:not([data-theme="light"])) .icon-moon { display: block; }
|
||||
}
|
||||
|
||||
/* Mobile hamburger */
|
||||
.nav-toggle {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-toggle-bar {
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 2px;
|
||||
background-color: var(--text);
|
||||
border-radius: 1px;
|
||||
transition: transform var(--duration-fast) var(--ease-out),
|
||||
opacity var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.nav-toggle {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
position: absolute;
|
||||
top: var(--nav-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: var(--space-4) var(--space-6);
|
||||
background-color: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-links.open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const toggle = document.querySelector(".nav-toggle");
|
||||
const menu = document.getElementById("nav-menu");
|
||||
if (toggle && menu) {
|
||||
toggle.addEventListener("click", () => {
|
||||
const expanded = toggle.getAttribute("aria-expanded") === "true";
|
||||
toggle.setAttribute("aria-expanded", String(!expanded));
|
||||
menu.classList.toggle("open");
|
||||
});
|
||||
}
|
||||
|
||||
const themeToggle = document.querySelector(".theme-toggle");
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener("click", () => {
|
||||
const html = document.documentElement;
|
||||
const current = html.getAttribute("data-theme");
|
||||
const isDark =
|
||||
current === "dark" ||
|
||||
(!current && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||
const next = isDark ? "light" : "dark";
|
||||
html.setAttribute("data-theme", next);
|
||||
localStorage.setItem("theme", next);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,172 @@
|
||||
---
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
interface Props {
|
||||
posts: CollectionEntry<"posts">[];
|
||||
}
|
||||
|
||||
const { posts } = Astro.props;
|
||||
|
||||
const fmtDate = (d: Date) =>
|
||||
d.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
---
|
||||
|
||||
<div class="posts-card card">
|
||||
<div class="posts-header">
|
||||
<h3 class="widget-title">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
|
||||
Recent Posts
|
||||
</h3>
|
||||
<a href="/posts" class="posts-view-all">View all →</a>
|
||||
</div>
|
||||
|
||||
{posts.length > 0 ? (
|
||||
<ul class="posts-list">
|
||||
{posts.map((post) => (
|
||||
<li class="post-item">
|
||||
<a href={`/posts/${post.id}/`} class="post-link">
|
||||
<div class="post-thumb">
|
||||
{post.data.image ? (
|
||||
<img src={post.data.image} alt="" class="thumb-img" loading="lazy" />
|
||||
) : (
|
||||
<span class="thumb-placeholder">{post.data.title.charAt(0)}</span>
|
||||
)}
|
||||
</div>
|
||||
<div class="post-info">
|
||||
<div class="post-meta">
|
||||
<span class="badge">{post.data.category}</span>
|
||||
<span class="text-muted text-xs">{fmtDate(post.data.date)}</span>
|
||||
</div>
|
||||
<strong class="post-title">{post.data.title}</strong>
|
||||
{post.data.description && (
|
||||
<p class="post-desc">{post.data.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p class="text-muted text-sm">No posts yet.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.posts-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.posts-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.posts-view-all {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.posts-view-all:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.posts-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.post-item {
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.post-item:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.post-link {
|
||||
display: grid;
|
||||
grid-template-columns: 140px 1fr;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-3) var(--space-2);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: background-color var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
.post-link:hover {
|
||||
background-color: var(--bg-surface-hover);
|
||||
}
|
||||
|
||||
.post-thumb {
|
||||
aspect-ratio: 3 / 2;
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
background-color: var(--bg-surface-hover);
|
||||
}
|
||||
|
||||
.thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
filter: grayscale(100%);
|
||||
transition: filter var(--duration-normal) var(--ease-out);
|
||||
}
|
||||
|
||||
.post-link:hover .thumb-img {
|
||||
filter: grayscale(0%);
|
||||
}
|
||||
|
||||
.thumb-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.post-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.post-title {
|
||||
font-size: var(--text-base);
|
||||
color: var(--text);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.post-desc {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-muted);
|
||||
margin-top: var(--space-1);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,199 @@
|
||||
---
|
||||
import type { GitHubRepo } from "@/lib/github";
|
||||
import configRepos from "@/data/repos.json";
|
||||
|
||||
interface ConfigRepo {
|
||||
name: string;
|
||||
owner?: string;
|
||||
description: string;
|
||||
url: string;
|
||||
stars: number;
|
||||
forks: number;
|
||||
languages: { name: string; color: string }[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
repos: GitHubRepo[];
|
||||
}
|
||||
|
||||
const { repos: apiRepos } = Astro.props;
|
||||
|
||||
const useConfig = configRepos.length > 0;
|
||||
const items: ConfigRepo[] = useConfig
|
||||
? (configRepos as ConfigRepo[])
|
||||
: apiRepos.map((r) => ({
|
||||
name: r.name,
|
||||
owner: "avinal",
|
||||
description: r.description || "",
|
||||
url: r.html_url,
|
||||
stars: r.stargazers_count,
|
||||
forks: 0,
|
||||
languages: r.language ? [{ name: r.language, color: "" }] : [],
|
||||
}));
|
||||
---
|
||||
|
||||
<div class="repos-section">
|
||||
<h3 class="widget-title">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
||||
Repositories
|
||||
</h3>
|
||||
<div class="repo-grid">
|
||||
{items.map((repo) => (
|
||||
<a href={repo.url} class="repo-card card" target="_blank" rel="noopener noreferrer">
|
||||
<div class="repo-top">
|
||||
<div class="repo-name-row">
|
||||
{repo.owner && <span class="repo-owner">{repo.owner}/</span>}
|
||||
<span class="repo-name">{repo.name}</span>
|
||||
</div>
|
||||
<p class="repo-desc">{repo.description}</p>
|
||||
</div>
|
||||
<div class="repo-bottom">
|
||||
<div class="repo-meta">
|
||||
{repo.stars > 0 && (
|
||||
<span class="meta-badge">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
||||
{repo.stars.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
{repo.forks > 0 && (
|
||||
<span class="meta-badge">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M18 9v1a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V9"/><path d="M12 12v3"/></svg>
|
||||
{repo.forks.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div class="repo-langs">
|
||||
{repo.languages.slice(0, 2).map((lang) => (
|
||||
<span class="lang-tag">
|
||||
<span class="lang-dot" style={lang.color ? `background:${lang.color}` : undefined} />
|
||||
{lang.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.repos-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.repo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.repo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.repo-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-4) var(--space-5);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
transition: border-color var(--duration-fast) var(--ease-out),
|
||||
box-shadow var(--duration-fast) var(--ease-out),
|
||||
transform var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
.repo-card:hover {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 4px 12px color-mix(in srgb, var(--accent) 10%, transparent);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.repo-top {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.repo-name-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0;
|
||||
margin-bottom: var(--space-2);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.repo-owner {
|
||||
color: var(--text-muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.repo-name {
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.repo-desc {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
line-height: var(--leading-relaxed);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.repo-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
padding-top: var(--space-3);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.repo-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.meta-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.meta-badge svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.repo-langs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.lang-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.lang-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: var(--radius-full);
|
||||
display: inline-block;
|
||||
background-color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user