diff --git a/app/about/page.tsx b/app/about/page.tsx index af5bae7..ac8f6ba 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -41,14 +41,14 @@ export default function About() { institution: "Southern University of Science and Technology", role: "Bachelor of Science", subtitle: "Mathematics", - grade: "3.73/4.00 (89.96/100)", + grade: "3.73 / 4.00", }, { years: "2026 – 2026", institution: "University of California, Berkeley", role: "Visiting Student", subtitle: "Computer Science and Data Science", - grade: "4.00/4.00 (100/100)", + grade: "4.00 / 4.00", }, { years: "2025 – 2025", @@ -126,7 +126,7 @@ export default function About() { href={href} target="_blank" rel="noopener noreferrer" - className="group block py-4 -mx-2 px-2 rounded-sm hover:bg-zinc-100 dark:hover:bg-zinc-700/40 transition-all duration-150 no-underline border-b border-zinc-100 dark:border-zinc-800/60 last:border-0" + className="group block py-4 -mx-2 px-2 rounded-sm hover:bg-[#e8f0f4] dark:hover:bg-[#243640] transition-all duration-150 no-underline border-b border-zinc-100 dark:border-zinc-800/60 last:border-0" style={{ textDecoration: "none" }} > diff --git a/app/components/nav.tsx b/app/components/nav.tsx index b4439f4..7e37523 100644 --- a/app/components/nav.tsx +++ b/app/components/nav.tsx @@ -16,13 +16,13 @@ function isNavActive(pathname: string, href: string) { return pathname === href || pathname.startsWith(`${href}/`); } -/** Active nav — accent text + rounded underline; hover — text highlight only */ +/** Active nav stays in the shared link palette; hover is handled globally. */ function navLinkClassName(active: boolean) { const base = "nav-link text-sm"; if (active) { - return `${base} nav-link--active text-black dark:text-white`; + return `${base} nav-link--active`; } - return `${base} text-zinc-500 dark:text-zinc-400 hover:text-black dark:hover:text-white`; + return base; } export default function Nav() { @@ -55,7 +55,7 @@ export default function Nav() { href="/" onClick={() => setIsOpen(false)} style={{ fontFamily: "var(--font-nunito)", fontWeight: 600 }} - className="text-base tracking-tight text-zinc-700 dark:text-zinc-300 hover:text-black dark:hover:text-white" + className="site-logo text-base tracking-tight" > Kai T. Chen diff --git a/app/components/oxford-dul/course-project-link.tsx b/app/components/oxford-dul/course-project-link.tsx index 676e238..3e7a09e 100644 --- a/app/components/oxford-dul/course-project-link.tsx +++ b/app/components/oxford-dul/course-project-link.tsx @@ -14,7 +14,7 @@ export default function CourseProjectLink({ entry, variant = "list" }: CoursePro const nameStyle = { fontFamily: "var(--font-nunito)", fontWeight: 600, - fontSize: variant === "card" ? 20 : 18, + fontSize: variant === "list" ? 18 : 20, fontStyle: "italic" as const, }; @@ -59,8 +59,8 @@ export default function CourseProjectLink({ entry, variant = "list" }: CoursePro if (variant === "card") { const className = external - ? "mag-card block group no-underline" - : "mag-card block group no-underline"; + ? "mag-card project-card-theme block group no-underline" + : "mag-card project-card-theme block group no-underline"; const content = ( <> @@ -105,7 +105,7 @@ export default function CourseProjectLink({ entry, variant = "list" }: CoursePro } const listClassName = - "group block py-2 -mx-2 px-2 rounded-sm hover:bg-zinc-100 dark:hover:bg-zinc-700/40 transition-all duration-150 no-underline"; + "group block py-2 -mx-2 px-2 rounded-sm hover:bg-[#e8f0f4] dark:hover:bg-[#243640] transition-all duration-150 no-underline"; const listContent = ( <> diff --git a/app/components/pinned-project-link.tsx b/app/components/pinned-project-link.tsx index 7f53f6b..f0920fe 100644 --- a/app/components/pinned-project-link.tsx +++ b/app/components/pinned-project-link.tsx @@ -20,7 +20,7 @@ export default function PinnedProjectLink({ const nameStyle = { fontFamily: "var(--font-nunito)", fontWeight: 600, - fontSize: variant === "card" ? 20 : 18, + fontSize: variant === "list" ? 18 : 20, fontStyle: "italic" as const, }; @@ -30,7 +30,7 @@ export default function PinnedProjectLink({ href={href} target="_blank" rel="noopener noreferrer" - className="mag-card block group no-underline" + className="mag-card project-card-theme block group no-underline" style={{ textDecoration: "none" }} >
@@ -73,7 +73,7 @@ export default function PinnedProjectLink({ href={href} target="_blank" rel="noopener noreferrer" - className="group block py-2 -mx-2 px-2 rounded-sm hover:bg-zinc-100 dark:hover:bg-zinc-700/40 transition-all duration-150 no-underline" + className="group block py-2 -mx-2 px-2 rounded-sm hover:bg-[#e8f0f4] dark:hover:bg-[#243640] transition-all duration-150 no-underline" style={{ textDecoration: "none" }} >
diff --git a/app/components/playlist-description.tsx b/app/components/playlist-description.tsx index b867c03..a676e34 100644 --- a/app/components/playlist-description.tsx +++ b/app/components/playlist-description.tsx @@ -11,7 +11,7 @@ const CREDIT_CLASS = "text-zinc-400/90 dark:text-zinc-600"; const PLACE_LINK_WRAP = "group/place inline rounded-sm px-0.5 -mx-0.5 " + - "hover:bg-zinc-100 dark:hover:bg-zinc-700/40 transition-all duration-150 no-underline"; + "hover:bg-[#e8f0f4] dark:hover:bg-[#243640] transition-all duration-150 no-underline"; const PLACE_LINK_TEXT = "underline underline-offset-2 decoration-zinc-300 dark:decoration-zinc-600 " + @@ -21,7 +21,7 @@ const PLACE_LINK_TEXT = const CREDIT_LINK_WRAP = "group/credit inline rounded-sm px-0.5 -mx-0.5 " + - "hover:bg-zinc-100 dark:hover:bg-zinc-700/40 transition-all duration-150 no-underline"; + "hover:bg-[#e8f0f4] dark:hover:bg-[#243640] transition-all duration-150 no-underline"; const CREDIT_LINK_TEXT = "underline underline-offset-2 decoration-zinc-300 dark:decoration-zinc-600 " + diff --git a/app/components/weather-card.tsx b/app/components/weather-card.tsx index 211ed68..33a1e15 100644 --- a/app/components/weather-card.tsx +++ b/app/components/weather-card.tsx @@ -41,11 +41,14 @@ export default function WeatherCard({ initialWeather }: WeatherCardProps) {
diff --git a/app/globals.css b/app/globals.css index 56d651d..54d7b8c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -6,6 +6,9 @@ :root { --background: #ffffff; --foreground: #171717; + --link-color: #628ea7; + --link-hover-color: #f8b364; + --card-hover-shadow: #fbe1c4; --contribution-empty: #e4e4e7; --contribution-l1: #d4d4d8; --contribution-l2: #a1a1aa; @@ -14,9 +17,9 @@ /* Match nav + card UI English (see app/components/nav.tsx) */ --font-ui-en: var(--font-nunito), ui-sans-serif, system-ui, sans-serif; /* Border hierarchy for mag-card */ - --color-border-primary: #a1a1aa; /* zinc-400 — bottom accent */ - --color-border-secondary: #d4d4d8; /* zinc-300 — sides */ - --color-border-tertiary: #e4e4e7; /* zinc-200 — shadow */ + --color-border-primary: #628ea7; + --color-border-secondary: #b7cbd6; + --color-border-tertiary: #dfe8ed; --mag-card-radius: 6px; } @@ -28,9 +31,10 @@ --contribution-l2: #52525b; --contribution-l3: #71717a; --contribution-l4: #a1a1aa; - --color-border-primary: #71717a; /* zinc-500 */ - --color-border-secondary: #3f3f46; /* zinc-700 */ - --color-border-tertiary: #27272a; /* zinc-800 */ + --color-border-primary: #628ea7; + --color-border-secondary: #456577; + --color-border-tertiary: #243640; + --card-hover-shadow: #6a4a27; } @theme inline { @@ -50,6 +54,47 @@ body { color: var(--foreground); } +/* Consistent press feedback for every interactive button. The individual + `scale` property composes with components that already use `transform`. */ +button:not(:disabled) { + scale: 1; + cursor: pointer; +} + +button:not(:disabled):active { + scale: 0.97; +} + +/* Site-wide link palette, adapted from g-luo.github.io. */ +a:any-link:not(.mag-card) { + color: var(--link-color); + text-decoration: none; + transition: color 0.2s ease; +} + +a:any-link:not(.mag-card):hover, +a:any-link:not(.mag-card):active { + color: var(--link-hover-color); + text-decoration: none; +} + +a:any-link:not(.mag-card):focus-visible { + color: var(--link-hover-color); + outline: 2px solid currentColor; + outline-offset: 3px; + border-radius: 2px; +} + +/* Wordmark in the site header. */ +a.site-logo:any-link { + color: var(--link-color); +} + +a.site-logo:any-link:hover, +a.site-logo:any-link:active { + color: var(--link-hover-color); +} + /* Theme toggle + card chrome — avoid universal * transitions (paint cost on hover) */ html, body, @@ -142,20 +187,21 @@ html.theme-switching *::after { overflow: hidden; transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, background-color 0.2s ease; } + .mag-card:hover { transform: translateY(-3px); - box-shadow: 7px 7px 0px 0px var(--color-border-tertiary); - border-bottom-color: #000000; - border-right-color: #000000; - background: #fafafa; + box-shadow: 7px 7px 0px 0px var(--card-hover-shadow); + border-bottom-color: var(--link-hover-color); + border-right-color: var(--link-hover-color); + background: #fffaf4; } .dark .mag-card { background: #1F1F23; } .dark .mag-card:hover { - border-bottom-color: #ffffff; - border-right-color: #ffffff; - background: #26262B; + border-bottom-color: var(--link-hover-color); + border-right-color: var(--link-hover-color); + background: #29251f; } /* Nested card inside a parent mag-card — same language + hover, scaled down */ @@ -173,18 +219,18 @@ html.theme-switching *::after { } .mag-card-inset:hover { transform: translateY(-3px); - box-shadow: 5px 5px 0 0 var(--color-border-tertiary); - border-bottom-color: #000000; - border-right-color: #000000; - background: #fafafa; + box-shadow: 5px 5px 0 0 var(--card-hover-shadow); + border-bottom-color: var(--link-hover-color); + border-right-color: var(--link-hover-color); + background: #fffaf4; } .dark .mag-card-inset { background: #26262B; } .dark .mag-card-inset:hover { - border-bottom-color: #ffffff; - border-right-color: #ffffff; - background: #2E2E33; + border-bottom-color: var(--link-hover-color); + border-right-color: var(--link-hover-color); + background: #302b24; } /* Inside a hovering parent shell — accent only, no extra lift */ .mag-card:hover .mag-card-inset:hover { @@ -201,6 +247,15 @@ html.theme-switching *::after { background: #1F1F23; } +/* Projects index cards use the blue theme for their full-card hover surface. */ +.mag-card.project-card-theme:hover { + background: #e8f0f4; +} + +.dark .mag-card.project-card-theme:hover { + background: #243640; +} + /* Magazine label — same UI font as nav (Nunito), title case + hairline */ .mag-label { display: flex; @@ -257,11 +312,11 @@ html.theme-switching *::after { display: block; height: 2px; border-radius: 9999px; - background: #d4d4d8; + background: #b7cbd6; transition: background-color 0.15s ease, width 0.15s ease; } .dark .page-toc-line { - background: #52525b; + background: #456577; } .page-toc-line--l0 { width: 0.875rem; @@ -270,10 +325,10 @@ html.theme-switching *::after { width: 0.5rem; } .page-toc-line--active { - background: #3f3f46; + background: var(--link-color); } .dark .page-toc-line--active { - background: #e4e4e7; + background: var(--link-color); } .page-toc-panel { position: absolute; @@ -330,7 +385,7 @@ html.theme-switching *::after { font-size: 0.72rem; line-height: 1.35; font-weight: 500; - color: #71717a; + color: var(--link-color); padding: 0.3rem 0.55rem; border-radius: 6px; cursor: pointer; @@ -340,29 +395,29 @@ html.theme-switching *::after { transition: background-color 0.12s ease, color 0.12s ease; } .dark .page-toc-link { - color: #a1a1aa; + color: var(--link-color); } .page-toc-link:hover { - background: #f4f4f5; - color: #3f3f46; + background: #fffaf4; + color: var(--link-hover-color); } .dark .page-toc-link:hover { - background: #3f3f46; - color: #e4e4e7; + background: #29251f; + color: var(--link-hover-color); } .page-toc-link--active { - color: #000000; + color: var(--link-color); } .dark .page-toc-link--active { - color: #ffffff; + color: var(--link-color); } .page-toc-link--active:hover { - color: #000000; - background: #f4f4f5; + color: var(--link-hover-color); + background: #fffaf4; } .dark .page-toc-link--active:hover { - color: #ffffff; - background: #2E2E33; + color: var(--link-hover-color); + background: #29251f; } @media (prefers-reduced-motion: reduce) { .page-toc-rail { @@ -379,27 +434,11 @@ html.theme-switching *::after { } } -/* Nav links — pill-shaped active underline */ +/* Nav links — color carries state; no decorative underline. */ .nav-link { position: relative; - display: inline-block; - padding-bottom: 0.375rem; -} -.nav-link::after { - content: ""; - position: absolute; - left: 0; - right: 0; - bottom: 0; - height: 2px; - border-radius: 9999px; - background: transparent; -} -.nav-link--active::after { - background: #000000; -} -.dark .nav-link--active::after { - background: #ffffff; + display: inline-flex; + align-items: center; } /* ── Mag chip — pill/card control (see app/components/mag-chip.tsx) ─ */ @@ -413,23 +452,24 @@ html.theme-switching *::after { border-radius: var(--mag-card-radius); border: 1px solid var(--color-border-secondary); background: rgb(255 255 255 / 0.8); - color: #71717a; + color: var(--link-color); box-shadow: 2px 2px 0 0 var(--color-border-tertiary); transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease; } .dark .mag-chip { background: rgb(31 31 35 / 0.8); - color: #a1a1aa; + color: var(--link-color); } .mag-chip:hover { - color: #000000; - border-color: rgb(0 0 0 / 0.35); - background: #fafafa; + color: var(--link-hover-color); + border-color: var(--link-hover-color); + box-shadow: 2px 2px 0 0 var(--card-hover-shadow); + background: #fffaf4; } .dark .mag-chip:hover { - color: #ffffff; - border-color: rgb(255 255 255 / 0.4); - background: #26262B; + color: var(--link-hover-color); + border-color: var(--link-hover-color); + background: #29251f; } a.mag-chip, button.mag-chip { @@ -441,36 +481,131 @@ button.mag-chip:disabled { opacity: 0.55; } -/* ── Home social links — resting color differs by theme; hover brand color is identical ─ */ +/* ── Home social links — shared palette + subtle up-right lift ─ */ .home-social-link { display: inline-flex; align-items: center; justify-content: center; - opacity: 0.5; - color: #3f3f46; /* zinc-700 */ - transition: color 0.3s ease, opacity 0.3s ease; + opacity: 1; + color: var(--link-color); + transform-origin: 50% 50%; + transition: + transform 0.2s ease, + color 0.2s ease; } .dark .home-social-link { - color: #d4d4d8; /* zinc-300 */ + color: var(--link-color); } .home-social-link:hover { - opacity: 1; + color: var(--link-hover-color); + transform: translateY(-2px) rotate(-6deg); } .home-social-link--email:hover, .home-social-link--github:hover, .home-social-link--x:hover, .home-social-link--linkedin:hover, -.home-social-link--spotify:hover { color: #000000; } /* zinc-950 — 深灰 */ +.home-social-link--spotify:hover { color: var(--link-hover-color); } .dark .home-social-link--email:hover, .dark .home-social-link--github:hover, .dark .home-social-link--x:hover, .dark .home-social-link--linkedin:hover, -.dark .home-social-link--spotify:hover { color: #ffffff; } /* zinc-50 — 近白 */ +.dark .home-social-link--spotify:hover { color: var(--link-hover-color); } .home-social-link svg { width: 1.75rem; height: 1.75rem; } +@media (prefers-reduced-motion: reduce) { + .home-social-link { + transition: color 0.2s ease; + } + + .home-social-link:hover { + transform: none; + } +} + +/* Weather temperature unit switch — visibly interactive, without adding heavy chrome. */ +.weather-unit-toggle { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.2rem 0.35rem; + margin: -0.2rem -0.35rem; + border: 1px solid transparent; + border-radius: 5px; + background: transparent; + color: var(--link-color); + line-height: 1; + cursor: pointer; + transition: + color 0.25s ease, + border-color 0.25s ease, + background-color 0.25s ease, + transform 0.25s cubic-bezier(0.22, 1, 0.36, 1); +} + +.weather-unit-toggle:hover { + color: var(--link-hover-color); + border-color: var(--link-hover-color); + background: #fffaf4; + transform: translateY(-1px); +} + +.dark .weather-unit-toggle:hover { + background: #29251f; +} + +.weather-unit-toggle:active { + transform: translateY(0) scale(0.97); +} + +.weather-unit-toggle:focus-visible { + color: var(--link-hover-color); + outline: 2px solid currentColor; + outline-offset: 3px; +} + +.weather-unit-toggle:disabled { + color: #a1a1aa; + cursor: default; + opacity: 0.65; + transform: none; +} + +.weather-unit-toggle-icon { + font-size: 0.65em; + opacity: 0; + transform: translateX(-0.15rem) translateY(-0.02em); + transition: opacity 0.2s ease, transform 0.2s ease; +} + +.weather-unit-toggle:hover .weather-unit-toggle-icon, +.weather-unit-toggle:focus-visible .weather-unit-toggle-icon { + opacity: 0.72; + transform: translateX(0) translateY(-0.02em); +} + +@media (prefers-reduced-motion: reduce) { + button:not(:disabled):active { + scale: 1; + } + + .weather-unit-toggle { + transition: color 0.2s ease, border-color 0.2s ease, background-color 0.2s ease; + } + + .weather-unit-toggle-icon { + transition: opacity 0.2s ease; + transform: none; + } + + .weather-unit-toggle:hover, + .weather-unit-toggle:active { + transform: none; + } +} + .mag-chip-md { font-size: 14px; padding: 0.25rem 0.625rem; diff --git a/app/misc/page.tsx b/app/misc/page.tsx index 581070c..9f29d98 100644 --- a/app/misc/page.tsx +++ b/app/misc/page.tsx @@ -1,5 +1,7 @@ import type { Metadata } from "next"; +import { MUSIC_FOR_LIFE_BASE } from "@/lib/spotify-playlists"; import { HoverLinkArrow, HoverLinkDestinationHint } from "../components/hover-link-hint"; +import MagChip from "../components/mag-chip"; export const metadata: Metadata = { title: "Misc — Kai T. Chen", @@ -32,6 +34,8 @@ const THING_GROUPS = [ [ { name: "Alcove", href: "https://tryalcove.com/" }, { name: "Arc", href: "https://arc.net/" }, + { name: "Dropbox", href: "https://www.dropbox.com/" }, + { name: "Notion", href: "https://www.notion.com/" }, { name: "Raycast", href: "https://www.raycast.com/" }, { name: "Wispr Flow", href: "https://wisprflow.ai/" }, ], @@ -44,6 +48,8 @@ const THING_GROUPS = [ { name: "Claude Code", href: "https://claude.com/product/claude-code" }, { name: "Cursor", href: "https://cursor.com/" }, { name: "iTerm2", href: "https://iterm2.com/" }, + { name: "Syncthing", href: "https://syncthing.net/" }, + { name: "Tailscale", href: "https://tailscale.com/" }, ], ], }, @@ -69,6 +75,67 @@ const THING_GROUPS = [ ], ], }, + { + category: "Musicians", + rows: [ + [ + { name: "adieu", href: "https://www.adieu-web.com/" }, + { name: "CHPTRS", href: "https://wearechptrs.com/" }, + { name: "Fujii Kaze", href: "https://fujiikaze.com/" }, + { name: "FUKUSHIGE MARI", href: "https://fukushigemari.com/" }, + { name: "Hoshino Gen", href: "https://www.hoshinogen.com/en/" }, + { name: "Hitsujibungaku", href: "https://www.hitsujibungaku.info/" }, + { name: "Ikuta Lilas", href: "https://lilasikuta.jp/" }, + { name: "Lavt", href: "https://lavt.jp/" }, + { name: "Official HIGE DANdism", href: "https://higedan.com/?lang=en" }, + { name: "RADWIMPS", href: "https://radwimps.jp/en/" }, + { name: "Stefanie Sun", href: "https://www.instagram.com/stefsunyanzi/" }, + { name: "Suda Masaki", href: "https://sudamasaki-music.com/" }, + { name: "Sumino Hayato", href: "https://hayatosum.com/" }, + { name: "Taylor Swift", href: "https://www.taylorswift.com/" }, + { name: "TOMOO", href: "https://www.tomoo.jp/" }, + { name: "Vaundy", href: "https://vaundy.jp/?lang=en" }, + { name: "wacci", href: "https://www.instagram.com/wacci_official/?hl=en" }, + ], + ], + }, + { + category: "TV Shows", + rows: [ + [ + { + name: "Clarkson’s Farm", + href: "https://www.primevideo.com/detail/0SHGKA0J8D4G01ZGD647627NEJ?jic=8%7CEgRzdm9k&ref_=atv_dp_season_select_s1", + }, + { + name: "The Grand Tour", + href: "https://www.primevideo.com/detail/0PQETKSPCYRRYT0IFUIS9G1CQ2?ref_=atv_dp_season_select_s1", + }, + { name: "Top Gear", href: "https://www.topgear.com/" }, + ], + ], + }, +]; + +const countThingItems = (categories: string[]) => + categories.reduce( + (total, category) => + total + (THING_GROUPS.find((group) => group.category === category)?.rows.flat().length ?? 0), + 0, + ); + +const LEFT_THING_CATEGORIES = ["Productivity Tools", "Musicians"]; +const RIGHT_THING_CATEGORIES = ["Developer Tools", "TV Shows", "Content Creators"]; +const travelBelongsOnLeft = + countThingItems(LEFT_THING_CATEGORIES) < countThingItems(RIGHT_THING_CATEGORIES); + +const THING_LAYOUT = [ + travelBelongsOnLeft + ? ["Productivity Tools", "Travel Tools", "Musicians"] + : LEFT_THING_CATEGORIES, + travelBelongsOnLeft + ? RIGHT_THING_CATEGORIES + : ["Developer Tools", "Travel Tools", "TV Shows", "Content Creators"], ]; type AttentionItem = { @@ -116,6 +183,7 @@ const RESOURCES = [ href: "https://developer.apple.com/learn/", hintLabel: "Apple Developer", }, + { name: "Berkeleytime", href: "https://berkeleytime.com/" }, { name: "BlueDot Impact", href: "https://bluedot.org/" }, { name: "csdiy.wiki", href: "https://csdiy.wiki/en/", hintLabel: "csdiy" }, { @@ -123,11 +191,20 @@ const RESOURCES = [ href: "https://www.cs.usfca.edu/~galles/visualization/Algorithms.html", hintLabel: "USF", }, + { name: "Diddly Squat Farm Shop", href: "https://diddlysquatfarmshop.com/", hintLabel: "Diddly Squat" }, + { name: "Fanxinzhui", href: "https://www.fanxinzhui.com/" }, + { name: "Google Summer of Code", href: "https://summerofcode.withgoogle.com/", hintLabel: "Google" }, + { name: "GSoC Organizations", href: "https://www.gsocorganizations.dev/", hintLabel: "GSoC" }, { name: "How to Vim Like a Pro", href: "https://levelup.gitconnected.com/how-to-vim-like-a-pro-a-how-to-guide-for-maximum-productivity-c01773ee346a", hintLabel: "Level Up Coding", }, + { + name: "Jeremy Clarkson Collection", + href: "https://wodzudio.feishu.cn/drive/folder/WbLnfO2kwlRGeHdvWJncN42Bnwf", + hintLabel: "Feishu", + }, { name: "John DeNero", href: "https://www.youtube.com/@JohnDeNero" }, { name: "Mapbox", href: "https://www.mapbox.com/" }, { @@ -143,10 +220,12 @@ const RESOURCES = [ }, { name: "Thoughts For The Weekend", - href: "https://www.youtube.com/playlist?list=PLcs1ZorNr2uTGPZPZnBa408qLVHjbMTzT", + href: "https://www.youtube.com/playlist?list=PLcs1ZorNr2uTGPZPZnBa408qLVHjbMTzT&themeRefresh=1", }, { name: "TinyURL", href: "https://tinyurl.com/" }, { name: "Using Git", href: "https://cs61bl.org/resources/using-git/", hintLabel: "cs61bl" }, + { name: "Vercel", href: "https://vercel.com/home" }, + { name: "Vimeo", href: "https://vimeo.com/" }, ]; const REMEMBRANCE = [ @@ -155,6 +234,11 @@ const REMEMBRANCE = [ href: "https://en.wikipedia.org/wiki/Armenian_genocide", note: "Ottoman Empire · 1915 – 1917", }, + { + name: "Fang Lizhi: The Astrophysicist Who Stood Up for a Democratic China", + href: "https://www.hoover.org/research/fang-lizhi-astrophysicist-who-stood-democratic-china", + note: "Fang Lizhi · 1936 – 2012", + }, { name: "Nanjing Massacre", href: "https://en.wikipedia.org/wiki/Nanjing_Massacre", @@ -176,56 +260,83 @@ export default function Misc() {
- {/* Things I Love — nested cards (matches homepage Projects) */} -
+ {/* Things I Love — full width, two columns within the card */} +
Things I Love
-
- {THING_GROUPS.filter(({ rows }) => rows.some((row) => row.length > 0)).map( - ({ category, rows }) => ( -
- {/* Resources — full width */} + {/* Resources — full width, two columns within the card */}
Resources
-
+
{RESOURCES.map(({ name, href, hintLabel }) => (
@@ -254,7 +365,7 @@ export default function Misc() { href={href} target="_blank" rel="noopener noreferrer" - className="group block py-2 -mx-2 px-2 rounded-sm hover:bg-zinc-100 dark:hover:bg-zinc-700/40 transition-all duration-150 no-underline" + className="group block py-2 -mx-2 px-2 rounded-sm hover:bg-[#e8f0f4] dark:hover:bg-[#243640] transition-all duration-150 no-underline" style={{ textDecoration: "none" }} >
@@ -286,7 +397,7 @@ export default function Misc() { href={href} target="_blank" rel="noopener noreferrer" - className="group block py-2 -mx-2 px-2 rounded-sm hover:bg-zinc-100 dark:hover:bg-zinc-700/40 transition-all duration-150 no-underline" + className="group block py-2 -mx-2 px-2 rounded-sm hover:bg-[#e8f0f4] dark:hover:bg-[#243640] transition-all duration-150 no-underline" style={{ textDecoration: "none" }} >
diff --git a/app/music-for-life/[id]/page.tsx b/app/music-for-life/[id]/page.tsx index f24c1dc..047e9ed 100644 --- a/app/music-for-life/[id]/page.tsx +++ b/app/music-for-life/[id]/page.tsx @@ -76,7 +76,7 @@ export default async function PlaylistDetailPage({ params }: PageProps) { href={playlist.url} target="_blank" rel="noopener noreferrer" - className="group flex items-center justify-between gap-3 rounded-sm -mx-1 px-1 py-0.5 hover:bg-zinc-100 dark:hover:bg-zinc-700/40 transition-all duration-150 no-underline" + className="group flex items-center justify-between gap-3 rounded-sm -mx-1 px-1 py-0.5 hover:bg-[#e8f0f4] dark:hover:bg-[#243640] transition-all duration-150 no-underline" style={{ textDecoration: "none" }} >
diff --git a/app/music-for-life/page.tsx b/app/music-for-life/page.tsx index 310cd34..9157735 100644 --- a/app/music-for-life/page.tsx +++ b/app/music-for-life/page.tsx @@ -45,44 +45,47 @@ export default async function Playlists() {
- {playlists.length === 0 ? ( -

- Spotify isn't answering right now — check back in a bit. -

- ) : ( - playlists.map((playlist, i) => ( - + +
+
Spotify Playlists
+
+ {playlists.length === 0 ? ( +

+ Spotify isn't answering right now — check back in a bit. +

+ ) : ( + playlists.map((playlist) => ( -
+
{playlist.coverArt ? ( {`${playlist.name} ) : ( -
+
)}
-
+
@@ -90,7 +93,7 @@ export default async function Playlists() { style={{ fontFamily: "var(--font-nunito)", fontWeight: 600, - fontSize: 22, + fontSize: 18, fontStyle: "italic", }} className="text-zinc-800 dark:text-zinc-200 truncate group-hover:text-black dark:group-hover:text-white transition-colors duration-150" @@ -141,9 +144,11 @@ export default async function Playlists() {
- - )) - )} + )) + )} +
+
+
); } diff --git a/app/page.tsx b/app/page.tsx index 2b54f9e..35fae3c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,14 +1,10 @@ -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"; import { getBerkeleyWeather } from "./lib/weather"; -/** Keep homepage Personal Projects in sync with /projects (GitHub pinned repos). */ +/** Refresh the homepage weather snapshot. */ export const revalidate = 120; const PERSON_JSON_LD = { @@ -37,11 +33,7 @@ const PERSON_JSON_LD = { } as const; export default async function Home() { - const [projects, courseProjects, weather] = await Promise.all([ - getPersonalProjects(), - getCourseProjects(), - getBerkeleyWeather(), - ]); + const weather = await getBerkeleyWeather(); return (