more about me section

This commit is contained in:
zoop 2026-09-08 22:02:46 -04:00
parent 011c123f8f
commit f6daa5b349
No known key found for this signature in database
GPG Key ID: 15C0D336C9E08F3F
9 changed files with 487 additions and 64 deletions

View File

@ -15,3 +15,13 @@ SPOTIFY_REFRESH_TOKEN=
# currently-reading widget — personal token from https://hardcover.app/account/api
HARDCOVER_TOKEN=
# "the counter" — all-time pageviews + visitors from Umami. needs the website id
# plus EITHER an api key OR a share-url id. leave all blank to hide the block.
UMAMI_WEBSITE_ID=
# api base — default is Umami Cloud; self-hosted: https://<your-host>/api
UMAMI_URL=
# mode A: an api key (Umami Cloud account → api keys)
UMAMI_API_KEY=
# mode B (no key): enable "Share URL" on the website, put its id here
UMAMI_SHARE_ID=

View File

@ -1,62 +1,111 @@
---
const TZ = "America/New_York";
import { Icon } from "astro-icon/components";
const TZ = "America/New_York";
const now = new Date();
const parts = (opts: Intl.DateTimeFormatOptions) =>
const fmt = (opts: Intl.DateTimeFormatOptions) =>
new Intl.DateTimeFormat("en-US", { timeZone: TZ, ...opts }).formatToParts(now);
const hh = parts({ hour: "2-digit", hour12: false }).find((p) => p.type === "hour")?.value ?? "--";
const mm = parts({ minute: "2-digit" }).find((p) => p.type === "minute")?.value ?? "--";
const zone =
parts({ timeZoneName: "short" }).find((p) => p.type === "timeZoneName")?.value ?? "ET";
const t = fmt({ hour: "numeric", minute: "2-digit", hour12: true });
const hour = t.find((p) => p.type === "hour")?.value ?? "--";
const minute = t.find((p) => p.type === "minute")?.value ?? "--";
const mer = (t.find((p) => p.type === "dayPeriod")?.value ?? "").toUpperCase();
const zone = fmt({ timeZoneName: "short" }).find((p) => p.type === "timeZoneName")?.value ?? "ET";
---
<div class="clock" data-tz={TZ} role="img" aria-label={`zach's local time — ${hh}:${mm} ${zone}`}>
<span class="clock__scr">
<span class="clock__time"
>{hh}<span class="clock__sep">:</span>{mm}</span
<div
class="clock"
data-tz={TZ}
role="img"
aria-label={`zach's local time — ${hour}:${minute} ${mer} ${zone}`}
>
<span class="clock__zone" id="clock-zone">{zone}</span>
<Icon class="clock__ico" name="ph:clock" aria-hidden="true" />
<span class="clock__time" id="clock-time">
<span class="clock__grp">
{[...hour].map((d) => <span class="clock__digit">{d}</span>)}
</span>
<span class="clock__sep">:</span>
<span class="clock__grp">
{[...minute].map((d) => <span class="clock__digit">{d}</span>)}
</span>
</span>
<span class="clock__mer" id="clock-mer">{mer}</span>
<span class="clock__zone" id="clock-zone">{zone}</span>
</div>
<style>
.clock {
display: inline-flex;
align-items: center;
padding: 0.34rem 0.66rem;
background: #0e0d0b;
border: 1px solid var(--line);
border-radius: 7px;
box-shadow:
inset 0 1px 3px rgba(0, 0, 0, 0.65),
inset 0 0 0 1px rgba(255, 180, 84, 0.04);
}
.clock__scr {
display: inline-flex;
align-items: baseline;
gap: 0.6ch;
padding: 0.42rem 0.8rem;
background: var(--paper-2);
border: 1px solid var(--line);
border-radius: 8px;
font-family: var(--mono);
font-size: 0.95rem;
letter-spacing: 0.06em;
font-variant-numeric: tabular-nums;
letter-spacing: 0.14em;
}
.clock__ico {
width: 1em;
height: 1em;
flex: none;
color: var(--faint);
margin-right: 0.1ch;
}
.clock__time {
font-size: 0.9rem;
color: #ffb454;
text-shadow: 0 0 6px rgba(255, 180, 84, 0.5);
display: inline-flex;
align-items: center;
gap: 0.3ch;
}
.clock__grp {
display: inline-flex;
gap: 0.18ch;
}
.clock__digit {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.05em;
padding: 0.14em 0.1em 0.16em;
background: linear-gradient(#201e1b, #191613);
border: 1px solid #2c2926;
border-radius: 3px;
font-weight: 600;
color: var(--accent);
text-shadow: 0 0 6px rgba(255, 171, 94, 0.32);
}
.clock__digit::after {
content: "";
position: absolute;
left: 0;
right: 0;
top: 50%;
height: 1px;
background: rgba(0, 0, 0, 0.55);
transform: translateY(-0.5px);
}
.clock__sep {
color: var(--accent);
font-weight: 600;
text-shadow: 0 0 6px rgba(255, 171, 94, 0.32);
animation: clock-blink 1s steps(1, end) infinite;
}
@keyframes clock-blink {
50% {
opacity: 0.2;
opacity: 0.25;
}
}
.clock__mer,
.clock__zone {
font-size: 0.6rem;
letter-spacing: 0.18em;
color: #806b4f;
font-size: 0.72em;
letter-spacing: 0.14em;
color: var(--faint);
}
.clock__mer {
margin-left: 0.1ch;
}
@media (prefers-reduced-motion: reduce) {
.clock__sep {
@ -67,22 +116,31 @@ const zone =
<script>
const root = document.querySelector<HTMLElement>(".clock");
const timeEl = root?.querySelector<HTMLElement>(".clock__time");
const timeEl = document.getElementById("clock-time");
const merEl = document.getElementById("clock-mer");
const zoneEl = document.getElementById("clock-zone");
if (root && timeEl) {
const tz = root.dataset.tz || "America/New_York";
const timeFmt = new Intl.DateTimeFormat("en-US", {
timeZone: tz,
hour: "2-digit",
hour: "numeric",
minute: "2-digit",
hour12: false,
hour12: true,
});
const zoneFmt = new Intl.DateTimeFormat("en-US", { timeZone: tz, timeZoneName: "short" });
const tiles = (s: string) =>
`<span class="clock__grp">${[...s]
.map((d) => `<span class="clock__digit">${d}</span>`)
.join("")}</span>`;
const tick = () => {
const d = new Date();
const [h, m] = timeFmt.format(d).split(":");
timeEl.innerHTML = `${h}<span class="clock__sep">:</span>${m}`;
const z = zoneFmt.formatToParts(d).find((p) => p.type === "timeZoneName")?.value;
const p = timeFmt.formatToParts(d);
const h = p.find((x) => x.type === "hour")?.value ?? "--";
const m = p.find((x) => x.type === "minute")?.value ?? "--";
const mer = (p.find((x) => x.type === "dayPeriod")?.value ?? "").toUpperCase();
timeEl.innerHTML = `${tiles(h)}<span class="clock__sep">:</span>${tiles(m)}`;
if (merEl) merEl.textContent = mer;
const z = zoneFmt.formatToParts(d).find((x) => x.type === "timeZoneName")?.value;
if (zoneEl && z) zoneEl.textContent = z;
};
tick();

View File

@ -1,15 +1,57 @@
---
import { Icon } from "astro-icon/components";
const clip = (s: string, n = 22): string =>
s && s.length > n ? s.slice(0, n - 1).trimEnd() + "…" : (s ?? "");
let np: any = null;
let top: any[] = [];
let configured = false;
try {
const r = await fetch(new URL("/api/spotify.json", Astro.url), {
headers: { accept: "application/json" },
});
const d: any = await r.json();
configured = !!d?.configured;
np = d?.nowPlaying ?? null;
top = Array.isArray(d?.top) ? d.top : [];
} catch {
}
const hasData = configured && (!!np || top.length > 0);
let mLabel = "now playing";
let mSub = "";
let mArt = "";
let mLive = false;
let mPct: number | null = null;
if (np?.isPlaying) {
mLive = true;
mLabel = clip(np.title);
mSub = np.artist ?? "";
mArt = np.art ?? "";
if (np.progressMs != null && np.durationMs)
mPct = Math.min(100, (np.progressMs / np.durationMs) * 100);
} else if (np?.playedAt) {
mLabel = "last played";
mSub = `${clip(np.title, 26)} · ${np.artist}`;
mArt = np.art ?? "";
} else if (top.length) {
mLabel = "top tracks";
mSub = "last 4 weeks";
mArt = top[0]?.art ?? "";
}
const mTitleAttr = np ? `${np.title} — ${np.artist}` : undefined;
---
<div class="music" id="music">
<button
class="music__trigger"
class:list={["music__trigger", { "music__trigger--live": mLive }]}
type="button"
data-music-open
aria-haspopup="dialog"
data-umami-event="music-open"
hidden
title={mTitleAttr}
hidden={!hasData}
>
<img
class="music__art"
@ -18,15 +60,18 @@ import { Icon } from "astro-icon/components";
width="42"
height="42"
decoding="async"
hidden
src={mArt || undefined}
hidden={!mArt}
/>
<span class="music__eq" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
<span class="music__main">
<span class="music__label" id="music-label">now playing</span>
<span class="music__sub" id="music-sub" aria-hidden="true"></span>
<span class="music__label" id="music-label">{mLabel}</span>
<span class="music__sub" id="music-sub" aria-hidden="true">{mSub}</span>
</span>
<span class="music__arw" aria-hidden="true">&#8594;</span>
<span class="music__bar" id="music-bar" aria-hidden="true" hidden><i></i></span>
<span class="music__bar" id="music-bar" aria-hidden="true" hidden={mPct == null}>
<i style={mPct == null ? undefined : `--p:${mPct}%`}></i>
</span>
</button>
<dialog class="music-wall" data-music-dialog aria-label="music">
@ -105,7 +150,7 @@ import { Icon } from "astro-icon/components";
.music__trigger:focus-visible {
border-color: var(--faint);
color: var(--ink);
padding-bottom: 0.45rem;
padding-bottom: 1.15rem;
}
.music__trigger:focus-visible {
outline: 2px solid var(--accent);
@ -148,21 +193,26 @@ import { Icon } from "astro-icon/components";
min-width: 0;
}
.music__sub {
position: absolute;
left: 0.35rem;
right: 0.6rem;
bottom: 0.26rem;
font-size: 0.66rem;
color: var(--faint);
max-height: 0;
opacity: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
opacity: 0;
transform: translateY(3px);
pointer-events: none;
transition:
max-height 0.22s ease,
opacity 0.18s ease;
opacity 0.18s ease,
transform 0.2s ease;
}
.music__trigger:hover .music__sub,
.music__trigger:focus-visible .music__sub {
max-height: 1.4em;
opacity: 1;
transform: none;
}
.music__bar {
position: absolute;

View File

@ -1,5 +1,47 @@
---
interface RBook {
title: string;
author: string;
cover: string | null;
url: string;
progress: number | null;
}
let current: RBook[] = [];
let want: RBook[] = [];
let recent: RBook[] = [];
try {
const r = await fetch(new URL("/api/reading.json", Astro.url), {
headers: { accept: "application/json" },
});
const d: any = await r.json();
if (d?.configured) {
current = Array.isArray(d.current) ? d.current : [];
want = Array.isArray(d.want) ? d.want : [];
recent = Array.isArray(d.recent) ? d.recent : [];
}
} catch {
}
const hasData = current.length > 0 || want.length > 0 || recent.length > 0;
const lead: RBook | null = current[0] ?? recent[0] ?? want[0] ?? null;
let rLabel = "reading";
let rSub = "";
if (lead) {
if (current.length) {
rLabel = lead.title;
rSub = lead.author;
} else if (recent.length) {
rLabel = "last read";
rSub = `${lead.title} · ${lead.author}`;
} else {
rLabel = "want to read";
rSub = `${lead.title} · ${lead.author}`;
}
}
const rCover = lead?.cover ?? "";
const rPct = current.length && lead?.progress != null ? lead.progress : null;
---
<div class="reading" id="books">
@ -9,7 +51,7 @@
data-reading-open
aria-haspopup="dialog"
data-umami-event="reading-open"
hidden
hidden={!hasData}
>
<img
class="reading__cover"
@ -18,15 +60,23 @@
width="28"
height="40"
decoding="async"
hidden
src={rCover || undefined}
hidden={!rCover}
/>
<span class="reading__ico" aria-hidden="true">&#9634;</span>
<span class="reading__ico" aria-hidden="true" hidden={!!rCover}>&#9634;</span>
<span class="reading__main">
<span class="reading__label" id="reading-label">reading</span>
<span class="reading__sub" id="reading-sub" aria-hidden="true"></span>
<span class="reading__label" id="reading-label">{rLabel}</span>
<span class="reading__sub" id="reading-sub" aria-hidden="true">{rSub}</span>
</span>
<span class="reading__arw" aria-hidden="true">&#8594;</span>
<span class="reading__bar" id="reading-bar" aria-hidden="true" hidden><i></i></span>
<span
class="reading__bar"
id="reading-bar"
aria-hidden="true"
hidden={rPct == null}
>
<i style={rPct == null ? undefined : `--p:${rPct}%`}></i>
</span>
</button>
<dialog class="reading-wall" data-reading-dialog aria-label="reading">
@ -92,7 +142,7 @@
.reading__trigger:focus-visible {
border-color: var(--faint);
color: var(--ink);
padding-bottom: 0.45rem;
padding-bottom: 1.15rem;
}
.reading__trigger:focus-visible {
outline: 2px solid var(--accent);
@ -131,21 +181,26 @@
white-space: nowrap;
}
.reading__sub {
position: absolute;
left: 0.4rem;
right: 0.6rem;
bottom: 0.26rem;
font-size: 0.66rem;
color: var(--faint);
max-height: 0;
opacity: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
opacity: 0;
transform: translateY(3px);
pointer-events: none;
transition:
max-height 0.22s ease,
opacity 0.18s ease;
opacity 0.18s ease,
transform 0.2s ease;
}
.reading__trigger:hover .reading__sub,
.reading__trigger:focus-visible .reading__sub {
max-height: 1.4em;
opacity: 1;
transform: none;
}
.reading__bar {
position: absolute;

171
src/components/Stats.astro Normal file
View File

@ -0,0 +1,171 @@
---
import { Icon } from "astro-icon/components";
const fmt = (n: number) => new Intl.NumberFormat("en-US").format(n);
let pv = 0;
let vis = 0;
let ok = false;
try {
const r = await fetch(new URL("/api/stats.json", Astro.url), {
headers: { accept: "application/json" },
});
const d: any = await r.json();
if (d?.configured && (d.pageviews || d.visitors)) {
pv = Math.max(0, Math.round(Number(d.pageviews) || 0));
vis = Math.max(0, Math.round(Number(d.visitors) || 0));
ok = true;
}
} catch {
}
const width = Math.max(6, String(pv).length);
const digits = ok ? String(pv).padStart(width, "0") : "";
let lead = true;
const cells = [...digits].map((c, i) => {
if (c !== "0" || i === digits.length - 1) lead = false;
return { c, lead };
});
---
<div class="stats" id="stats" hidden={!ok}>
<Icon class="stats__ico" name="ph:eye" aria-hidden="true" />
<span class="stats__reel" id="stats-reel">
{cells.map((x) => <span class:list={["stats__d", { "stats__d--lead": x.lead }]}>{x.c}</span>)}
</span>
<span class="stats__unit">views</span>
<span class="stats__sub" id="stats-sub" aria-hidden="true">{ok ? `${fmt(vis)} visitors` : ""}</span>
</div>
<style>
.stats {
position: relative;
display: inline-flex;
align-items: center;
gap: 0.6ch;
padding: 0.3rem 0.62rem;
background: var(--paper-2);
border: 1px solid var(--line);
border-radius: 7px;
overflow: hidden;
font-family: var(--mono);
transition:
padding 0.22s ease,
border-color 0.18s ease;
}
.stats:hover,
.stats:focus-within {
padding-bottom: 1rem;
border-color: var(--faint);
}
.stats__ico {
width: 0.95em;
height: 0.95em;
flex: none;
color: var(--faint);
}
.stats__reel {
display: inline-flex;
gap: 0.16ch;
font-variant-numeric: tabular-nums;
}
.stats__d {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 0.92em;
padding: 0.09em 0.06em 0.11em;
background: linear-gradient(#201e1b, #191613);
border: 1px solid #2c2926;
border-radius: 2px;
font-size: 0.82rem;
font-weight: 600;
color: var(--accent);
text-shadow: 0 0 4px rgba(255, 171, 94, 0.3);
}
.stats__d::after {
content: "";
position: absolute;
left: 0;
right: 0;
top: 50%;
height: 1px;
background: rgba(0, 0, 0, 0.55);
transform: translateY(-0.5px);
}
.stats__d--lead {
color: var(--faint);
text-shadow: none;
}
.stats__unit {
font-size: 0.66rem;
letter-spacing: 0.04em;
color: var(--faint);
}
.stats__sub {
position: absolute;
left: 0.62rem;
right: 0.62rem;
bottom: 0.22rem;
font-size: 0.62rem;
letter-spacing: 0.03em;
color: var(--faint);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
opacity: 0;
transform: translateY(3px);
pointer-events: none;
transition:
opacity 0.18s ease,
transform 0.2s ease;
}
.stats:hover .stats__sub,
.stats:focus-within .stats__sub {
opacity: 1;
transform: none;
}
</style>
<script>
const fmt = (n: number) => new Intl.NumberFormat("en-US").format(n);
async function load(): Promise<void> {
let d: any;
try {
const r = await fetch("/api/stats.json", { headers: { accept: "application/json" } });
d = await r.json();
} catch {
return;
}
if (
(!d || !d.configured || (!d.pageviews && !d.visitors)) &&
location.search.includes("mockstats")
)
d = { configured: true, pageviews: 128473, visitors: 41902 };
if (!d || !d.configured || (!d.pageviews && !d.visitors)) return;
const block = document.getElementById("stats");
const reel = document.getElementById("stats-reel");
const sub = document.getElementById("stats-sub");
if (!block || !reel) return;
const pv = Math.max(0, Math.round(Number(d.pageviews) || 0));
const vis = Math.max(0, Math.round(Number(d.visitors) || 0));
const width = Math.max(6, String(pv).length);
const digits = String(pv).padStart(width, "0");
let lead = true;
reel.innerHTML = [...digits]
.map((c, i) => {
if (c !== "0" || i === digits.length - 1) lead = false;
return `<span class="stats__d${lead ? " stats__d--lead" : ""}">${c}</span>`;
})
.join("");
if (sub) sub.textContent = `${fmt(vis)} visitors`;
block.hidden = false;
}
load();
</script>

View File

@ -3,7 +3,7 @@ export const site = {
name: "zach",
wordmark: "ZACH",
domain: "zachy.cc",
version: "v3.0.1",
version: "v3.1.2",
tagline: "i make cool stuff",
description:
"i make cool things sometimes.",

View File

@ -0,0 +1,71 @@
import type { APIRoute } from "astro";
import { env as cfEnv } from "cloudflare:workers";
export const prerender = false;
type Env = Record<string, string | undefined>;
function getEnv(): Env {
const nodeEnv = typeof process !== "undefined" ? process.env : {};
let workerEnv: Env = {};
try {
workerEnv = cfEnv as unknown as Env;
} catch {
}
return { ...nodeEnv, ...workerEnv };
}
const num = (v: any): number =>
typeof v === "number" ? v : typeof v?.value === "number" ? v.value : 0;
export const GET: APIRoute = async () => {
const e = getEnv();
const base = (e.UMAMI_URL || "https://api.umami.is/v1").replace(/\/+$/, "");
const id = e.UMAMI_WEBSITE_ID;
const key = e.UMAMI_API_KEY;
const shareId = e.UMAMI_SHARE_ID;
const json = (data: unknown, status = 200) =>
new Response(JSON.stringify(data), {
status,
headers: { "content-type": "application/json", "cache-control": "public, max-age=300" },
});
const hidden = { configured: false, pageviews: 0, visitors: 0 };
if (!(shareId || (id && key))) return json(hidden);
const headers: Record<string, string> = { accept: "application/json" };
let websiteId = id;
try {
if (key) {
headers["x-umami-api-key"] = key;
} else if (shareId) {
const origin = base.replace(/\/v1$/, "").replace(/\/api$/, "");
const r = await fetch(`${origin}/api/share/${shareId}`, {
headers: { accept: "application/json" },
});
const s: any = await r.json();
if (s?.token) {
headers["x-umami-share-token"] = s.token;
headers["x-umami-share-context"] = "1";
}
if (s?.websiteId) websiteId = s.websiteId;
}
if (!websiteId) return json({ ...hidden, configured: true });
const url = `${base}/websites/${websiteId}/stats?startAt=0&endAt=${Date.now()}`;
const res = await fetch(url, { headers });
const data: any = await res.json();
const pageviews = num(data?.pageviews);
const visitors = num(data?.visitors);
if (!pageviews && !visitors) return json({ ...hidden, configured: true });
return json({ configured: true, pageviews, visitors });
} catch {
return json({ ...hidden, configured: true });
}
};

View File

@ -3,6 +3,7 @@ import Base from "../layouts/Base.astro";
import Music from "../components/Music.astro";
import Reading from "../components/Reading.astro";
import Clock from "../components/Clock.astro";
import Stats from "../components/Stats.astro";
import Webrings from "../components/Webrings.astro";
import Buttons from "../components/Buttons.astro";
import { site, elsewhere } from "../data/site";
@ -42,6 +43,9 @@ import { site, elsewhere } from "../data/site";
<Music />
<Reading />
</div>
<div class="foot">
<Stats />
</div>
<div class="foot foot--clock">
<Clock />
</div>

View File

@ -30,6 +30,8 @@ const mmss = (ms: number): string => {
const $ = (id: string) => document.getElementById(id);
const esc = (s: string): string =>
s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]!);
const clamp = (s: string, n = 22): string =>
s.length > n ? s.slice(0, n - 1).trimEnd() + "…" : s;
function renderNowPlaying(np: NP): void {
const section = $("np-section");
@ -162,7 +164,8 @@ async function load(): Promise<void> {
if (label) {
if (np?.isPlaying) {
trigger.classList.add("music__trigger--live");
label.textContent = np.title;
label.textContent = clamp(np.title);
trigger.title = `${np.title}${np.artist}`;
if (sub) sub.textContent = np.artist;
if (bar && fill && np.progressMs != null && np.durationMs) {
fill.style.setProperty("--p", `${Math.min(100, (np.progressMs / np.durationMs) * 100)}%`);
@ -170,7 +173,8 @@ async function load(): Promise<void> {
}
} else if (np?.playedAt) {
label.textContent = "last played";
if (sub) sub.textContent = `${np.title} · ${np.artist}`;
trigger.title = `${np.title}${np.artist}`;
if (sub) sub.textContent = `${clamp(np.title, 26)} · ${np.artist}`;
} else {
label.textContent = "top tracks";
if (sub) sub.textContent = "last 4 weeks";