187 lines
8.1 KiB
TypeScript
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>
|
|
);
|
|
}
|