diff --git a/src/components/ProjectCard.tsx b/src/components/ProjectCard.tsx index 42b5478..645a57a 100644 --- a/src/components/ProjectCard.tsx +++ b/src/components/ProjectCard.tsx @@ -96,9 +96,17 @@ export default function ProjectCard({ fontFamily: "'JetBrains Mono', monospace", fontSize: '11px', color: 'var(--dim)', + display: 'flex', + alignItems: 'center', + gap: '8px', + flexWrap: 'wrap', }} > - {featured ? `FEATURED · ${project.num}` : project.num} + {featured ? `FEATURED · ${project.num}` : project.num} + · + + shipped {project.shippedAt} + {featured && live && (
('all'); const sectionRef = useRef(null); @@ -13,6 +31,21 @@ export default function ProjectsSection() { (p) => filter === 'all' || p.tags.includes(filter as 'fe' | 'be' | 'ds' | 'oss'), ); + const stats = useMemo(() => { + const repos = generated.repos as GeneratedRepo[]; + const languages = new Set(repos.map((r) => r.language).filter(Boolean)); + const latest = repos.reduce( + (acc, r) => (r.pushedAt > acc ? r.pushedAt : acc), + '0', + ); + return { + shipped: PROJECTS.length, + repos: repos.length, + languages: languages.size, + lastShipped: latest === '0' ? '—' : relativeShipped(latest), + }; + }, []); + // IntersectionObserver to trigger .reveal → .reveal.in useEffect(() => { const section = sectionRef.current; @@ -38,6 +71,13 @@ export default function ProjectsSection() { return () => observer.disconnect(); }, [filter]); + const statTiles: { value: string | number; label: string }[] = [ + { value: stats.shipped, label: 'Projects shipped' }, + { value: stats.repos, label: 'Active repos' }, + { value: stats.languages, label: 'Languages' }, + { value: stats.lastShipped, label: 'Last shipped' }, + ]; + return ( <> {/* Responsive collapse styles for bento spans */} @@ -47,9 +87,12 @@ export default function ProjectsSection() { grid-column: span 2 !important; grid-row: span 1 !important; } + #projects .stats-strip { + grid-template-columns: repeat(2, 1fr) !important; + } } @media (max-width: 640px) { - #projects { padding: 60px 0 !important; } + #projects { padding: 72px 0 !important; } #projects .bento { grid-template-columns: repeat(2, 1fr) !important; grid-auto-rows: 180px !important; @@ -63,6 +106,14 @@ export default function ProjectsSection() { align-items: stretch !important; margin-bottom: 28px !important; } + #projects .projects-foot { + flex-direction: column !important; + align-items: stretch !important; + gap: 12px !important; + } + #projects .projects-lede { + font-size: 15px !important; + } } `} @@ -70,7 +121,7 @@ export default function ProjectsSection() { id="projects" className="projects" ref={sectionRef} - style={{ padding: '100px 0' }} + style={{ padding: '140px 0' }} >
{/* Section head */} @@ -81,33 +132,112 @@ export default function ProjectsSection() { {filtered.length} entries
- {/* Projects head: title + filters */} -
+ {/* Title block: title + lede */} +

Things I've shipped{' '} recently.

+

+ A working portfolio of production deploys, side projects, and open-source + experiments — the things I've actually pushed to{' '} + main. Updated automatically + from GitHub, curated by hand. +

+
+ + {/* Stats strip */} +
+ {statTiles.map((tile) => ( +
+
+ {tile.value} +
+
+ {tile.label} +
+
+ ))} +
+ + {/* Filters row */} +
+
+ {'// filter by stack'} +
@@ -117,17 +247,65 @@ export default function ProjectsSection() { style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', - gap: '18px', - gridAutoRows: '200px', + gap: '20px', + gridAutoRows: '220px', }} > {filtered.map((project) => ( - + ))}
+ + {/* Bottom CTA strip */} +
+
+ {`// showing ${filtered.length} of ${PROJECTS.length} — full archive available`} +
+ { + e.currentTarget.style.borderColor = 'var(--magenta)'; + e.currentTarget.style.color = 'var(--magenta)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.borderColor = 'var(--line)'; + e.currentTarget.style.color = 'var(--text)'; + }} + > + View all projects + + +
diff --git a/src/lib/projects-static.ts b/src/lib/projects-static.ts index f58d057..2724ad1 100644 --- a/src/lib/projects-static.ts +++ b/src/lib/projects-static.ts @@ -32,6 +32,8 @@ export interface BentoProject { preview: Preview; span: Span; href: string; + shippedAt: string; + shippedAtISO: string; featured?: boolean; live?: { stats: { value: string; label: string; up?: boolean }[]; @@ -224,6 +226,12 @@ const CURATION: Curation[] = [ }, ]; +function formatShipped(iso: string): string { + const d = new Date(iso); + const month = d.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' }); + return `${month} ${d.getUTCFullYear()}`; +} + export const PROJECTS: BentoProject[] = CURATION.map((c) => { const r = repo(c.fullName); return { @@ -235,6 +243,8 @@ export const PROJECTS: BentoProject[] = CURATION.map((c) => { preview: c.preview, span: c.span, href: r.homepageUrl ?? r.url, + shippedAt: formatShipped(r.pushedAt), + shippedAtISO: r.pushedAt, featured: c.featured, live: c.live, };