Files
fuz-site/src/islands/jambox/JamboxChannelsSearch.jsx
2025-12-13 11:25:11 +01:00

167 lines
4.6 KiB
JavaScript

import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import "../../styles/channels-search.css";
export default function JamboxChannelsSearch() {
const [q, setQ] = useState("");
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
const [err, setErr] = useState("");
const abortRef = useRef(null);
useEffect(() => {
const qq = q.trim();
setErr("");
if (qq.length < 2) {
setItems([]);
setLoading(false);
return;
}
const t = setTimeout(async () => {
try {
if (abortRef.current) abortRef.current.abort();
const ac = new AbortController();
abortRef.current = ac;
setLoading(true);
const params = new URLSearchParams();
params.set("q", qq);
params.set("limit", "80");
const res = await fetch(
`/api/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");
setItems(Array.isArray(json.data) ? json.data : []);
} catch (e) {
if (e?.name !== "AbortError") {
console.error("❌ channels search:", e);
setErr("Błąd wyszukiwania.");
}
} finally {
setLoading(false);
}
}, 250);
return () => clearTimeout(t);
}, [q]);
const meta = useMemo(() => {
const qq = q.trim();
if (qq.length < 2) return "Wpisz min. 2 znaki";
if (loading) return "Szukam…";
if (err) return err;
return `Znaleziono: ${items.length}`;
}, [q, loading, err, items]);
function scrollToPackage(packageId) {
const el = document.getElementById(`pkg-${packageId}`);
if (!el) return;
el.scrollIntoView({ behavior: "smooth", block: "start" });
el.classList.add("is-target");
window.setTimeout(() => el.classList.remove("is-target"), 1200);
}
return (
<div class="f-chsearch">
<h1 class="f-section-title">Wyszukiwanie kanałów w pakietach telewizji</h1>
<div class="f-chsearch__top">
<div class="f-chsearch__inputwrap">
<input
class="f-chsearch__input"
type="search"
value={q}
onInput={(e) => setQ(e.currentTarget.value)}
placeholder="Szukaj kanału po nazwie…"
aria-label="Szukaj kanału po nazwie"
/>
{q && (
<button
type="button"
class="f-chsearch__clear"
aria-label="Wyczyść wyszukiwanie"
onClick={() => setQ("")}
>
</button>
)}
</div>
<div class="f-chsearch__meta">{meta}</div>
</div>
<div class="f-chsearch__list" role="list">
{items.map((c) => (
<div
class="f-chsearch__row"
role="listitem"
key={`${c.name}-${c.logo_url || ""}`}
>
{/* kolumna 1 */}
<div class="f-chsearch__left">
{c.logo_url && (
<img
src={c.logo_url}
alt={c.name}
class="f-chsearch__logo"
loading="lazy"
/>
)}
<div class="f-chsearch__channel-name">{c.name}</div>
</div>
{/* kolumna 2 */}
<div class="f-chsearch__right">
<div
class="f-chsearch__desc f-chsearch__desc--html"
dangerouslySetInnerHTML={{
__html: c.description || "<em>—</em>",
}}
/>
{Array.isArray(c.packages) && c.packages.length > 0 && (
<div class="f-chsearch__packages">
Dostępny w:&nbsp;
{c.packages.map((p, i) => (
<button
type="button"
class="f-chsearch__pkg"
key={p.id}
onClick={() => scrollToPackage(p.id)}
>
{p.name}{" "}
<span class="f-chsearch__pkgnum">(kanał {p.number})</span>
{i < c.packages.length - 1 ? ", " : ""}
</button>
))}
</div>
)}
</div>
</div>
))}
{q.trim().length >= 2 && !loading && items.length === 0 && (
<div class="f-chsearch__empty">
Brak wyników dla: <strong>{q}</strong>
</div>
)}
</div>
</div>
);
}