From c97374c9cbd6f2b987e08cdd7b796134361ffd6a Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Fri, 18 Sep 2026 20:00:20 +0200 Subject: [PATCH] Add Dutch TV logo picker from tv-logo/tv-logos for live channels. Admin can search NL logos like EPG and store the jsDelivr CDN URL. --- apps/admin-ui/src/app/live-lists/page.tsx | 120 ++++++++++++++++++++ apps/master-api/src/admin/routes.ts | 5 + apps/master-api/src/viewer/live-channels.ts | 76 +++++++++++++ 3 files changed, 201 insertions(+) diff --git a/apps/admin-ui/src/app/live-lists/page.tsx b/apps/admin-ui/src/app/live-lists/page.tsx index 613e9b4..5cd1bb8 100644 --- a/apps/admin-ui/src/app/live-lists/page.tsx +++ b/apps/admin-ui/src/app/live-lists/page.tsx @@ -38,6 +38,7 @@ type LiveListDetail = { }; type EpgOption = { id: string; name: string }; +type LogoOption = { id: string; name: string; url: string }; type ChannelForm = { name: string; @@ -80,6 +81,8 @@ export default function LiveListsPage() { const [detail, setDetail] = useState(null); const [epgOptions, setEpgOptions] = useState([]); const [epgFilter, setEpgFilter] = useState(""); + const [logoOptions, setLogoOptions] = useState([]); + const [logoFilter, setLogoFilter] = useState(""); const [newListName, setNewListName] = useState(""); const [listName, setListName] = useState(""); const [listEnabled, setListEnabled] = useState(true); @@ -111,6 +114,10 @@ export default function LiveListsPage() { .then((r) => r.json()) .then((d) => setEpgOptions(d.channels ?? [])) .catch(() => undefined); + fetch("/api/v1/admin/live-lists/logo-options", { credentials: "include" }) + .then((r) => r.json()) + .then((d) => setLogoOptions(d.logos ?? [])) + .catch(() => undefined); }, [loadLists]); useEffect(() => { @@ -136,6 +143,24 @@ export default function LiveListsPage() { .slice(0, 200); }, [epgOptions, epgFilter]); + const filteredLogos = useMemo(() => { + const q = logoFilter.trim().toLowerCase(); + if (!q) return logoOptions.slice(0, 80); + return logoOptions + .filter( + (o) => + o.name.toLowerCase().includes(q) || + o.id.toLowerCase().includes(q) || + o.url.toLowerCase().includes(q) + ) + .slice(0, 80); + }, [logoOptions, logoFilter]); + + function pickLogo(logo: LogoOption) { + setChannelForm((f) => ({ ...f, logoUrl: logo.url })); + setLogoFilter(logo.name); + } + async function createList(e: FormEvent) { e.preventDefault(); const name = newListName.trim(); @@ -211,11 +236,16 @@ export default function LiveListsPage() { function startCreateChannel() { setEditingChannelId("new"); setChannelForm(emptyChannelForm()); + setLogoFilter(""); + setEpgFilter(""); } function startEditChannel(c: LiveChannelRow) { setEditingChannelId(c.id); setChannelForm(formFromChannel(c)); + const match = logoOptions.find((o) => o.url === c.logoUrl); + setLogoFilter(match?.name ?? ""); + setEpgFilter(""); } async function saveChannel(e: FormEvent) { @@ -446,14 +476,104 @@ export default function LiveListsPage() { autoComplete="off" /> + + + {filteredLogos.map((o) => ( + + ))} + + {filteredLogos.length > 0 && ( +
+ {filteredLogos.slice(0, 24).map((o) => ( + + ))} +
+ )} + {channelForm.logoUrl.trim() && ( +

+ {/* eslint-disable-next-line @next/next/no-img-element */} + Logo preview + Preview +

+ )}