"use client"; // Per-kind renderers for a source's payload. Each takes the (already ok) payload // and renders a compact card body. Unknown kinds fall back to JSON. import type { WeatherPayload } from "@/lib/schemas/sources/weather"; import type { NewsPayload } from "@/lib/schemas/sources/news"; import type { BriefsPayload } from "@/lib/schemas/sources/briefs"; import type { OnThisDayPayload } from "@/lib/schemas/sources/onthisday"; import type { ObsidianPayload } from "@/lib/schemas/sources/obsidian"; import type { GitHubPayload } from "@/lib/schemas/sources/github"; import type { VercelPayload } from "@/lib/schemas/sources/vercel"; import type { MastodonPayload } from "@/lib/schemas/sources/mastodon"; import type { BlueskyPayload } from "@/lib/schemas/sources/bluesky"; import type { AqiPayload } from "@/lib/schemas/sources/aqi"; import type { MarketsPayload } from "@/lib/schemas/sources/markets"; import type { HackerNewsPayload } from "@/lib/schemas/sources/hackernews"; import type { UptimePayload } from "@/lib/schemas/sources/uptime"; import type { CalendarPayload } from "@/lib/schemas/sources/calendar"; import type { SportsPayload } from "@/lib/schemas/sources/sports"; import type { LinksPayload } from "@/lib/schemas/sources/links"; import type { CiderPayload } from "@/lib/schemas/sources/cider"; import type { SourceKind } from "@/lib/schemas/source"; const muted = { color: "var(--text-muted)" }; const faint = { color: "var(--text-faint)" }; function dayName(iso: string) { return new Date(iso + "T00:00:00").toLocaleDateString(undefined, { weekday: "short" }); } function fmtTime(iso: string) { return new Date(iso).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" }); } function aqiColor(v: number | null): string { if (v == null) return "inherit"; if (v <= 50) return "var(--green)"; if (v <= 100) return "var(--yellow)"; if (v <= 150) return "var(--orange)"; if (v <= 200) return "var(--red)"; return "var(--purple-400)"; } function uvLabel(v: number): string { if (v <= 2) return "Low"; if (v <= 5) return "Moderate"; if (v <= 7) return "High"; if (v <= 10) return "Very High"; return "Extreme"; } function WeatherBody({ p, row }: { p: WeatherPayload; row: boolean }) { const deg = p.units === "metric" ? "°C" : "°F"; const wind = p.units === "metric" ? "km/h" : "mph"; const showFeels = p.current.feelsLike != null && Math.abs(p.current.feelsLike - p.current.temp) >= 2; const highPollen = p.pollen.filter((pl) => pl.level !== "Low"); const current = (
{p.alerts.map((a, i) => (
⚠ {a.event}
))}
{p.current.temp} {deg} {p.current.text} {showFeels && ( feels {p.current.feelsLike} {deg} )}

{p.location} · {p.current.wind} {wind} {p.current.humidity != null ? ` · ${p.current.humidity}% RH` : ""} {p.current.uvIndex != null ? ` · UV ${p.current.uvIndex} ${uvLabel(p.current.uvIndex)}` : ""} {p.aqi != null && ( {" "}· AQI {p.aqi} {p.aqiCategory} )}

{(p.sunrise || p.sunset || p.moonPhase) && (

{p.sunrise ? `↑ ${fmtTime(p.sunrise)}` : ""} {p.sunset ? ` · ↓ ${fmtTime(p.sunset)}` : ""} {p.moonPhase ? ` · ${p.moonEmoji} ${p.moonPhase}` : ""}

)} {highPollen.length > 0 && (

Pollen: {highPollen.map((pl) => `${pl.label} ${pl.level}`).join(" · ")}

)}
); const forecast = (
{p.daily.map((d) => (
{dayName(d.date)}
{d.max}°
{d.min}°
{d.precipProb != null && d.precipProb > 0 && (
{d.precipProb}%
)}
))}
); // Short/wide tiles: current condition and forecast sit side by side. return row ? (
{current} {forecast}
) : (
{current}
{forecast}
); } function NewsBody({ p }: { p: NewsPayload }) { return ( ); } function BriefsBody({ p }: { p: BriefsPayload }) { const live = p.categories.filter((c) => c.stories.length > 0); if (live.length === 0) return

No briefs yet.

; return (
{live.map((c) => (

{c.name}

))}
); } function Entries({ list }: { list: OnThisDayPayload["events"] }) { return ( ); } function OnThisDayBody({ p }: { p: OnThisDayPayload }) { return (
{p.births.length > 0 && (

Born

)} {p.deaths.length > 0 && (

Died

)}
); } function ObsidianBody({ p }: { p: ObsidianPayload }) { return ( ); } function shortTime(iso: string) { return new Date(iso).toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit", }); } function GitHubBody({ p }: { p: GitHubPayload }) { return (

@{p.login} · {p.publicRepos} repos · {p.followers} followers

); } function VercelBody({ p }: { p: VercelPayload }) { return ( ); } function SocialBody({ items }: { items: Array<{ who: string; text: string; tag: string; link?: string | null }> }) { if (items.length === 0) return

Nothing recent.

; return ( ); } function calEventLabel(iso: string, allDay: boolean): string { const d = new Date(iso); const now = new Date(); const todayStr = now.toDateString(); const isToday = d.toDateString() === todayStr; const datePart = isToday ? "Today" : d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" }); if (allDay) return datePart; const timePart = d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" }); return `${datePart} ${timePart}`; } function CalendarBody({ p }: { p: CalendarPayload }) { const cutoff = Date.now() + 7 * 24 * 60 * 60 * 1000; const events = p.events.filter((e) => new Date(e.start).getTime() <= cutoff); if (events.length === 0) return

No upcoming events.

; return ( ); } function leagueName(path: string) { return (path.split("/").pop() ?? path).toUpperCase(); } type Game = SportsPayload["games"][number]; function gameBucket(g: Game): "live" | "today" | "upcoming" { if (g.state === "in" || g.state === "post") return "live"; if (g.startTime) { const d = new Date(g.startTime); return d.toDateString() === new Date().toDateString() ? "today" : "upcoming"; } return "today"; } function gameLabel(g: Game): { text: string; isLive: boolean; isFinal: boolean } { const isLive = g.state === "in"; const isFinal = g.state === "post"; if (isLive || isFinal) { const score = g.awayScore != null && g.homeScore != null ? `${g.awayScore}–${g.homeScore}` : "—"; const suffix = isFinal ? (g.status.includes("OT") ? " F/OT" : " F") : ` ${g.status}`; return { text: score + suffix, isLive, isFinal }; } if (g.startTime) { const d = new Date(g.startTime); const isToday = d.toDateString() === new Date().toDateString(); const datePart = isToday ? "" : d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" }) + " "; return { text: datePart + d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" }), isLive, isFinal }; } return { text: g.status, isLive, isFinal }; } const BUCKET_LABELS = { live: "Live / Final", today: "Today", upcoming: "Upcoming" } as const; function SportsSection({ label, games }: { label: string; games: Game[] }) { const groups: Record = {}; for (const g of games) (groups[g.league] ||= []).push(g); return (

{label}

{Object.entries(groups).map(([league, gs]) => (
{leagueName(league)}
    {gs.map((g, i) => { const { text, isLive } = gameLabel(g); return (
  • {text} {g.away} @ {g.home}
  • ); })}
))}
); } function SportsBody({ p }: { p: SportsPayload }) { const cutoff = Date.now() + 7 * 24 * 60 * 60 * 1000; const games = p.games.filter((g) => !g.startTime || new Date(g.startTime).getTime() <= cutoff); if (games.length === 0) return

No games.

; const buckets: Record<"live" | "today" | "upcoming", Game[]> = { live: [], today: [], upcoming: [] }; for (const g of games) buckets[gameBucket(g)].push(g); const sections = (["live", "today", "upcoming"] as const).filter((k) => buckets[k].length > 0); return (
{sections.map((k) => ( ))}
); } function LinksBody({ p }: { p: LinksPayload }) { if (p.links.length === 0) return

No links yet.

; return (
{p.links.map((l, i) => ( {l.label} ))}
); } function AqiBody({ p }: { p: AqiPayload }) { return (
{p.aqi ?? "—"} {p.category}

{p.location} {p.pm25 != null ? ` · PM2.5 ${p.pm25}` : ""} {p.pm10 != null ? ` · PM10 ${p.pm10}` : ""} {p.ozone != null ? ` · O₃ ${Math.round(p.ozone)}` : ""}

{p.pollen.length > 0 && ( )}
); } function MarketsBody({ p }: { p: MarketsPayload }) { if (p.quotes.length === 0) return

No quotes.

; return ( ); } function HackerNewsBody({ p }: { p: HackerNewsPayload }) { if (p.stories.length === 0) return

Nothing yet.

; return ( ); } function UptimeBody({ p }: { p: UptimePayload }) { if (p.sites.length === 0) return

No URLs configured.

; return ( ); } function CiderBody({ p }: { p: CiderPayload }) { if (!p.playing || !p.track) { return

Nothing playing.

; } const { name, artist, album, artworkUrl, durationMs, currentMs } = p.track; const pct = durationMs > 0 ? Math.min(100, (currentMs / durationMs) * 100) : 0; function fmtDuration(ms: number) { const s = Math.floor(ms / 1000); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; } async function skipNext() { await fetch("/api/cider/next", { method: "POST" }); } return (
{artworkUrl && ( // eslint-disable-next-line @next/next/no-img-element {album} )}

{name}

{artist}

{album}

{fmtDuration(currentMs)} {fmtDuration(durationMs)}
); } export function CardBody({ kind, payload, rows, }: { kind: SourceKind; payload: unknown; rows: number; }) { const shortTile = rows <= 1; switch (kind) { case "weather": return ; case "news": case "feeds": return ; case "briefs": return ; case "onthisday": return ; case "obsidian": return ; case "github": return ; case "vercel": return ; case "mastodon": return ( ({ who: n.account, text: n.text, tag: n.type, link: n.url, }))} /> ); case "bluesky": return ( ({ who: n.author, text: n.text, tag: n.reason, }))} /> ); case "aqi": return ; case "markets": return ; case "hackernews": return ; case "uptime": return ; case "calendar": return ; case "sports": return ; case "links": return ; case "cider": return ; default: return (
          {JSON.stringify(payload, null, 2)}
        
); } }