Refaktoryzacja Card, Modali
This commit is contained in:
@@ -1,90 +1,18 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { useChannelSearch } from "../../hooks/useChannelSearch.js";
|
||||
import { useMemo, useState } from "preact/hooks";
|
||||
import "../../styles/jambox-search.css";
|
||||
|
||||
export default function JamboxChannelsSearch() {
|
||||
const [q, setQ] = useState("");
|
||||
const [items, setItems] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [err, setErr] = useState("");
|
||||
// ✅ 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
|
||||
const [wanted, setWanted] = useState([]); // [{ name, logo_url, packages:[{id,name}], thematic_packages:[{tid,name}] }]
|
||||
// Koszyk kanałów ("Chciałbym mieć te kanały")
|
||||
const [wanted, setWanted] = useState([]);
|
||||
|
||||
const abortRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const qq = q.trim();
|
||||
setErr("");
|
||||
|
||||
if (qq.length === 0) {
|
||||
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/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("jambox-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 === 0) return "";
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// ✅ koszyk: dodaj/usuń kanał
|
||||
// ==========================
|
||||
const isWanted = (c) =>
|
||||
wanted.some(
|
||||
(w) =>
|
||||
@@ -126,17 +54,16 @@ export default function JamboxChannelsSearch() {
|
||||
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 (tylko te liczymy w dopasowaniu "głównych")
|
||||
// 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;
|
||||
|
||||
// ======= GŁÓWNE =======
|
||||
// jeśli nie ma żadnego kanału "bazowego", nie ma co liczyć dopasowania bazowych
|
||||
// Jeśli nie ma żadnego kanału "bazowego", zwracamy tylko tematyczne
|
||||
if (baseWantedLen === 0) {
|
||||
// nadal zwracamy tematyczne
|
||||
const thematicMap = new Map();
|
||||
for (const ch of wanted) {
|
||||
const tp = Array.isArray(ch.thematic_packages) ? ch.thematic_packages : [];
|
||||
@@ -152,7 +79,8 @@ export default function JamboxChannelsSearch() {
|
||||
return { exact: [], ranked: [], thematic, baseWantedLen, wantedLen: wanted.length };
|
||||
}
|
||||
|
||||
const counts = new Map(); // key = packageName
|
||||
// Zlicz pakiety
|
||||
const counts = new Map();
|
||||
for (const ch of baseWanted) {
|
||||
const pkgs = Array.isArray(ch.packages) ? ch.packages : [];
|
||||
for (const p of pkgs) {
|
||||
@@ -166,17 +94,19 @@ export default function JamboxChannelsSearch() {
|
||||
|
||||
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);
|
||||
|
||||
// ======= TEMATYCZNE (dodatki) =======
|
||||
const thematicMap = new Map(); // key = tid
|
||||
// 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) {
|
||||
@@ -194,12 +124,26 @@ export default function JamboxChannelsSearch() {
|
||||
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 (
|
||||
<div class="f-chsearch">
|
||||
<h1 class="f-section-title">Wyszukiwanie kanałów w pakietach telewizji</h1>
|
||||
|
||||
{/* ✅ SEKCJA "CHCIAŁBYM MIEĆ TE KANAŁY" */}
|
||||
{/* SEKCJA "CHCIAŁBYM MIEĆ TE KANAŁY" */}
|
||||
<div class="f-chsearch__wanted">
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div class="text-lg font-semibold">Chciałbym mieć te kanały</div>
|
||||
@@ -213,7 +157,7 @@ export default function JamboxChannelsSearch() {
|
||||
|
||||
{wanted.length === 0 ? (
|
||||
<div class="opacity-80 mt-2">
|
||||
Dodaj kanały z listy wyników — pokażę pakiety, które zawierają
|
||||
Dodaj kanały z listy wyników – pokażę pakiety, które zawierają
|
||||
wszystkie wybrane kanały.
|
||||
</div>
|
||||
) : (
|
||||
@@ -242,13 +186,13 @@ export default function JamboxChannelsSearch() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ✅ SUGESTIE PAKIETÓW */}
|
||||
{/* SUGESTIE PAKIETÓW */}
|
||||
<div class="f-chsearch__wanted-packages">
|
||||
<div class="font-semibold">
|
||||
Pakiety pasujące do wybranych kanałów:
|
||||
</div>
|
||||
|
||||
{/* ======= GŁÓWNE (jak było) ======= */}
|
||||
{/* Pakiety główne */}
|
||||
<div class="mt-2">
|
||||
<div class="opacity-80">Pakiety główne:</div>
|
||||
|
||||
@@ -283,7 +227,7 @@ export default function JamboxChannelsSearch() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ======= TEMATYCZNE — dodatki (bez liczenia) ======= */}
|
||||
{/* Pakiety tematyczne – dodatki */}
|
||||
{packageSuggestions.thematic.length > 0 && (
|
||||
<div class="mt-4">
|
||||
<div class="opacity-80">
|
||||
@@ -294,10 +238,10 @@ export default function JamboxChannelsSearch() {
|
||||
{packageSuggestions.thematic.map((p) => (
|
||||
<button
|
||||
type="button"
|
||||
key={p.tid}
|
||||
class="f-chsearch-pkg"
|
||||
onClick={() => window.open(
|
||||
// `/internet-telewizja/pakiety-tematyczne#tid-${encodeURIComponent(p.tid)}`,
|
||||
`/premium/${p.tid}`,
|
||||
`/premium/${p.tid}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer"
|
||||
)}
|
||||
@@ -314,37 +258,38 @@ export default function JamboxChannelsSearch() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* SEARCH */}
|
||||
{/* WYSZUKIWARKA */}
|
||||
<div class="f-chsearch__top">
|
||||
<div class="f-chsearch__inputwrap">
|
||||
<input
|
||||
id="channel-search"
|
||||
class="f-chsearch__input"
|
||||
type="search"
|
||||
value={q}
|
||||
onInput={(e) => setQ(e.currentTarget.value)}
|
||||
value={search.query}
|
||||
onInput={(e) => search.setQuery(e.currentTarget.value)}
|
||||
placeholder="Szukaj kanału po nazwie…"
|
||||
aria-label="Szukaj kanału po nazwie"
|
||||
/>
|
||||
|
||||
{q && (
|
||||
{search.query && (
|
||||
<button
|
||||
type="button"
|
||||
class="f-chsearch__clear"
|
||||
aria-label="Wyczyść wyszukiwanie"
|
||||
onClick={() => setQ("")}
|
||||
onClick={() => search.clear()}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="f-chsearch-meta">{meta}</div>
|
||||
{/* ✅ Meta z hooka zamiast ręcznego useMemo */}
|
||||
<div class="f-chsearch-meta">{search.meta}</div>
|
||||
</div>
|
||||
|
||||
{/* LIST */}
|
||||
{/* LISTA WYNIKÓW */}
|
||||
<div class="f-chsearch__list" role="list">
|
||||
{items.map((c) => {
|
||||
{search.items.map((c) => {
|
||||
const selected = isWanted(c);
|
||||
|
||||
return (
|
||||
@@ -353,7 +298,7 @@ export default function JamboxChannelsSearch() {
|
||||
role="listitem"
|
||||
key={`${c.name}-${c.logo_url || ""}`}
|
||||
>
|
||||
{/* kolumna 1 */}
|
||||
{/* Kolumna lewa */}
|
||||
<div class="f-chsearch__left">
|
||||
{c.logo_url && (
|
||||
<img
|
||||
@@ -366,7 +311,6 @@ export default function JamboxChannelsSearch() {
|
||||
|
||||
<div class="f-chsearch__channel-name">{c.name}</div>
|
||||
|
||||
{/* ✅ przycisk dodaj/usuń */}
|
||||
<div class="mt-2">
|
||||
{!selected ? (
|
||||
<button
|
||||
@@ -374,7 +318,7 @@ export default function JamboxChannelsSearch() {
|
||||
class="btn btn-outline"
|
||||
onClick={() => addWanted(c)}
|
||||
>
|
||||
Dodaj do “Chciałbym mieć”
|
||||
Dodaj do "Chciałbym mieć"
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
@@ -388,7 +332,7 @@ export default function JamboxChannelsSearch() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* kolumna 2 */}
|
||||
{/* Kolumna prawa */}
|
||||
<div class="f-chsearch__right">
|
||||
<div
|
||||
class="f-chsearch__desc f-chsearch__desc--html"
|
||||
@@ -397,6 +341,7 @@ export default function JamboxChannelsSearch() {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Pakiety główne */}
|
||||
{Array.isArray(c.packages) && c.packages.length > 0 && (
|
||||
<div class="f-chsearch__packages">
|
||||
Dostępny w pakietach:
|
||||
@@ -415,6 +360,7 @@ export default function JamboxChannelsSearch() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pakiety tematyczne */}
|
||||
{Array.isArray(c.thematic_packages) &&
|
||||
c.thematic_packages.length > 0 && (
|
||||
<div class="f-chsearch__packages">
|
||||
@@ -425,7 +371,6 @@ export default function JamboxChannelsSearch() {
|
||||
type="button"
|
||||
class="f-chsearch-pkg"
|
||||
onClick={() => window.open(
|
||||
// `/premium#tid-${encodeURIComponent(p.tid)}`,
|
||||
`/premium/${p.tid}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer"
|
||||
@@ -443,12 +388,44 @@ export default function JamboxChannelsSearch() {
|
||||
);
|
||||
})}
|
||||
|
||||
{q.trim().length >= 1 && !loading && items.length === 0 && (
|
||||
{/* Pusta lista */}
|
||||
{search.query.trim().length >= 1 && !search.loading && search.items.length === 0 && (
|
||||
<div class="f-chsearch-empty">
|
||||
Brak wyników dla: <strong>{q}</strong>
|
||||
Brak wyników dla: <strong>{search.query}</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
✅ 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
|
||||
*/
|
||||
Reference in New Issue
Block a user