diff --git a/src/islands/jambox/JamboxChannelsSearch.jsx b/src/islands/jambox/JamboxChannelsSearch.jsx index bf068bc..a3a7df2 100644 --- a/src/islands/jambox/JamboxChannelsSearch.jsx +++ b/src/islands/jambox/JamboxChannelsSearch.jsx @@ -7,6 +7,9 @@ export default function JamboxChannelsSearch() { const [loading, setLoading] = useState(false); const [err, setErr] = useState(""); + // ✅ koszyk kanałów + const [wanted, setWanted] = useState([]); // [{ name, logo_url, packages:[{id,name}] }] + const abortRef = useRef(null); useEffect(() => { @@ -31,13 +34,10 @@ export default function JamboxChannelsSearch() { params.set("q", qq); params.set("limit", "80"); - const res = await fetch( - `/api/jambox/jambox-channels-search?${params.toString()}`, - { - signal: ac.signal, - headers: { Accept: "application/json" }, - } - ); + const res = await fetch(`/api/jambox/jambox-channels-search?${params.toString()}`, { + signal: ac.signal, + headers: { Accept: "application/json" }, + }); const json = await res.json(); if (!res.ok || !json.ok) throw new Error(json?.error || "API_ERROR"); @@ -59,31 +59,182 @@ export default function JamboxChannelsSearch() { const meta = useMemo(() => { const qq = q.trim(); if (qq.length === 0) return ""; - // "Zacznij pisać, aby wyszukać" if (loading) return "Szukam…"; if (err) return err; return `Znaleziono: ${items.length}`; }, [q, loading, err, items]); -function scrollToPackage(packageName) { - const key = String(packageName || "").trim(); - if (!key) return; + 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; + 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); } - el.scrollIntoView({ behavior: "smooth", block: "start" }); - el.classList.add("is-target"); - window.setTimeout(() => el.classList.remove("is-target"), 5400); -} + // ========================== + // ✅ koszyk: dodaj/usuń kanał + // ========================== + 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 : [], + }, + ]; + }); + } + + function removeWantedByName(name) { + setWanted((prev) => + prev.filter((w) => String(w.name || "").toLowerCase() !== String(name || "").toLowerCase()) + ); + } + + function clearWanted() { + setWanted([]); + } + + // ========================================= + // ✅ pakiety, które zawierają WSZYSTKIE kanały + // ========================================= + const packageSuggestions = useMemo(() => { + if (!wanted.length) return { exact: [], ranked: [] }; + + // mapa pakietu -> { id,name,count } + const counts = new Map(); // key = packageName + for (const ch of wanted) { + 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()); + + // ✅ exact: pakiety z count == wanted.length (czyli zawierają wszystkie) + const exact = all + .filter((p) => p.count === wanted.length) + .sort((a, b) => a.name.localeCompare(b.name, "pl")); + + // ✅ ranked: najlepsze dopasowania gdy exact puste (malejąco count) + const ranked = all + .filter((p) => p.count < wanted.length) + .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name, "pl")) + .slice(0, 12); + + return { exact, ranked }; + }, [wanted]); return (

Wyszukiwanie kanałów w pakietach telewizji

+ {/* ✅ SEK CJA "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:
+ + {packageSuggestions.exact.length > 0 ? ( +
+ {packageSuggestions.exact.map((p) => ( + + ))} +
+ ) : ( + <> +
+ Nie ma jednego pakietu zawierającego wszystkie wybrane kanały. + Poniżej najlepsze dopasowania: +
+ +
+ {packageSuggestions.ranked.map((p) => ( + + ))} +
+ + )} +
+ + )} +
+ + {/* SEARCH */}
{meta}
+ {/* LIST */}
- {items.map((c) => ( -
- {/* kolumna 1 */} -
- {c.logo_url && ( - - )} + {items.map((c) => { + const selected = isWanted(c); -
{c.name}
-
+ return ( +
+ {/* kolumna 1 */} +
+ {c.logo_url && ( + + )} - {/* kolumna 2 */} -
-
—", - }} - /> +
{c.name}
- {Array.isArray(c.packages) && c.packages.length > 0 && ( -
- Dostępny w pakietach:  - {c.packages.map((p, i) => ( - - - - {/* {i < c.packages.length - 1 ? ", " : ""} */} - - ))} + {/* ✅ przycisk dodaj/usuń */} +
+ {!selected ? ( + + ) : ( + + )}
- )} +
+ + {/* kolumna 2 */} +
+
—" }} + /> + + {Array.isArray(c.packages) && c.packages.length > 0 && ( +
+ Dostępny w pakietach:  + {c.packages.map((p) => ( + + + + ))} +
+ )} +
-
- ))} + ); + })} {q.trim().length >= 1 && !loading && items.length === 0 && (
diff --git a/src/styles/channels-search.css b/src/styles/channels-search.css index ff42fbd..daa4135 100644 --- a/src/styles/channels-search.css +++ b/src/styles/channels-search.css @@ -131,4 +131,34 @@ .f-chsearch__input[type="search"] { appearance: none; - } \ No newline at end of file + } + + /* ----- */ + .f-chsearch__wanted { + @apply mb-6 p-4 rounded-2xl border border-[--f-input-border]; +} + +.f-chsearch__wanted-list { + @apply mt-3 flex flex-wrap gap-2; +} + +.f-chsearch__wanted-chip { + @apply inline-flex items-center gap-2 rounded-full border border-[--f-border-color] px-3 py-1.5; + background: rgba(148, 163, 184, 0.08); +} + +.f-chsearch__wanted-logo { + @apply w-6 h-6 rounded-full object-contain; +} + +.f-chsearch__wanted-name { + @apply text-sm font-medium; +} + +.f-chsearch__wanted-remove { + @apply text-sm opacity-70 hover:opacity-100; +} + +.f-chsearch__wanted-packages { + @apply mt-4; +}