Skip to content
33 changes: 1 addition & 32 deletions app/about/page.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -43,52 +42,27 @@ 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",
institution: "University of California, Berkeley",
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",
institution: "Tongzhou Senior High School of Jiangsu Province",
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) => (
<div
key={i}
className="py-4 md:py-3 border-b border-zinc-100 dark:border-zinc-800/60 last:border-0"
Expand Down Expand Up @@ -127,11 +101,6 @@ export default function About() {
{[content, grade].filter(Boolean).join(" · ")}
</p>
) : null}
{coursework ? (
<div className="mt-2">
<CourseworkTip courses={coursework} />
</div>
) : null}
</div>
))}
</div>
Expand Down
4 changes: 3 additions & 1 deletion app/components/oxford-dul/course-project-link.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Link from "next/link";
import { HoverLinkArrow, HoverLinkDestinationHint } from "../hover-link-hint";
import ProjectStars from "../project-stars";
import type { CourseProjectEntry } from "@/app/lib/course-projects";

type CourseProjectLinkProps = {
Expand All @@ -8,7 +9,7 @@ type CourseProjectLinkProps = {
};

export default function CourseProjectLink({ entry, variant = "list" }: CourseProjectLinkProps) {
const { href, institution, title, grade, summary, tags, external, hintLabel } = entry;
const { href, institution, title, grade, summary, tags, external, hintLabel, stars } = entry;

const nameStyle = {
fontFamily: "var(--font-nunito)",
Expand All @@ -30,6 +31,7 @@ export default function CourseProjectLink({ entry, variant = "list" }: CoursePro
<p style={nameStyle} className={nameClass}>
{title}
</p>
<ProjectStars stars={stars} />
{grade ? (
<span
style={{ fontFamily: "var(--font-nunito)", fontWeight: 400, fontSize: 10 }}
Expand Down
6 changes: 4 additions & 2 deletions app/components/oxford-dul/projects-split.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { ReactNode } from "react";
import CourseProjectLink from "./course-project-link";
import { COURSE_PROJECTS } from "@/app/lib/course-projects";
import type { CourseProjectEntry } from "@/app/lib/course-projects";
import PinnedProjectLink from "../pinned-project-link";
import type { PersonalProject } from "@/app/lib/personal-projects";

type ProjectsSplitProps = {
courseProjects: CourseProjectEntry[];
personalProjects: PersonalProject[];
personalVariant?: "list" | "card";
personalEmptyMessage?: string;
Expand All @@ -26,14 +27,15 @@ function NestedProjectsCard({
}

export default function ProjectsSplit({
courseProjects,
personalProjects,
personalVariant = "list",
personalEmptyMessage = "Pin repositories on your GitHub profile to show them here.",
}: ProjectsSplitProps) {
return (
<div className="space-y-5">
<NestedProjectsCard label="Course Projects">
{COURSE_PROJECTS.map((entry) => (
{courseProjects.map((entry) => (
<CourseProjectLink
key={entry.id}
entry={entry}
Expand Down
4 changes: 2 additions & 2 deletions app/components/pinned-project-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export default function PinnedProjectLink({
</span>
))}
</div>
<HoverLinkDestinationHint href={href} label="GitHub" />
<HoverLinkDestinationHint href={href} label={href.includes("github.com") ? "GitHub" : new URL(href).hostname} />
</div>
</a>
);
Expand All @@ -84,7 +84,7 @@ export default function PinnedProjectLink({
</p>
<ProjectStars stars={stars} archived={archived} />
</div>
<HoverLinkDestinationHint href={href} label="GitHub" />
<HoverLinkDestinationHint href={href} label={href.includes("github.com") ? "GitHub" : new URL(href).hostname} />
</div>
{desc ? (
<p
Expand Down
69 changes: 66 additions & 3 deletions app/lib/course-projects.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { cache } from "react";

export type CourseProjectEntry = {
id: string;
href: string;
Expand All @@ -8,18 +10,79 @@ export type CourseProjectEntry = {
tags: string[];
external?: boolean;
hintLabel?: string;
/** GitHub repo backing this entry — About/Topics/stars are fetched live */
repo?: string;
/** Live star count merged in by getCourseProjects */
stars?: number;
};

/**
* Course projects shown on / and /projects. For GitHub-backed entries the
* summary, tags, and stars mirror the repo's About, Topics, and star count —
* fetched live; the values below are fallbacks kept in sync by hand.
*/
export const COURSE_PROJECTS: CourseProjectEntry[] = [
{
id: "kai-course-notes",
href: "https://kai-course-notes.kaichen.dev",
title: "Kai Course Notes",
institution: "SUSTech · UC Berkeley · Oxford & beyond",
summary:
"23 courses in one ecosystem — notes, homework, and projects on GitHub, web notes on Notion, per-course zip downloads, and the Oxford deep-learning portfolio.",
tags: ["LaTeX", "Notes", "Showcase"],
"Course notes & materials — handwritten & LaTeX notes, homework, and projects. Web-native notes on the Notion branch.",
tags: [
"computer-science",
"course-notes",
"coursework",
"latex",
"lecture-notes",
"mathematics",
"oxford",
"statistics",
"study-notes",
"sustech",
"uc-berkeley",
],
external: true,
hintLabel: "kai-course-notes.kaichen.dev",
repo: "kaiiiichen/Kai-Course-Notes",
},
];

const REVALIDATE_SECONDS = 120;

/**
* COURSE_PROJECTS with the GitHub-backed fields (About → summary,
* Topics → tags, stars) merged in live — same cadence as personal projects.
*/
export const getCourseProjects = cache(async function getCourseProjects(): Promise<
CourseProjectEntry[]
> {
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;
}
})
);
});
33 changes: 22 additions & 11 deletions app/lib/personal-projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,38 @@ 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[] = [
{
name: "kaichen.dev",
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: [],
},
];

Expand All @@ -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;
Expand All @@ -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,
}));
});
1 change: 1 addition & 0 deletions app/misc/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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/" },
],
Expand Down
9 changes: 7 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 (
<div className="max-w-[1180px] mx-auto px-4 md:px-12 py-16 space-y-14">
<script
Expand Down Expand Up @@ -140,7 +145,7 @@ export default async function Home() {
<div className="fade-up" style={{ animationDelay: "60ms" }}>
<div className="mag-card">
<div className="mag-label">Projects</div>
<ProjectsSplit personalProjects={projects} personalVariant="list" />
<ProjectsSplit courseProjects={courseProjects} personalProjects={projects} personalVariant="list" />
</div>
</div>
</div>
Expand Down
5 changes: 3 additions & 2 deletions app/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Metadata } from "next";
import PinnedProjectLink from "../components/pinned-project-link";
import CourseProjectLink from "../components/oxford-dul/course-project-link";
import { COURSE_PROJECTS } from "../lib/course-projects";
import { getCourseProjects } from "../lib/course-projects";
import { getPersonalProjects } from "../lib/personal-projects";

/** Same GitHub pinned cache window as the homepage Projects card. */
Expand All @@ -15,6 +15,7 @@ export const metadata: Metadata = {

export default async function Projects() {
const projects = await getPersonalProjects();
const courseProjects = await getCourseProjects();

return (
<div className="max-w-[1180px] mx-auto px-4 md:px-12 py-16 space-y-8">
Expand All @@ -33,7 +34,7 @@ export default async function Projects() {
<div className="fade-up" style={{ animationDelay: "40ms" }}>
<div className="mag-label">Course Projects</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{COURSE_PROJECTS.map((entry) => (
{courseProjects.map((entry) => (
<CourseProjectLink key={entry.id} entry={entry} variant="card" />
))}
</div>
Expand Down