diff --git a/src/components/CategoryArt.astro b/src/components/CategoryArt.astro new file mode 100644 index 0000000..f104dba --- /dev/null +++ b/src/components/CategoryArt.astro @@ -0,0 +1,181 @@ +--- +/** + * CategoryArt — terminal-style SVG illustration per category. + * Matches the banner-gen visual language (dark #0d1220 canvas, terminal + * window with traffic-light dots, JetBrains Mono, status colors) but with a + * per-category accent color, so each category is scannable at a glance. + * Inline SVG inherits the blog's self-hosted JetBrains Mono for text lines. + */ +import { categoryMeta } from '../lib/categories'; + +interface Props { + slug: string; +} + +const { slug } = Astro.props; +const meta = categoryMeta(slug); + +// line types: prompt (accent), cmd #e6edf3, ok #3fb950, hl #f5a524, dim #6b7c99 +type Line = { t: 'prompt' | 'cmd' | 'ok' | 'hl' | 'dim'; c: string }; + +const ART: Record = { + ai: { + title: 'n8n — ai workflows', + lines: [ + { t: 'dim', c: 'telegram ⇄ llm ⇄ memory' }, + { t: 'prompt', c: '>' }, + { t: 'cmd', c: ' user: "help me book a slot"' }, + { t: 'ok', c: 'bot: done · calendar updated' }, + { t: 'hl', c: 'mem0 long-term memory · on' }, + ], + glyph: 'text', + gc: 'λ', + }, + 'case-studies': { + title: 'mr@hoelee — portfolio', + lines: [ + { t: 'dim', c: 'drwxr-xr-x hoelee projects' }, + { t: 'prompt', c: '$' }, + { t: 'cmd', c: ' cd digikedai-bot && ship' }, + { t: 'ok', c: 'deployed · measured · written up' }, + { t: 'hl', c: '→ end-to-end build logs' }, + ], + glyph: 'check', + gc: '', + }, + devops: { + title: 'root@homelab', + lines: [ + { t: 'dim', c: '~140 containers · 6 hosts' }, + { t: 'prompt', c: '$' }, + { t: 'cmd', c: ' docker compose up -d' }, + { t: 'ok', c: 'traefik · pihole · n8n — Up' }, + { t: 'ok', c: 'backups 3-2-1 · verified' }, + ], + glyph: 'text', + gc: '$', + }, + engineering: { + title: 'src — spring & wordpress', + lines: [ + { t: 'dim', c: 'Java · PHP · TypeScript' }, + { t: 'prompt', c: '$' }, + { t: 'cmd', c: ' mvn clean package' }, + { t: 'ok', c: 'BUILD SUCCESS · 42s' }, + { t: 'hl', c: '{ "status": "200 OK" }' }, + ], + glyph: 'text', + gc: '', + }, + notes: { + title: 'scratchpad — gotchas', + lines: [ + { t: 'dim', c: '3-minute reads' }, + { t: 'hl', c: 'fix · traefik forward-auth' }, + { t: 'hl', c: 'fix · cv 301 → 404 chain' }, + { t: 'dim', c: '→ lessons, fast' }, + ], + glyph: 'text', + gc: '!', + }, + tutorials: { + title: 'learn — follow along', + lines: [ + { t: 'dim', c: 'copy · run · understand' }, + { t: 'prompt', c: '$' }, + { t: 'cmd', c: ' npm create astro@latest' }, + { t: 'ok', c: 'step 01 · scaffolded' }, + { t: 'ok', c: 'step 02 · deployed' }, + ], + glyph: 'text', + gc: '>_', + }, + web3: { + title: 'foundry — testnet 31337', + lines: [ + { t: 'dim', c: 'solidity · honest scope' }, + { t: 'prompt', c: '$' }, + { t: 'cmd', c: ' forge test' }, + { t: 'ok', c: '[PASS] testMintERC721' }, + { t: 'ok', c: '[PASS] testTransferERC20' }, + ], + glyph: 'text', + gc: '0x', + }, +}; + +const fallback: (typeof ART)[string] = { + title: slug, + lines: [ + { t: 'dim', c: 'mr hoelee · blog' }, + { t: 'prompt', c: '$' }, + { t: 'cmd', c: ' ls posts' }, + ], + glyph: 'text', + gc: '#', +}; +const art = ART[slug] ?? fallback; +const accent = meta?.accent ?? '#295cff'; + +const LINE_COLORS: Record = { + prompt: accent, + cmd: '#e6edf3', + ok: '#3fb950', + hl: '#f5a524', + dim: '#6b7c99', +}; +--- + + + + + + + + + + + + + + {art.title} + + + {art.lines.map((ln, i) => ( + {ln.c} + ))} + + + {art.glyph === 'text' ? ( + {art.gc} + ) : ( + + + + + )} + \ No newline at end of file diff --git a/src/components/CategoryGrid.astro b/src/components/CategoryGrid.astro new file mode 100644 index 0000000..68fe908 --- /dev/null +++ b/src/components/CategoryGrid.astro @@ -0,0 +1,54 @@ +--- +import CategoryArt from './CategoryArt.astro'; +import { ALL_CATEGORIES } from '../lib/categories'; + +interface Props { + /** Per-slug post count for the current locale. */ + counts: Record; + lang?: 'en' | 'zh'; +} + +const { counts, lang = 'en' } = Astro.props; +const isZh = lang === 'zh'; + +const label = (count: number) => { + if (isZh) return count === 0 ? '0 篇 · 敬请期待' : `${count} 篇`; + return count === 0 ? '0 posts · coming soon' : `${count} post${count === 1 ? '' : 's'}`; +}; +--- + +
    + {ALL_CATEGORIES.map((cat) => { + const count = counts[cat.slug] ?? 0; + const href = count > 0 ? `/${isZh ? 'zh/' : ''}categories/${cat.slug}/` : null; + const cardBodyAttrs = href ? { href } : {}; + const body = ( + <> +
    + +
    +
    +

    + + {cat.name[isZh ? 'zh' : 'en']} +

    +

    {cat.blurb[isZh ? 'zh' : 'en']}

    + + {count} {label(count)} + +
    + + ); + return ( +
  • + {href ? ( + + {body} + + ) : ( +
    {body}
    + )} +
  • + ); + })} +
\ No newline at end of file diff --git a/src/components/PostList.astro b/src/components/PostList.astro index 31e3a8a..0cbdd8f 100644 --- a/src/components/PostList.astro +++ b/src/components/PostList.astro @@ -4,13 +4,23 @@ import type { CollectionEntry } from 'astro:content'; interface Props { posts: CollectionEntry<'posts'>[]; limit?: number; + lang?: 'en' | 'zh'; } -const { posts, limit } = Astro.props; +const { posts, limit, lang = 'en' } = Astro.props; const shown = limit ? posts.slice(0, limit) : posts; +const isZh = lang === 'zh'; const fmt = (d: Date) => - d.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); + d.toLocaleDateString(isZh ? 'zh-CN' : 'en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }); + +const updatedLabel = isZh ? '更新于' : 'Updated'; + +const catHref = (cat: string) => `/${isZh ? 'zh/categories' : 'categories'}/${cat}/`; const og = (post: CollectionEntry<'posts'>) => post.data.ogImage || '/og-default.png'; --- @@ -28,13 +38,13 @@ const og = (post: CollectionEntry<'posts'>) => post.data.ogImage || '/og-default {post.data.updatedDate && ( <> · - - Updated {fmt(post.data.updatedDate)} + + {updatedLabel} {fmt(post.data.updatedDate)} )} · - {post.data.category} + {post.data.category} {post.data.tags.map((t) => {t})}

{post.data.description}

diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 0bc3b1b..beaf507 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -135,7 +135,7 @@ const nav = isZh