diff --git a/app/about/page.tsx b/app/about/page.tsx index bee955c..af5bae7 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -1,5 +1,4 @@ import type { Metadata } from "next"; -import CourseworkTip from "../components/coursework-tip"; import { HoverLinkArrow, HoverLinkDestinationHint } from "../components/hover-link-hint"; type AboutRole = { @@ -43,24 +42,6 @@ export default function About() { role: "Bachelor of Science", subtitle: "Mathematics", grade: "3.73/4.00 (89.96/100)", - coursework: [ - "MA101 Mathematical Analysis I", - "MA107 Advanced Linear Algebra I", - "MA121 Advanced Linear Algebra II (H)", - "MA122 Mathematical Analysis II (H)", - "MA204 Mathematical Statistics", - "MA215 Probability Theory", - "MA219 Abstract Algebra (H)", - "MA230 Ordinary Differential Equations A (H)", - "MA231 Mathematical Analysis III (H)", - "MA232 Complex Analysis (H)", - "MA234 Introduction to Big Data Analysis", - "MA305 Numerical Analysis", - "MA336 Partial Differential Equations (H)", - "MA337 Real Analysis (H)", - "MA340 Fourier Analysis", - "MAT7092 Stochastic Processes", - ], }, { years: "2026 – 2026", @@ -68,19 +49,12 @@ export default function About() { role: "Visiting Student", subtitle: "Computer Science and Data Science", grade: "4.00/4.00 (100/100)", - coursework: [ - { name: "COMPSCI 61A", href: "https://inst.eecs.berkeley.edu/~cs61a/sp26/" }, - { name: "DATA C100", href: "https://ds100.org/sp26/" }, - ], }, { years: "2025 – 2025", institution: "University of Oxford", role: "Summer Visiting Student", grade: "First Class (A+)", - coursework: [ - { name: "Deep Unsupervised Learning", href: "/projects/oxford-dul-2025" }, - ], }, { years: "2020 – 2023", @@ -88,7 +62,7 @@ export default function About() { role: "High School Diploma", content: "Top 0.42% in Gaokao (1848th in 445k+ in Jiangsu Province)", }, - ].map(({ years, institution, role, subtitle, content, grade, coursework }, i) => ( + ].map(({ years, institution, role, subtitle, content, grade }, i) => (
{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, + // GitHub data takes priority; fallback only if null/undefined + summary: json.description ?? entry.summary, + tags: 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..e625aba 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", + href: "https://devpost.com/software/watchtower-ai-qgi0sa", 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,8 @@ export const getPersonalProjects = cache(async function getPersonalProjects(): P ...project, stars: live[i]?.stars ?? project.stars, archived: live[i]?.archived ?? project.archived, + // GitHub data takes priority; fallback only if fetch failed + desc: live[i] ? (live[i]!.desc ?? project.desc) : project.desc, + stack: live[i] ? (live[i]!.stack ?? project.stack) : project.stack, })); }); diff --git a/app/misc/page.tsx b/app/misc/page.tsx index 3aef805..581070c 100644 --- a/app/misc/page.tsx +++ b/app/misc/page.tsx @@ -41,6 +41,7 @@ const THING_GROUPS = [ category: "Developer Tools", rows: [ [ + { name: "Claude Code", href: "https://claude.com/product/claude-code" }, { name: "Cursor", href: "https://cursor.com/" }, { name: "iTerm2", href: "https://iterm2.com/" }, ], 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 (