feat: add view transitions, smooth scroll, bundle analyzer, SSE radio stream, and AnimatePresence animations
Local Build and Deploy / deploy (push) Successful in 1m24s
Local Build and Deploy / deploy (push) Successful in 1m24s
This commit is contained in:
1 parent
57c3fe601a
commit
f53d25e0a5
11 files changed
+519
-229
No files matched your search
@@ -9,12 +9,9 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
* radio via a hidden <audio> element, with play/pause, a volume slider, the
|
||||
* current DJ / now-playing text, and a live listener count.
|
||||
*
|
||||
* It is a CLIENT component and must NOT import prisma / siteSettings — all data
|
||||
* comes from the public JSON API:
|
||||
* - GET /api/radio/config → { radio_enabled, radio_stream_url, radio_name, … }
|
||||
* - GET /api/radio/now-playing → provider passthrough ({ nowPlaying } | AzureCast | { song, artist } …)
|
||||
* - GET /api/radio/listeners → { listeners: number | null }
|
||||
* These are polled every ~15s; the interval is cleared on unmount.
|
||||
* It is a CLIENT component and must NOT import prisma / siteSettings.
|
||||
* Config is fetched once on mount; now-playing + listeners come via SSE
|
||||
* (/api/radio/stream) for real-time updates.
|
||||
*
|
||||
* If the radio is disabled or no stream URL is configured, it renders nothing.
|
||||
* The server-side <RadioPlayerGate> already guards on the same settings so the
|
||||
@@ -22,8 +19,6 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
* for the runtime config refresh.
|
||||
*/
|
||||
|
||||
const POLL_MS = 15000;
|
||||
|
||||
type RadioConfig = {
|
||||
enabled: boolean;
|
||||
streamUrl: string;
|
||||
@@ -140,6 +135,8 @@ function parseListeners(data: unknown): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
import { useEventSource } from "@/lib/use-event-source";
|
||||
|
||||
export default function RadioPlayer() {
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
|
||||
@@ -151,42 +148,31 @@ export default function RadioPlayer() {
|
||||
const [listeners, setListeners] = useState<number | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Poll config + now-playing + listeners every POLL_MS; clear on unmount.
|
||||
// Subscribe to SSE stream for real-time now-playing + listeners.
|
||||
const { data: streamData } = useEventSource<{
|
||||
nowPlaying: unknown;
|
||||
listeners: number | null;
|
||||
}>("/api/radio/stream", { enabled: true });
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function getJson(url: string): Promise<unknown | null> {
|
||||
try {
|
||||
const res = await fetch(url, { cache: "no-store" });
|
||||
if (!res.ok) return null;
|
||||
return await res.json();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!streamData) return;
|
||||
if (streamData.nowPlaying != null) {
|
||||
setNowPlaying(parseNowPlaying(streamData.nowPlaying));
|
||||
setDj(parseDj(streamData.nowPlaying));
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const [cfg, np, lst] = await Promise.all([
|
||||
getJson("/api/radio/config"),
|
||||
getJson("/api/radio/now-playing"),
|
||||
getJson("/api/radio/listeners"),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
|
||||
if (cfg !== null) setConfig(parseConfig(cfg));
|
||||
if (np !== null) {
|
||||
setNowPlaying(parseNowPlaying(np));
|
||||
setDj(parseDj(np));
|
||||
}
|
||||
if (lst !== null) setListeners(parseListeners(lst));
|
||||
if (streamData.listeners != null) {
|
||||
setListeners(parseListeners({ listeners: streamData.listeners }));
|
||||
}
|
||||
}, [streamData]);
|
||||
|
||||
void refresh();
|
||||
const id = setInterval(() => void refresh(), POLL_MS);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearInterval(id);
|
||||
};
|
||||
// Fetch config once on mount (rarely changes).
|
||||
useEffect(() => {
|
||||
fetch("/api/radio/config", { cache: "no-store" })
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((data) => {
|
||||
if (data !== null) setConfig(parseConfig(data));
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Keep the <audio> volume in sync with the slider.
|
||||
|
||||
Reference in new issue
Block a user