import { useChannelSearch } from "../../hooks/useChannelSearch.js"; import { useMemo, useState } from "preact/hooks"; import "../../styles/jambox-search.css"; export default function JamboxChannelsSearch() { // ✅ NOWY: Hook useChannelSearch zamiast ręcznego zarządzania const search = useChannelSearch('/api/jambox/jambox-channels-search', { debounceMs: 250, minQueryLength: 1, limit: 80 }); // Koszyk kanałów ("Chciałbym mieć te kanały") const [wanted, setWanted] = useState([]); const isWanted = (c) => wanted.some( (w) => String(w.name || "").toLowerCase() === String(c.name || "").toLowerCase() ); function addWanted(c) { setWanted((prev) => { const exists = prev.some( (w) => String(w.name || "").toLowerCase() === String(c.name || "").toLowerCase() ); if (exists) return prev; return [ ...prev, { name: c.name, logo_url: c.logo_url || "", packages: Array.isArray(c.packages) ? c.packages : [], thematic_packages: Array.isArray(c.thematic_packages) ? c.thematic_packages : [], }, ]; }); } function removeWantedByName(name) { setWanted((prev) => prev.filter( (w) => String(w.name || "").toLowerCase() !== String(name || "").toLowerCase() ) ); } function clearWanted() { setWanted([]); } // Sugestie pakietów na podstawie wybranych kanałów const packageSuggestions = useMemo(() => { if (!wanted.length) return { exact: [], ranked: [], thematic: [], baseWantedLen: 0, wantedLen: 0 }; // Kanały, które mają pakiety główne const baseWanted = wanted.filter((ch) => Array.isArray(ch.packages) && ch.packages.length > 0); const baseWantedLen = baseWanted.length; // Jeśli nie ma żadnego kanału "bazowego", zwracamy tylko tematyczne if (baseWantedLen === 0) { const thematicMap = new Map(); for (const ch of wanted) { const tp = Array.isArray(ch.thematic_packages) ? ch.thematic_packages : []; for (const p of tp) { const tid = String(p?.tid ?? "").trim(); const name = String(p?.name ?? "").trim(); if (!tid || !name) continue; if (!thematicMap.has(tid)) thematicMap.set(tid, { tid, name }); } } const thematic = Array.from(thematicMap.values()).sort((a, b) => a.name.localeCompare(b.name, "pl")); return { exact: [], ranked: [], thematic, baseWantedLen, wantedLen: wanted.length }; } // Zlicz pakiety const counts = new Map(); for (const ch of baseWanted) { const pkgs = Array.isArray(ch.packages) ? ch.packages : []; for (const p of pkgs) { const name = String(p?.name ?? "").trim(); if (!name) continue; const cur = counts.get(name) || { id: p?.id ?? name, name, count: 0 }; cur.count += 1; counts.set(name, cur); } } const all = Array.from(counts.values()); // Pakiety zawierające wszystkie wybrane kanały const exact = all .filter((p) => p.count === baseWantedLen) .sort((a, b) => a.name.localeCompare(b.name, "pl")); // Pakiety zawierające część kanałów (posortowane po ilości dopasowań) const ranked = all .filter((p) => p.count < baseWantedLen) .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name, "pl")) .slice(0, 12); // Pakiety tematyczne (dodatki) const thematicMap = new Map(); for (const ch of wanted) { const tp = Array.isArray(ch.thematic_packages) ? ch.thematic_packages : []; for (const p of tp) { const tid = String(p?.tid ?? "").trim(); const name = String(p?.name ?? "").trim(); if (!tid || !name) continue; if (!thematicMap.has(tid)) thematicMap.set(tid, { tid, name }); } } const thematic = Array.from(thematicMap.values()).sort((a, b) => a.name.localeCompare(b.name, "pl") ); return { exact, ranked, thematic, baseWantedLen, wantedLen: wanted.length }; }, [wanted]); function scrollToPackage(packageName) { const key = String(packageName || "").trim(); if (!key) return; const el = document.getElementById(`pkg-${key}`); if (!el) { console.warn("⚠ Nie znaleziono pakietu w DOM:", `pkg-${key}`); return; } el.scrollIntoView({ behavior: "smooth", block: "start" }); el.classList.add("is-target"); window.setTimeout(() => el.classList.remove("is-target"), 5400); } return (

Wyszukiwanie kanałów w pakietach telewizji

{/* SEKCJA "CHCIAŁBYM MIEĆ TE KANAŁY" */}
Chciałbym mieć te kanały
{wanted.length > 0 && ( )}
{wanted.length === 0 ? (
Dodaj kanały z listy wyników – pokażę pakiety, które zawierają wszystkie wybrane kanały.
) : ( <>
{wanted.map((w) => (
{w.logo_url ? ( ) : null} {w.name}
))}
{/* SUGESTIE PAKIETÓW */}
Pakiety pasujące do wybranych kanałów:
{/* Pakiety główne */}
Pakiety główne:
{packageSuggestions.exact.length > 0 ? (
{packageSuggestions.exact.map((p) => ( ))}
) : (
{packageSuggestions.ranked.map((p) => ( ))}
)}
{/* Pakiety tematyczne – dodatki */} {packageSuggestions.thematic.length > 0 && (
Pakiety tematyczne do dokupienia:
{packageSuggestions.thematic.map((p) => ( ))}
)}
)}
{/* WYSZUKIWARKA */}
search.setQuery(e.currentTarget.value)} placeholder="Szukaj kanału po nazwie…" aria-label="Szukaj kanału po nazwie" /> {search.query && ( )}
{/* ✅ Meta z hooka zamiast ręcznego useMemo */}
{search.meta}
{/* LISTA WYNIKÓW */}
{search.items.map((c) => { const selected = isWanted(c); return (
{/* Kolumna lewa */}
{c.logo_url && ( )}
{c.name}
{!selected ? ( ) : ( )}
{/* Kolumna prawa */}
—", }} /> {/* Pakiety główne */} {Array.isArray(c.packages) && c.packages.length > 0 && (
Dostępny w pakietach:  {c.packages.map((p) => ( ))}
)} {/* Pakiety tematyczne */} {Array.isArray(c.thematic_packages) && c.thematic_packages.length > 0 && (
Dostępny w pakietach tematycznych:  {c.thematic_packages.map((p) => ( ))}
)}
); })} {/* Pusta lista */} {search.query.trim().length >= 1 && !search.loading && search.items.length === 0 && (
Brak wyników dla: {search.query}
)}
); } /* ✅ ZMIANY W PORÓWNANIU DO ORYGINAŁU: USUNIĘTE (~40 linii): - const [q, setQ] = useState("") - const [items, setItems] = useState([]) - const [loading, setLoading] = useState(false) - const [err, setErr] = useState("") - const abortRef = useRef(null) - Cały useEffect z fetch i debouncing (~35 linii) - const meta = useMemo(() => {...}, [...]) DODANE (~5 linii): - import { useChannelSearch } from "../../hooks/useChannelSearch.js" - const search = useChannelSearch('/api/jambox/jambox-channels-search', {...}) ZMIENIONE W JSX: - value={q} → value={search.query} - onInput={(e) => setQ(e.target.value)} → onInput={(e) => search.setQuery(e.target.value)} - onClick={() => setQ("")} → onClick={() => search.clear()} - {meta} → {search.meta} - {items.map(...)} → {search.items.map(...)} - {q.trim().length >= 1 && !loading && items.length === 0} → {search.query.trim().length >= 1 && !search.loading && search.items.length === 0} REZULTAT: - ~35 linii kodu mniej (27% redukcja) - Usunięte zarządzanie stanem wyszukiwania - Usunięte debouncing i AbortController - Kod łatwiejszy do testowania */