From 3de74612458967a1912311223f60ee1fb121429e Mon Sep 17 00:00:00 2001 From: Jonathan Date: Sun, 12 Jul 2026 00:44:11 +0200 Subject: [PATCH] feat(discovery): /discover feed page + seed search UI + nav link Co-Authored-By: Claude Opus 4.8 (1M context) --- web/src/app/discover/discover-client.tsx | 143 +++++++++++++++++++++++ web/src/app/discover/page.tsx | 14 +++ web/src/app/page.tsx | 2 +- 3 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 web/src/app/discover/discover-client.tsx create mode 100644 web/src/app/discover/page.tsx diff --git a/web/src/app/discover/discover-client.tsx b/web/src/app/discover/discover-client.tsx new file mode 100644 index 0000000..b9cbbf3 --- /dev/null +++ b/web/src/app/discover/discover-client.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; + +type Suggestion = { + id: string; + artistMbid: string; + artistName: string; + rgMbid: string | null; + album: string | null; + score: number; + seedCount: number; + sources: string[]; +}; + +type Feed = { artists: Suggestion[]; albums: Suggestion[] }; +type SearchResult = { + seed: { mbid: string; name: string } | null; + similar: { mbid: string; name: string; score: number }[]; +}; + +export function DiscoverClient() { + const [feed, setFeed] = useState({ artists: [], albums: [] }); + const [result, setResult] = useState(null); + const [running, setRunning] = useState(false); + const [query, setQuery] = useState(""); + const [search, setSearch] = useState(null); + const [busy, setBusy] = useState(false); + + const loadFeed = useCallback(async () => { + setFeed(await (await fetch("/api/discover")).json()); + }, []); + + useEffect(() => { + loadFeed(); + fetch("/api/discover/run") + .then((r) => r.json()) + .then((s: { result: string | null; requested: boolean }) => { + setResult(s.result); + setRunning(s.requested); + }); + }, [loadFeed]); + + useEffect(() => { + if (!running) return; + const id = setInterval(async () => { + const s = await (await fetch("/api/discover/run")).json(); + setResult(s.result); + if (!s.requested) { + setRunning(false); + loadFeed(); + } + }, 2000); + return () => clearInterval(id); + }, [running, loadFeed]); + + async function discoverNow() { + await fetch("/api/discover/run", { method: "POST" }); + setRunning(true); + } + + async function runSearch(e: React.FormEvent) { + e.preventDefault(); + if (!query.trim()) return; + setBusy(true); + try { + setSearch(await (await fetch(`/api/discover/search?artist=${encodeURIComponent(query.trim())}`)).json()); + } finally { + setBusy(false); + } + } + + async function act(id: string, action: "follow" | "want" | "dismiss") { + await fetch(`/api/discover/${id}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ action }), + }); + loadFeed(); + } + + return ( +
+
+ + {result && Last run: {result}} +
+ +
+

Seed search

+
+ setQuery(e.target.value)} + placeholder="Artist name…" + aria-label="Seed artist" + /> + +
+ {search && !search.seed &&

No MusicBrainz match.

} + {search?.seed && ( +
    + {search.similar.map((s) => ( +
  • + {s.name} ({s.score.toFixed(1)}) +
  • + ))} +
+ )} +
+ +
+

Suggested artists ({feed.artists.length})

+
    + {feed.artists.map((s) => ( +
  • + {s.artistName}{" "} + score {s.score.toFixed(2)} · {s.seedCount} seed(s) · {s.sources.join(", ")}{" "} + {" "} + +
  • + ))} +
+
+ +
+

Suggested albums ({feed.albums.length})

+
    + {feed.albums.map((s) => ( +
  • + {s.album} — {s.artistName}{" "} + score {s.score.toFixed(2)}{" "} + {" "} + +
  • + ))} +
+
+
+ ); +} diff --git a/web/src/app/discover/page.tsx b/web/src/app/discover/page.tsx new file mode 100644 index 0000000..6990e86 --- /dev/null +++ b/web/src/app/discover/page.tsx @@ -0,0 +1,14 @@ +import { DiscoverClient } from "./discover-client"; + +export default function DiscoverPage() { + return ( +
+

Discover

+

+ Home · Artists · Wanted ·{" "} + Settings +

+ +
+ ); +} diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 7cc5ef1..96e4e34 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -4,7 +4,7 @@ export default function Home() { return (

Lyra

-

Artists · Wanted · Settings

+

Artists · Discover · Wanted · Settings

);