diff --git a/frontend/src/components/HistoryCard.astro b/frontend/src/components/HistoryCard.astro new file mode 100644 index 0000000..1432658 --- /dev/null +++ b/frontend/src/components/HistoryCard.astro @@ -0,0 +1,12 @@ +--- +import { HistoryList } from './HistoryList'; +interface Props { station: string; } +const { station } = Astro.props; +--- +
+
+
再生履歴
+
// recently played
+
+ +
diff --git a/frontend/src/components/HistoryList.tsx b/frontend/src/components/HistoryList.tsx new file mode 100644 index 0000000..0b84a65 --- /dev/null +++ b/frontend/src/components/HistoryList.tsx @@ -0,0 +1,48 @@ +import { useEffect, useState } from 'react'; +import type { HistoryEntry } from '@lib/types'; +import { fmtRelative } from '@lib/format'; + +interface Props { + station: string; +} + +export function HistoryList({ station }: Props) { + const [entries, setEntries] = useState(null); + const [now, setNow] = useState(() => new Date()); + + useEffect(() => { + const ctrl = new AbortController(); + const load = async () => { + try { + const r = await fetch(`/now-playing/${station}.history.json`, { signal: ctrl.signal }); + if (!r.ok) { setEntries([]); return; } + const arr = await r.json() as HistoryEntry[]; + setEntries(Array.isArray(arr) ? arr : []); + } catch (err) { + if ((err as Error).name !== 'AbortError') setEntries([]); + } + }; + void load(); + const id = setInterval(load, 15_000); + const tick = setInterval(() => setNow(new Date()), 30_000); + return () => { ctrl.abort(); clearInterval(id); clearInterval(tick); }; + }, [station]); + + if (entries === null) return
loading…
; + if (!entries.length) return
no recent tracks
; + + return ( +
+ {entries.slice(0, 10).map((e, i) => { + const start = new Date(parseInt(e.started_at, 10) * 1000); + return ( +
+ {fmtRelative(start, now)} + {e.title || '—'} + {e.artist || '—'} +
+ ); + })} +
+ ); +} diff --git a/frontend/src/styles/components/history-card.css b/frontend/src/styles/components/history-card.css new file mode 100644 index 0000000..999115d --- /dev/null +++ b/frontend/src/styles/components/history-card.css @@ -0,0 +1,28 @@ +.full-secthead { margin-bottom: 14px; } +.full-secthead-jp { font-family: var(--f-pixel); font-size: 18px; color: var(--lemon); letter-spacing: 4px; } +.full-secthead-en { font-family: var(--f-pixel); font-size: 22px; color: var(--cream); letter-spacing: 1px; margin-top: 2px; } + +.card { + background: var(--plum); + border: 2px solid var(--ink); + padding: 22px 24px; + position: relative; +} +.card::before { + content: ""; position: absolute; inset: 4px; border: 1px dashed #ff3ea522; pointer-events: none; +} + +.history-list { display: flex; flex-direction: column; } +.history-row { + display: grid; grid-template-columns: 50px 1fr auto; + gap: 12px; padding: 8px 4px; + border-bottom: 1px dashed #ff3ea522; + font-size: 13px; +} +.history-row:last-child { border-bottom: none; } +.history-row .t { font-family: var(--f-mono); color: var(--cyan); } +.history-row .title { font-family: var(--f-pixel); font-size: 14px; color: var(--cream); } +.history-row .artist { font-family: var(--f-mono); color: #fff4e8aa; font-size: 12px; } +.history-empty { + font-family: var(--f-mono); font-size: 13px; color: #fff4e8aa; padding: 8px 4px; +}