diff --git a/.env.example b/.env.example
index f819923..3689356 100644
--- a/.env.example
+++ b/.env.example
@@ -25,8 +25,10 @@ LASTFM_API_KEY=
# --- GitHub ------------------------------------------------------------------
# Personal access token or fine-grained token with repo read access.
-# Powers /api/github/contributions and /api/github/stars.
+# Powers /api/github/contributions, /api/github/stars, and pinned repos on the site.
GITHUB_TOKEN=
+# Optional: GitHub username for pinned repos + activity (default: kaiiiichen).
+GITHUB_LOGIN=
# --- Sentry (optional) -------------------------------------------------------
# Omit entirely for local dev / CI with no error reporting.
diff --git a/README.md b/README.md
index d77f57a..b465f90 100644
--- a/README.md
+++ b/README.md
@@ -83,9 +83,9 @@ This repository is a **[Next.js 16](https://nextjs.org/)** application using the
The site combines:
-- A **marketing-style home page** (identity, listening status, weather, projects, Substack headlines).
-- **Dynamic data** from Last.fm, GitHub, Open-Meteo, and optional Supabase-backed gallery and listening history.
-- **MDX-powered notes** under `/notes` with math (KaTeX), GitHub-flavored Markdown, and syntax-highlighted code blocks.
+- A **marketing-style home page** (identity, listening status, weather, **GitHub pinned repositories** via GraphQL with a static fallback, Substack headlines, and links to **news.kaichen.dev** and the **Berkeley library hours** page).
+- **Dynamic data** from Last.fm, GitHub, Open-Meteo, optional Supabase-backed gallery and listening history, and **UC Berkeley library hours** scraped from the official hours page (cached, JSON API available).
+- **MDX-powered notes** under `/notes` with math (KaTeX), GitHub-flavored Markdown, and syntax-highlighted code blocks — multiple course segments (UC Berkeley and SUSTech); see [MDX lecture notes](#mdx-lecture-notes).
- **Optional observability** via Sentry (client, server, edge) and Vercel Analytics / Speed Insights.
There is **no** `middleware.ts` in this repo; auth for admin flows uses Supabase OAuth and route handlers under `app/auth/`.
@@ -162,15 +162,17 @@ kaichen.dev/
│ ├── notes/ # Notes index, course pages, MDX note routes
│ ├── gallery/ # Public gallery + OG
│ ├── admin/ # Supabase-auth gallery admin; /admin/gallery → redirect /admin
-│ ├── api/ # Route handlers (Last.fm, GitHub, weather, guestbook)
+│ ├── api/ # Route handlers (Last.fm, GitHub, weather, guestbook, UCB libraries)
+│ ├── berkeley-libraries/ # UC Berkeley library hours (HTML from lib.berkeley.edu)
│ ├── auth/callback/ # Supabase OAuth exchange → redirect
│ ├── components/ # UI: nav, cards, theme, weather, listening, GitHub heatmap, …
│ ├── hooks/ # e.g. use-now-playing.ts
-│ └── lib/ # og.tsx, substack RSS helpers
+│ └── lib/ # og.tsx, substack RSS, GitHub pinned repos (GraphQL)
├── components/notes/ # MDX shortcodes: Theorem, Proof, Definition, Example, NoteBlock
├── lib/ # Shared server-oriented helpers + Vitest tests
│ ├── supabase.ts # Lazy anon Supabase client (getSupabaseAnon)
│ ├── now-playing.ts # Types for Last.fm payload
+│ ├── ucb-library-hours.ts # Fetch + parse lib.berkeley.edu/hours (Cheerio)
│ ├── weather-open-meteo.ts
│ └── *.test.ts
├── mdx-components.tsx # MDX element mapping + shortcode registration
@@ -205,6 +207,7 @@ kaichen.dev/
| Framework | Next.js **16.2** (App Router), React **19**, TypeScript **5** |
| Styling | Tailwind CSS **4** (`@tailwindcss/postcss`), custom CSS in `app/globals.css` |
| Content | **MDX** via `@mdx-js/loader` + `remark-gfm`, `remark-math`, `rehype-katex`, `rehype-highlight` |
+| Scraping | **cheerio** — parses UC Berkeley library hours HTML server-side |
| Fonts | `@fontsource/*` (Nunito, Bitter, JetBrains Mono), `geist` (sans/mono CSS variables) |
| Auth / data | Supabase (`@supabase/supabase-js`, `@supabase/ssr`) for OAuth, gallery, optional listening DB writes |
| Monitoring | `@sentry/nextjs` (optional DSN), Vercel Analytics + Speed Insights |
@@ -218,16 +221,17 @@ Pinned versions are in [`package.json`](package.json).
| Route | What it does |
| --- | --- |
-| `/` | Identity block, Last.fm line + card, Berkeley weather, project list with live GitHub stars, Substack RSS snippets |
+| `/` | Identity block, Last.fm line + card, Berkeley weather, **pinned GitHub repos** (GraphQL + fallback list), Substack RSS snippets, links to **news.kaichen.dev** and **`/berkeley-libraries`** |
| `/about` | Education, experience, courses, volunteering |
| `/projects` | Project cards + **GitHub contribution calendar** (client component, data from `/api/github/contributions`) |
-| `/notes` | Index of courses / note collections |
-| `/notes/...` | Nested segments; individual notes are often `page.mdx` (e.g. CS61A Scheme topics) |
+| `/notes` | Index of courses (see [MDX lecture notes](#mdx-lecture-notes)); links to external [SUSTech-Kai-Notes](https://github.com/kaiiiichen/SUSTech-Kai-Notes) for broader collections |
+| `/notes/...` | Nested segments per course (e.g. `cs61a`, `data100`, `cs217`, `ma121`–`ma337`); individual notes are `page.mdx` |
+| `/berkeley-libraries` | UC Berkeley library **open/closed** status and hours, parsed from [lib.berkeley.edu/hours](https://www.lib.berkeley.edu/hours); data revalidates every **15 minutes** |
| `/gallery` | Photo grid + lightbox; data from Supabase `gallery_photos` + Storage |
| `/admin` | Google OAuth via Supabase; **restricted to an allowlisted email** in client code — **you must enforce the same rules in Supabase RLS** for production safety |
| `/admin/gallery` | Redirects to `/admin` |
-The main nav **Blog** link points to external [Substack](https://substack.com/@kaiiiichen); there is no `/blog` route in-app.
+**External nav (no in-app route):** the main nav includes **News** → [news.kaichen.dev](https://news.kaichen.dev) and **Blog** → [Substack](https://kaiiiichen.substack.com/); there is no `/blog` or `/news` route in this repo.
**Open Graph:** several routes ship `opengraph-image` route handlers for social previews. Set [`metadataBase`](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#metadatabase) in `app/layout.tsx` if you see build warnings about resolving OG image URLs.
@@ -243,10 +247,13 @@ All handlers live under `app/api/`.
| `GET /api/github/contributions` | GraphQL contribution calendar + REST search for latest commit + REST repo metadata for star counts | `dynamic = force-dynamic`; `Cache-Control: no-store`; requires `GITHUB_TOKEN` |
| `GET /api/github/stars?repo=owner/name` | Returns `stargazers_count` and `archived` for a repo | `revalidate = 3600`; optional `GITHUB_TOKEN` for rate limits |
| `GET /api/weather` | Open-Meteo forecast for fixed Berkeley coordinates | `fetch` with `next.revalidate = 600` |
+| `GET /api/ucb-libraries` | Same payload as `/berkeley-libraries`: JSON with `libraries`, `fetchedAt`, `sourceUrl`, or `ok: false` + `error` | Uses `getUCBLibraryHours()`; upstream fetch `revalidate: 900` (15 minutes) |
| `POST /api/guestbook` | JSON body `{ email, message }` → insert into Supabase `guestbook` via anon client | No auth; relies on **Supabase RLS** and sensible limits in the database |
**Guestbook** is only referenced from API + docs; ensure any front-end or future form respects abuse concerns (rate limits, validation) at the edge or in Supabase policies.
+**Berkeley libraries:** parsing depends on the HTML structure of lib.berkeley.edu. If the upstream page changes, [`lib/ucb-library-hours.ts`](lib/ucb-library-hours.ts) may need updates (see error responses when zero libraries parse).
+
---
## Environment variables
@@ -262,7 +269,8 @@ Copy [`.env.example`](.env.example) to `.env.local`. **Never commit** real secre
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Supabase anonymous key (browser + server routes using `getSupabaseAnon()`). |
| `SUPABASE_SERVICE_ROLE_KEY` | **Server-only.** Used by `/api/lastfm/now-playing` for DB writes and any server path that must bypass RLS — keep off the client bundle. |
| `LASTFM_API_KEY` | Last.fm API. If unset, the now-playing API returns a graceful “not playing” / DB fallback without calling Last.fm. |
-| `GITHUB_TOKEN` | Fine-grained or classic PAT for GitHub API (contributions + stars). If missing, some features error or return empty data. |
+| `GITHUB_TOKEN` | Fine-grained or classic PAT for GitHub API (contributions + stars + **pinned repos** on the home page). If missing, contribution/stars features may error or return empty data; pinned projects fall back to a **static list** in [`app/lib/github-pinned.ts`](app/lib/github-pinned.ts). |
+| `GITHUB_LOGIN` | Optional. GitHub username for **pinned repositories** and related API calls (defaults to `kaiiiichen` if unset). Set when forking so the home page shows your pins. |
### Sentry (optional)
@@ -288,12 +296,14 @@ That path is gitignored — do not commit it.
## MDX lecture notes
- Notes are **route segments** with `page.mdx` files (e.g. `app/notes/cs61a/scheme-quote/page.mdx`), not a separate `content/` directory.
+- The index at [`/notes`](app/notes/page.tsx) lists courses by code (examples: **CS61A**, **Data 100**, **CS217**, **MA121**–**MA337**). Each course has a `page.tsx` hub and nested folders for individual notes.
- Shared layout: [`app/notes/layout.tsx`](app/notes/layout.tsx) (imports KaTeX CSS, width/padding).
- MDX components and typography are centralized in [`mdx-components.tsx`](mdx-components.tsx).
- Custom shortcodes (Theorem, Definition, Proof, Example, NoteBlock) live in [`components/notes/`](components/notes/) and are registered globally for MDX.
- Metadata in MDX files often uses `export const metadata = { title, description }` (Next.js metadata), not always YAML frontmatter.
+- Large PDFs or archives for a course may live under `public/notes/...` and be linked from MDX; keep binary paths in sync if you move files.
-To add a new note: create a folder + `page.mdx` under `app/notes/`, match existing note headers (breadcrumb, title block) for visual consistency, and run `npm run build` to validate the MDX pipeline.
+To add a new course: add a card on the notes index, create `app/notes//page.tsx` plus note folders with `page.mdx`, match existing note headers (breadcrumb, title block) for visual consistency, and run `npm run build` to validate the MDX pipeline.
---
@@ -308,6 +318,7 @@ To add a new note: create a folder + `page.mdx` under `app/notes/`, match existi
| **Open-Meteo** | Weather (no API key) |
| **Supabase** | Auth, gallery tables + storage, guestbook insert, listening history (optional) |
| **Substack RSS** | Home page “latest posts” (`app/lib/substack.ts`) |
+| **lib.berkeley.edu** | Library hours HTML (scraped server-side; not an official API) |
---
@@ -422,7 +433,8 @@ Replace at minimum:
| --- | --- |
| Copy, links, projects list | `app/page.tsx`, `app/projects/page.tsx`, `app/about/page.tsx` |
| Last.fm username | `app/api/lastfm/now-playing/route.ts` |
-| GitHub login / repos | `app/api/github/contributions/route.ts`, `app/components/project-stars.tsx`, home page `PROJECTS` |
+| GitHub login / repos / pins | `app/api/github/contributions/route.ts`, `app/components/project-stars.tsx`, [`app/lib/github-pinned.ts`](app/lib/github-pinned.ts), env `GITHUB_LOGIN` |
+| Berkeley library page | `lib/ucb-library-hours.ts`, `app/berkeley-libraries/page.tsx`, `app/api/ucb-libraries/route.ts` |
| Supabase project + admin allowlist | `lib/supabase.ts`, `app/admin/page.tsx`, Supabase dashboard (RLS, Storage) |
| Substack feeds | `app/lib/substack.ts` |
| Weather location | `app/api/weather/route.ts`, weather UI components |
diff --git a/app/admin/page.tsx b/app/admin/page.tsx
index 5e1efd4..1ec447a 100644
--- a/app/admin/page.tsx
+++ b/app/admin/page.tsx
@@ -70,10 +70,17 @@ export default function Admin() {
}, [user]);
async function signIn() {
- const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? location.origin;
+ const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? window.location.origin;
+ let nextPath = "/admin";
+ const fromQuery = new URLSearchParams(window.location.search).get("next");
+ if (fromQuery && fromQuery.startsWith("/") && !fromQuery.startsWith("//")) {
+ nextPath = fromQuery;
+ }
+ const callback = new URL("/auth/callback", siteUrl);
+ callback.searchParams.set("next", nextPath);
await supabase.auth.signInWithOAuth({
provider: "google",
- options: { redirectTo: `${siteUrl}/auth/callback?next=/admin` },
+ options: { redirectTo: callback.toString() },
});
}
diff --git a/app/api/github/contributions/route.ts b/app/api/github/contributions/route.ts
index 1a4615b..0d29434 100644
--- a/app/api/github/contributions/route.ts
+++ b/app/api/github/contributions/route.ts
@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
+import { getPinnedProjects, GITHUB_PROFILE_LOGIN } from "@/app/lib/github-pinned";
export const dynamic = "force-dynamic";
@@ -38,12 +39,8 @@ export async function GET() {
const from = new Date(now);
from.setFullYear(now.getFullYear() - 1);
- const STAR_REPOS = [
- "kaiiiichen/kaichen-dev",
- "kaiiiichen/SUSTech-Kai-Notes",
- "kaiiiichen/SudoSodoku",
- "kaiiiichen/kai-chen.xyz",
- ];
+ const pinned = await getPinnedProjects();
+ const STAR_REPOS = pinned.map((p) => p.repo);
const [graphqlRes, commitsRes, ...starResults] = await Promise.all([
fetch("https://api.github.com/graphql", {
@@ -56,14 +53,14 @@ export async function GET() {
body: JSON.stringify({
query: QUERY,
variables: {
- login: "kaiiiichen",
+ login: GITHUB_PROFILE_LOGIN,
from: from.toISOString(),
to: now.toISOString(),
},
}),
}),
fetch(
- "https://api.github.com/search/commits?q=author:kaiiiichen&sort=author-date&order=desc&per_page=1",
+ `https://api.github.com/search/commits?q=author:${GITHUB_PROFILE_LOGIN}&sort=author-date&order=desc&per_page=1`,
{
cache: "no-store",
headers: {
diff --git a/app/api/github/stars/route.ts b/app/api/github/stars/route.ts
index c86cd96..a8d9c15 100644
--- a/app/api/github/stars/route.ts
+++ b/app/api/github/stars/route.ts
@@ -1,6 +1,11 @@
import { NextRequest, NextResponse } from "next/server";
-export const revalidate = 3600; // cache 1 hour
+/** ISR: refresh repo metadata often enough for archive / visibility to stay accurate */
+export const revalidate = 120;
+
+const cacheHeaders = {
+ "Cache-Control": "public, s-maxage=120, stale-while-revalidate=60",
+};
export async function GET(req: NextRequest) {
const repo = req.nextUrl.searchParams.get("repo");
@@ -12,14 +17,27 @@ export async function GET(req: NextRequest) {
Accept: "application/vnd.github+json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
- next: { revalidate: 3600 },
+ next: { revalidate: 120 },
});
- if (!res.ok) return NextResponse.json({ stars: 0 });
+ if (!res.ok) {
+ return NextResponse.json({ stars: 0 }, { headers: cacheHeaders });
+ }
const data = await res.json();
- return NextResponse.json({
- stars: data.stargazers_count ?? 0,
- archived: data.archived ?? false,
- });
+ const visibility =
+ typeof data.visibility === "string"
+ ? (data.visibility as "public" | "private")
+ : data.private
+ ? "private"
+ : "public";
+
+ return NextResponse.json(
+ {
+ stars: data.stargazers_count ?? 0,
+ archived: data.archived ?? false,
+ visibility,
+ },
+ { headers: cacheHeaders }
+ );
}
diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts
index 457df3e..e18c149 100644
--- a/app/auth/callback/route.ts
+++ b/app/auth/callback/route.ts
@@ -2,10 +2,17 @@ import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
+/** OAuth may pass `next=` empty; `?? "/admin"` does not catch "", which redirects to site root. */
+function safeNextPath(raw: string | null): string {
+ if (raw == null || raw === "") return "/admin";
+ if (!raw.startsWith("/") || raw.startsWith("//")) return "/admin";
+ return raw;
+}
+
export async function GET(request: NextRequest) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
- const next = searchParams.get("next") ?? "/admin";
+ const next = safeNextPath(searchParams.get("next"));
// On Vercel, request.url uses localhost internally — use x-forwarded-host for the real domain
const forwardedHost = request.headers.get("x-forwarded-host");
diff --git a/app/berkeley-libraries/page.tsx b/app/berkeley-libraries/page.tsx
index f6385bc..7cef2ed 100644
--- a/app/berkeley-libraries/page.tsx
+++ b/app/berkeley-libraries/page.tsx
@@ -6,7 +6,7 @@ import type { UCBLibrary } from "@/lib/ucb-library-hours";
export const metadata: Metadata = {
title: "UC Berkeley Library Hours",
description:
- "Live open/closed status for UC Berkeley libraries, from lib.berkeley.edu (refreshed at most every 15 minutes).",
+ "Library hours parsed from lib.berkeley.edu/hours; fetch cache revalidates every 15 minutes (Next.js revalidate: 900).",
openGraph: {
title: "UC Berkeley Library Hours · Kai Chen",
description: "Library availability scraped from the official Berkeley hours page.",
@@ -106,7 +106,7 @@ export default async function BerkeleyLibrariesPage() {
style={{ fontFamily: "'Bitter'", fontWeight: 400, fontSize: 16, lineHeight: 1.75 }}
className="text-zinc-600 dark:text-zinc-400 mt-4 max-w-2xl"
>
- Parsed from the official{" "}
+ The server fetches the public{" "}
library hours
{" "}
- page. Data is cached for up to 15 minutes to avoid hammering their servers. Overnight and
- special hours may only appear on each library's detail page — check the official site
- when in doubt.
+ HTML and parses list items server-side: each library gets a name, link to its visit page,
+ and—when the list text matches simple time patterns—a same-day hours window and an
+ open/closed guess. The underlying HTTP fetch is cached by Next.js with{" "}
+ revalidate: 900 (15 minutes), so the live
+ lib.berkeley.edu page is not downloaded on every request. Lines that don't match those
+ patterns (unusual wording, overnight spans, holidays) may parse as unknown or incomplete—check
+ the official site or each library's page when it matters.
diff --git a/app/components/nav.tsx b/app/components/nav.tsx
index 207731c..44d1387 100644
--- a/app/components/nav.tsx
+++ b/app/components/nav.tsx
@@ -17,11 +17,16 @@ const NAV_LINKS = [
];
export default function Nav() {
+ const [mounted, setMounted] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const navRef = useRef(null);
const [waveRect, setWaveRect] = useState<{ left: number; width: number; top: number } | null>(null);
const waveLeaveFrame = useRef(0);
+ useEffect(() => {
+ queueMicrotask(() => setMounted(true));
+ }, []);
+
const onWaveEnter = useCallback((e: React.MouseEvent) => {
const el = e.currentTarget;
const r = el.getBoundingClientRect();
@@ -157,7 +162,7 @@ export default function Nav() {
- {typeof document !== "undefined"
+ {mounted
? createPortal(
(null);
useEffect(() => {
- fetch(`/api/github/stars?repo=${encodeURIComponent(repo)}`)
+ fetch(`/api/github/stars?repo=${encodeURIComponent(repo)}`, { cache: "no-store" })
.then((r) => r.json())
.then((d) => {
if (typeof d.stars === "number") {
- setMeta({ stars: d.stars, archived: d.archived ?? false });
+ const vis = d.visibility;
+ setMeta({
+ stars: d.stars,
+ archived: d.archived ?? false,
+ ...(vis === "public" || vis === "private" ? { visibility: vis } : {}),
+ });
}
})
.catch(() => {});
@@ -19,13 +28,23 @@ export default function ProjectStars({ repo }: { repo: string }) {
if (meta === null) return null;
+ const badgeClass =
+ "px-1.5 py-0.5 rounded-sm bg-zinc-100 dark:bg-zinc-800 text-zinc-400 dark:text-zinc-500";
+
return (
<>
+ {meta.visibility === "public" && (
+
+ public
+
+ )}
+ {meta.visibility === "private" && (
+
+ private
+
+ )}
{meta.archived && (
-
+
archived
)}
diff --git a/app/gallery/page.tsx b/app/gallery/page.tsx
index 09679d8..8de2950 100644
--- a/app/gallery/page.tsx
+++ b/app/gallery/page.tsx
@@ -205,7 +205,7 @@ export default function Gallery() {
{/* Admin entry — bottom, minimal icon */}
diff --git a/app/layout.tsx b/app/layout.tsx
index 7e8e0a9..6c82cf5 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -51,9 +51,13 @@ export default function RootLayout({
suppressHydrationWarning
>
-
+
diff --git a/app/lib/github-pinned.ts b/app/lib/github-pinned.ts
new file mode 100644
index 0000000..e6afdb6
--- /dev/null
+++ b/app/lib/github-pinned.ts
@@ -0,0 +1,131 @@
+export type PinnedProject = {
+ name: string;
+ desc: string;
+ href: string;
+ repo: string;
+ stack: string[];
+};
+
+/** Matches previous static list when GITHUB_TOKEN is missing or the API fails */
+const FALLBACK_PINNED: PinnedProject[] = [
+ {
+ name: "kaichen.dev",
+ desc: "Personal website and digital identity system.",
+ href: "https://github.com/kaiiiichen/kaichen.dev",
+ repo: "kaiiiichen/kaichen.dev",
+ stack: ["Next.js", "TypeScript", "Tailwind"],
+ },
+ {
+ name: "SUSTech-Kai-Notes",
+ desc: "Open lecture notes for 20+ math and CS courses.",
+ href: "https://github.com/kaiiiichen/SUSTech-Kai-Notes",
+ repo: "kaiiiichen/SUSTech-Kai-Notes",
+ stack: ["LaTeX"],
+ },
+ {
+ name: "SudoSodoku",
+ desc: "Terminal-style Sudoku for iOS. Minimalist, focus-driven.",
+ href: "https://github.com/kaiiiichen/SudoSodoku",
+ repo: "kaiiiichen/SudoSodoku",
+ stack: ["Swift", "SwiftUI"],
+ },
+ {
+ name: "kai-chen.xyz",
+ desc: "Previous personal website, v1. Static.",
+ href: "https://github.com/kaiiiichen/kai-chen.xyz",
+ repo: "kaiiiichen/kai-chen.xyz",
+ stack: ["HTML", "CSS"],
+ },
+];
+
+/** GitHub profile whose pinned repos we mirror (override for forks / different accounts) */
+export const GITHUB_PROFILE_LOGIN = process.env.GITHUB_LOGIN ?? "kaiiiichen";
+
+const PINNED_QUERY = `
+ query PinnedRepositories($login: String!) {
+ user(login: $login) {
+ pinnedItems(first: 6, types: [REPOSITORY]) {
+ nodes {
+ ... on Repository {
+ name
+ description
+ url
+ nameWithOwner
+ languages(first: 8, orderBy: { field: SIZE, direction: DESC }) {
+ nodes {
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+`;
+
+/**
+ * Repositories pinned on github.com/{login} (same order as profile).
+ * Requires GITHUB_TOKEN. Falls back to a static list when unavailable.
+ */
+export async function getPinnedProjects(): Promise {
+ const token = process.env.GITHUB_TOKEN;
+ if (!token) return FALLBACK_PINNED;
+
+ try {
+ const res = await fetch("https://api.github.com/graphql", {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ query: PINNED_QUERY,
+ variables: { login: GITHUB_PROFILE_LOGIN },
+ }),
+ next: { revalidate: 120 },
+ });
+
+ const json: {
+ data?: {
+ user?: {
+ pinnedItems?: {
+ nodes?: Array<{
+ name?: string;
+ description?: string | null;
+ url?: string;
+ nameWithOwner?: string;
+ languages?: { nodes?: Array<{ name?: string | null } | null> | null };
+ } | null>;
+ };
+ };
+ };
+ errors?: unknown[];
+ } = await res.json();
+
+ if (!res.ok || json.errors?.length) {
+ return FALLBACK_PINNED;
+ }
+
+ const nodes = json.data?.user?.pinnedItems?.nodes;
+ if (!Array.isArray(nodes)) return FALLBACK_PINNED;
+
+ const out: PinnedProject[] = [];
+ for (const node of nodes) {
+ if (!node?.nameWithOwner || !node.url || !node.name) continue;
+ const langNames = (node.languages?.nodes ?? [])
+ .map((n) => n?.name)
+ .filter((n): n is string => Boolean(n));
+ out.push({
+ name: node.name,
+ desc: node.description?.trim() ?? "",
+ href: node.url,
+ repo: node.nameWithOwner,
+ stack: langNames.slice(0, 6),
+ });
+ }
+
+ return out;
+ } catch {
+ return FALLBACK_PINNED;
+ }
+}
diff --git a/app/notes/cs217/dsaa/page.mdx b/app/notes/cs217/dsaa/page.mdx
new file mode 100644
index 0000000..4dd9f43
--- /dev/null
+++ b/app/notes/cs217/dsaa/page.mdx
@@ -0,0 +1,51 @@
+import Link from "next/link";
+
+export const metadata = {
+ title: "Lecture Notes | CS217 DSAA (H) | Kai Chen",
+ description:
+ "Mathematical analysis of algorithms, data structure implementation details, and complexity theory.",
+};
+
+
+
+ ←
+ CS217
+
+
+
+
+
+
+
+These notes are compatible with the **CS217 Data Structures and Algorithm Analysis (H) (Fall 2025)** at SUSTech, and part of the course-notes-and-resources initiative: [SUSTech-Kai-Notes](https://github.com/kaiiiichen/SUSTech-Kai-Notes).
+
+Specific focus is placed on the mathematical analysis of algorithms, data structure
+implementation details, and complexity theory.
+
+
+
+
diff --git a/app/notes/cs217/page.tsx b/app/notes/cs217/page.tsx
new file mode 100644
index 0000000..f4924b5
--- /dev/null
+++ b/app/notes/cs217/page.tsx
@@ -0,0 +1,114 @@
+import Link from "next/link";
+
+const NOTES = [
+ {
+ num: "01",
+ title: "Lecture Notes",
+ desc: "LaTeX lecture notes for the CS217 Data Structures & Algorithm Analysis (H) course at SUSTech.",
+ topics: [] as string[],
+ href: "/notes/cs217/dsaa",
+ },
+];
+
+export default function CS217Page() {
+ return (
+
+
+ {/* Header */}
+
+
+
+ ←
+ Notes
+
+
+
+
+
+ CS217
+
+
+ SUSTech
+
+
+
+
+ Data Structures & Algorithm Analysis (H)
+
+
+
+ Mathematical analysis of algorithms, data structure implementation details, and complexity theory.
+
+
+
+
+
+
+ {NOTES.map(({ num, title, desc, topics, href }) => (
+
+
+
+
+ {desc}
+
+
+
+
+ {topics.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+ {num} ↗
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/app/notes/cs61a/page.tsx b/app/notes/cs61a/page.tsx
index 416d3e3..eba8208 100644
--- a/app/notes/cs61a/page.tsx
+++ b/app/notes/cs61a/page.tsx
@@ -67,6 +67,13 @@ export default function CS61APage() {
Big ideas of computing — abstraction, recursion, and the interplay between programs and the data they manipulate.
+
+ Under construction
+
+
diff --git a/app/notes/data100/page.tsx b/app/notes/data100/page.tsx
new file mode 100644
index 0000000..f91f01f
--- /dev/null
+++ b/app/notes/data100/page.tsx
@@ -0,0 +1,130 @@
+import Link from "next/link";
+
+/** Empty for now — note cards will be added later (same grid as CS61A). */
+const NOTES: {
+ num: string;
+ title: string;
+ desc: string;
+ topics: string[];
+ href: string;
+}[] = [];
+
+export default function Data100Page() {
+ return (
+
+
+
+
+
+ ←
+ Notes
+
+
+
+
+
+ Data 100
+
+
+ UC Berkeley
+
+
+
+
+ Principles and Techniques of Data Science
+
+
+
+ Inference, prediction, and data manipulation at scale — from tabular data to the ideas behind modern data science pipelines.
+
+
+
+ Under construction
+
+
+
+
+
+ {NOTES.length === 0 ? (
+
+
+ Note cards will appear here — this section is under construction.
+
+
+ ) : (
+
+ {NOTES.map(({ num, title, desc, topics, href }) => (
+
+
+
+
+ {desc}
+
+
+
+
+ {topics.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+ {num} ↗
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/app/notes/ma121/ala2/page.mdx b/app/notes/ma121/ala2/page.mdx
new file mode 100644
index 0000000..e39fce8
--- /dev/null
+++ b/app/notes/ma121/ala2/page.mdx
@@ -0,0 +1,42 @@
+import Link from "next/link";
+
+export const metadata = {
+ title: "Handwritten Notes | MA121 Advanced Linear Algebra II (H) | Kai Chen",
+ description:
+ "Linear Algebra based on MODULES.",
+};
+
+
+
+ ←
+ MA121
+
+
+
+
+
+
+
+These **handwritten** notes are compatible with **MA121 Advanced Linear Algebra II (H) (Spring 2024)** at SUSTech.
+
+This course is teaching Linear Algebra based on MODULES, where you may NOT have basis.
+
+
+
+
diff --git a/app/notes/ma121/page.tsx b/app/notes/ma121/page.tsx
new file mode 100644
index 0000000..b31bc4a
--- /dev/null
+++ b/app/notes/ma121/page.tsx
@@ -0,0 +1,113 @@
+import Link from "next/link";
+
+const NOTES = [
+ {
+ num: "01",
+ title: "Lecture Notes",
+ desc: "Handwritten notes for MA121 Advanced Linear Algebra II (H) (Spring 2024) at SUSTech.",
+ topics: [] as string[],
+ href: "/notes/ma121/ala2",
+ },
+];
+
+export default function MA121Page() {
+ return (
+
+
+
+
+
+ ←
+ Notes
+
+
+
+
+
+ MA121
+
+
+ SUSTech
+
+
+
+
+ Advanced Linear Algebra II (H)
+
+
+
+ Linear Algebra based on MODULES.
+
+
+
+
+
+
+ {NOTES.map(({ num, title, desc, topics, href }) => (
+
+
+
+
+ {desc}
+
+
+
+
+ {topics.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+ {num} ↗
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/app/notes/ma215/page.tsx b/app/notes/ma215/page.tsx
new file mode 100644
index 0000000..a4de66e
--- /dev/null
+++ b/app/notes/ma215/page.tsx
@@ -0,0 +1,113 @@
+import Link from "next/link";
+
+const NOTES = [
+ {
+ num: "01",
+ title: "Lecture Notes",
+ desc: "Handwritten notes for MA215 Probability Theory (Fall 2024) at SUSTech.",
+ topics: [] as string[],
+ href: "/notes/ma215/pt",
+ },
+];
+
+export default function MA215Page() {
+ return (
+
+
+
+
+
+ ←
+ Notes
+
+
+
+
+
+ MA215
+
+
+ SUSTech
+
+
+
+
+ Probability Theory
+
+
+
+ Probability theory without measure theory in the undergraduate level.
+
+
+
+
+
+
+ {NOTES.map(({ num, title, desc, topics, href }) => (
+
+
+
+
+ {desc}
+
+
+
+
+ {topics.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+ {num} ↗
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/app/notes/ma215/pt/page.mdx b/app/notes/ma215/pt/page.mdx
new file mode 100644
index 0000000..15914e9
--- /dev/null
+++ b/app/notes/ma215/pt/page.mdx
@@ -0,0 +1,42 @@
+import Link from "next/link";
+
+export const metadata = {
+ title: "Handwritten notes | MA215 Probability Theory | Kai Chen",
+ description:
+ "Handwritten notes for MA215 Probability Theory (Fall 2024) at SUSTech.",
+};
+
+
+
+ ←
+ MA215
+
+
+
+
+
+
+
+These **handwritten** notes are compatible with **MA215 Probability Theory (Fall 2024)** at SUSTech, and part of the course-notes-and-resources initiative: [SUSTech-Kai-Notes](https://github.com/kaiiiichen/SUSTech-Kai-Notes).
+
+This course is a well-developed probability course in the undergraduate level, covering almost all you need to understand the concepts in probability theory.
+
+
+
+
diff --git a/app/notes/ma219/aa/page.mdx b/app/notes/ma219/aa/page.mdx
new file mode 100644
index 0000000..6fdc201
--- /dev/null
+++ b/app/notes/ma219/aa/page.mdx
@@ -0,0 +1,42 @@
+import Link from "next/link";
+
+export const metadata = {
+ title: "Handwritten Notes | MA219 Abstract Algebra (H) | Kai Chen",
+ description:
+ "Groups, rings, and fields.",
+};
+
+
+
+ ←
+ MA219
+
+
+
+
+
+
+
+These **handwritten** notes are compatible with **MA219 Abstract Algebra (H) (Fall 2024)** at SUSTech, and part of the course-notes-and-resources initiative: [SUSTech-Kai-Notes](https://github.com/kaiiiichen/SUSTech-Kai-Notes). They are distributed as a **ZIP** archive of several note files.
+
+Groups, rings, and fields.
+
+
+
+
diff --git a/app/notes/ma219/page.tsx b/app/notes/ma219/page.tsx
new file mode 100644
index 0000000..8f65d3a
--- /dev/null
+++ b/app/notes/ma219/page.tsx
@@ -0,0 +1,113 @@
+import Link from "next/link";
+
+const NOTES = [
+ {
+ num: "01",
+ title: "Lecture Notes",
+ desc: "Handwritten notes for MA219 Abstract Algebra (H) (Fall 2024) at SUSTech.",
+ topics: [] as string[],
+ href: "/notes/ma219/aa",
+ },
+];
+
+export default function MA219Page() {
+ return (
+
+
+
+
+
+ ←
+ Notes
+
+
+
+
+
+ MA219
+
+
+ SUSTech
+
+
+
+
+ Abstract Algebra (H)
+
+
+
+ Groups, rings, and fields.
+
+
+
+
+
+
+ {NOTES.map(({ num, title, desc, topics, href }) => (
+
+
+
+
+ {desc}
+
+
+
+
+ {topics.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+ {num} ↗
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/app/notes/ma230/odea/page.mdx b/app/notes/ma230/odea/page.mdx
new file mode 100644
index 0000000..d4e499b
--- /dev/null
+++ b/app/notes/ma230/odea/page.mdx
@@ -0,0 +1,42 @@
+import Link from "next/link";
+
+export const metadata = {
+ title: "Handwritten Notes | MA230 ODE A (H) | Kai Chen",
+ description:
+ "Classics about Ordinary Differential Equations.",
+};
+
+
+
+ ←
+ MA230
+
+
+
+
+
+
+
+These **handwritten** notes are compatible with **MA230 Ordinary Differential Equations A (H) (Spring 2025)** at SUSTech, and part of the course-notes-and-resources initiative: [SUSTech-Kai-Notes](https://github.com/kaiiiichen/SUSTech-Kai-Notes).
+
+Classics about Ordinary Differential Equations.
+
+
+
+
diff --git a/app/notes/ma230/page.tsx b/app/notes/ma230/page.tsx
new file mode 100644
index 0000000..1033263
--- /dev/null
+++ b/app/notes/ma230/page.tsx
@@ -0,0 +1,113 @@
+import Link from "next/link";
+
+const NOTES = [
+ {
+ num: "01",
+ title: "Lecture Notes",
+ desc: "Lecture notes for MA230 Ordinary Differential Equations A (H) (Spring 2025) at SUSTech.",
+ topics: [] as string[],
+ href: "/notes/ma230/odea",
+ },
+];
+
+export default function MA230Page() {
+ return (
+
+
+
+
+
+ ←
+ Notes
+
+
+
+
+
+ MA230
+
+
+ SUSTech
+
+
+
+
+ Ordinary Differential Equations A (H)
+
+
+
+ Classics about Ordinary Differential Equations.
+
+
+
+
+
+
+ {NOTES.map(({ num, title, desc, topics, href }) => (
+
+
+
+
+ {desc}
+
+
+
+
+ {topics.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+ {num} ↗
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/app/notes/ma231/ma3/page.mdx b/app/notes/ma231/ma3/page.mdx
new file mode 100644
index 0000000..f656e4a
--- /dev/null
+++ b/app/notes/ma231/ma3/page.mdx
@@ -0,0 +1,42 @@
+import Link from "next/link";
+
+export const metadata = {
+ title: "Handwritten Notes | MA231 Mathematical Analysis III (H) | Kai Chen",
+ description:
+ "Series convergence, integral with parameter, and Fourier analysis. Almost everything you need to know about exchanging limits.",
+};
+
+
+
+ ←
+ MA231
+
+
+
+
+
+
+
+These **handwritten** notes are compatible with **MA231 Mathematical Analysis III (H) (Fall 2024)** at SUSTech.
+
+Series convergence, integral with parameter, and Fourier analysis. Almost everything you need to know about exchanging limits.
+
+
+
+
diff --git a/app/notes/ma231/page.tsx b/app/notes/ma231/page.tsx
new file mode 100644
index 0000000..2e33b35
--- /dev/null
+++ b/app/notes/ma231/page.tsx
@@ -0,0 +1,113 @@
+import Link from "next/link";
+
+const NOTES = [
+ {
+ num: "01",
+ title: "Handwritten notes",
+ desc: "Lecture notes for MA231 Mathematical Analysis III (H) (Fall 2024) at SUSTech.",
+ topics: [] as string[],
+ href: "/notes/ma231/ma3",
+ },
+];
+
+export default function MA231Page() {
+ return (
+
+
+
+
+
+ ←
+ Notes
+
+
+
+
+
+ MA231
+
+
+ SUSTech
+
+
+
+
+ Mathematical Analysis III (H)
+
+
+
+ Series convergence, integral with parameter, and Fourier analysis. Almost everything you need to know about exchanging limits.
+
+
+
+
+
+
+ {NOTES.map(({ num, title, desc, topics, href }) => (
+
+
+
+
+ {desc}
+
+
+
+
+ {topics.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+ {num} ↗
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/app/notes/ma232/ca/page.mdx b/app/notes/ma232/ca/page.mdx
new file mode 100644
index 0000000..5581b5c
--- /dev/null
+++ b/app/notes/ma232/ca/page.mdx
@@ -0,0 +1,42 @@
+import Link from "next/link";
+
+export const metadata = {
+ title: "Handwritten Notes | MA232 Complex Analysis (H) | Kai Chen",
+ description:
+ "A unique Complex Analysis course based on Introduction to Complex Analysis by Б. В. Шабат (B. V. Shabat). And a lot about complex geometry.",
+};
+
+
+
+ ←
+ MA232
+
+
+
+
+
+
+
+These **handwritten** notes are compatible with **MA232 Complex Analysis (H) (Spring 2025)** at SUSTech, and part of the course-notes-and-resources initiative: [SUSTech-Kai-Notes](https://github.com/kaiiiichen/SUSTech-Kai-Notes).
+
+A unique Complex Analysis course based on Introduction to Complex Analysis by Б. В. Шабат (B. V. Shabat). And a lot about complex geometry.
+
+
+
+
diff --git a/app/notes/ma232/page.tsx b/app/notes/ma232/page.tsx
new file mode 100644
index 0000000..5677de2
--- /dev/null
+++ b/app/notes/ma232/page.tsx
@@ -0,0 +1,113 @@
+import Link from "next/link";
+
+const NOTES = [
+ {
+ num: "01",
+ title: "Lecture Notes",
+ desc: "Lecture notes for MA232 Complex Analysis (H) (Spring 2025) at SUSTech.",
+ topics: [] as string[],
+ href: "/notes/ma232/ca",
+ },
+];
+
+export default function MA232Page() {
+ return (
+
+
+
+
+
+ ←
+ Notes
+
+
+
+
+
+ MA232
+
+
+ SUSTech
+
+
+
+
+ Complex Analysis (H)
+
+
+
+ A unique Complex Analysis course based on Introduction to Complex Analysis by Б. В. Шабат (B. V. Shabat). And a lot about complex geometry.
+
+
+
+
+
+
+ {NOTES.map(({ num, title, desc, topics, href }) => (
+
+
+
+
+ {desc}
+
+
+
+
+ {topics.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+ {num} ↗
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/app/notes/ma337/page.tsx b/app/notes/ma337/page.tsx
new file mode 100644
index 0000000..7f8a64b
--- /dev/null
+++ b/app/notes/ma337/page.tsx
@@ -0,0 +1,113 @@
+import Link from "next/link";
+
+const NOTES = [
+ {
+ num: "01",
+ title: "Lecture Notes",
+ desc: "LaTeX lecture notes for the MA337 Real Analysis (H) course at SUSTech.",
+ topics: [] as string[],
+ href: "/notes/ma337/ra",
+ },
+];
+
+export default function MA337Page() {
+ return (
+
+
+
+
+
+ ←
+ Notes
+
+
+
+
+
+ MA337
+
+
+ SUSTech
+
+
+
+
+ Real Analysis (H)
+
+
+
+ Remark: The term 'Real Analysis' refers to the study of MEASURE and INTEGRATION theory in China. This honors course focuses on the study of GENERAL measure and integration theory.
+
+
+
+
+
+
+ {NOTES.map(({ num, title, desc, topics, href }) => (
+
+
+
+
+ {desc}
+
+
+
+
+ {topics.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+ {num} ↗
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/app/notes/ma337/ra/page.mdx b/app/notes/ma337/ra/page.mdx
new file mode 100644
index 0000000..4186b8d
--- /dev/null
+++ b/app/notes/ma337/ra/page.mdx
@@ -0,0 +1,66 @@
+import Link from "next/link";
+
+export const metadata = {
+ title: "Lecture Notes | MA337 Real Analysis (H) | Kai Chen",
+ description:
+ "General measure theory.",
+};
+
+
+
+ ←
+ MA337
+
+
+
+
+
+
+
+These notes are compatible with the **MA337 course (Fall 2025)** at SUSTech, and part of
+the course-notes-and-resources initiative: [SUSTech-Kai-Notes](https://github.com/kaiiiichen/SUSTech-Kai-Notes).
+
+I tried my best to make sure every statement and proof make sense, and I made some
+complements, which might be useful, to the original contents of the course. For instance,
+the Dynkin classes.
+
+**Main reference**:
+
+A.N. Kolmogorov and S.V. Fomin, Elements of the Theory of Functions and Functional
+Analysis: translated from the first (1954) Russian edition by Leo F. Boron, Gra Ylock
+Press, 1957.
+(There is also a Chinese version of the book published by Higher Education Press, 2006.)
+
+**Remark**:
+
+I’ve stopped updating this course notes project due to personal reasons.
+
+If you are interested in finishing it and fill the blanks with details, please feel free to contact me.
+
+
+
+
diff --git a/app/notes/page.tsx b/app/notes/page.tsx
index ec98fee..05b697b 100644
--- a/app/notes/page.tsx
+++ b/app/notes/page.tsx
@@ -1,14 +1,86 @@
import Link from "next/link";
-const COURSES = [
- {
- code: "CS61A",
- name: "Structure and Interpretation of Computer Programs",
- institution: "UC Berkeley",
- count: 1,
- href: "/notes/cs61a",
- },
-];
+const COURSES: {
+ code: string;
+ name: string;
+ institution: string;
+ count: number;
+ href: string;
+ underConstruction?: boolean;
+}[] = [
+ {
+ code: "CS61A",
+ name: "Structure and Interpretation of Computer Programs",
+ institution: "UC Berkeley",
+ count: 2,
+ href: "/notes/cs61a",
+ underConstruction: true,
+ },
+ {
+ code: "Data 100",
+ name: "Principles and Techniques of Data Science",
+ institution: "UC Berkeley",
+ count: 0,
+ href: "/notes/data100",
+ underConstruction: true,
+ },
+ {
+ code: "CS217",
+ name: "Data Structures & Algorithm Analysis (H)",
+ institution: "SUSTech",
+ count: 1,
+ href: "/notes/cs217",
+ },
+ {
+ code: "MA337",
+ name: "Real Analysis (H)",
+ institution: "SUSTech",
+ count: 1,
+ href: "/notes/ma337",
+ },
+ {
+ code: "MA232",
+ name: "Complex Analysis (H)",
+ institution: "SUSTech",
+ count: 1,
+ href: "/notes/ma232",
+ },
+ {
+ code: "MA231",
+ name: "Mathematical Analysis III (H)",
+ institution: "SUSTech",
+ count: 1,
+ href: "/notes/ma231",
+ },
+ {
+ code: "MA230",
+ name: "Ordinary Differential Equations A (H)",
+ institution: "SUSTech",
+ count: 1,
+ href: "/notes/ma230",
+ },
+ {
+ code: "MA219",
+ name: "Abstract Algebra (H)",
+ institution: "SUSTech",
+ count: 1,
+ href: "/notes/ma219",
+ },
+ {
+ code: "MA215",
+ name: "Probability Theory",
+ institution: "SUSTech",
+ count: 1,
+ href: "/notes/ma215",
+ },
+ {
+ code: "MA121",
+ name: "Advanced Linear Algebra II (H)",
+ institution: "SUSTech",
+ count: 1,
+ href: "/notes/ma121",
+ },
+ ];
export default function NotesPage() {
return (
@@ -25,18 +97,21 @@ export default function NotesPage() {
style={{ fontFamily: "'Bitter'", fontWeight: 400, fontSize: 14, lineHeight: 1.8 }}
className="text-zinc-400 dark:text-zinc-600 mt-3"
>
- Personal notes collections (under contruction).
+ Personal notes collections. All things here are made by myself. For more general notes collection, please check out
+
+ SUSTech-Kai-Notes
+ .
- {/* Course cards */}
-
- {COURSES.map(({ code, name, institution, count, href }) => (
+ {/* Course cards — two per row on md+ */}
+
+ {COURSES.map(({ code, name, institution, count, href, underConstruction }) => (
@@ -67,7 +142,11 @@ export default function NotesPage() {
style={{ fontFamily: "'Nunito'", fontWeight: 400, fontSize: 11 }}
className="text-zinc-300 dark:text-zinc-700 group-hover:text-[#C4894F]/60 dark:group-hover:text-[#D9A870]/60 transition-colors"
>
- {count === 0 ? "coming soon" : `${count} note${count !== 1 ? "s" : ""}`}
+ {underConstruction
+ ? "under construction"
+ : count === 0
+ ? "coming soon"
+ : `${count} note${count !== 1 ? "s" : ""}`}
diff --git a/app/page.tsx b/app/page.tsx
index 4f50848..d595cfb 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -3,41 +3,11 @@ import ListeningLine from "./components/listening-line";
import ListeningCard from "./components/listening-card";
import WeatherCard from "./components/weather-card";
import ProjectStars from "./components/project-stars";
+import { getPinnedProjects } from "./lib/github-pinned";
import { getSubstackPosts } from "./lib/substack";
-const PROJECTS = [
- {
- name: "kaichen.dev",
- desc: "Personal website and digital identity system.",
- href: "https://github.com/kaiiiichen/kaichen.dev",
- repo: "kaiiiichen/kaichen.dev",
- stack: ["Next.js", "TypeScript", "Tailwind"],
- },
- {
- name: "SUSTech Kai Notes",
- desc: "Open lecture notes for 20+ math and CS courses.",
- href: "https://github.com/kaiiiichen/SUSTech-Kai-Notes",
- repo: "kaiiiichen/SUSTech-Kai-Notes",
- stack: ["LaTeX"],
- },
- {
- name: "SudoSodoku",
- desc: "Terminal-style Sudoku for iOS. Minimalist, focus-driven.",
- href: "https://github.com/kaiiiichen/SudoSodoku",
- repo: "kaiiiichen/SudoSodoku",
- stack: ["Swift", "SwiftUI"],
- },
- {
- name: "kai-chen.xyz",
- desc: "Previous personal website, v1. Static.",
- href: "https://github.com/kaiiiichen/kai-chen.xyz",
- repo: "kaiiiichen/kai-chen.xyz",
- stack: ["HTML", "CSS"],
- },
-];
-
export default async function Home() {
- const substackPosts = await getSubstackPosts();
+ const [substackPosts, projects] = await Promise.all([getSubstackPosts(), getPinnedProjects()]);
return (
@@ -198,20 +168,26 @@ export default async function Home() {
- {/* ── Layer 3: Projects | Notes + Blog ───────────────────── */}
- {/* Desktop: 2-col grid, Projects spans both rows on the left */}
+ {/* ── Layer 3: Projects | Blog ─────────────────────────────── */}
- {/* Projects — left column, spans 2 rows on desktop */}
-
+
Projects
- {PROJECTS.map(({ name, desc, href, repo, stack }) => (
+ {projects.length === 0 ? (
+
+ Pin repositories on your GitHub profile to show them here.
+
+ ) : (
+ projects.map(({ name, desc, href, repo, stack }) => (
-
- {desc}
-
+ {desc ? (
+
+ {desc}
+
+ ) : null}
+ {stack.length > 0 ? (
{stack.map((tag) => (
))}
+ ) : null}
- ))}
+ ))
+ )}
- {/* Notes — right column, top */}
-
-
- {/* Blog — right column, bottom */}
+ {/* Blog */}
Blog
{substackPosts.length === 0 ? (
diff --git a/app/projects/page.tsx b/app/projects/page.tsx
index 644389e..32d323a 100644
--- a/app/projects/page.tsx
+++ b/app/projects/page.tsx
@@ -1,38 +1,10 @@
import ProjectStars from "../components/project-stars";
import GitHubActivity from "../components/GitHubActivity";
+import { getPinnedProjects } from "../lib/github-pinned";
-const PROJECTS = [
- {
- name: "kaichen.dev",
- desc: "Personal website and digital identity system.",
- href: "https://github.com/kaiiiichen/kaichen.dev",
- repo: "kaiiiichen/kaichen.dev",
- stack: ["Next.js", "TypeScript", "Tailwind"],
- },
- {
- name: "SUSTech-Kai-Notes",
- desc: "Open lecture notes for 20+ math and CS courses.",
- href: "https://github.com/kaiiiichen/SUSTech-Kai-Notes",
- repo: "kaiiiichen/SUSTech-Kai-Notes",
- stack: ["LaTeX"],
- },
- {
- name: "SudoSodoku",
- desc: "Terminal-style Sudoku for iOS. Minimalist, focus-driven.",
- href: "https://github.com/kaiiiichen/SudoSodoku",
- repo: "kaiiiichen/SudoSodoku",
- stack: ["Swift", "SwiftUI"],
- },
- {
- name: "kai-chen.xyz",
- desc: "Previous personal website, v1. Static.",
- href: "https://github.com/kaiiiichen/kai-chen.xyz",
- repo: "kaiiiichen/kai-chen.xyz",
- stack: ["HTML", "CSS"],
- },
-];
+export default async function Projects() {
+ const projects = await getPinnedProjects();
-export default function Projects() {
return (
@@ -53,11 +25,19 @@ export default function Projects() {
- {/* Project cards */}
+ {/* Project cards — same repos & order as GitHub profile “Pinned” */}
- {PROJECTS.map(({ name, desc, href, repo, stack }) => (
+ {projects.length === 0 ? (
+
+ Pin repositories on your GitHub profile to show them here.
+
+ ) : (
+ projects.map(({ name, desc, href, repo, stack }) => (
{/* Description */}
-
- {desc}
-
+ {desc ? (
+
+ {desc}
+
+ ) : null}
{/* Footer: stack + GitHub */}
@@ -109,7 +91,8 @@ export default function Projects() {
- ))}
+ ))
+ )}
{/* GitHub Activity */}
diff --git a/public/notes/cs217/DSAA.pdf b/public/notes/cs217/DSAA.pdf
new file mode 100644
index 0000000..12e7abc
Binary files /dev/null and b/public/notes/cs217/DSAA.pdf differ
diff --git a/public/notes/cs217/DSAA.zip b/public/notes/cs217/DSAA.zip
new file mode 100644
index 0000000..00d032e
Binary files /dev/null and b/public/notes/cs217/DSAA.zip differ
diff --git a/public/notes/ma121/ma121.pdf b/public/notes/ma121/ma121.pdf
new file mode 100644
index 0000000..4e5ca75
Binary files /dev/null and b/public/notes/ma121/ma121.pdf differ
diff --git a/public/notes/ma215/PT.pdf b/public/notes/ma215/PT.pdf
new file mode 100644
index 0000000..9e7a5f4
Binary files /dev/null and b/public/notes/ma215/PT.pdf differ
diff --git a/public/notes/ma219/AA.zip b/public/notes/ma219/AA.zip
new file mode 100644
index 0000000..793f7bb
Binary files /dev/null and b/public/notes/ma219/AA.zip differ
diff --git a/public/notes/ma230/ODEA.pdf b/public/notes/ma230/ODEA.pdf
new file mode 100644
index 0000000..d849c61
Binary files /dev/null and b/public/notes/ma230/ODEA.pdf differ
diff --git a/public/notes/ma231/MA3.pdf b/public/notes/ma231/MA3.pdf
new file mode 100644
index 0000000..4942f0c
Binary files /dev/null and b/public/notes/ma231/MA3.pdf differ
diff --git a/public/notes/ma232/CA.pdf b/public/notes/ma232/CA.pdf
new file mode 100644
index 0000000..f272c1c
Binary files /dev/null and b/public/notes/ma232/CA.pdf differ
diff --git a/public/notes/ma337/RA.pdf b/public/notes/ma337/RA.pdf
new file mode 100644
index 0000000..3a29403
Binary files /dev/null and b/public/notes/ma337/RA.pdf differ
diff --git a/public/notes/ma337/RA.zip b/public/notes/ma337/RA.zip
new file mode 100644
index 0000000..ef04a38
Binary files /dev/null and b/public/notes/ma337/RA.zip differ