diff --git a/src/content/internet-telewizja/section.yaml b/src/content/internet-telewizja/section.yaml index 20256fe..0710afb 100644 --- a/src/content/internet-telewizja/section.yaml +++ b/src/content/internet-telewizja/section.yaml @@ -1,4 +1,17 @@ sections: + - title: Pakiety tematyczne + image: + button: + text: "Poznaj ofertę pakietów tematycznych →" + url: "/internet-telewizja/pakiety-tematyczne" + title: "Poznaj ofertę pakietów tematycznych" + content: | + Dolore no invidunt ipsum justo. Et et dolor gubergren ipsum. + Ipsum luptatum magna dolore nonumy tempor stet volutpat ut nobis nonumy invidunt labore autem consequat nulla dolor amet vel. + Doming ea dolor lorem justo sed velit takimata nobis clita ad ipsum. Sed esse erat est at ipsum dolore ut sadipscing diam voluptua sea ut. + Dolores ad eos invidunt ut blandit tempor lorem sed est ipsum elit eos diam erat sed amet. Voluptua voluptua ea amet duis molestie tempor amet aliquyam et takimata stet ea accusam soluta eum aliquyam diam accumsan. Labore odio et sed ut possim takimata nonumy sadipscing feugiat option facilisi invidunt vulputate sadipscing accusam. Facilisis diam clita dolor sed eirmod dolor dolor. Diam no kasd laoreet blandit gubergren. + Aliquyam ea nulla euismod sanctus sed eirmod exerci invidunt dolores nonumy. + - title: "Dekoder Arris 4302 HD" image: "arris4302.webp" button: diff --git a/src/content/internet-telewizja/tv-addons.yaml b/src/content/internet-telewizja/tv-addons.yaml index ae262cb..00e5fb2 100644 --- a/src/content/internet-telewizja/tv-addons.yaml +++ b/src/content/internet-telewizja/tv-addons.yaml @@ -1,120 +1,204 @@ tytul: Dodatkowe pakiety TV -opis: "Rozszerz ofertę telewizyjną o dodatkowe pakiety." -cena_opis: "zł/mies." +opis: | + Rozszerz ofertę telewizyjną o dodatkowe pakiety. + Lorem ipsum dolor sit amet nulla. Elitr eum sanctus diam rebum accusam est ex. + Hendrerit erat commodo lorem gubergren vulputate dolor labore amet eros justo lorem no sea. + Facer mazim eos nonumy rebum dolor euismod. Sed est in sed odio. Vero illum vero aliquyam nonumy duis. + Labore et rebum elitr amet sanctus in aliquyam dignissim lorem accusam et rebum tempor kasd. + +cena_opis: zł/mies. dodatki: - id: canal_seriale_filmy - nazwa: "Canal+ Seriale i Filmy" + nazwa: CANAL+ Seriale i Filmy + tid: 49 typ: checkbox - opis: "Pakiet filmowo-serialowy Canal+." + opis: Pakiet filmowo-serialowy Canal+. cena: - - pakiety: [Smart, Optimum, Platinum, Podstawowy, Korzystny, Bogaty] + - pakiety: + - Smart + - Optimum + - Platinum + - Podstawowy + - Korzystny + - Bogaty 12m: 24.99 bezterminowo: 28.99 - - id: canal_super_sport - nazwa: "Canal+ Super Sport" + nazwa: CANAL+ Super Sport typ: checkbox - opis: "Pakiet sportowy Canal+." + opis: Pakiet sportowy Canal+. cena: - - pakiety: [Smart, Optimum, Platinum, Podstawowy, Korzystny, Bogaty] + - pakiety: + - Smart + - Optimum + - Platinum + - Podstawowy + - Korzystny + - Bogaty 12m: 64.99 bezterminowo: 68.99 - + tid: 48 - id: cinemax - nazwa: "Cinemax" + nazwa: Cinemax typ: checkbox - opis: "Kanały Cinemax." + opis: Kanały Cinemax. cena: - # SGT (PLUS): 10 / 15 - - pakiety: [Podstawowy, Korzystny, Bogaty] - 12m: 10.00 - bezterminowo: 15.00 - # EVIO: jedna cena 14.90 - - pakiety: [Smart, Optimum, Platinum] - # 12m: 14.90 - bezterminowo: 14.90 - + - pakiety: + - Podstawowy + - Korzystny + - Bogaty + 12m: 10 + bezterminowo: 15 + - pakiety: + - Smart + - Optimum + - Platinum + bezterminowo: 14.9 + tid: 18 - id: eleven - nazwa: "Eleven" + nazwa: Eleven typ: checkbox - opis: "Kanały Eleven Sports." + opis: Kanały Eleven Sports. cena: - - pakiety: [Podstawowy, Korzystny, Bogaty] - 12m: 15.00 - bezterminowo: 25.00 - + - pakiety: + - Podstawowy + - Korzystny + - Bogaty + 12m: 15 + bezterminowo: 25 + tid: 61 - id: filmbox - nazwa: "Filmbox" + nazwa: FilmBox+ typ: checkbox - opis: "Kanały FilmBox." + opis: Kanały FilmBox. cena: - - pakiety: [Podstawowy, Korzystny, Bogaty] - 12m: 10.00 - bezterminowo: 15.00 - + - pakiety: + - Podstawowy + - Korzystny + - Bogaty + 12m: 10 + bezterminowo: 15 + tid: 19 - id: hbo_max_podstawowy - nazwa: "HBO + Max Podstawowy" + nazwa: HBO + Max Podstawowy typ: checkbox opis: | W ramach Pakietu Podstawowego HBO Max możesz oglądać filmy i seriale w jakości FullHD na dwóch urządzeniach jednocześnie. Pakiet Podstawowy HBO Max to również dostęp do bogatej Biblioteki TVN oraz możliwość śledzenia kanału live TVN. Treści dostępne w Pakiecie Podstawowym wyświetlane są wraz z reklamami. cena: - - pakiety: [Smart, Optimum, Platinum, Podstawowy, Korzystny, Bogaty] + - pakiety: + - Smart + - Optimum + - Platinum + - Podstawowy + - Korzystny + - Bogaty 12m: 27.99 bezterminowo: 29.99 - + tid: 20 - id: hbo_max_standardowy - nazwa: "HBO + Max Standardowy" + nazwa: HBO + Max Standardowy typ: checkbox - opis: "HBO + Max (wariant standardowy)." + opis: HBO + Max (wariant standardowy). cena: - - pakiety: [Smart, Optimum, Platinum, Podstawowy, Korzystny, Bogaty] + - pakiety: + - Smart + - Optimum + - Platinum + - Podstawowy + - Korzystny + - Bogaty 12m: 36.99 bezterminowo: 39.99 - + tid: 96 - id: hbo_max_premium - nazwa: "HBO + Max Premium" + nazwa: HBO + Max Premium typ: checkbox - opis: "HBO + Max (wariant premium)." + opis: HBO + Max (wariant premium). cena: - - pakiety: [Smart, Optimum, Platinum, Podstawowy, Korzystny, Bogaty] + - pakiety: + - Smart + - Optimum + - Platinum + - Podstawowy + - Korzystny + - Bogaty 12m: 44.99 bezterminowo: 49.99 - + tid: 97 - id: wiecej_sportu_plus - nazwa: "Więcej Sportu Plus" + nazwa: Więcej Sportu Plus typ: checkbox - opis: "Dodatkowy pakiet sportowy." + opis: Dodatkowy pakiet sportowy. cena: - - pakiety: [Podstawowy, Korzystny, Bogaty] - 12m: 15.00 - bezterminowo: 25.00 - + - pakiety: + - Podstawowy + - Korzystny + - Bogaty + 12m: 15 + bezterminowo: 25 + tid: 79 - id: wiecej_erotyki - nazwa: "Więcej Erotyki" + nazwa: Więcej Erotyki typ: checkbox - opis: "Pakiet kanałów erotycznych." + opis: Pakiet kanałów erotycznych. cena: - - pakiety: [Podstawowy, Korzystny, Bogaty] - 12m: 15.00 - bezterminowo: 25.00 - + - pakiety: + - Podstawowy + - Korzystny + - Bogaty + 12m: 15 + bezterminowo: 25 + tid: 80 - id: disney_standard - nazwa: "Disney+ Standard" + nazwa: Disney+ Standard typ: checkbox opis: | - Odkryj hity filmowe, nowe seriale i produkcje oryginalne ze świata Disneya, Pixara, Gwiezdnych wojen, Marvela, a także produkcje Hulu, National Geographic i FX - cena: - - pakiety: [Smart, Optimum, Platinum, Podstawowy, Korzystny, Bogaty] - bezterminowo: 34.99 + Historie na całe życie czekają. Odkryj hity filmowe, nowe seriale i produkcje oryginalne ze świata Disneya, Pixara, Gwiezdnych wojen, Marvela, a także produkcje Hulu, National Geographic i FX. + Oglądaj, kiedy chcesz. + Ochrona rodzicielska - Zadbaj o bezpieczeństwo dzięki intuicyjnej kontroli rodzicielskiej. treściami + Jednoczesne oglądanie - Oglądaj na czterech ekranach jednocześnie, na obsługiwanych urządzeniach. + Rozrywka bez granic - Tysiące godzin seriali, filmów i produkcji oryginalnych. + Wygodne oglądanie - Możliwość oglądania jak chcesz, kiedy chcesz. + + - Oglądaj na 2 urządzeniach jednocześnie + - Full HD + - Bez reklam + - Pobieranie offline + cena: + - pakiety: + - Smart + - Optimum + - Platinum + - Podstawowy + - Korzystny + - Bogaty + bezterminowo: 34.99 - id: disney_premium - nazwa: "Disney+ Premium" + nazwa: Disney+ Premium typ: checkbox opis: | - Odkryj hity filmowe, nowe seriale i produkcje oryginalne ze świata Disneya, Pixara, Gwiezdnych wojen, Marvela, a także produkcje Hulu, National Geographic i FX + Historie na całe życie czekają. Odkryj hity filmowe, nowe seriale i produkcje oryginalne ze świata Disneya, Pixara, Gwiezdnych wojen, Marvela, a także produkcje Hulu, National Geographic i FX. + Oglądaj, kiedy chcesz. + + Ochrona rodzicielska - Zadbaj o bezpieczeństwo dzięki intuicyjnej kontroli rodzicielskiej. treściami + Jednoczesne oglądanie - Oglądaj na czterech ekranach jednocześnie, na obsługiwanych urządzeniach. + Rozrywka bez granic - Tysiące godzin seriali, filmów i produkcji oryginalnych. + Wygodne oglądanie - Możliwość oglądania jak chcesz, kiedy chcesz. + + - Oglądaj na 4 urządzeniach jednocześnie + - 4K UHD / HDR / Dolby Atmos* + - Bez reklam + - Pobieranie offline cena: - - pakiety: [Smart, Optimum, Platinum, Podstawowy, Korzystny, Bogaty] - bezterminowo: 59.99 \ No newline at end of file + - pakiety: + - Smart + - Optimum + - Platinum + - Podstawowy + - Korzystny + - Bogaty + bezterminowo: 59.99 diff --git a/src/data/ServicesRange.db b/src/data/ServicesRange.db index da07d75..d8a9b8e 100644 Binary files a/src/data/ServicesRange.db and b/src/data/ServicesRange.db differ diff --git a/src/data/ServicesRange.db-shm b/src/data/ServicesRange.db-shm index fe9ac28..0bf83d6 100644 Binary files a/src/data/ServicesRange.db-shm and b/src/data/ServicesRange.db-shm differ diff --git a/src/data/ServicesRange.db-wal b/src/data/ServicesRange.db-wal index e69de29..b851cab 100644 Binary files a/src/data/ServicesRange.db-wal and b/src/data/ServicesRange.db-wal differ diff --git a/src/islands/jambox/AddonChannelsModal.jsx b/src/islands/jambox/AddonChannelsModal.jsx new file mode 100644 index 0000000..33f1254 --- /dev/null +++ b/src/islands/jambox/AddonChannelsModal.jsx @@ -0,0 +1,111 @@ +import { useEffect, useMemo, useState } from "preact/hooks"; + +function cleanPkgName(v) { + const s = String(v || "").trim(); + if (!s) return null; + if (s.length > 64) return null; + return s; +} + +function getNearestSectionEl(el) { + return el?.closest?.("[data-addon-section]") ?? null; +} + +export default function AddonChannelsGrid(props) { + const packageName = cleanPkgName(props?.packageName); + const fallbackImage = String(props?.fallbackImage || "").trim(); + const title = String(props?.title || "").trim(); + const aboveFold = props?.aboveFold === true; + + const [loading, setLoading] = useState(false); + const [err, setErr] = useState(""); + const [items, setItems] = useState([]); + + const rootRef = useMemo(() => ({ current: null }), []); + + const channelsWithLogo = useMemo(() => { + return (items || []).filter((x) => String(x?.logo_url || "").trim()); + }, [items]); + + async function load() { + if (!packageName) return; + setLoading(true); + setErr(""); + try { + const url = `/api/jambox/jambox-channels-package?package=${encodeURIComponent( + packageName, + )}`; + const res = await fetch(url); + const json = await res.json().catch(() => null); + if (!res.ok || !json?.ok) throw new Error(json?.error || "FETCH_ERROR"); + setItems(Array.isArray(json.data) ? json.data : []); + } catch (e) { + setErr(String(e?.message || e)); + setItems([]); + } finally { + setLoading(false); + } + } + + useEffect(() => { + load(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [packageName]); + + useEffect(() => { + const el = rootRef.current; + const section = getNearestSectionEl(el); + if (!section) return; + + const hasIcons = channelsWithLogo.length > 0; + const hasFallback = !!fallbackImage; + const hasMedia = hasIcons || hasFallback; + + section.setAttribute("data-has-media", hasMedia ? "1" : "0"); + }, [channelsWithLogo.length, fallbackImage]); + + const hasIcons = channelsWithLogo.length > 0; + const visible = hasIcons ? channelsWithLogo.slice(0, 60) : []; + + return ( +
(rootRef.current = el)}> + {hasIcons ? ( +
+ {visible.map((ch, idx) => { + const logo = String(ch?.logo_url || "").trim(); + const name = String(ch?.name || "").trim(); + + return ( +
+ {logo ? ( + + ) : ( +
+ )} +
{name}
+
+ ); + })} +
+ ) : fallbackImage ? ( + {title + ) : ( +
+ {loading ? "Ładowanie kanałów" : err ? `Błąd: ${err}` : "Brak kanałów"} +
+ )} +
+ ); +} diff --git a/src/islands/jambox/JamboxAddonsModal.jsx b/src/islands/jambox/JamboxAddonsModal.jsx index 7e3d7bb..bb3a1cf 100644 --- a/src/islands/jambox/JamboxAddonsModal.jsx +++ b/src/islands/jambox/JamboxAddonsModal.jsx @@ -59,6 +59,7 @@ function normalizeAddons(addons) { krok: a.krok != null ? Number(a.krok) : 1, opis: a.opis ? String(a.opis) : "", cena: a.cena ?? 0, + tid: String(a.tid), })); } @@ -335,7 +336,16 @@ export default function JamboxAddonsModal({
{a.nazwa}
- {a.opis &&
{a.opis}
} + {/* {a.opis &&
{a.opis}
} */} + + Przejdź do szczegółowch informacji o pakiecie tematycznnym + {termPricing && (
e.stopPropagation()}> @@ -587,7 +597,7 @@ export default function JamboxAddonsModal({
- {/* ✅ DEKODER (sekcja) */} + {/* DEKODER */}
- {/* ✅ TV ADDONS (sekcja) */} + {/* TV ADDONS */}
- {/* ✅ TELEFON (sekcja) */} + {/* TELEFON */}
- {/* ✅ DODATKI (sekcja) */} + {/* DODATKI */}
- {/* ✅ PODSUMOWANIE (sekcja) */} + {/* PODSUMOWANIE */}
- {/* ✅ pływająca suma jak w internecie */} - {/*
e.stopPropagation()}> -
- Suma - - {money(totalMonthly)} {cenaOpis} - -
-
*/}
- 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 (

Wyszukiwanie kanałów w pakietach telewizji

- {/* ✅ SEK CJA "CHCIAŁBYM MIEĆ TE KANAŁY" */} + {/* ✅ SEKCJA "CHCIAŁBYM MIEĆ TE KANAŁY" */}
Chciałbym mieć te kanały
@@ -166,7 +197,8 @@ export default function JamboxChannelsSearch() { {wanted.length === 0 ? (
- 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.
) : ( <> @@ -174,7 +206,12 @@ export default function JamboxChannelsSearch() { {wanted.map((w) => (
{w.logo_url ? ( - + ) : null} {w.name} - ))} -
- ) : ( - <> -
- Nie ma jednego pakietu zawierającego wszystkie wybrane kanały. - Poniżej najlepsze dopasowania: + {/* ======= GŁÓWNE (jak było) ======= */} +
+
Pakiety główne:
+ + {packageSuggestions.exact.length > 0 ? ( +
+ {packageSuggestions.exact.map((p) => ( + + ))}
- + ) : (
{packageSuggestions.ranked.map((p) => (
- + )} +
+ + {/* ======= TEMATYCZNE — dodatki (bez liczenia) ======= */} + {packageSuggestions.thematic.length > 0 && ( +
+
+ Pakiety tematyczne do dokupienia: +
+ +
+ {packageSuggestions.thematic.map((p) => ( + + {p.name} + + ))} +
+
)}
@@ -268,11 +331,20 @@ export default function JamboxChannelsSearch() { const selected = isWanted(c); return ( -
+
{/* kolumna 1 */}
{c.logo_url && ( - + )}
{c.name}
@@ -280,11 +352,19 @@ export default function JamboxChannelsSearch() { {/* ✅ przycisk dodaj/usuń */}
{!selected ? ( - ) : ( - )} @@ -295,7 +375,9 @@ export default function JamboxChannelsSearch() {
—" }} + dangerouslySetInnerHTML={{ + __html: c.description || "", + }} /> {Array.isArray(c.packages) && c.packages.length > 0 && ( @@ -303,13 +385,40 @@ export default function JamboxChannelsSearch() { Dostępny w pakietach:  {c.packages.map((p) => ( - ))}
)} + + {Array.isArray(c.thematic_packages) && + c.thematic_packages.length > 0 && ( +
+ Dostępny w pakietach tematycznych:  + {c.thematic_packages.map((p) => ( + + + {p.name} + + + ))} +
+ )}
); diff --git a/src/pages/api/jambox/import-jambox-tematyczne-channels.js b/src/pages/api/jambox/import-jambox-tematyczne-channels.js new file mode 100644 index 0000000..266e6a1 --- /dev/null +++ b/src/pages/api/jambox/import-jambox-tematyczne-channels.js @@ -0,0 +1,347 @@ +import path from "node:path"; +import fs from "node:fs/promises"; +import yaml from "js-yaml"; +import { XMLParser } from "fast-xml-parser"; +import Database from "better-sqlite3"; + +const CHANNELS_URL = "https://www.jambox.pl/xml/listakanalow.xml"; + +const DB_PATH = + process.env.FUZ_DB_PATH || + path.join(process.cwd(), "src", "data", "ServicesRange.db"); + +function isAuthorized(request) { + const expected = import.meta.env.JAMBOX_ADMIN_TOKEN; + if (!expected) return false; + const token = request.headers.get("x-admin-token"); + return token === expected; +} + +function getDb() { + const db = new Database(DB_PATH); + db.pragma("journal_mode = WAL"); + return db; +} + +async function fetchXml(url) { + const res = await fetch(url, { + headers: { accept: "application/xml,text/xml,*/*" }, + }); + if (!res.ok) throw new Error(`XML HTTP ${res.status} for ${url}`); + return await res.text(); +} + +function parseNodes(xmlText) { + const parser = new XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: "@_", + trimValues: true, + }); + const json = parser.parse(xmlText); + const nodes = json?.xml?.node ?? json?.node ?? []; + return Array.isArray(nodes) ? nodes : [nodes]; +} + +function toInt(v) { + const n = Number(v); + return Number.isFinite(n) ? n : null; +} + +function parseCsvIds(v) { + const s = String(v ?? "").trim(); + if (!s) return []; + return s + .split(",") + .map((x) => toInt(x.trim())) + .filter((n) => n != null); +} + +function decodeEntities(input) { + if (!input) return ""; + let s = String(input) + .replace(/ /g, " ") + .replace(/–/g, "–") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/'/g, "'") + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">"); + s = s.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d))); + s = s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => + String.fromCodePoint(parseInt(h, 16)), + ); + return s; +} + +function htmlToMarkdown(input) { + if (!input) return ""; + + let html = ""; + if (typeof input === "string") html = input; + else if (input?.p) { + if (typeof input.p === "string") html = `

${input.p}

`; + else if (Array.isArray(input.p)) + html = input.p.map((p) => `

${p}

`).join(""); + } else html = String(input); + + let s = decodeEntities(html); + + s = s + .replace(/<\s*(ul|ol)[^>]*>/gi, "\n__LIST_START__\n") + .replace(/<\/\s*(ul|ol)\s*>/gi, "\n__LIST_END__\n") + .replace(/<\s*li[^>]*>/gi, "__LI__") + .replace(/<\/\s*li\s*>/gi, "\n") + .replace(/<\s*br\s*\/?\s*>/gi, "\n") + .replace(/<\/\s*(p|div)\s*>/gi, "\n") + .replace(/<\s*(p|div)[^>]*>/gi, "") + .replace(/<[^>]+>/g, "") + .replace(/\r/g, "") + .replace(/[ \t]+\n/g, "\n") + .replace(/\n{3,}/g, "\n\n") + .trim(); + + const lines = s.split("\n").map((x) => x.trim()); + const out = []; + let inList = false; + + for (const line of lines) { + if (!line) { + if (!inList) out.push(""); + continue; + } + if (line === "__LIST_START__") { + inList = true; + continue; + } + if (line === "__LIST_END__") { + inList = false; + out.push(""); + continue; + } + if (inList && line.startsWith("__LI__")) { + out.push(`- ${line.replace("__LI__", "").trim()}`); + continue; + } + out.push(line); + } + + return out.join("\n").trim(); +} + +function extractLogoUrl(node) { + const logo = node?.field_logo_fid; + if (!logo) return null; + + // w listakanalow.xml jest zwykle bezpośredni URL + if (typeof logo === "string") { + const s = logo.trim(); + if (!s) return null; + const m = s.match(/src="([^"]+)"/); + return m?.[1] ?? s; + } + + if (logo?.img?.["@_src"]) return logo.img["@_src"]; + return null; +} + +async function downloadLogoAsBase64(url) { + try { + const res = await fetch(url); + if (!res.ok) return null; + const ct = res.headers.get("content-type") || "image/png"; + const buf = Buffer.from(await res.arrayBuffer()); + if (!buf.length) return null; + return `data:${ct};base64,${buf.toString("base64")}`; + } catch { + return null; + } +} + +function ensureTidColumn(db) { + const cols = db + .prepare("PRAGMA table_info(jambox_channels)") + .all() + .map((r) => String(r.name || "").toLowerCase()); + + if (!cols.includes("tid")) { + db.exec("ALTER TABLE jambox_channels ADD COLUMN tid INTEGER;"); + } +} + +export async function POST({ request }) { + if (!isAuthorized(request)) { + return new Response(JSON.stringify({ ok: false, error: "Unauthorized" }), { + status: 401, + headers: { "content-type": "application/json; charset=utf-8" }, + }); + } + + const body = await request.json().catch(() => ({})); + const dryRun = body?.dryRun === true; + + const YAML_PATH = + import.meta.env.JAMBOX_TV_ADDONS_YAML_PATH || + path.join( + process.cwd(), + "src", + "content", + "internet-telewizja", + "tv-addons.yaml", + ); + + const db = getDb(); + + // tabela + unikalność do ON CONFLICT + db.exec(` + CREATE TABLE IF NOT EXISTS jambox_channels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nazwa TEXT NOT NULL, + pckg_name TEXT NOT NULL, + image TEXT, + opis TEXT, + pckg_addons INTEGER NOT NULL DEFAULT (0) + ); + CREATE UNIQUE INDEX IF NOT EXISTS ux_jambox_channels_name_pkg + ON jambox_channels(nazwa, pckg_name); + `); + + // ✅ dorób kolumnę tid (dla addonów) + ensureTidColumn(db); + + const delAddons = db.prepare( + `DELETE FROM jambox_channels WHERE pckg_addons = 1`, + ); + + // ✅ zapisujemy pckg_addons=1 i tid + const upsert = db.prepare(` + INSERT INTO jambox_channels (nazwa, pckg_name, image, opis, pckg_addons, tid) + VALUES (@nazwa, @pckg_name, @image, @opis, 1, @tid) + ON CONFLICT(nazwa, pckg_name) DO UPDATE SET + image = COALESCE(excluded.image, jambox_channels.image), + opis = COALESCE(excluded.opis, jambox_channels.opis), + pckg_addons = 1, + tid = COALESCE(excluded.tid, jambox_channels.tid) + `); + + const logoCache = new Map(); + const rows = []; + + try { + // 1) YAML -> tid dodatku + const rawYaml = await fs.readFile(YAML_PATH, "utf8"); + const doc = yaml.load(rawYaml); + + if (!doc || !Array.isArray(doc.dodatki)) { + return new Response( + JSON.stringify({ ok: false, error: "YAML: brak doc.dodatki" }), + { + status: 400, + headers: { "content-type": "application/json; charset=utf-8" }, + }, + ); + } + + // tid -> nazwa pakietu (z YAML) + const addonTidToName = new Map(); + for (const a of doc.dodatki) { + const tid = toInt(a?.tid); + const name = String(a?.nazwa ?? "").trim(); + if (tid != null && name) addonTidToName.set(tid, name); + } + const addonTids = new Set(addonTidToName.keys()); + + // 2) XML kanałów + const xml = await fetchXml(CHANNELS_URL); + const nodes = parseNodes(xml); + + for (const node of nodes) { + const channelName = String( + node?.nazwa_kanalu ?? node?.nazwa ?? "", + ).trim(); + if (!channelName) continue; + + const pkgIds = parseCsvIds(node?.pakiety_id); + if (!pkgIds.length) continue; + + // interesują nas tylko te pakiety_id, które są w YAML dodatków (tid) + const matchedAddonTids = pkgIds.filter((tid) => addonTids.has(tid)); + if (!matchedAddonTids.length) continue; + + const opis = htmlToMarkdown(node?.opis) || null; + + const key = channelName.toLowerCase(); + let img = logoCache.get(key); + if (img === undefined) { + const logoUrl = extractLogoUrl(node); + img = logoUrl ? await downloadLogoAsBase64(logoUrl) : null; + logoCache.set(key, img); + } + + for (const tid of matchedAddonTids) { + const pckgName = addonTidToName.get(tid); + if (!pckgName) continue; + + rows.push({ + nazwa: channelName, + pckg_name: pckgName, + image: img, + opis, + tid, // ✅ kluczowe: zapis tid do bazy + }); + } + } + + // 3) REFRESH: usuń wszystkie addonowe i wrzuć nowe + if (!dryRun) { + const trx = db.transaction((data) => { + const info = delAddons.run(); + for (const r of data) upsert.run(r); + return info.changes; + }); + + const deleted = trx(rows); + + return new Response( + JSON.stringify({ + ok: true, + dryRun, + deleted_addon_rows: deleted, + inserted_rows: rows.length, + db: DB_PATH, + yaml: YAML_PATH, + uniqueAddonsUsed: addonTidToName.size, + tidsUsed: addonTids.size, + }), + { headers: { "content-type": "application/json; charset=utf-8" } }, + ); + } + + return new Response( + JSON.stringify({ + ok: true, + dryRun: true, + would_delete_addon_rows: "(unknown in dryRun)", + would_insert_rows: rows.length, + db: DB_PATH, + yaml: YAML_PATH, + uniqueAddonsUsed: addonTidToName.size, + tidsUsed: addonTids.size, + }), + { headers: { "content-type": "application/json; charset=utf-8" } }, + ); + } catch (e) { + console.error("import addon jambox_channels:", e); + return new Response( + JSON.stringify({ ok: false, error: String(e?.message || e) }), + { + status: 500, + headers: { "content-type": "application/json; charset=utf-8" }, + }, + ); + } finally { + try { + db.close(); + } catch {} + } +} diff --git a/src/pages/api/jambox/import-jambox-tematyczne.js b/src/pages/api/jambox/import-jambox-tematyczne.js new file mode 100644 index 0000000..ed58fcc --- /dev/null +++ b/src/pages/api/jambox/import-jambox-tematyczne.js @@ -0,0 +1,291 @@ +import path from "node:path"; +import fs from "node:fs/promises"; +import yaml from "js-yaml"; +import { XMLParser } from "fast-xml-parser"; + +const PLUS_THEMATIC_URL = + "https://www.jambox.pl/xml/slownik-pakietytematyczneplus.xml"; +const PREMIUM_URL = "https://www.jambox.pl/xml/slownik-pakietypremium.xml"; + +/** + * .env: JAMBOX_ADMIN_TOKEN="..." + */ +function isAuthorized(request) { + const expected = import.meta.env.JAMBOX_ADMIN_TOKEN; + if (!expected) return false; + const token = request.headers.get("x-admin-token"); + return token === expected; +} + +async function fetchXml(url) { + const res = await fetch(url); + if (!res.ok) { + throw new Error( + `Błąd pobierania XML: ${res.status} ${res.statusText} (${url})`, + ); + } + return await res.text(); +} + +/** + * XML: + * + * ...... + * ... + * + */ +function parseNodes(xmlText) { + const parser = new XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: "@_", + }); + + const json = parser.parse(xmlText); + const root = json.xml ?? json; + + let nodes = root?.node ?? []; + if (!nodes) return []; + if (!Array.isArray(nodes)) nodes = [nodes]; + + return nodes; +} + +function toInt(v) { + const n = Number(v); + return Number.isFinite(n) ? n : null; +} + +function normalizeName(name) { + let s = String(name || "").trim(); + + // często XML ma "Pakiet ..." a u Ciebie w YAML jest bez "Pakiet" + s = s.replace(/^pakiet\s+/i, ""); + + // "+Max" -> "+ Max" + s = s.replace(/\+([A-Za-zĄĆĘŁŃÓŚŹŻ])/g, "+ $1"); + + // pojedyncze spacje + s = s.replace(/\s+/g, " ").trim(); + + return s; +} + +function stripDiacritics(s) { + return String(s || "") + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, ""); +} + +function slugifyId(name) { + const s = stripDiacritics(String(name || "").toLowerCase()) + .replace(/\+/g, " plus ") + .replace(/&/g, " and ") + .replace(/[^a-z0-9]+/g, "_") + .replace(/^_+|_+$/g, "") + .replace(/_+/g, "_"); + return s || "pakiet"; +} + +function buildXmlMap(nodesA, nodesB) { + const byTid = new Map(); + const byNormName = new Map(); + + function add(nodes, source) { + for (const n of nodes || []) { + const tid = toInt(n?.tid); + const rawName = String(n?.name ?? "").trim(); + const name = normalizeName(rawName); + + if (!tid || !name) continue; + + if (!byTid.has(tid)) byTid.set(tid, { tid, name, rawName, source }); + + const key = stripDiacritics(name).toLowerCase(); + if (!byNormName.has(key)) byNormName.set(key, { tid, name, rawName, source }); + } + } + + add(nodesA, "thematic_plus"); + add(nodesB, "premium"); + + return { byTid, byNormName }; +} + +export async function POST({ request }) { + try { + if (!isAuthorized(request)) { + return new Response(JSON.stringify({ ok: false, error: "Unauthorized" }), { + status: 401, + headers: { "Content-Type": "application/json" }, + }); + } + + const body = await request.json().catch(() => ({})); + const dryRun = body?.dryRun === true; + + // domyślnie: tylko aktualizacja (bez dopisywania nowych) + const addMissing = body?.addMissing === true; + const updateNames = body?.updateNames !== false; // default true + + const YAML_PATH = + import.meta.env.JAMBOX_TV_ADDONS_YAML_PATH || + path.join( + process.cwd(), + "src", + "content", + "internet-telewizja", + "tv-addons.yaml", + ); + + // 1) XML + const [xmlA, xmlB] = await Promise.all([ + fetchXml(PLUS_THEMATIC_URL), + fetchXml(PREMIUM_URL), + ]); + + const nodesA = parseNodes(xmlA); + const nodesB = parseNodes(xmlB); + + const { byTid, byNormName } = buildXmlMap(nodesA, nodesB); + + // 2) YAML + const rawYaml = await fs.readFile(YAML_PATH, "utf8"); + const doc = yaml.load(rawYaml); + + if (!doc || !Array.isArray(doc.dodatki)) { + return new Response( + JSON.stringify({ ok: false, error: "YAML: brak doc.dodatki" }), + { status: 400, headers: { "Content-Type": "application/json" } }, + ); + } + + const changedItems = []; + const unchangedItems = []; + const skippedItems = []; + const addedItems = []; + + // indeksy istniejących (dla addMissing) + const existingByTid = new Map(); + const existingByNormName = new Map(); + + for (const a of doc.dodatki) { + const tid = toInt(a?.tid); + const nm = normalizeName(String(a?.nazwa ?? "")); + if (tid != null) existingByTid.set(tid, a); + if (nm) existingByNormName.set(stripDiacritics(nm).toLowerCase(), a); + } + + // 2a) aktualizacje + for (const addon of doc.dodatki) { + const id = addon?.id ?? null; + + const before = { + tid: toInt(addon?.tid), + nazwa: String(addon?.nazwa ?? ""), + }; + + let match = null; + + // najpierw po tid (najpewniejsze) + if (before.tid != null && byTid.has(before.tid)) { + match = byTid.get(before.tid); + } else { + // potem po nazwie + const key = stripDiacritics(normalizeName(before.nazwa)).toLowerCase(); + if (key && byNormName.has(key)) match = byNormName.get(key); + } + + if (!match) { + skippedItems.push({ id, reason: "brak dopasowania w XML", before }); + continue; + } + + const after = { + tid: match.tid, + nazwa: updateNames ? match.name : before.nazwa, + }; + + const willChange = + before.tid !== after.tid || before.nazwa !== after.nazwa; + + if (!willChange) { + unchangedItems.push({ id, tid: before.tid, nazwa: before.nazwa }); + continue; + } + + if (!dryRun) { + addon.tid = after.tid; + if (updateNames) addon.nazwa = after.nazwa; + } + + changedItems.push({ + id, + before, + after, + matchedFrom: match.source, + }); + } + + // 2b) opcjonalnie dopisz brakujące pakiety z XML do YAML + if (addMissing) { + for (const [tid, it] of byTid.entries()) { + if (existingByTid.has(tid)) continue; + + const key = stripDiacritics(it.name).toLowerCase(); + if (existingByNormName.has(key)) continue; + + const newId = slugifyId(it.name); + + const newItem = { + id: newId, + nazwa: it.name, + tid: tid, + typ: "checkbox", + opis: "", + cena: [], + }; + + if (!dryRun) doc.dodatki.push(newItem); + + addedItems.push({ id: newId, tid, nazwa: it.name, source: it.source }); + } + } + + if (!dryRun && (changedItems.length > 0 || addedItems.length > 0)) { + const newYaml = yaml.dump(doc, { + lineWidth: -1, + noRefs: true, + sortKeys: false, + }); + await fs.writeFile(YAML_PATH, newYaml, "utf8"); + } + + return new Response( + JSON.stringify({ + ok: true, + dryRun, + yamlPath: YAML_PATH, + options: { addMissing, updateNames }, + xmlCounts: { + thematic_plus: nodesA.length, + premium: nodesB.length, + uniqueByTid: byTid.size, + }, + changed: changedItems.length, + added: addedItems.length, + unchanged: unchangedItems.length, + skipped: skippedItems.length, + changedItems, + addedItems, + skippedItems, + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + } catch (err) { + console.error("update-tv-addons error:", err); + return new Response( + JSON.stringify({ ok: false, error: String(err?.message ?? err) }), + { status: 500, headers: { "Content-Type": "application/json" } }, + ); + } +} diff --git a/src/pages/api/jambox/jambox-channels-search.js b/src/pages/api/jambox/jambox-channels-search.js index 18b5a44..96b0363 100644 --- a/src/pages/api/jambox/jambox-channels-search.js +++ b/src/pages/api/jambox/jambox-channels-search.js @@ -23,6 +23,18 @@ function slugifyPkg(name) { .replace(/(^-|-$)/g, ""); } +function uniqByKey(list, keyFn) { + const seen = new Set(); + const out = []; + for (const it of list || []) { + const k = keyFn(it); + if (!k || seen.has(k)) continue; + seen.add(k); + out.push(it); + } + return out; +} + export function GET({ url }) { const q = (url.searchParams.get("q") || "").trim(); const limit = clamp(Number(url.searchParams.get("limit") || 50), 1, 200); @@ -44,10 +56,25 @@ export function GET({ url }) { .prepare( ` SELECT - nazwa AS name, - MAX(image) AS logo_url, - MAX(opis) AS description, - GROUP_CONCAT(pckg_name, '||') AS packages_blob + nazwa AS name, + MAX(image) AS logo_url, + MAX(opis) AS description, + + -- ✅ pakiety główne (pckg_addons = 0) + GROUP_CONCAT( + CASE WHEN IFNULL(pckg_addons, 0) = 0 THEN pckg_name END, + '||' + ) AS packages_blob, + + -- ✅ pakiety tematyczne (pckg_addons = 1) + tid + GROUP_CONCAT( + CASE + WHEN IFNULL(pckg_addons, 0) = 1 AND tid IS NOT NULL + THEN CAST(tid AS TEXT) || '::' || pckg_name + END, + '||' + ) AS thematic_blob + FROM jambox_channels WHERE nazwa LIKE ? ESCAPE '\\' GROUP BY nazwa @@ -58,23 +85,40 @@ export function GET({ url }) { .all(like, limit); const data = rows.map((r) => { + // ===== główne ===== const pkgsRaw = String(r.packages_blob || "") .split("||") .map((x) => x.trim()) .filter(Boolean); const packages = uniq(pkgsRaw) - .map((p) => ({ - id: slugifyPkg(p), - name: p, - })) + .map((p) => ({ id: slugifyPkg(p), name: p })) .sort((a, b) => a.name.localeCompare(b.name, "pl")); + // ===== tematyczne ===== + const thematicRaw = String(r.thematic_blob || "") + .split("||") + .map((x) => x.trim()) + .filter(Boolean); + + const thematic_packages = uniqByKey( + thematicRaw + .map((x) => { + const [tid, ...rest] = x.split("::"); + const name = rest.join("::").trim(); + const t = String(tid || "").trim(); + return t && name ? { tid: t, name } : null; + }) + .filter(Boolean), + (p) => `${p.tid}::${p.name}` + ).sort((a, b) => a.name.localeCompare(b.name, "pl")); + return { name: r.name, logo_url: r.logo_url || "", description: r.description || "", packages, + thematic_packages, // ✅ NOWE }; }); diff --git a/src/pages/internet-telewizja/pakiety-tematyczne.astro b/src/pages/internet-telewizja/pakiety-tematyczne.astro new file mode 100644 index 0000000..be1d642 --- /dev/null +++ b/src/pages/internet-telewizja/pakiety-tematyczne.astro @@ -0,0 +1,90 @@ +--- +import DefaultLayout from "../../layouts/DefaultLayout.astro"; +import yaml from "js-yaml"; +import fs from "node:fs"; +import Markdown from "../../islands/Markdown.jsx"; +import AddonChannelsGrid from "../../islands/jambox/AddonChannelsModal.jsx"; +import "../../styles/jambox-tematyczne.css"; + +/** Typy minimalne */ +type AddonPriceRow = { + pakiety?: string[] | any; + "12m"?: number | string; + bezterminowo?: number | string; +}; + +type TvAddon = { + id?: string; + nazwa?: string; + tid?: number; + typ?: string; + opis?: string; + image?: string; + cena?: AddonPriceRow[]; +}; + +type TvAddonsDoc = { + tytul?: string; + opis?: string; + cena_opis?: string; + dodatki?: TvAddon[]; +}; + +const doc = yaml.load( + fs.readFileSync("./src/content/internet-telewizja/tv-addons.yaml", "utf8"), +) as TvAddonsDoc; + +const pageTitle = doc?.tytul ?? "Dodatkowe pakiety TV"; +const pageDesc = doc?.opis ?? ""; +const addons: TvAddon[] = Array.isArray(doc?.dodatki) ? doc.dodatki : []; +--- + + +
+
+

{pageTitle}

+ {pageDesc && } +
+
+ + {addons.map((addon: TvAddon, index: number) => { + const isAboveFold = index === 0; + + const hasYamlImage = !!String(addon?.image ?? "").trim(); + const pkgName = String(addon?.nazwa ?? "").trim(); + + const assumeHasMedia = pkgName ? true : hasYamlImage; + const anchorId = addon?.tid != null ? String(addon.tid) : undefined; + + return ( +
+
+ {/* TEKST — lewa, od góry */} +
+ {pkgName &&

{pkgName}

} + {addon?.opis && } +
+ + {/* MEDIA — prawa */} +
+ {pkgName ? ( + + ) : null} +
+
+
+ ); + })} +
diff --git a/src/styles/jambox-tematyczne.css b/src/styles/jambox-tematyczne.css new file mode 100644 index 0000000..9e7c662 --- /dev/null +++ b/src/styles/jambox-tematyczne.css @@ -0,0 +1,87 @@ +/* =========================== + TV ADDONS — LAYOUT FIXES + =========================== */ + +/* f-section-grid ma u Ciebie items-center (centrowanie w pionie), + więc dla addonów wymuszamy start (od góry). */ +.f-addon-section { + align-items: start; /* override dla grid items */ +} + +/* Gdy island stwierdzi brak ikon i brak fallback image: + - 1 kolumna + - ukryj pusty blok mediów + UWAGA: NIE dodawaj margin:0, bo zabijesz mx-auto z f-section-grid */ +[data-addon-section][data-has-media="0"] { + grid-template-columns: 1fr !important; +} + +[data-addon-section][data-has-media="0"] .f-addon-media { + display: none; +} + +/* =========================== + KANAŁY / MEDIA — STYLE + (wyniesione z island) + =========================== */ + +.f-channels-grid{ + display:grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: .65rem; + padding: .75rem; + border-radius: 1.25rem; + border: 1px solid var(--f-border-color); + background: color-mix(in oklab, var(--f-background) 92%, black 8%); +} +@media (min-width: 520px){ + .f-channels-grid{ grid-template-columns: repeat(5, minmax(0,1fr)); } +} +@media (min-width: 768px){ + .f-channels-grid{ grid-template-columns: repeat(4, minmax(0,1fr)); } +} +@media (min-width: 1024px){ + .f-channels-grid{ grid-template-columns: repeat(5, minmax(0,1fr)); } +} + +.f-channel-item{ + display:flex; + flex-direction:column; + align-items:center; + text-align:center; + gap:.35rem; + min-width:0; +} +.f-channel-logo{ + /* width: 86px; + height: 86px; */ + object-fit: contain; + /* border-radius: 1rem; */ + padding: .25rem; + /* border: 1px solid color-mix(in oklab, var(--f-border-color) 85%, transparent 15%); */ + /* background: color-mix(in oklab, var(--f-background) 95%, black 5%); */ +} +.f-channel-logo-placeholder{ + width:56px;height:56px;border-radius:1rem; + /* border:1px dashed var(--f-border-color); */ + opacity:.6; +} +.f-channel-label{ + font-size: .78rem; + line-height: 1.0rem; + opacity:.85; + max-width: 9rem; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +.f-addon-fallback-image{ + width:100%; + height: 280px; + object-fit: cover; + border-radius: 1.25rem; + border: 1px solid var(--f-border-color); +} diff --git a/src/styles/map-google.css b/src/styles/map-google.css index 3c1c10b..0881811 100644 --- a/src/styles/map-google.css +++ b/src/styles/map-google.css @@ -14,23 +14,24 @@ } /* Mała karta mapy (inne sekcje) */ -.fuz-map--card { +/* .fuz-map--card { @apply w-full h-[350px] overflow-hidden mt-8; -} - +} */ +/* .map-range-container { @apply sticky top-[67px] z-[999] flex justify-center w-full pointer-events-auto; - /* position: absolute; + position: absolute; top: 20px; left: 50%; transform: translateX(-50%); z-index: 9000; display: flex; - justify-content: center; */ - + justify-content: center; } + */ + /* Pulsująca obwódka budynku */ .pulse-marker { width: 22px; diff --git a/src/styles/sections.css b/src/styles/sections.css index b1e0ff7..f9bd618 100644 --- a/src/styles/sections.css +++ b/src/styles/sections.css @@ -7,7 +7,7 @@ } .f-section { - @apply pt-1 pb-1 mx-2; + @apply pt-1 pb-1 mx-2 my-6;; } .f-section-center { diff --git a/src/styles/theme.css b/src/styles/theme.css index a2f27ff..80a5a9e 100644 --- a/src/styles/theme.css +++ b/src/styles/theme.css @@ -43,10 +43,6 @@ --card-ring: hsla(217 91% 60% / 0.45); --card-shadow-deep: hsla(221 47% 11% / 0.18); - /* - - - */ --surface-shadow-dark: var(--brand-hue) 50% 3%; --shadow-strength-dark: .8; @@ -69,7 +65,7 @@ /* --- Background and Text --- */ --f-background: var(--surface3-light); - --f-text: var(--text1-light); + --f-text: var(--text2-light); --f-header: var(--text1-light); --f-header-items: (var(--text1-light)); /*--- Navbar --- */ @@ -130,7 +126,7 @@ /* --- Background and Text --- */ --f-background: var(--surface1-dark); - --f-text: var(--text1-dark); + --f-text: var(--text2-dark); --f-header: var(--text1-dark); --f-header-items: (var(--text1-dark)); /*--- Navbar --- */