Pakiety tematyczne

This commit is contained in:
dm
2025-12-16 20:10:08 +01:00
parent ff05d0dee9
commit 4f0f171bdc
16 changed files with 1320 additions and 146 deletions

View File

@@ -8,7 +8,7 @@ export default function JamboxChannelsSearch() {
const [err, setErr] = useState("");
// ✅ koszyk kanałów
const [wanted, setWanted] = useState([]); // [{ name, logo_url, packages:[{id,name}] }]
const [wanted, setWanted] = useState([]); // [{ name, logo_url, packages:[{id,name}], thematic_packages:[{tid,name}] }]
const abortRef = useRef(null);
@@ -34,10 +34,13 @@ 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");
@@ -83,12 +86,16 @@ export default function JamboxChannelsSearch() {
// ✅ koszyk: dodaj/usuń kanał
// ==========================
const isWanted = (c) =>
wanted.some((w) => String(w.name || "").toLowerCase() === String(c.name || "").toLowerCase());
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()
(w) =>
String(w.name || "").toLowerCase() === String(c.name || "").toLowerCase()
);
if (exists) return prev;
@@ -98,6 +105,9 @@ export default function JamboxChannelsSearch() {
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
: [],
},
];
});
@@ -105,7 +115,10 @@ export default function JamboxChannelsSearch() {
function removeWantedByName(name) {
setWanted((prev) =>
prev.filter((w) => String(w.name || "").toLowerCase() !== String(name || "").toLowerCase())
prev.filter(
(w) =>
String(w.name || "").toLowerCase() !== String(name || "").toLowerCase()
)
);
}
@@ -114,12 +127,14 @@ export default function JamboxChannelsSearch() {
}
// =========================================
// ✅ pakiety, które zawierają WSZYSTKIE kanały
// ✅ sugestie pakietów dla koszyka
// - GŁÓWNE: exact/ranked (z count)
// - TEMATYCZNE: dodatki do dokupienia (bez liczenia)
// =========================================
const packageSuggestions = useMemo(() => {
if (!wanted.length) return { exact: [], ranked: [] };
if (!wanted.length) return { exact: [], ranked: [], thematic: [] };
// mapa pakietu -> { id,name,count }
// ======= GŁÓWNE =======
const counts = new Map(); // key = packageName
for (const ch of wanted) {
const pkgs = Array.isArray(ch.packages) ? ch.packages : [];
@@ -134,25 +149,41 @@ export default function JamboxChannelsSearch() {
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 };
// ======= TEMATYCZNE (dodatki) =======
const thematicMap = new Map(); // key = tid
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 };
}, [wanted]);
return (
<div class="f-chsearch">
<h1 class="f-section-title">Wyszukiwanie kanałów w pakietach telewizji</h1>
{/* ✅ SEK CJA "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>
@@ -166,7 +197,8 @@ 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ą wszystkie wybrane kanały.
Dodaj kanały z listy wyników pokażę pakiety, które zawierają
wszystkie wybrane kanały.
</div>
) : (
<>
@@ -174,7 +206,12 @@ export default function JamboxChannelsSearch() {
{wanted.map((w) => (
<div class="f-chsearch__wanted-chip" key={w.name}>
{w.logo_url ? (
<img src={w.logo_url} alt={w.name} class="f-chsearch__wanted-logo" loading="lazy" />
<img
src={w.logo_url}
alt={w.name}
class="f-chsearch__wanted-logo"
loading="lazy"
/>
) : null}
<span class="f-chsearch__wanted-name">{w.name}</span>
<button
@@ -191,29 +228,29 @@ export default function JamboxChannelsSearch() {
{/* ✅ SUGESTIE PAKIETÓW */}
<div class="f-chsearch__wanted-packages">
<div class="font-semibold">Pakiety pasujące do wybranych kanałów:</div>
<div class="font-semibold">
Pakiety pasujące do wybranych kanałów:
</div>
{packageSuggestions.exact.length > 0 ? (
<div class="flex flex-wrap gap-2 mt-2">
{packageSuggestions.exact.map((p) => (
<button
type="button"
key={p.name}
class="f-chsearch-pkg"
onClick={() => scrollToPackage(p.name)}
title="Kliknij, aby przewinąć do pakietu"
>
{p.name}
</button>
))}
</div>
) : (
<>
<div class="opacity-80 mt-2">
Nie ma jednego pakietu zawierającego wszystkie wybrane kanały.
Poniżej najlepsze dopasowania:
{/* ======= GŁÓWNE (jak było) ======= */}
<div class="mt-2">
<div class="opacity-80">Pakiety główne:</div>
{packageSuggestions.exact.length > 0 ? (
<div class="flex flex-wrap gap-2 mt-2">
{packageSuggestions.exact.map((p) => (
<button
type="button"
key={p.name}
class="f-chsearch-pkg"
onClick={() => scrollToPackage(p.name)}
title="Kliknij, aby przewinąć do pakietu"
>
{p.name}
</button>
))}
</div>
) : (
<div class="flex flex-wrap gap-2 mt-2">
{packageSuggestions.ranked.map((p) => (
<button
@@ -227,7 +264,33 @@ export default function JamboxChannelsSearch() {
</button>
))}
</div>
</>
)}
</div>
{/* ======= TEMATYCZNE — dodatki (bez liczenia) ======= */}
{packageSuggestions.thematic.length > 0 && (
<div class="mt-4">
<div class="opacity-80">
Pakiety tematyczne do dokupienia:
</div>
<div class="flex flex-wrap gap-2 mt-2">
{packageSuggestions.thematic.map((p) => (
<a
key={p.tid}
class="f-chsearch-pkg"
href={`/internet-telewizja/pakiety-tematyczne#${encodeURIComponent(
p.tid
)}`}
target="_blank"
rel="noopener noreferrer"
title="Otwórz w nowej karcie"
>
{p.name}
</a>
))}
</div>
</div>
)}
</div>
</>
@@ -268,11 +331,20 @@ export default function JamboxChannelsSearch() {
const selected = isWanted(c);
return (
<div class="f-chsearch__row" role="listitem" key={`${c.name}-${c.logo_url || ""}`}>
<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" />
<img
src={c.logo_url}
alt={c.name}
class="f-chsearch__logo"
loading="lazy"
/>
)}
<div class="f-chsearch__channel-name">{c.name}</div>
@@ -280,11 +352,19 @@ export default function JamboxChannelsSearch() {
{/* ✅ przycisk dodaj/usuń */}
<div class="mt-2">
{!selected ? (
<button type="button" class="btn btn-outline" onClick={() => addWanted(c)}>
<button
type="button"
class="btn btn-outline"
onClick={() => addWanted(c)}
>
Dodaj do Chciałbym mieć
</button>
) : (
<button type="button" class="btn btn-primary" onClick={() => removeWantedByName(c.name)}>
<button
type="button"
class="btn btn-primary"
onClick={() => removeWantedByName(c.name)}
>
Usuń z listy
</button>
)}
@@ -295,7 +375,9 @@ export default function JamboxChannelsSearch() {
<div class="f-chsearch__right">
<div
class="f-chsearch__desc f-chsearch__desc--html"
dangerouslySetInnerHTML={{ __html: c.description || "<em>—</em>" }}
dangerouslySetInnerHTML={{
__html: c.description || "<em>—</em>",
}}
/>
{Array.isArray(c.packages) && c.packages.length > 0 && (
@@ -303,13 +385,40 @@ export default function JamboxChannelsSearch() {
Dostępny w pakietach:&nbsp;
{c.packages.map((p) => (
<span key={p.id}>
<button type="button" class="f-chsearch-pkg" onClick={() => scrollToPackage(p.name)}>
<button
type="button"
class="f-chsearch-pkg"
onClick={() => scrollToPackage(p.name)}
title="Kliknij, aby przewinąć do pakietu"
>
{p.name}
</button>
</span>
))}
</div>
)}
{Array.isArray(c.thematic_packages) &&
c.thematic_packages.length > 0 && (
<div class="f-chsearch__packages">
Dostępny w pakietach tematycznych:&nbsp;
{c.thematic_packages.map((p) => (
<span key={`${p.tid}-${p.name}`}>
<a
class="f-chsearch-pkg"
href={`/internet-telewizja/pakiety-tematyczne#${encodeURIComponent(
p.tid
)}`}
target="_blank"
rel="noopener noreferrer"
title="Otwórz w nowej karcie"
>
{p.name}
</a>
</span>
))}
</div>
)}
</div>
</div>
);