"use client"; import * as React from "react"; import { motion } from "framer-motion"; import { CalendarDays, Compass, Sparkles, User } from "lucide-react"; import { api, bytes, type Discover as DiscoverData, type Recap } from "@/lib/api"; import { Skeletons } from "@/components/skeletons"; /** * Recommendations and a personal recap, both derived from muse's OWN request * history plus Deezer's related-artists graph. * * Deliberately not built on listening data: Navidrome owns play counts, * favourites and history, and a second source of truth for those would * disagree with the one the user actually listens through. */ export function DiscoverPanel({ onArtist }: { onArtist: (name: string) => void }) { const [d, setD] = React.useState(null); const [r, setR] = React.useState(null); React.useEffect(() => { api.discover().then(setD).catch(() => setD({ seeds: [], artists: [] })); api.recap().then(setR).catch(() => undefined); }, []); if (!d) return ; const empty = d.artists.length === 0 && (!r || r.tracks === 0); if (empty) return (

Nothing to go on yet

Recommendations are built from what you’ve actually added. Request a few tracks and this fills in.

); return (
{r && r.tracks > 0 && } {d.artists.length > 0 && (
} title="Because you added" sub={d.seeds.slice(0, 3).join(" · ")} />
{d.artists.map((a, i) => ( onArtist(a.name)} className="glass lift ring group cursor-pointer rounded-panel p-3 text-center" > {a.picture ? ( /* eslint-disable-next-line @next/next/no-img-element */ ) : (
)}
{a.name}
{a.fans ? (
{a.fans.toLocaleString()} fans
) : null}
))}
)}
); } function RecapPanel({ r }: { r: Recap }) { const maxDecade = Math.max(1, ...r.decades.map((d) => d.n)); return (
} title="Your year in muse" sub={`${r.tracks} tracks · ${bytes(r.bytes)}`} />
{r.artists.slice(0, 5).map((a, i) => ( ))} {r.genres.slice(0, 5).map((g, i) => ( ))} {r.genres.length === 0 && No genre data yet.} {r.decades.length === 0 && No release years yet.} {r.decades.map((d, i) => ( {d.decade}
{d.n}
))} {r.busiest_day && (
Busiest day {r.busiest_day.date} — {r.busiest_day.n} tracks
)}
); } function SectionHead({ icon, title, sub, }: { icon: React.ReactNode; title: string; sub?: string; }) { return (
{icon}

{title}

{sub && {sub}}
); } function Card({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
); } function Row({ i, label, n }: { i: number; label: string; n: number }) { return ( {i + 1} {label} {n} ); } function Muted({ children }: { children: React.ReactNode }) { return {children}; }