Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Instructions for **AI coding agents** (Cursor, Claude Code, Codex, Copilot, etc.) and anyone running **automated** edits against this repository.

**Human-oriented** contributor docs: [CONTRIBUTING.md](CONTRIBUTING.md). **Full project reference:** [README.md](README.md) (architecture, APIs, environment variables, CI, MDX, deployment).
**Human-oriented** contributor docs: [CONTRIBUTING.md](CONTRIBUTING.md). **Full project reference:** [README.md](README.md) (architecture, APIs, environment variables, CI, deployment).

---

Expand Down Expand Up @@ -98,7 +98,7 @@ Run `git status` and `git diff --staged` before committing.

| Topic | Document |
| --- | --- |
| APIs, env, CI, MDX, fork checklist | [README.md](README.md) |
| APIs, env, CI, fork checklist | [README.md](README.md) |
| Human contribution process | [CONTRIBUTING.md](CONTRIBUTING.md) |
| Short Claude Code stack summary | [CLAUDE.md](CLAUDE.md) |
| Security reporting | [SECURITY.md](SECURITY.md) |
Expand Down
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ Canonical documentation: **[README.md](README.md)** (setup, routes, APIs, env, C
| **Now playing** | `GET /api/spotify/now-playing` — Spotify Web API; optional Supabase writes (`listening_*`) with service role |
| **Client polling** | `app/hooks/use-now-playing.ts` — polls every **10s** (`cache: "no-store"`); API `Cache-Control: public, s-maxage=10, stale-while-revalidate=5` |
| **GitHub** | `GET /api/github/contributions` — GraphQL calendar; pinned repos (incl. `stargazerCount` / `isArchived`) + static fallback in `app/lib/github-pinned.ts` |
| **Weather** | `GET /api/weather` — Open-Meteo (Berkeley, CA); current temp, **feels-like**, **humidity**, condition, rain chance; `next.revalidate = 600`; local clock in card via `app/components/berkeley-time.tsx` (`America/Los_Angeles`) |
| **Weather** | `GET /api/weather` — Open-Meteo (Berkeley, CA); current temp, **feels-like**, **humidity**, **rain chance**; `next.revalidate = 600`; local clock in card via `app/components/berkeley-time.tsx` (`America/Los_Angeles`) |
| **Home UI** | Identity + **social links** (brand-colored hovers), **Listening** / **Location** cards — `app/components/listening-card.tsx`, `weather-card.tsx` |
| **Nav** | **About · Projects · Misc** — `app/components/nav.tsx` (active link accent + underline) |
| **Footer** | `app/components/site-footer.tsx` in root layout — every page |
| **Typography** | Geist `font-sans` on `body`; **Nunito** for nav + `.mag-card` / `.mag-label` (`app/globals.css`) |
| **Notes** | External Notion page linked from navigation |
| **Observability** | Sentry via `instrumentation*.ts` + `sentry.*.config.ts` (DSN optional); Vercel Analytics / Speed Insights in root layout |
| **Theme** | `app/components/theme-provider.tsx` + inline script in `app/layout.tsx` — default **light** when unset |

Expand All @@ -44,7 +45,7 @@ npm run lint && npm run typecheck && npm run test && npm run build
- [`lib/spotify-now-playing-helpers.ts`](lib/spotify-now-playing-helpers.ts) — pure helpers used by that route (tested)
- [`lib/listening-supabase.ts`](lib/listening-supabase.ts) — optional Supabase reads/writes for listening history
- [`lib/weather-open-meteo.ts`](lib/weather-open-meteo.ts) — Open-Meteo payload parsing (tested)
- [`app/lib/substack.ts`](app/lib/substack.ts) — RSS fetch + parse (tested)
- [`app/components/pinned-project-link.tsx`](app/components/pinned-project-link.tsx) — shared pinned-repo markup (home list + projects cards)
- [`app/page.tsx`](app/page.tsx) — home layout, social URLs, cards
- [`app/globals.css`](app/globals.css) — `.mag-card`, `.mag-label`, theme surfaces
- [`next.config.ts`](next.config.ts) — image remote patterns, `withSentryConfig`
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ If you touch **OAuth callbacks**, **API routes**, or **Supabase policies**, coor

## Forking for your own site

See the **Forking this project** section in [README.md](README.md) for a checklist of files to replace (Spotify app, GitHub repos, Supabase, Substack, etc.).
See the **Forking this project** section in [README.md](README.md) for a checklist of files to replace (Spotify app, GitHub repos, Supabase, etc.).

---

Expand Down
78 changes: 36 additions & 42 deletions README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Metadata } from "next";
export const metadata: Metadata = {
title: "About — Kai T. Chen",
description:
"Education, current focus, experience, and volunteering — Maths at SUSTech, visiting UC Berkeley.",
"Education, current focus, experience, and volunteering — Maths at SUSTech, visiting at UC Berkeley.",
};

export default function About() {
Expand All @@ -22,7 +22,7 @@ export default function About() {

{/* Personality / side */}
<div className="fade-up" style={{ animationDelay: "30ms" }}>
<div style={{ fontFamily: "'Nunito'", fontWeight: 400, fontSize: 17, lineHeight: 1.9 }} className="text-zinc-700 dark:text-zinc-300">
<div style={{ fontFamily: "'Nunito'", fontWeight: 400, fontSize: 17, lineHeight: 1.9 }} className="text-zinc-400 dark:text-zinc-600 mt-3">
<p>I study Maths for my bachelor&apos;s degree and I&apos;m always awed by the beauty of it (as the motivation to learn it through so much hard work)! I love the outline of analysis proofs and I&apos;m especially obsessed with algebra structures.</p>
<p>I also learn some computer science and data science, and I find myself better at them than Maths lol. I&apos;m drawn to the problem solving, ideas behind product design, and the empathy I feel when I&apos;m building something for humans.</p>
<p className="mt-1">I really love tinkering stuff. I dream of building something that can help people / make people happy with a fantastic user experience! I also dream of building a tool that could change people&apos;s lives for the better.</p>
Expand Down
21 changes: 21 additions & 0 deletions app/api/spotify/last-month-top/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { NextResponse } from "next/server";
import {
createListeningSupabase,
getLastMonthTopFiveTracks,
} from "@/lib/listening-supabase";

export const revalidate = 600;

export async function GET() {
const db = createListeningSupabase();
const tracks = await getLastMonthTopFiveTracks(db);

return NextResponse.json(
{ tracks },
{
headers: {
"Cache-Control": "public, s-maxage=600, stale-while-revalidate=120",
},
}
);
}
28 changes: 28 additions & 0 deletions app/components/gwwc-badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";

import HoverTip from "./hover-tip";

export default function GwwcBadge() {
return (
<HoverTip
tip={
<span className="flex flex-col gap-1.5 text-left">
<span>Giving What We Can — 10% Pledge Member.</span>
<span>I commit to donate at least 10% of my income to effective charities.</span>
</span>
}
placement="top"
tipClassName="max-w-[280px] px-3.5 py-2.5 text-[13px] leading-relaxed text-left"
>
<a
href="https://www.givingwhatwecan.org/pledge"
target="_blank"
rel="noopener noreferrer"
aria-label="Giving What We Can 10% Pledge"
className="no-underline hover:opacity-70 transition-opacity duration-150 inline-flex"
>
<span style={{ fontSize: "0.75em" }}>🔸</span>
</a>
</HoverTip>
);
}
119 changes: 119 additions & 0 deletions app/components/home-social-links.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"use client";

import type { ReactNode } from "react";
import HoverTip from "./hover-tip";

const SOCIAL_LINKS: Array<{
href: string;
label: string;
ariaLabel: string;
external?: boolean;
hoverClass: string;
icon: ReactNode;
}> = [
{
href: "mailto:kaichen.dev.37@gmail.com",
label: "Email",
ariaLabel: "Email",
hoverClass:
"hover:text-[#EA4335] dark:hover:text-[#FCA5A5]",
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<rect x="2" y="4" width="20" height="16" rx="2" />
<path d="m2 7 10 7 10-7" />
</svg>
),
},
{
href: "https://signal.me/#eu/CNyQcDgQ0KabVX2JyPske78Rwe0HQVHAstI2x1WyyfY_ivPGcChGG44wbzDzi23D",
label: "Signal",
ariaLabel: "Signal",
external: true,
hoverClass: "hover:text-[#3B45FD] dark:hover:text-[#9DBBF8]",
icon: (
<svg viewBox="0 0 128 128" fill="currentColor" aria-hidden>
<path d="M64 0c3.32 0 6.582.253 9.766.74l-.916 5.931A58.5 58.5 0 0 0 64 6c-3.009 0-5.964.23-8.85.67L54.235.74A64.5 64.5 0 0 1 64 0m15.188 1.813-1.424 5.83a57.7 57.7 0 0 1 16.352 6.779l3.115-5.13a63.6 63.6 0 0 0-18.043-7.479m22.635 10.554-3.545 4.84a58.3 58.3 0 0 1 12.514 12.516l4.841-3.546a64.4 64.4 0 0 0-13.81-13.81m16.885 18.403-5.129 3.115a57.7 57.7 0 0 1 6.778 16.351l5.83-1.423a63.6 63.6 0 0 0-7.479-18.044Zm8.552 23.465-5.931.915A58.4 58.4 0 0 1 122 64c0 3.01-.229 5.965-.671 8.85l5.931.916c.487-3.184.74-6.446.74-9.766s-.253-6.581-.74-9.765m-13.681 39.881a57.7 57.7 0 0 0 6.778-16.352l5.83 1.424a63.6 63.6 0 0 1-7.48 18.043zm-2.787 4.162 4.841 3.546a64.4 64.4 0 0 1-13.81 13.809l-3.546-4.841a58.3 58.3 0 0 0 12.515-12.514m-16.677 15.301 3.116 5.129a63.6 63.6 0 0 1-18.044 7.479l-1.423-5.83a57.7 57.7 0 0 0 16.351-6.778m-21.265 7.75.915 5.931c-3.184.487-6.445.74-9.765.74s-6.582-.253-9.766-.74l.916-5.93c2.884.441 5.84.67 8.85.67a58.4 58.4 0 0 0 8.85-.671m-22.614-.971-1.424 5.829a63.5 63.5 0 0 1-13.823-5.125l-6.074 1.418-1.363-5.843 8.208-1.916 1.953.995a57.5 57.5 0 0 0 12.523 4.642m-27.748-2.54 1.363 5.843-10.411 2.43C6.51 127.707.293 121.489 1.91 114.56l2.429-10.411 5.843 1.363-2.43 10.412c-.606 2.598 1.726 4.93 4.324 4.324zm-11.125-17.37L5.52 99.085l1.418-6.074a63.6 63.6 0 0 1-5.125-13.824l5.829-1.423a57.6 57.6 0 0 0 4.642 12.523l.994 1.953zM6.67 72.85l-5.93.915A64.5 64.5 0 0 1 0 64c0-3.32.253-6.582.74-9.766l5.931.916A58.5 58.5 0 0 0 6 64c0 3.01.229 5.966.67 8.85m.973-22.614-5.83-1.424a63.6 63.6 0 0 1 7.48-18.043l5.129 3.115a57.7 57.7 0 0 0-6.779 16.352m9.565-20.513-4.84-3.546a64.4 64.4 0 0 1 13.809-13.81l3.546 4.84a58.3 58.3 0 0 0-12.515 12.515Zm16.677-15.302-3.116-5.129a63.6 63.6 0 0 1 18.044-7.48l1.423 5.83a57.7 57.7 0 0 0-16.351 6.78Z" />
<path d="M116 64c0 28.719-23.281 52-52 52-9.11 0-17.672-2.342-25.117-6.457a3.3 3.3 0 0 0-2.351-.341L13.4 114.599l5.397-23.13a3.3 3.3 0 0 0-.34-2.352C14.341 81.671 12 73.109 12 64c0-28.719 23.281-52 52-52s52 23.281 52 52" />
</svg>
),
},
{
href: "https://github.com/kaiiiichen",
label: "GitHub",
ariaLabel: "GitHub",
external: true,
hoverClass: "hover:text-[#181717] dark:hover:text-white",
icon: (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden>
<path d="M12 2C6.477 2 2 6.477 2 12c0 4.418 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.009-.868-.013-1.703-2.782.604-3.369-1.341-3.369-1.341-.454-1.154-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0 1 12 6.836a9.59 9.59 0 0 1 2.504.337c1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.202 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.163 22 16.418 22 12c0-5.523-4.477-10-10-10z" />
</svg>
),
},
{
href: "https://linkedin.com/in/kaiiiichen",
label: "LinkedIn",
ariaLabel: "LinkedIn",
external: true,
hoverClass: "hover:text-[#0A66C2] dark:hover:text-[#70B5F9]",
icon: (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden>
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
),
},
{
href: "https://open.spotify.com/user/31xktcnl7hl34lu4windhu5uwji4?si=46cd7158fc2a4ab0",
label: "Spotify",
ariaLabel: "Spotify profile",
external: true,
hoverClass: "hover:text-[#1DB954] dark:hover:text-[#1ED760]",
icon: (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden>
<path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z" />
</svg>
),
},
];

export default function HomeSocialLinks() {
const linkClass =
"text-zinc-700 dark:text-zinc-300 opacity-50 transition-all duration-300 ease-out hover:opacity-100 [&_svg]:size-7";

return (
<div className="flex items-center justify-center gap-5 md:h-full [&_svg]:size-7">
{SOCIAL_LINKS.map(({ href, label, ariaLabel, external, hoverClass, icon }) => (
<HoverTip key={label} tip={label} placement="top">
{external ? (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={ariaLabel}
className={`${linkClass} ${hoverClass}`}
>
{icon}
</a>
) : (
<a href={href} aria-label={ariaLabel} className={`${linkClass} ${hoverClass}`}>
{icon}
</a>
)}
</HoverTip>
))}

<HoverTip
tip="For private or sensitive messages, please reach out via Signal."
placement="top"
>
<button
type="button"
aria-label="Contact guidance"
className="inline-flex size-5 items-center justify-center rounded-full border border-zinc-300 dark:border-zinc-600 text-[10px] leading-none text-zinc-400 dark:text-zinc-500 opacity-60 transition-all duration-300 hover:opacity-100 hover:border-[#C4894F] hover:text-[#C4894F] dark:hover:border-[#D9A870] dark:hover:text-[#D9A870] bg-transparent cursor-default"
style={{ fontFamily: "'Nunito'", fontWeight: 600 }}
>
?
</button>
</HoverTip>
</div>
);
}
Loading