From c85b7b4a0164d708bf555bb98593e18d44c9595a Mon Sep 17 00:00:00 2001 From: AltanEsmer Date: Sat, 9 May 2026 18:55:43 +0300 Subject: [PATCH 1/2] feat(projects): expose shippedAt date on bento cards Surface each project's last-pushed date from projects.generated.json through BentoProject as `shippedAt` (formatted) and `shippedAtISO`, and render it next to the card number so every card answers "when?". Co-Authored-By: Claude Opus 4.7 --- src/components/ProjectCard.tsx | 10 +++++++++- src/lib/projects-static.ts | 10 ++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) 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 && (
{ 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, }; From 6101abdb3b4c016495c87f293a817084fb3d6b3f Mon Sep 17 00:00:00 2001 From: AltanEsmer Date: Sat, 9 May 2026 18:55:50 +0300 Subject: [PATCH 2/2] feat(home): elevate "shipped recently" section Make the homepage projects block read like a portfolio anchor instead of a logo wall: bigger section padding, a confident lede paragraph under the title, a 4-tile stats strip (shipped / repos / languages / last shipped), more breathing room in the bento, and a bottom CTA linking to the full /projects archive. Co-Authored-By: Claude Opus 4.7 --- src/components/ProjectsSection.tsx | 224 ++++++++++++++++++++++++++--- 1 file changed, 201 insertions(+), 23 deletions(-) diff --git a/src/components/ProjectsSection.tsx b/src/components/ProjectsSection.tsx index d440aa5..58423f9 100644 --- a/src/components/ProjectsSection.tsx +++ b/src/components/ProjectsSection.tsx @@ -1,10 +1,28 @@ 'use client'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import Link from 'next/link'; import { PROJECTS } from '@/lib/projects-static'; +import generated from '@/lib/projects.generated.json'; import ProjectCard from '@/components/ProjectCard'; import ProjectFilters, { type Filter } from '@/components/ProjectFilters'; +interface GeneratedRepo { + language: string | null; + pushedAt: string; +} + +function relativeShipped(iso: string): string { + const then = new Date(iso).getTime(); + const now = Date.now(); + const days = Math.max(0, Math.round((now - then) / 86400000)); + if (days <= 1) return 'today'; + if (days < 7) return `${days}d ago`; + if (days < 30) return `${Math.round(days / 7)}w ago`; + if (days < 365) return `${Math.round(days / 30)}mo ago`; + return `${Math.round(days / 365)}y ago`; +} + export default function ProjectsSection() { const [filter, setFilter] = useState('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 + + +