From 6d38676a1b1339dfd6a6bae949f12bbc806543b7 Mon Sep 17 00:00:00 2001 From: zoop Date: Wed, 9 Sep 2026 20:09:07 -0400 Subject: [PATCH] skeleton loader --- src/components/Clock.astro | 4 ++ src/components/Music.astro | 36 ++++++++++++++--- src/components/Reading.astro | 5 ++- src/components/Stats.astro | 27 +++++++++---- src/layouts/Base.astro | 8 ++++ src/pages/api/reading.json.ts | 5 ++- src/pages/api/spotify.json.ts | 19 +++++---- src/pages/api/stats.json.ts | 5 ++- src/pages/index.astro | 19 ++++++--- src/scripts/music.ts | 37 ++++++++++++++---- src/scripts/reading.ts | 24 ++++++++---- src/styles/global.css | 73 ++++++++++++++++++++++++++++++----- 12 files changed, 206 insertions(+), 56 deletions(-) diff --git a/src/components/Clock.astro b/src/components/Clock.astro index 0331c3a..ce2c81d 100644 --- a/src/components/Clock.astro +++ b/src/components/Clock.astro @@ -55,6 +55,10 @@ const zone = fmt({ timeZoneName: "short" }).find((p) => p.type === "timeZoneName letter-spacing: 0.1em; color: var(--faint); } + .clock__mer { + margin-left: auto; + padding-left: 0.8ch; + } diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index 0744001..0d214f7 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -37,6 +37,14 @@ const kaoCount = kaoMs > 0 ? `${Math.floor(kaoMs / 86_400_000)}d` : "in effect"; + { + path === "/" && ( + <> + + + + ) + } +

{site.name} — {site.tagline}

{site.bio}

@@ -39,14 +52,10 @@ import { site, elsewhere } from "../data/site";

more about me

-
+
-
-
-
-
diff --git a/src/scripts/music.ts b/src/scripts/music.ts index ca54bfa..305fce2 100644 --- a/src/scripts/music.ts +++ b/src/scripts/music.ts @@ -131,16 +131,22 @@ function mockData() { }; } -async function load(): Promise { +type MusicState = "ready" | "idle" | "off"; + +async function load(): Promise { let data: any; try { - const res = await fetch("/api/spotify.json", { headers: { accept: "application/json" } }); - data = await res.json(); + const mm = (window as any).__mm?.spotify as Promise | undefined; + data = mm + ? await mm + : await fetch("/api/spotify.json", { headers: { accept: "application/json" } }).then((r) => + r.json(), + ); } catch { - return; + return "idle"; } - if ((!data || !data.configured) && location.search.includes("mockmusic")) data = mockData(); - if (!data || !data.configured) return; + if (location.search.includes("mockmusic")) data = mockData(); + if (!data || !data.configured) return "off"; if (data.nowPlaying) renderNowPlaying(data.nowPlaying as NP); if (Array.isArray(data.top)) renderTop(data.top as Track[]); @@ -180,7 +186,24 @@ async function load(): Promise { if (sub) sub.textContent = "last 4 weeks"; } } + return "ready"; } + return "idle"; } -load(); +const host = document.getElementById("music"); +const apply = (state: MusicState) => { + host?.setAttribute("data-state", state); + if (state === "idle") { + const trigger = document.querySelector(".music__trigger"); + if (trigger) { + trigger.hidden = false; + trigger.disabled = true; + const label = document.getElementById("music-label"); + if (label) label.textContent = "not playing"; + } + } +}; +load() + .then(apply) + .catch(() => apply("idle")); diff --git a/src/scripts/reading.ts b/src/scripts/reading.ts index 3aafbc0..4f059c0 100644 --- a/src/scripts/reading.ts +++ b/src/scripts/reading.ts @@ -28,20 +28,24 @@ function bookCard(b: Book, mini = false): string { )}">${inner}${prog}${meta}`; } -async function load(): Promise { +async function load(): Promise { let data: any; try { - const res = await fetch("/api/reading.json", { headers: { accept: "application/json" } }); - data = await res.json(); + const mm = (window as any).__mm?.reading as Promise | undefined; + data = mm + ? await mm + : await fetch("/api/reading.json", { headers: { accept: "application/json" } }).then((r) => + r.json(), + ); } catch { - return; + return false; } - if (!data || !data.configured) return; + if (!data || !data.configured) return false; const current: Book[] = Array.isArray(data.current) ? data.current : []; const want: Book[] = Array.isArray(data.want) ? data.want : []; const recent: Book[] = Array.isArray(data.recent) ? data.recent : []; - if (!current.length && !want.length && !recent.length) return; + if (!current.length && !want.length && !recent.length) return false; if (current.length) { const block = $("reading-current"); @@ -75,7 +79,7 @@ async function load(): Promise { } const trigger = document.querySelector(".reading__trigger"); - if (!trigger) return; + if (!trigger) return false; trigger.hidden = false; const lead = current[0] || recent[0] || want[0]; @@ -107,6 +111,10 @@ async function load(): Promise { if (sub) sub.textContent = `${lead.title} · ${lead.author}`; } } + return true; } -load(); +const host = document.getElementById("books"); +load() + .then((shown) => host?.setAttribute("data-state", shown ? "ready" : "empty")) + .catch(() => host?.setAttribute("data-state", "empty")); diff --git a/src/styles/global.css b/src/styles/global.css index e95a49d..11b0ca9 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -436,18 +436,71 @@ img { letter-spacing: 0.05em; } -.foot { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 0.55rem 0.8rem; +.mm-grid { + width: min(30rem, 100%); + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.6rem; } -.foot > .music, -.foot > .reading { - margin-top: 0; +.mm-grid > * { + min-width: 0; } -.foot + .foot { - margin-top: 0.7rem; +.mm-grid > script { + display: none; +} +.mm-grid > *[data-state="empty"], +.mm-grid > *[data-state="off"] { + display: none; +} + +.music__trigger, +.reading__trigger, +.stats__trigger, +.clock { + width: 100%; +} + +.chip-skel { + display: none; + width: 100%; + height: 2.05rem; + border-radius: 8px; + background: var(--paper-2); + border: 1px solid var(--line); + position: relative; + overflow: hidden; +} +[data-state="loading"] > .chip-skel { + display: block; +} +[data-state="loading"] > .music__trigger, +[data-state="loading"] > .reading__trigger, +[data-state="loading"] > .stats__trigger { + display: none; +} +.chip-skel::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.045), + transparent + ); + transform: translateX(-100%); + animation: chip-skel 1.5s ease-in-out infinite; +} +@keyframes chip-skel { + to { + transform: translateX(100%); + } +} +@media (prefers-reduced-motion: reduce) { + .chip-skel::after { + animation: none; + opacity: 0.4; + } } .colophon {