Files
streamrip-ui/frontend/app/discover/page.tsx
T

187 lines
8.1 KiB
TypeScript

"use client";
import Image from "next/image";
import { useEffect, useState } from "react";
import DownloadDialog, { type SearchItem } from "../DownloadDialog";
import BasketButton from "../components/BasketButton";
type Genre = { id: number; name: string; cover?: string | null };
const KINDS = [
{ key: "new", label: "Neuheiten" },
{ key: "tracks", label: "Top-Titel" },
{ key: "albums", label: "Top-Alben" },
{ key: "artists", label: "Top-Artists" },
{ key: "playlists", label: "Playlists" },
] as const;
type Kind = (typeof KINDS)[number]["key"];
export default function DiscoverPage() {
const [genres, setGenres] = useState<Genre[]>([]);
const [genreId, setGenreId] = useState(0);
const [kind, setKind] = useState<Kind>("new");
const [results, setResults] = useState<SearchItem[]>([]);
const [radios, setRadios] = useState<{ id: number; title: string; cover?: string | null }[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [selected, setSelected] = useState<SearchItem | null>(null);
const [radioMsg, setRadioMsg] = useState("");
const [nonce, setNonce] = useState(0);
const [openRadio, setOpenRadio] = useState<number | null>(null);
const [radioTracks, setRadioTracks] = useState<SearchItem[]>([]);
const [radioLoading, setRadioLoading] = useState(false);
useEffect(() => {
fetch("/api/discover/genres").then((r) => r.json()).then((b) => setGenres(b.genres ?? [])).catch(() => {});
}, []);
useEffect(() => {
setLoading(true);
setError("");
fetch(`/api/discover?kind=${kind}&genre_id=${genreId}&limit=24`)
.then((r) => {
if (!r.ok) throw new Error(`Fehler (${r.status})`);
return r.json();
})
.then((b) => setResults(b.results ?? []))
.catch((e: unknown) => setError(e instanceof Error ? e.message : "Fehler."))
.finally(() => setLoading(false));
fetch(`/api/discover/radios?genre_id=${genreId}`)
.then((r) => r.json())
.then((b) => setRadios(b.radios ?? []))
.catch(() => {});
}, [kind, genreId, nonce]);
async function toggleRadio(radioId: number) {
if (openRadio === radioId) {
setOpenRadio(null);
setRadioTracks([]);
return;
}
setOpenRadio(radioId);
setRadioTracks([]);
setRadioMsg("");
setRadioLoading(true);
try {
const t = await fetch(`/api/discover/radios/${radioId}/tracks?limit=25`).then((r) => {
if (!r.ok) throw new Error(`Fehler (${r.status})`);
return r.json();
});
setRadioTracks(t.tracks ?? []);
} catch (e: unknown) {
setRadioMsg(e instanceof Error ? e.message : "Fehler.");
} finally {
setRadioLoading(false);
}
}
return (
<div>
<h1 style={{ fontSize: 36, margin: "8px 0 4px", letterSpacing: "-0.02em" }}>Entdecken</h1>
<p className="muted" style={{ marginTop: 0 }}>Neuheiten und Charts nach Genre — direkt laden oder in den Korb legen.</p>
<div style={{ display: "flex", gap: 8, margin: "16px 0", flexWrap: "wrap" }}>
{KINDS.map((k) => (
<button key={k.key} onClick={() => setKind(k.key)} className={kind === k.key ? "btn btn-primary" : "btn"}>
{k.label}
</button>
))}
</div>
<div style={{ display: "flex", gap: 8, marginBottom: 20, flexWrap: "wrap" }}>
<button onClick={() => setGenreId(0)} className={genreId === 0 ? "btn btn-primary" : "btn"}>
Alle Genres
</button>
{genres.map((g) => (
<button key={g.id} onClick={() => setGenreId(g.id)} className={genreId === g.id ? "btn btn-primary" : "btn"}>
{g.name}
</button>
))}
</div>
{error && <p className="error">{error}</p>}
{loading && <p className="muted">Lädt …</p>}
{!loading && results.length === 0 && !error && (
<div>
<p className="muted">Derzeit keine Daten von Deezer für diese Auswahl.</p>
<button onClick={() => setNonce((n) => n + 1)} className="btn">
Erneut versuchen
</button>
</div>
)}
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))", gap: 14 }}>
{results.map((item) => {
const href =
item.media_type === "artist" ? `/artist/${item.id}` :
item.media_type === "album" ? `/album/${item.id}` :
item.media_type === "playlist" ? `/playlist/${item.id}` : null;
const cover = item.cover ? (
<Image src={item.cover} alt={item.title} width={300} height={300} style={{ width: "100%", height: "auto", borderRadius: 6 }} />
) : (
<div style={{ width: "100%", aspectRatio: "1", background: "var(--surface-2)", borderRadius: 6 }} />
);
return (
<div key={`${item.media_type}-${item.id}`} className="card">
{href ? <a href={href} title="Details ansehen">{cover}</a> : cover}
<div style={{ fontWeight: 600, marginTop: 8 }}>{item.title}</div>
<div className="muted" style={{ fontSize: 13 }}>{item.artist}</div>
{item.preview_url && <audio controls preload="none" src={item.preview_url} style={{ width: "100%", marginTop: 8 }} />}
<button onClick={() => setSelected(item)} className="btn" style={{ marginTop: 8, width: "100%" }}>
Download
</button>
<div style={{ marginTop: 8 }}>
<BasketButton item={{ source: "deezer", media_type: item.media_type, id: item.id, title: item.title, artist: item.artist }} />
</div>
</div>
);
})}
</div>
{selected && <DownloadDialog item={selected} onClose={() => setSelected(null)} />}
{radios.length > 0 && (
<section style={{ marginTop: 28 }}>
<h2 style={{ fontSize: 24 }}>Sender {genreId !== 0 ? "in diesem Genre" : ""}</h2>
{radioMsg && <p>{radioMsg}</p>}
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(160px, 1fr))", gap: 12 }}>
{radios.map((radio) => (
<div key={radio.id} className="card">
{radio.cover ? (
<Image src={radio.cover} alt={radio.title} width={200} height={200} style={{ width: "100%", height: "auto", borderRadius: 6 }} />
) : (
<div style={{ width: "100%", aspectRatio: "1", background: "var(--surface-2)", borderRadius: 6 }} />
)}
<div style={{ fontWeight: 600, marginTop: 6, fontSize: 14 }}>{radio.title}</div>
<button onClick={() => toggleRadio(radio.id)} className="btn" style={{ marginTop: 8, width: "100%" }}>
{openRadio === radio.id ? "Schließen" : "Lieder ansehen"}
</button>
</div>
))}
</div>
{openRadio !== null && (
<div className="card" style={{ marginTop: 12, padding: 12 }}>
{radioLoading && <p className="muted">Lädt …</p>}
{!radioLoading && radioTracks.length === 0 && <p className="muted">Keine Tracks gefunden.</p>}
<ol style={{ listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 8 }}>
{radioTracks.map((t) => (
<li key={t.id} style={{ display: "flex", alignItems: "center", gap: 12 }}>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 600 }}>{t.title}</div>
<div className="muted" style={{ fontSize: 13 }}>{t.artist}</div>
</div>
{t.preview_url && <audio controls preload="none" src={t.preview_url} style={{ width: 180, height: 32 }} />}
<BasketButton item={{ source: "deezer", media_type: "track", id: t.id, title: t.title, artist: t.artist }} />
<button onClick={() => setSelected(t)} className="btn" style={{ whiteSpace: "nowrap" }}>
Download
</button>
</li>
))}
</ol>
</div>
)}
</section>
)}
</div>
);
}