From 333ac6b9ae24acf198c602b5f7532678c613912d Mon Sep 17 00:00:00 2001
From: Kai Chen
{title}
{ + const token = process.env.GITHUB_TOKEN; + + return Promise.all( + COURSE_PROJECTS.map(async (entry) => { + if (!entry.repo) return entry; + try { + const res = await fetch(`https://api.github.com/repos/${entry.repo}`, { + headers: token ? { Authorization: `Bearer ${token}` } : undefined, + next: { revalidate: REVALIDATE_SECONDS, tags: ["github-stars"] }, + }); + if (!res.ok) return entry; + const json: { + description?: string | null; + topics?: string[]; + stargazers_count?: number; + } = await res.json(); + return { + ...entry, + summary: json.description ?? entry.summary, + tags: json.topics && json.topics.length > 0 ? json.topics : entry.tags, + stars: + typeof json.stargazers_count === "number" ? json.stargazers_count : entry.stars, + }; + } catch { + return entry; + } + }) + ); +}); diff --git a/app/lib/personal-projects.ts b/app/lib/personal-projects.ts index 7ee1bd3..81f53cf 100644 --- a/app/lib/personal-projects.ts +++ b/app/lib/personal-projects.ts @@ -12,8 +12,9 @@ export type PersonalProject = { }; /** - * Personal projects shown on / and /projects — maintained by hand. - * Add, remove, or reorder entries here; only star counts are fetched live. + * Personal projects shown on / and /projects. Each entry mirrors its GitHub + * repo: desc = About, stack = Topics, stars = live count — all fetched live; + * the values below are fallbacks kept in sync by hand. */ export const PERSONAL_PROJECTS: PersonalProject[] = [ { @@ -21,28 +22,28 @@ export const PERSONAL_PROJECTS: PersonalProject[] = [ desc: "my personal space at a corner of human made internet :D @kaiiiichen", href: "https://github.com/kaiiiichen/kaichen.dev", repo: "kaiiiichen/kaichen.dev", - stack: ["TypeScript", "CSS", "Python", "Shell", "JavaScript"], + stack: ["app-router", "nextjs", "personal-website", "react", "tailwindcss", "typrescript"], }, { name: "SudoSodoku", - desc: "A terminal-style Sudoku experience for iOS, designed for logical purists.", - href: "https://github.com/SudoSodokuApp/SudoSodoku", + desc: "sudo solve — a terminal-fantasy Sudoku for iOS.", + href: "https://sudosodoku.kaichen.dev/", repo: "SudoSodokuApp/SudoSodoku", - stack: ["Swift", "Shell"], + stack: ["combine", "game", "gamekit", "ios", "ios-app", "mvvm", "puzzle-game", "sudoku", "swift", "swiftui"], }, { name: "buttercut", desc: "Buttercut is a personal-site theme for Next.js (App Router, React 19) that wants you to ship something beautiful without wrestling the codebase every weekend.", - href: "https://github.com/kaiiiichen/buttercut", + href: "https://buttercut.kaichen.dev/", repo: "kaiiiichen/buttercut", - stack: ["TypeScript", "CSS", "JavaScript"], + stack: ["app-router", "nextjs", "personal-website", "react", "tailwindcss", "template", "theme", "typrescript"], }, { name: "WatchTower-AI", desc: "[UC Berkeley AI Hackathon 2026] Is the AI down, or is it you? WatchTower AI probes Claude, GPT & Gemini in real time, tells you whose fault it is, and catches outages before the official status page does.", href: "https://github.com/kaiiiichen/WatchTower-AI", repo: "kaiiiichen/WatchTower-AI", - stack: ["Python", "TypeScript", "CSS", "JavaScript", "Dockerfile"], + stack: [], }, ]; @@ -68,10 +69,17 @@ export const getPersonalProjects = cache(async function getPersonalProjects(): P next: { revalidate: STARS_REVALIDATE_SECONDS, tags: ["github-stars"] }, }); if (!res.ok) return null; - const json: { stargazers_count?: number; archived?: boolean } = await res.json(); + const json: { + stargazers_count?: number; + archived?: boolean; + description?: string | null; + topics?: string[]; + } = await res.json(); return { stars: typeof json.stargazers_count === "number" ? json.stargazers_count : undefined, archived: json.archived, + desc: json.description ?? undefined, + stack: json.topics, }; } catch { return null; @@ -83,5 +91,7 @@ export const getPersonalProjects = cache(async function getPersonalProjects(): P ...project, stars: live[i]?.stars ?? project.stars, archived: live[i]?.archived ?? project.archived, + desc: live[i]?.desc ?? project.desc, + stack: live[i]?.stack && live[i]!.stack!.length > 0 ? live[i]!.stack! : project.stack, })); }); diff --git a/app/page.tsx b/app/page.tsx index 3a24409..2b54f9e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,6 +2,7 @@ import JumpText from "./components/jump-text"; import ListeningCard from "./components/listening-card"; import WeatherCard from "./components/weather-card"; import ProjectsSplit from "./components/oxford-dul/projects-split"; +import { getCourseProjects } from "./lib/course-projects"; import IdentityRow from "./components/identity-row"; import { TocSection } from "./components/toc-section"; import { getPersonalProjects } from "./lib/personal-projects"; @@ -36,7 +37,11 @@ const PERSON_JSON_LD = { } as const; export default async function Home() { - const [projects, weather] = await Promise.all([getPersonalProjects(), getBerkeleyWeather()]); + const [projects, courseProjects, weather] = await Promise.all([ + getPersonalProjects(), + getCourseProjects(), + getBerkeleyWeather(), + ]); return (