Dorabiamy funkcjonalnosci w TV
This commit is contained in:
@@ -1,18 +1,18 @@
|
||||
sections:
|
||||
- title: Dodatkowe możliwości naszej telewizji"
|
||||
image: "ekosystem-kyanit.webp"
|
||||
content: |
|
||||
- **Catchup** — na wybranych kanałach możesz obejrzeć audycję z ostatnich 7 dni. [Więcej →](#catchup "Przeczytaj o usłudze CatchUp")
|
||||
# - title: Dodatkowe możliwości naszej telewizji"
|
||||
# image: "ekosystem-kyanit.webp"
|
||||
# content: |
|
||||
# - **Catchup** — na wybranych kanałach możesz obejrzeć audycję z ostatnich 7 dni. [Więcej →](#catchup "Przeczytaj o usłudze CatchUp")
|
||||
|
||||
- **Nagrywanie** — nagraj interesującą Cię audycję i obejrzyj ją kiedy chcesz. [Więcej →](#nagrywarka "Przeczytaj o nagrywaniu audycji")
|
||||
# - **Nagrywanie** — nagraj interesującą Cię audycję i obejrzyj ją kiedy chcesz. [Więcej →](#nagrywarka "Przeczytaj o nagrywaniu audycji")
|
||||
|
||||
- **StartOver** — obejrzyj od początku audycję, która już się rozpoczęła (do 3h wstecz). [Więcej →](#startover "Przeczytaj o usłudze StartOver")
|
||||
# - **StartOver** — obejrzyj od początku audycję, która już się rozpoczęła (do 3h wstecz). [Więcej →](#startover "Przeczytaj o usłudze StartOver")
|
||||
|
||||
- **Nagrywanie serii** — zaplanuj nagrywanie kolejnych odcinków ulubionego serialu. [Więcej →](#nagrywanie_cykliczne "Przeczytaj o nagrywaniu cyklicznym")
|
||||
# - **Nagrywanie serii** — zaplanuj nagrywanie kolejnych odcinków ulubionego serialu. [Więcej →](#nagrywanie_cykliczne "Przeczytaj o nagrywaniu cyklicznym")
|
||||
|
||||
- **Pauzowanie** — zatrzymuj i cofaj audycje.
|
||||
# - **Pauzowanie** — zatrzymuj i cofaj audycje.
|
||||
|
||||
- **Wyszukiwarka tekstowa** — wyszukaj dowolną frazę audycji i zaplanuj nagranie.
|
||||
# - **Wyszukiwarka tekstowa** — wyszukaj dowolną frazę audycji i zaplanuj nagranie.
|
||||
|
||||
- title: "Dekoder telewizyjny"
|
||||
image: "VIP4302.png"
|
||||
@@ -33,26 +33,26 @@ sections:
|
||||
- Przedni panel zawiera m.in. diodę LED i odbiornik podczerwieni
|
||||
- Wymiary modelu (szer/dł/wys): 130 x 130 x 26 mm
|
||||
|
||||
- type: "iframe-channels"
|
||||
title: Sprawdź listę kanałów w interesującym Cię pakiecie
|
||||
content: ""
|
||||
# - type: "iframe-channels"
|
||||
# title: Sprawdź listę kanałów w interesującym Cię pakiecie
|
||||
# content: ""
|
||||
|
||||
iframe_sets:
|
||||
- id: "canal_smart"
|
||||
name: "SMART"
|
||||
p: 86
|
||||
- id: "canal_optimum"
|
||||
name: "OPTIMUM"
|
||||
p: 87
|
||||
- id: "canal_platinum"
|
||||
name: "PLATINUM"
|
||||
p: 88
|
||||
- id: "canal_podstawowy"
|
||||
name: "PODSTAWOWY"
|
||||
p: 75
|
||||
- id: "canal_korzystny"
|
||||
name: "KORZYSTNY"
|
||||
p: 76
|
||||
- id: "canal_bogaty"
|
||||
name: "BOGATY"
|
||||
p: 77
|
||||
# iframe_sets:
|
||||
# - id: "canal_smart"
|
||||
# name: "SMART"
|
||||
# p: 86
|
||||
# - id: "canal_optimum"
|
||||
# name: "OPTIMUM"
|
||||
# p: 87
|
||||
# - id: "canal_platinum"
|
||||
# name: "PLATINUM"
|
||||
# p: 88
|
||||
# - id: "canal_podstawowy"
|
||||
# name: "PODSTAWOWY"
|
||||
# p: 75
|
||||
# - id: "canal_korzystny"
|
||||
# name: "KORZYSTNY"
|
||||
# p: 76
|
||||
# - id: "canal_bogaty"
|
||||
# name: "BOGATY"
|
||||
# p: 77
|
||||
|
||||
Binary file not shown.
@@ -141,13 +141,11 @@ function OfferCard({ plan, contractLabel, onConfigureAddons }) {
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Umowa – już tylko z przełącznika */}
|
||||
<li class="f-card-row">
|
||||
<span class="f-card-label">Umowa</span>
|
||||
<span class="f-card-value">{effectiveContract}</span>
|
||||
</li>
|
||||
|
||||
{/* Aktywacja – z pola plan.price_installation */}
|
||||
<li class="f-card-row">
|
||||
<span class="f-card-label">Aktywacja</span>
|
||||
<span class="f-card-value">
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import "../../styles/offers/offers-table.css"; //
|
||||
|
||||
const BUILDING_MAP = {
|
||||
jednorodzinny: 1,
|
||||
wielorodzinny: 2,
|
||||
};
|
||||
|
||||
const CONTRACT_MAP = {
|
||||
"24m": 1,
|
||||
bezterminowa: 2,
|
||||
};
|
||||
|
||||
export default function JamboxBasePackages({
|
||||
source = "PLUS",
|
||||
title,
|
||||
selected = {},
|
||||
}) {
|
||||
const [packages, setPackages] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
// wyliczamy kody na podstawie tego samego selected, którego używasz w OffersCards
|
||||
const buildingCode = useMemo(() => {
|
||||
const val = selected?.budynek;
|
||||
return BUILDING_MAP[val] ?? 1; // domyślnie jednorodzinny
|
||||
}, [selected?.budynek]);
|
||||
|
||||
const contractCode = useMemo(() => {
|
||||
const val = selected?.umowa;
|
||||
return CONTRACT_MAP[val] ?? 1; // domyślnie 24m
|
||||
}, [selected?.umowa]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (source && source !== "ALL") {
|
||||
params.set("source", source);
|
||||
}
|
||||
if (buildingCode) {
|
||||
params.set("building", String(buildingCode));
|
||||
}
|
||||
if (contractCode) {
|
||||
params.set("contract", String(contractCode));
|
||||
}
|
||||
|
||||
const query = params.toString();
|
||||
const url = `/api/jambox/base-packages${query ? `?${query}` : ""}`;
|
||||
|
||||
const res = await fetch(url);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (!cancelled) {
|
||||
setPackages(Array.isArray(json.data) ? json.data : []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Błąd pobierania pakietów JAMBOX:", err);
|
||||
if (!cancelled) {
|
||||
setError("Nie udało się załadować pakietów JAMBOX.");
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [source, buildingCode, contractCode]);
|
||||
|
||||
const effectiveTitle =
|
||||
title ||
|
||||
(source === "PLUS"
|
||||
? "Pakiety podstawowe JAMBOX PLUS"
|
||||
: source === "EVIO"
|
||||
? "Pakiety podstawowe JAMBOX EVIO"
|
||||
: "Pakiety podstawowe JAMBOX");
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section class="f-offers">
|
||||
<h2 class="f-offers-title">{effectiveTitle}</h2>
|
||||
<p>Ładowanie pakietów...</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<section class="f-offers">
|
||||
<h2 class="f-offers-title">{effectiveTitle}</h2>
|
||||
<p class="text-red-600">{error}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (!packages.length) {
|
||||
return (
|
||||
<section class="f-offers">
|
||||
<h2 class="f-offers-title">{effectiveTitle}</h2>
|
||||
<p>Brak pakietów do wyświetlenia.</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section class="f-offers">
|
||||
{effectiveTitle && <h2 class="f-offers-title">{effectiveTitle}</h2>}
|
||||
|
||||
<div class={`f-offers-grid f-count-${packages.length}`}>
|
||||
{packages.map((pkg) => (
|
||||
<JamboxPackageCard key={`${pkg.source}-${pkg.tid}`} pkg={pkg} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function JamboxPackageCard({ pkg }) {
|
||||
const updatedDate = pkg.updated_at
|
||||
? new Date(pkg.updated_at).toLocaleDateString("pl-PL")
|
||||
: "-";
|
||||
|
||||
const hasPrice = pkg.price_monthly != null;
|
||||
const hasInstall = pkg.price_installation != null;
|
||||
|
||||
return (
|
||||
<div class="f-card">
|
||||
<div class="f-card-header">
|
||||
<div class="f-card-name">{pkg.name}</div>
|
||||
|
||||
{/* TU zamiast JAMBOX PLUS/EVIO pokazujemy cenę */}
|
||||
<div class="f-card-price">
|
||||
{hasPrice
|
||||
? `${pkg.price_monthly} zł/mies.`
|
||||
: pkg.source === "PLUS"
|
||||
? "JAMBOX PLUS"
|
||||
: "JAMBOX EVIO"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="f-card-features">
|
||||
{/* ID / slug jako techniczne info */}
|
||||
<li class="f-card-row">
|
||||
<span class="f-card-label">ID pakietu</span>
|
||||
<span class="f-card-value">{pkg.tid}</span>
|
||||
</li>
|
||||
|
||||
<li class="f-card-row">
|
||||
<span class="f-card-label">Slug</span>
|
||||
<span class="f-card-value">{pkg.slug || "—"}</span>
|
||||
</li>
|
||||
|
||||
{/* nowa linia: cena instalacji z bazy */}
|
||||
<li class="f-card-row">
|
||||
<span class="f-card-label">Aktywacja</span>
|
||||
<span class="f-card-value">
|
||||
{hasInstall ? `${pkg.price_installation} zł` : "—"}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
{/* opcjonalnie informacja o źródle pakietu */}
|
||||
<li class="f-card-row">
|
||||
<span class="f-card-label">Platforma</span>
|
||||
<span class="f-card-value">
|
||||
{pkg.source === "PLUS" ? "JAMBOX PLUS" : "JAMBOX EVIO"}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li class="f-card-row">
|
||||
<span class="f-card-label">Ostatnia aktualizacja</span>
|
||||
<span class="f-card-value">{updatedDate}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { useState } from "preact/hooks";
|
||||
|
||||
import OffersSwitches from "./Offers/OffersSwitches.jsx";
|
||||
import OffersCards from "./Offers/OffersCards.jsx"; // <-- WAŻNE!!
|
||||
import OffersExtraServices from "./Offers/OffersExtraServices.jsx";
|
||||
// import OffersExtraServices from "./Offers/OffersExtraServices.jsx";
|
||||
|
||||
export default function OffersIsland({ data }) {
|
||||
const switches = data.przelaczniki ?? [];
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function RangeForm() {
|
||||
let timeoutStreet = null;
|
||||
|
||||
async function fetchCitySuggestions(q) {
|
||||
const res = await fetch(`/api/cities-autocomplete?q=${encodeURIComponent(q)}`);
|
||||
const res = await fetch(`/api/range/cities-autocomplete?q=${encodeURIComponent(q)}`);
|
||||
setCitySuggest(await res.json());
|
||||
setHighlightIndex(-1);
|
||||
}
|
||||
@@ -102,7 +102,7 @@ export default function RangeForm() {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/has-streets?city=${encodeURIComponent(currentCity)}`);
|
||||
const res = await fetch(`/api/range/has-streets?city=${encodeURIComponent(currentCity)}`);
|
||||
const { hasStreets } = await res.json();
|
||||
|
||||
if (!hasStreets) {
|
||||
@@ -126,7 +126,7 @@ export default function RangeForm() {
|
||||
|
||||
async function fetchStreetSuggestions(q, c) {
|
||||
const res = await fetch(
|
||||
`/api/streets-autocomplete?city=${encodeURIComponent(c)}&q=${encodeURIComponent(q)}`
|
||||
`/api/range/streets-autocomplete?city=${encodeURIComponent(c)}&q=${encodeURIComponent(q)}`
|
||||
);
|
||||
setStreetSuggest(await res.json());
|
||||
setStreetHighlightIndex(-1);
|
||||
@@ -207,7 +207,7 @@ export default function RangeForm() {
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const res = await fetch("/api/search", {
|
||||
const res = await fetch("/api/range/search", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ city, street, number }),
|
||||
|
||||
410
src/islands/jambox/JamboxAddonsModal.jsx
Normal file
410
src/islands/jambox/JamboxAddonsModal.jsx
Normal file
@@ -0,0 +1,410 @@
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import "../../styles/modal.css";
|
||||
import "../../styles/offers/offers-table.css";
|
||||
|
||||
export default function JamboxAddonsModal({ isOpen, onClose, pkg }) {
|
||||
const [phonePlans, setPhonePlans] = useState([]);
|
||||
const [addons, setAddons] = useState([]);
|
||||
|
||||
const [tvAddons, setTvAddons] = useState([]);
|
||||
const [selectedTvAddonTids, setSelectedTvAddonTids] = useState([]);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const [selectedPhoneId, setSelectedPhoneId] = useState(null);
|
||||
const [selectedAddonIds, setSelectedAddonIds] = useState([]);
|
||||
|
||||
// akordeony
|
||||
const [openPhoneId, setOpenPhoneId] = useState(null);
|
||||
const [baseOpen, setBaseOpen] = useState(true);
|
||||
|
||||
const formatFeatureValue = (val) => {
|
||||
if (val === true || val === "true") return "✓";
|
||||
if (val === false || val === "false" || val == null) return "✕";
|
||||
return val;
|
||||
};
|
||||
|
||||
const handlePhoneSelect = (id) => {
|
||||
if (id === null) {
|
||||
setSelectedPhoneId(null);
|
||||
setOpenPhoneId(null);
|
||||
return;
|
||||
}
|
||||
setSelectedPhoneId(id);
|
||||
setOpenPhoneId((prev) => (prev === id ? null : id));
|
||||
};
|
||||
|
||||
const toggleAddon = (addonId) => {
|
||||
setSelectedAddonIds((prev) =>
|
||||
prev.includes(addonId)
|
||||
? prev.filter((x) => x !== addonId)
|
||||
: [...prev, addonId]
|
||||
);
|
||||
};
|
||||
|
||||
// reset po otwarciu / zmianie pakietu
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
setSelectedPhoneId(null);
|
||||
setSelectedAddonIds([]);
|
||||
setOpenPhoneId(null);
|
||||
setBaseOpen(true);
|
||||
setError("");
|
||||
setSelectedTvAddonTids([]);
|
||||
}, [isOpen, pkg?.id]);
|
||||
|
||||
// load danych
|
||||
useEffect(() => {
|
||||
if (!isOpen || !pkg?.id) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function loadData() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
// telefon
|
||||
const phoneRes = await fetch("/api/phone/plans");
|
||||
if (!phoneRes.ok) throw new Error(`HTTP ${phoneRes.status} (phone)`);
|
||||
const phoneJson = await phoneRes.json();
|
||||
const phoneData = Array.isArray(phoneJson.data) ? phoneJson.data : [];
|
||||
|
||||
// dodatki JAMBOX (dla pakietu)
|
||||
const addonsRes = await fetch(`/api/jambox/addons?packageId=${pkg.id}`);
|
||||
if (!addonsRes.ok) throw new Error(`HTTP ${addonsRes.status} (addons)`);
|
||||
const addonsJson = await addonsRes.json();
|
||||
const addonsData = Array.isArray(addonsJson.data) ? addonsJson.data : [];
|
||||
|
||||
const tvRes = await fetch(`/api/jambox/tv-addons?packageId=${pkg.id}`);
|
||||
if (!tvRes.ok) throw new Error(`HTTP ${tvRes.status} (tv-addons)`);
|
||||
|
||||
const tvJson = await tvRes.json();
|
||||
const tvData = Array.isArray(tvJson.data) ? tvJson.data : [];
|
||||
|
||||
if (!cancelled) {
|
||||
setPhonePlans(phoneData);
|
||||
setAddons(addonsData);
|
||||
setTvAddons(tvData);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd ładowania danych do JamboxAddonsModal:", err);
|
||||
if (!cancelled) setError("Nie udało się załadować danych dodatkowych usług.");
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
loadData();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isOpen, pkg?.id]);
|
||||
|
||||
if (!isOpen || !pkg) return null;
|
||||
|
||||
const basePrice = Number(pkg.price_monthly || 0);
|
||||
|
||||
const phonePrice = useMemo(() => {
|
||||
if (!selectedPhoneId) return 0;
|
||||
const p = phonePlans.find((x) => x.id === selectedPhoneId);
|
||||
return Number(p?.price_monthly || 0);
|
||||
}, [selectedPhoneId, phonePlans]);
|
||||
|
||||
// UWAGA: backend może zwrócić { id, price } albo { addon_id, price }
|
||||
const addonsPrice = useMemo(() => {
|
||||
return selectedAddonIds.reduce((sum, addonId) => {
|
||||
const a = addons.find((x) => (x.id ?? x.addon_id) === addonId);
|
||||
return sum + Number(a?.price || 0);
|
||||
}, 0);
|
||||
}, [selectedAddonIds, addons]);
|
||||
|
||||
|
||||
|
||||
const tvAddonsPrice = useMemo(() => {
|
||||
return selectedTvAddonTids.reduce((sum, tid) => {
|
||||
const a = tvAddons.find((x) => Number(x.tid) === Number(tid));
|
||||
return sum + Number(a?.price || 0);
|
||||
}, 0);
|
||||
}, [selectedTvAddonTids, tvAddons]);
|
||||
|
||||
const totalMonthly = basePrice + phonePrice + addonsPrice + tvAddonsPrice;
|
||||
|
||||
const toggleTvAddon = (tid) => {
|
||||
const t = Number(tid);
|
||||
setSelectedTvAddonTids((prev) =>
|
||||
prev.includes(t) ? prev.filter((x) => x !== t) : [...prev, t]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="fuz-modal-overlay" onClick={onClose}>
|
||||
<button
|
||||
class="fuz-modal-close"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="fuz-modal-panel fuz-modal-panel--compact"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div class="fuz-modal-inner">
|
||||
<h2 class="fuz-modal-title">Konfiguracja usług dodatkowych</h2>
|
||||
|
||||
{/* PAKIET JAMBOX jako akordeon (jak internet w Twoim modalu) */}
|
||||
<div class="fuz-modal-section">
|
||||
<div class={`fuz-accordion-item ${baseOpen ? "is-open" : ""}`}>
|
||||
<button
|
||||
type="button"
|
||||
class="fuz-accordion-header"
|
||||
onClick={() => setBaseOpen((prev) => !prev)}
|
||||
>
|
||||
<span class="fuz-modal-phone-name">{pkg.name}</span>
|
||||
<span class="fuz-modal-phone-price">
|
||||
{basePrice ? `${basePrice.toFixed(2)} zł/mies.` : "—"}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{baseOpen && pkg.features && pkg.features.length > 0 && (
|
||||
<div class="fuz-accordion-body">
|
||||
<ul class="f-card-features">
|
||||
{pkg.features.map((f, idx) => (
|
||||
<li class="f-card-row" key={idx}>
|
||||
<span class="f-card-label">{f.label}</span>
|
||||
<span class="f-card-value">
|
||||
{formatFeatureValue(f.value)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading && <p>Ładowanie danych...</p>}
|
||||
{error && <p class="text-red-600">{error}</p>}
|
||||
|
||||
{!loading && !error && (
|
||||
<>
|
||||
|
||||
<div class="fuz-modal-section">
|
||||
<h3>Pakiety dodatkowe TV</h3>
|
||||
|
||||
{tvAddons.length === 0 ? (
|
||||
<p>Brak pakietów dodatkowych TV dla tego pakietu.</p>
|
||||
) : (
|
||||
<div class="fuz-addon-list">
|
||||
{tvAddons.map((a) => {
|
||||
const tid = Number(a.tid);
|
||||
const checked = selectedTvAddonTids.includes(tid);
|
||||
const priceNum = Number(a.price || 0);
|
||||
|
||||
return (
|
||||
<label class="fuz-addon-item" key={`tv-${tid}`}>
|
||||
<div class="fuz-addon-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={() => toggleTvAddon(tid)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="fuz-addon-main">
|
||||
<div class="fuz-addon-name">{a.name}</div>
|
||||
{/* jeśli chcesz pokazać typ/kind */}
|
||||
<div class="fuz-addon-desc">{a.description}</div>
|
||||
</div>
|
||||
|
||||
<div class="fuz-addon-price">
|
||||
{Number.isFinite(priceNum) ? `${priceNum.toFixed(2)} zł/mies.` : "—"}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* TELEFON (identycznie jak w InternetAddonsModal) */}
|
||||
<div class="fuz-modal-section">
|
||||
<h3>Usługa telefoniczna</h3>
|
||||
|
||||
{phonePlans.length === 0 ? (
|
||||
<p>Brak dostępnych pakietów telefonicznych.</p>
|
||||
) : (
|
||||
<div class="fuz-modal-phone-list fuz-accordion">
|
||||
{/* OPCJA: brak telefonu */}
|
||||
<div class="fuz-accordion-item fuz-accordion-item--no-phone">
|
||||
<button
|
||||
type="button"
|
||||
class="fuz-accordion-header"
|
||||
onClick={() => handlePhoneSelect(null)}
|
||||
>
|
||||
<span class="fuz-accordion-header-left">
|
||||
<input
|
||||
type="radio"
|
||||
name="phone-plan"
|
||||
checked={selectedPhoneId === null}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePhoneSelect(null);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<span class="fuz-modal-phone-name">
|
||||
Nie potrzebuję telefonu
|
||||
</span>
|
||||
</span>
|
||||
<span class="fuz-modal-phone-price">0,00 zł/mies.</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* PAKIETY TELEFONU */}
|
||||
{phonePlans.map((p) => {
|
||||
const isSelected = selectedPhoneId === p.id;
|
||||
const isOpen = openPhoneId === p.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
class={`fuz-accordion-item ${isOpen ? "is-open" : ""}`}
|
||||
key={p.id}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="fuz-accordion-header"
|
||||
onClick={() => handlePhoneSelect(p.id)}
|
||||
>
|
||||
<span class="fuz-accordion-header-left">
|
||||
<input
|
||||
type="radio"
|
||||
name="phone-plan"
|
||||
checked={isSelected}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePhoneSelect(p.id);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<span class="fuz-modal-phone-name">{p.name}</span>
|
||||
</span>
|
||||
<span class="fuz-modal-phone-price">
|
||||
{Number(p.price_monthly || 0).toFixed(2)} zł/mies.
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div class="fuz-accordion-body">
|
||||
{p.features && p.features.length > 0 && (
|
||||
<ul class="f-card-features">
|
||||
{p.features
|
||||
.filter(
|
||||
(f) =>
|
||||
!String(f.label || "")
|
||||
.toLowerCase()
|
||||
.includes("aktyw")
|
||||
)
|
||||
.map((f, idx) => (
|
||||
<li class="f-card-row" key={idx}>
|
||||
<span class="f-card-label">{f.label}</span>
|
||||
<span class="f-card-value">{f.value}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* DODATKI JAMBOX (checkbox, cena z jambox_package_addon_options.price) */}
|
||||
<div class="fuz-modal-section">
|
||||
<h3>Dodatkowe usługi</h3>
|
||||
|
||||
{addons.length === 0 ? (
|
||||
<p>Brak usług dodatkowych dla tego pakietu.</p>
|
||||
) : (
|
||||
<div class="fuz-addon-list">
|
||||
{addons.map((a) => {
|
||||
const addonId = a.id ?? a.addon_id; // defensywnie
|
||||
const checked = selectedAddonIds.includes(addonId);
|
||||
|
||||
return (
|
||||
<label class="fuz-addon-item" key={addonId}>
|
||||
<div class="fuz-addon-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={() => toggleAddon(addonId)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="fuz-addon-main">
|
||||
<div class="fuz-addon-name">{a.name}</div>
|
||||
{a.description && (
|
||||
<div class="fuz-addon-desc">{a.description}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="fuz-addon-price">
|
||||
{Number(a.price || 0).toFixed(2)} zł/mies.
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* PODSUMOWANIE */}
|
||||
<div class="fuz-modal-section fuz-summary">
|
||||
<h3>Podsumowanie miesięczne</h3>
|
||||
|
||||
<div class="fuz-summary-list">
|
||||
<div class="fuz-summary-row">
|
||||
<span>Pakiet</span>
|
||||
<span>
|
||||
{basePrice ? `${basePrice.toFixed(2)} zł/mies.` : "—"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="fuz-summary-row">
|
||||
<span>Pakiety TV</span>
|
||||
<span>
|
||||
{tvAddonsPrice ? `${tvAddonsPrice.toFixed(2)} zł/mies.` : "—"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="fuz-summary-row">
|
||||
<span>Telefon</span>
|
||||
<span>{phonePrice ? `${phonePrice.toFixed(2)} zł/mies.` : "—"}</span>
|
||||
</div>
|
||||
|
||||
<div class="fuz-summary-row">
|
||||
<span>Dodatki</span>
|
||||
<span>{addonsPrice ? `${addonsPrice.toFixed(2)} zł/mies.` : "—"}</span>
|
||||
</div>
|
||||
|
||||
<div class="fuz-summary-total">
|
||||
<span>Łącznie</span>
|
||||
<span>{totalMonthly.toFixed(2)} zł/mies.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
154
src/islands/jambox/JamboxChannelsModal.jsx
Normal file
154
src/islands/jambox/JamboxChannelsModal.jsx
Normal file
@@ -0,0 +1,154 @@
|
||||
// src/islands/JamboxChannelsModal.jsx
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "../../styles/modal.css";
|
||||
import "../../styles/offers/offers-table.css";
|
||||
|
||||
export default function JamboxChannelsModal({ isOpen, onClose, pkg }) {
|
||||
const [channels, setChannels] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const q = query.trim().toLowerCase();
|
||||
|
||||
const filtered = !q
|
||||
? channels
|
||||
: channels.filter((ch) =>
|
||||
(ch.name || "").toLowerCase().includes(q)
|
||||
);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !pkg?.id) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function loadChannels() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
setChannels([]);
|
||||
setQuery("");
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({ packageId: String(pkg.id) });
|
||||
const res = await fetch(`/api/jambox/channels?${params.toString()}`);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
if (!cancelled) {
|
||||
setChannels(Array.isArray(json.data) ? json.data : []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd pobierania listy kanałów:", err);
|
||||
if (!cancelled) {
|
||||
setError("Nie udało się załadować listy kanałów.");
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
loadChannels();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isOpen, pkg?.id]);
|
||||
|
||||
if (!isOpen || !pkg) return null;
|
||||
|
||||
return (
|
||||
<div class="fuz-modal-overlay" onClick={onClose}>
|
||||
<button
|
||||
class="fuz-modal-close"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="fuz-modal-panel fuz-modal-panel--channels"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div class="">
|
||||
<h2 class="fuz-modal-title">Kanały w pakiecie {pkg.name}</h2>
|
||||
|
||||
<div class="jmb-search">
|
||||
<input
|
||||
class="jmb-search-input"
|
||||
type="search"
|
||||
value={query}
|
||||
onInput={(e) => setQuery(e.currentTarget.value)}
|
||||
placeholder="Szukaj kanału po nazwie…"
|
||||
aria-label="Szukaj kanału po nazwie"
|
||||
/>
|
||||
{query && (
|
||||
<button class="jmb-search-clear" type="button" onClick={() => setQuery("")}>
|
||||
Wyczyść
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!loading && !error && (
|
||||
<div class="jmb-search-meta">
|
||||
Wyniki: <strong>{filtered.length}</strong> / {channels.length}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && <p>Ładowanie kanałów...</p>}
|
||||
{error && <p class="text-red-600">{error}</p>}
|
||||
|
||||
{!loading && !error && (
|
||||
<>
|
||||
{filtered.length === 0 ? (
|
||||
<p>Brak kanałów spełniających kryteria.</p>
|
||||
) : (
|
||||
<div class="">
|
||||
<div class="f-section-channel">
|
||||
{filtered.map((ch) => (
|
||||
<div class="jmb-channel-card" key={ch.number}>
|
||||
<div class="jmb-channel-inner">
|
||||
{/* FRONT */}
|
||||
<div class="jmb-channel-face jmb-channel-front">
|
||||
{ch.logo_url && (
|
||||
<img
|
||||
src={ch.logo_url}
|
||||
alt={ch.name}
|
||||
class="jmb-channel-logo"
|
||||
loading="lazy"
|
||||
/>
|
||||
)}
|
||||
<div class="jmb-channel-name">{ch.name}</div>
|
||||
<div class="jmb-channel-number">kanał {ch.number}</div>
|
||||
</div>
|
||||
|
||||
{/* BACK */}
|
||||
<div class="jmb-channel-face jmb-channel-back">
|
||||
<div class="jmb-channel-back-title">{ch.name}</div>
|
||||
<div
|
||||
class="jmb-channel-desc"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: ch.description || "<em>Brak opisu kanału.</em>",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
187
src/islands/jambox/OffersJamboxCards.jsx
Normal file
187
src/islands/jambox/OffersJamboxCards.jsx
Normal file
@@ -0,0 +1,187 @@
|
||||
// src/islands/JamboxBasePackages.jsx
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "../../styles/offers/offers-table.css";
|
||||
import JamboxChannelsModal from "./JamboxChannelsModal.jsx";
|
||||
import JamboxAddonsModal from "./JamboxAddonsModal.jsx";
|
||||
|
||||
export default function JamboxBasePackages({ source = "ALL" }) {
|
||||
const [selected, setSelected] = useState({});
|
||||
const [packages, setPackages] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const [channelsModalOpen, setChannelsModalOpen] = useState(false);
|
||||
const [addonsModalOpen, setAddonsModalOpen] = useState(false);
|
||||
|
||||
const [activePackage, setActivePackage] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined" && window.fuzSwitchState) {
|
||||
const { selected: sel } = window.fuzSwitchState;
|
||||
if (sel) setSelected(sel);
|
||||
}
|
||||
|
||||
function handler(e) {
|
||||
const detail = e.detail || {};
|
||||
if (detail.selected) setSelected(detail.selected);
|
||||
}
|
||||
|
||||
window.addEventListener("fuz:switch-change", handler);
|
||||
return () => window.removeEventListener("fuz:switch-change", handler);
|
||||
}, []);
|
||||
|
||||
const buildingCode = Number(selected.budynek) || 1;
|
||||
const contractCode = Number(selected.umowa) || 1;
|
||||
|
||||
useEffect(() => {
|
||||
if (!buildingCode || !contractCode) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.set("source", String(source || "ALL"));
|
||||
params.set("building", String(buildingCode));
|
||||
params.set("contract", String(contractCode));
|
||||
|
||||
const res = await fetch(`/api/jambox/base-packages?${params.toString()}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
const json = await res.json();
|
||||
if (!cancelled) setPackages(Array.isArray(json.data) ? json.data : []);
|
||||
} catch (err) {
|
||||
console.error("Błąd pobierania pakietów JAMBOX:", err);
|
||||
if (!cancelled) setError("Nie udało się załadować pakietów JAMBOX.");
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [buildingCode, contractCode, source]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section class="f-offers">
|
||||
<p>Ładowanie pakietów...</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<section class="f-offers">
|
||||
<p class="text-red-600">{error}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (!packages.length) {
|
||||
return (
|
||||
<section class="f-offers">
|
||||
<p>Brak pakietów do wyświetlenia.</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section class="f-offers">
|
||||
<div class={`f-offers-grid f-count-${packages.length || 1}`}>
|
||||
{packages.map((pkg) => (
|
||||
<JamboxPackageCard
|
||||
key={pkg.id}
|
||||
pkg={pkg}
|
||||
onShowChannels={() => {
|
||||
setActivePackage(pkg);
|
||||
setChannelsModalOpen(true);
|
||||
}}
|
||||
onConfigureAddons={() => {
|
||||
setActivePackage(pkg);
|
||||
setAddonsModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<JamboxChannelsModal
|
||||
isOpen={channelsModalOpen}
|
||||
onClose={() => setChannelsModalOpen(false)}
|
||||
pkg={activePackage}
|
||||
/>
|
||||
|
||||
<JamboxAddonsModal
|
||||
isOpen={addonsModalOpen}
|
||||
onClose={() => setAddonsModalOpen(false)}
|
||||
pkg={activePackage}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function JamboxPackageCard({ pkg, onShowChannels, onConfigureAddons }) {
|
||||
const basePrice = pkg.price_monthly;
|
||||
const installPrice = pkg.price_installation;
|
||||
|
||||
const featureRows = pkg.features || [];
|
||||
const hasPrice = basePrice != null;
|
||||
|
||||
return (
|
||||
<div class="f-card">
|
||||
<div class="f-card-header">
|
||||
<div class="f-card-name">{pkg.name}</div>
|
||||
|
||||
<div class="f-card-price">
|
||||
{hasPrice
|
||||
? `${basePrice} zł/mies.`
|
||||
: pkg.source === "PLUS"
|
||||
? "JAMBOX PLUS"
|
||||
: "JAMBOX EVIO"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="f-card-features">
|
||||
{featureRows.map((f, idx) => {
|
||||
let val = f.value;
|
||||
let display;
|
||||
|
||||
if (val === true || val === "true") display = "✓";
|
||||
else if (val === false || val === "false" || val == null) display = "✕";
|
||||
else display = val;
|
||||
|
||||
return (
|
||||
<li class="f-card-row" key={idx}>
|
||||
<span class="f-card-label">{f.label}</span>
|
||||
<span class="f-card-value">{display}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
<li class="f-card-row">
|
||||
<span class="f-card-label">Aktywacja</span>
|
||||
<span class="f-card-value">
|
||||
{installPrice != null ? `${installPrice} zł` : "—"}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button type="button" class="btn btn-primary mt-2" onClick={onShowChannels}>
|
||||
Pokaż listę kanałów
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary mt-2"
|
||||
onClick={onConfigureAddons}
|
||||
>
|
||||
Skonfiguruj usługi dodatkowe
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "../styles/offers/offers-table.css";
|
||||
import "../../styles/offers/offers-table.css";
|
||||
|
||||
export default function PhoneDbOffersCards({
|
||||
title = "Telefonia stacjonarna FUZ",
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { getDb } from "./db";
|
||||
|
||||
type CityRow = { city: string };
|
||||
|
||||
export const GET: APIRoute = async () => {
|
||||
const db = getDb();
|
||||
const rows = db.prepare("SELECT DISTINCT city FROM ranges ORDER BY city").all() as CityRow[];
|
||||
|
||||
return new Response(JSON.stringify(rows.map((x) => x.city)), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import Database from "better-sqlite3";
|
||||
const DB_PATH =
|
||||
process.env.FUZ_DB_PATH || "./src/data/ServicesRange.db";
|
||||
|
||||
let db: Database.Database;
|
||||
let db;
|
||||
|
||||
export function getDb() {
|
||||
if (!db) {
|
||||
54
src/pages/api/jambox/addons.js
Normal file
54
src/pages/api/jambox/addons.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const DB_PATH = process.env.FUZ_DB_PATH || "./src/data/ServicesRange.db";
|
||||
|
||||
export async function GET({ url }) {
|
||||
const db = new Database(DB_PATH, { readonly: true });
|
||||
|
||||
try {
|
||||
const packageId = Number(url.searchParams.get("packageId") || 0);
|
||||
if (!packageId) {
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: "MISSING_PACKAGE_ID" }),
|
||||
{ status: 400, headers: { "Content-Type": "application/json; charset=utf-8" } }
|
||||
);
|
||||
}
|
||||
|
||||
const rows = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
a.id AS id,
|
||||
a.name AS name,
|
||||
a.type AS type,
|
||||
CAST(o.price AS REAL) AS price
|
||||
FROM jambox_package_addon_options o
|
||||
JOIN internet_addons a
|
||||
ON a.id = o.addon_id
|
||||
WHERE o.package_id = ?
|
||||
ORDER BY a.type, a.name
|
||||
`
|
||||
)
|
||||
.all(packageId);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
count: rows.length,
|
||||
data: rows,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd w /api/jambox/addons:", err);
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: err.message || "DB_ERROR" }),
|
||||
{ status: 500, headers: { "Content-Type": "application/json; charset=utf-8" } }
|
||||
);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
// src/pages/api/jambox/base-packages.js
|
||||
//import { getDb } from "../db.js";
|
||||
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const DB_PATH =
|
||||
@@ -8,136 +10,108 @@ function getDb() {
|
||||
return new Database(DB_PATH, { readonly: true });
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* GET /api/jambox/base-packages
|
||||
* ?source=PLUS|EVIO
|
||||
* &building=1|2 (1=jednorodzinny, 2=wielorodzinny)
|
||||
* &contract=1|2 (1=24m, 2=bezterminowa)
|
||||
* GET /api/jambox/base-packages?source=PLUS|EVIO|ALL&building=1|2&contract=1|2
|
||||
*/
|
||||
export function GET({ url }) {
|
||||
const sourceParam = url.searchParams.get("source");
|
||||
const source = sourceParam ? sourceParam.toUpperCase() : null;
|
||||
|
||||
const sourceParam = url.searchParams.get("source") || "PLUS";
|
||||
const buildingParam = url.searchParams.get("building");
|
||||
const contractParam = url.searchParams.get("contract");
|
||||
const building = buildingParam ? Number(buildingParam) : null;
|
||||
const contract = contractParam ? Number(contractParam) : null;
|
||||
|
||||
const building = buildingParam ? Number(buildingParam) : 1;
|
||||
const contract = contractParam ? Number(contractParam) : 1;
|
||||
|
||||
const db = getDb();
|
||||
|
||||
try {
|
||||
let rows = [];
|
||||
const hasVariant =
|
||||
Number.isInteger(building) && Number.isInteger(contract);
|
||||
const rows = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
p.id AS package_id,
|
||||
p.source AS package_source,
|
||||
p.tid AS package_tid,
|
||||
p.name AS package_name,
|
||||
p.slug AS package_slug,
|
||||
p.sort_order AS package_sort_order,
|
||||
p.updated_at AS package_updated_at,
|
||||
|
||||
pr.price_monthly AS price_monthly,
|
||||
pr.price_installation AS price_installation,
|
||||
|
||||
f.id AS feature_id,
|
||||
f.label AS feature_label,
|
||||
fv.value AS feature_value
|
||||
|
||||
if (source === "PLUS" || source === "EVIO") {
|
||||
if (hasVariant) {
|
||||
// pakiety + ceny dla danego budynku/umowy
|
||||
const stmt = db.prepare(
|
||||
`
|
||||
SELECT
|
||||
p.id,
|
||||
p.source,
|
||||
p.tid,
|
||||
p.name,
|
||||
p.slug,
|
||||
p.sort_order,
|
||||
p.updated_at,
|
||||
pr.price_monthly,
|
||||
pr.price_installation,
|
||||
pr.currency
|
||||
FROM jambox_base_packages p
|
||||
|
||||
LEFT JOIN jambox_base_package_prices pr
|
||||
ON pr.package_id = p.id
|
||||
AND pr.building_type = ?
|
||||
AND pr.contract_type = ?
|
||||
WHERE p.source = ?
|
||||
ORDER BY p.sort_order ASC, p.name ASC;
|
||||
|
||||
LEFT JOIN jambox_package_feature_values fv
|
||||
ON fv.package_id = p.id
|
||||
|
||||
LEFT JOIN internet_features f
|
||||
ON f.id = fv.feature_id
|
||||
|
||||
WHERE (? = 'ALL' OR p.source = ?)
|
||||
ORDER BY p.sort_order ASC, p.id ASC, f.id ASC;
|
||||
`.trim()
|
||||
);
|
||||
rows = stmt.all(building, contract, source);
|
||||
} else {
|
||||
// tylko pakiety, bez cen
|
||||
const stmt = db.prepare(
|
||||
`
|
||||
SELECT
|
||||
p.id,
|
||||
p.source,
|
||||
p.tid,
|
||||
p.name,
|
||||
p.slug,
|
||||
p.sort_order,
|
||||
p.updated_at
|
||||
FROM jambox_base_packages p
|
||||
WHERE p.source = ?
|
||||
ORDER BY p.sort_order ASC, p.name ASC;
|
||||
`.trim()
|
||||
);
|
||||
rows = stmt.all(source);
|
||||
)
|
||||
.all(building, contract, sourceParam, sourceParam);
|
||||
|
||||
// grupowanie jak w /api/internet/plans
|
||||
const byPackage = new Map();
|
||||
|
||||
for (const row of rows) {
|
||||
if (!byPackage.has(row.package_id)) {
|
||||
byPackage.set(row.package_id, {
|
||||
id: row.package_id,
|
||||
source: row.package_source,
|
||||
tid: row.package_tid,
|
||||
name: row.package_name,
|
||||
slug: row.package_slug,
|
||||
sort_order: row.package_sort_order,
|
||||
updated_at: row.package_updated_at,
|
||||
price_monthly: row.price_monthly,
|
||||
price_installation: row.price_installation,
|
||||
features: [],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// bez filtra source (raczej nie użyjesz, ale niech będzie poprawnie)
|
||||
if (hasVariant) {
|
||||
const stmt = db.prepare(
|
||||
`
|
||||
SELECT
|
||||
p.id,
|
||||
p.source,
|
||||
p.tid,
|
||||
p.name,
|
||||
p.slug,
|
||||
p.sort_order,
|
||||
p.updated_at,
|
||||
pr.price_monthly,
|
||||
pr.price_installation,
|
||||
pr.currency
|
||||
FROM jambox_base_packages p
|
||||
LEFT JOIN jambox_base_package_prices pr
|
||||
ON pr.package_id = p.id
|
||||
AND pr.building_type = ?
|
||||
AND pr.contract_type = ?
|
||||
ORDER BY p.source ASC, p.sort_order ASC, p.name ASC;
|
||||
`.trim()
|
||||
);
|
||||
rows = stmt.all(building, contract);
|
||||
} else {
|
||||
const stmt = db.prepare(
|
||||
`
|
||||
SELECT
|
||||
p.id,
|
||||
p.source,
|
||||
p.tid,
|
||||
p.name,
|
||||
p.slug,
|
||||
p.sort_order,
|
||||
p.updated_at
|
||||
FROM jambox_base_packages p
|
||||
ORDER BY p.source ASC, p.sort_order ASC, p.name ASC;
|
||||
`.trim()
|
||||
);
|
||||
rows = stmt.all();
|
||||
|
||||
if (row.feature_id) {
|
||||
byPackage.get(row.package_id).features.push({
|
||||
id: row.feature_id,
|
||||
label: row.feature_label,
|
||||
value: row.feature_value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const data = Array.from(byPackage.values());
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
source: source ?? "ALL",
|
||||
source: sourceParam,
|
||||
building,
|
||||
contract,
|
||||
count: rows.length,
|
||||
data: rows,
|
||||
count: data.length,
|
||||
data,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Cache-Control": "public, max-age=60",
|
||||
"Cache-Control": "public, max-age=30",
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd odczytu z bazy jambox_base_packages:", err);
|
||||
console.error("❌ Błąd w /api/jambox/base-packages:", err);
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: "DB_ERROR" }),
|
||||
{
|
||||
@@ -147,7 +121,5 @@ export function GET({ url }) {
|
||||
},
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
64
src/pages/api/jambox/channels.js
Normal file
64
src/pages/api/jambox/channels.js
Normal file
@@ -0,0 +1,64 @@
|
||||
// src/pages/api/jambox/channels.js
|
||||
//import { getDb } from "../db.js";
|
||||
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const DB_PATH =
|
||||
process.env.FUZ_DB_PATH || "./src/data/ServicesRange.db";
|
||||
|
||||
function getDb() {
|
||||
return new Database(DB_PATH, { readonly: true });
|
||||
}
|
||||
|
||||
|
||||
export function GET({ url }) {
|
||||
const packageIdParam = url.searchParams.get("packageId");
|
||||
const packageId = Number(packageIdParam);
|
||||
|
||||
if (!packageId) {
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: "INVALID_PACKAGE_ID" }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
try {
|
||||
const rows = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
number,
|
||||
name,
|
||||
description,
|
||||
logo_url,
|
||||
guaranteed
|
||||
FROM jambox_package_channels
|
||||
WHERE package_id = ?
|
||||
ORDER BY number ASC;
|
||||
`.trim()
|
||||
)
|
||||
.all(packageId);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ ok: true, data: rows }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd w /api/jambox/channels:", err);
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: "DB_ERROR" }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
51
src/pages/api/jambox/tv-addons.js
Normal file
51
src/pages/api/jambox/tv-addons.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const DB_PATH = process.env.FUZ_DB_PATH || "./src/data/ServicesRange.db";
|
||||
|
||||
export async function GET({ url }) {
|
||||
const db = new Database(DB_PATH, { readonly: true });
|
||||
|
||||
try {
|
||||
const packageId = Number(url.searchParams.get("packageId") || 0);
|
||||
if (!packageId) {
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: "MISSING_PACKAGE_ID" }),
|
||||
{ status: 400, headers: { "Content-Type": "application/json; charset=utf-8" } }
|
||||
);
|
||||
}
|
||||
|
||||
const rows = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
a.tid AS tid,
|
||||
a.name AS name,
|
||||
a.kind AS kind,
|
||||
a.is_active AS is_active,
|
||||
CAST(p.price AS REAL) AS price,
|
||||
p.currency AS currency,
|
||||
a.description AS description
|
||||
FROM jambox_tv_addon_prices p
|
||||
JOIN jambox_tv_addons a
|
||||
ON a.tid = p.addon_tid
|
||||
WHERE p.package_id = ?
|
||||
AND a.is_active = 1
|
||||
ORDER BY a.kind, a.name
|
||||
`
|
||||
)
|
||||
.all(packageId);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ ok: true, count: rows.length, data: rows }),
|
||||
{ status: 200, headers: { "Content-Type": "application/json; charset=utf-8" } }
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd w /api/jambox/tv-addons:", err);
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: err.message || "DB_ERROR" }),
|
||||
{ status: 500, headers: { "Content-Type": "application/json; charset=utf-8" } }
|
||||
);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const DB_PATH =
|
||||
process.env.FUZ_DB_PATH || "./src/data/ServicesRange.db";
|
||||
const DB_PATH = process.env.FUZ_DB_PATH || "./src/data/ServicesRange.db";
|
||||
|
||||
export async function GET() {
|
||||
const db = new Database(DB_PATH, { readonly: true });
|
||||
@@ -69,7 +68,7 @@ export async function GET() {
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd w /api/phone/plans:", err);
|
||||
console.error("Błąd w /api/phone/plans:", err);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
ok: false,
|
||||
|
||||
15
src/pages/api/range/all-cities.js
Normal file
15
src/pages/api/range/all-cities.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { getDb } from "../db.js";
|
||||
|
||||
export async function GET() {
|
||||
const db = getDb();
|
||||
|
||||
const rows = db
|
||||
.prepare("SELECT DISTINCT city FROM ranges ORDER BY city")
|
||||
.all();
|
||||
|
||||
const cities = rows.map((row) => row.city);
|
||||
|
||||
return new Response(JSON.stringify(cities), {
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { getDb } from "./db";
|
||||
import { getDb } from "../db.js";
|
||||
|
||||
function normalize(s: string) {
|
||||
function normalize(s) {
|
||||
return s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
|
||||
}
|
||||
|
||||
type CityRow = { city: string };
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
export async function GET({ request }) {
|
||||
const q = new URL(request.url).searchParams.get("q")?.trim() || "";
|
||||
|
||||
if (q.length < 2)
|
||||
return new Response("[]", { headers: { "Content-Type": "application/json" } });
|
||||
if (q.length < 2) {
|
||||
return new Response("[]", {
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
const rows = db.prepare(`SELECT DISTINCT city FROM ranges`).all() as CityRow[];
|
||||
const rows = db.prepare(`SELECT DISTINCT city FROM ranges`).all();
|
||||
|
||||
const nq = normalize(q);
|
||||
|
||||
@@ -25,6 +25,6 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
.map((r) => r.city);
|
||||
|
||||
return new Response(JSON.stringify(result), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { getDb } from "./db";
|
||||
import { getDb } from "../db.js";
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
export async function GET({ request }) {
|
||||
const city = new URL(request.url).searchParams.get("city")?.trim() || "";
|
||||
|
||||
if (!city)
|
||||
if (!city) {
|
||||
return new Response(JSON.stringify({ hasStreets: false }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
@@ -18,10 +18,12 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
WHERE LOWER(city) = LOWER(?)
|
||||
AND TRIM(street) <> ''`
|
||||
)
|
||||
.get(city) as { cnt: number };
|
||||
.get(city);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ hasStreets: row.cnt > 0 }),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
{
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { getDb } from "./db";
|
||||
// src/pages/api/range/check-address.js (przykładowa nazwa)
|
||||
import { getDb } from "../db.js";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
export async function POST({ request }) {
|
||||
const { city, street, number } = await request.json();
|
||||
|
||||
const db = getDb();
|
||||
@@ -17,12 +17,13 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
)
|
||||
.get(city, street || "", number || "");
|
||||
|
||||
if (!row)
|
||||
if (!row) {
|
||||
return new Response(JSON.stringify({ ok: false }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ ok: true, result: row }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
};
|
||||
}
|
||||
40
src/pages/api/range/streets-autocomplete.js
Normal file
40
src/pages/api/range/streets-autocomplete.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { getDb } from "../db.js";
|
||||
|
||||
function normalize(s) {
|
||||
return s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
|
||||
}
|
||||
|
||||
export async function GET({ request }) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
const city = url.searchParams.get("city")?.trim() || "";
|
||||
const q = url.searchParams.get("q")?.trim() || "";
|
||||
|
||||
if (!city || q.length < 1) {
|
||||
return new Response("[]", {
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT DISTINCT street
|
||||
FROM ranges
|
||||
WHERE LOWER(city) = LOWER(?)
|
||||
AND TRIM(street) <> ''`
|
||||
)
|
||||
.all(city);
|
||||
|
||||
const pattern = normalize(q);
|
||||
|
||||
const filtered = rows
|
||||
.map((r) => r.street)
|
||||
.filter((street) => normalize(street).includes(pattern))
|
||||
.slice(0, 20);
|
||||
|
||||
return new Response(JSON.stringify(filtered), {
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { getDb } from "./db";
|
||||
|
||||
function normalize(s: string) {
|
||||
return s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
|
||||
}
|
||||
|
||||
type StreetRow = { street: string };
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
|
||||
const city = url.searchParams.get("city")?.trim() || "";
|
||||
const q = url.searchParams.get("q")?.trim() || "";
|
||||
|
||||
if (!city || q.length < 1)
|
||||
return new Response("[]", { headers: { "Content-Type": "application/json" } });
|
||||
|
||||
const db = getDb();
|
||||
|
||||
const rows = db.prepare(
|
||||
`SELECT DISTINCT street
|
||||
FROM ranges
|
||||
WHERE LOWER(city) = LOWER(?)
|
||||
AND TRIM(street) <> ''`
|
||||
).all(city) as StreetRow[];
|
||||
|
||||
const pattern = normalize(q);
|
||||
|
||||
const filtered = rows
|
||||
.map((s) => s.street)
|
||||
.filter((s) => normalize(s).includes(pattern))
|
||||
.slice(0, 20);
|
||||
|
||||
return new Response(JSON.stringify(filtered), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
};
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||
import OffersSwitches from "../../islands/Offers/OffersSwitches.jsx";
|
||||
import OffersJamboxCards from "../../islands/jambox/OffersJamboxCards.jsx";
|
||||
|
||||
import Hero from "../../components/hero/Hero.astro";
|
||||
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
||||
import Markdown from "../../islands/Markdown.jsx";
|
||||
import Modal from "../../islands/Modal.jsx";
|
||||
import OffersIsland from "../../islands/OffersIsland.jsx";
|
||||
import JamboxMozliwosci from "../../components/sections/SectionJamboxMozliwosci.astro";
|
||||
import JamboxBasePackages from "../../islands/Offers/JamboxBasePackages.jsx";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import fs from "fs";
|
||||
@@ -21,7 +22,7 @@ const page = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-telewizja/page.yaml", "utf8"),
|
||||
);
|
||||
const modalData = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-telewizja/modal.yaml", "utf8")
|
||||
fs.readFileSync("./src/content/internet-telewizja/modal.yaml", "utf8"),
|
||||
);
|
||||
|
||||
type Paragraph = {
|
||||
@@ -37,31 +38,30 @@ const rest = page.paragraphs.slice(1);
|
||||
---
|
||||
|
||||
<DefaultLayout seo={seo}>
|
||||
<Hero {...hero} />
|
||||
<!-- <Hero {...hero} /> -->
|
||||
|
||||
<section class="f-section">
|
||||
<!-- <section class="f-section">
|
||||
<div class="f-section-grid-single md:grid-cols-1">
|
||||
{page.title.map((line: any) => <h1 class="f-section-title">{line}</h1>)}
|
||||
{first.title && <h3>{first.title}</h3>}
|
||||
<Markdown text={first.content} />
|
||||
</div>
|
||||
</section>
|
||||
</section> -->
|
||||
|
||||
<section class="f-section">
|
||||
<div class="f-section-grid-single md:grid-cols-1 max-w-6xl mx-auto">
|
||||
<div class="f-section-grid-single md:grid-cols-1">
|
||||
<h1 class="f-section-title">Telewizja z interentem</h1>
|
||||
<div class="fuz-markdown max-w-none">
|
||||
<p>Wybierz rodzaj budynku i czas trwania umowy</p>
|
||||
</div>
|
||||
<OffersSwitches client:load />
|
||||
<OffersJamboxCards client:load />
|
||||
|
||||
<JamboxBasePackages
|
||||
client:load
|
||||
source=""
|
||||
title=""/>
|
||||
|
||||
<OffersIsland client:load data={data} />
|
||||
<!-- <OffersIsland client:load data={data} /> -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
{
|
||||
<!-- {
|
||||
rest.map((p: Paragraph) => (
|
||||
<section class="f-section">
|
||||
<div class="f-section-grid-single md:grid-cols-1">
|
||||
@@ -70,13 +70,11 @@ const rest = page.paragraphs.slice(1);
|
||||
</div>
|
||||
</section>
|
||||
))
|
||||
}
|
||||
} -->
|
||||
|
||||
<SectionRenderer src="./src/content/internet-telewizja/section.yaml" />
|
||||
|
||||
<JamboxMozliwosci />
|
||||
|
||||
|
||||
<Modal client:load modalData={modalData} />
|
||||
<!-- <JamboxMozliwosci /> -->
|
||||
|
||||
<!-- <Modal client:load modalData={modalData} /> -->
|
||||
</DefaultLayout>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
||||
import OffersPhoneCards from "../../islands/OffersPhoneCards.jsx";
|
||||
import OffersPhoneCards from "../../islands/phone/OffersPhoneCards.jsx";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import fs from "fs";
|
||||
@@ -19,17 +19,5 @@ const seo = yaml.load(
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- <OffersIsland client:load data={data} /> -->
|
||||
<!-- {
|
||||
rest.map((p: Paragraph) => (
|
||||
<section class="f-section">
|
||||
<div class="f-section-grid-single md:grid-cols-1">
|
||||
{p.title && <h3 class="f-section-title">{p.title}</h3>}
|
||||
<Markdown text={p.content.replace(/\n/g, "\n\n")} />
|
||||
</div>
|
||||
</section>
|
||||
))
|
||||
} -->
|
||||
|
||||
<SectionRenderer src="./src/content/telefon/section.yaml" />
|
||||
</DefaultLayout>
|
||||
|
||||
280
src/scripts/importJamboxChannelLists.js
Normal file
280
src/scripts/importJamboxChannelLists.js
Normal file
@@ -0,0 +1,280 @@
|
||||
// scripts/importJamboxChannelLists.js
|
||||
import Database from "better-sqlite3";
|
||||
import { XMLParser } from "fast-xml-parser";
|
||||
|
||||
const DB_PATH =
|
||||
process.env.FUZ_DB_PATH || "./src/data/ServicesRange.db";
|
||||
|
||||
// źródła: URL + docelowy pakiet (source + slug)
|
||||
const FEEDS = [
|
||||
{
|
||||
url: "https://www.jambox.pl/xml/listakanalow-smart.xml",
|
||||
source: "EVIO",
|
||||
slug: "smart",
|
||||
},
|
||||
{
|
||||
url: "https://www.jambox.pl/xml/listakanalow-optimum.xml",
|
||||
source: "EVIO",
|
||||
slug: "optimum",
|
||||
},
|
||||
{
|
||||
url: "https://www.jambox.pl/xml/listakanalow-platinum.xml",
|
||||
source: "EVIO",
|
||||
slug: "platinum",
|
||||
},
|
||||
{
|
||||
url: "https://www.jambox.pl/xml/listakanalow-pluspodstawowy.xml",
|
||||
source: "PLUS",
|
||||
slug: "podstawowy",
|
||||
},
|
||||
{
|
||||
url: "https://www.jambox.pl/xml/listakanalow-pluskorzystny.xml",
|
||||
source: "PLUS",
|
||||
slug: "korzystny",
|
||||
},
|
||||
{
|
||||
url: "https://www.jambox.pl/xml/listakanalow-plusbogaty.xml",
|
||||
source: "PLUS",
|
||||
slug: "bogaty",
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Helper: pobranie XML-a
|
||||
*/
|
||||
async function fetchXml(url) {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Błąd pobierania XML z ${url}: ${res.status} ${res.statusText}`
|
||||
);
|
||||
}
|
||||
return await res.text();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser XML -> tablica node'ów z <xml><node>...</node></xml>
|
||||
*/
|
||||
function parseNodes(xmlText) {
|
||||
const parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
attributeNamePrefix: "@_",
|
||||
});
|
||||
|
||||
const json = parser.parse(xmlText);
|
||||
let nodes = json.xml?.node ?? json.node ?? [];
|
||||
|
||||
if (!nodes) return [];
|
||||
if (!Array.isArray(nodes)) nodes = [nodes];
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prosta dekodacja encji HTML (– etc.)
|
||||
* – nie bawimy się w pełen parser HTML, tylko najczęstsze kody
|
||||
*/
|
||||
function decodeHtmlEntities(str) {
|
||||
if (!str) return "";
|
||||
return String(str)
|
||||
.replace(/–/g, "–")
|
||||
.replace(/ /g, " ")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
/**
|
||||
* Z node'a zrobimy obiekt kanału
|
||||
*/
|
||||
function mapChannelNode(node) {
|
||||
const number = Number(node.nr);
|
||||
const name = node.nazwa_kanalu ?? node.nazwa ?? null;
|
||||
|
||||
if (!number || !name) {
|
||||
console.warn("⚠ Pomijam node bez numeru / nazwy:", node);
|
||||
return null;
|
||||
}
|
||||
|
||||
// opis – chcemy HTML, ale z poprawionymi encjami
|
||||
let descriptionHtml = "";
|
||||
|
||||
if (typeof node.opis === "string") {
|
||||
descriptionHtml = node.opis;
|
||||
} else if (node.opis && node.opis.p) {
|
||||
// <opis><p>...</p></opis>
|
||||
if (typeof node.opis.p === "string") {
|
||||
descriptionHtml = `<p>${node.opis.p}</p>`;
|
||||
} else if (Array.isArray(node.opis.p)) {
|
||||
// kilka <p>...</p>
|
||||
descriptionHtml = node.opis.p
|
||||
.map((p) =>
|
||||
typeof p === "string" ? `<p>${p}</p>` : ""
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
}
|
||||
|
||||
descriptionHtml = decodeHtmlEntities(descriptionHtml.trim());
|
||||
|
||||
// logo – interesuje nas przede wszystkim src
|
||||
let logoUrl = null;
|
||||
const logoNode = node.field_logo_fid;
|
||||
|
||||
if (typeof logoNode === "string") {
|
||||
// próbujemy wyciągnąć src="..."
|
||||
const m = logoNode.match(/src="([^"]+)"/);
|
||||
if (m) logoUrl = m[1];
|
||||
} else if (logoNode && logoNode.img) {
|
||||
// fast-xml-parser: <field_logo_fid><img ... /></field_logo_fid>
|
||||
const img = logoNode.img;
|
||||
if (img["@_src"]) {
|
||||
logoUrl = img["@_src"];
|
||||
}
|
||||
}
|
||||
|
||||
// gwarantowany
|
||||
const guaranteed = String(node.gwarantowany || "")
|
||||
.toLowerCase()
|
||||
.includes("gwarantowany")
|
||||
? 1
|
||||
: 0;
|
||||
|
||||
return {
|
||||
number,
|
||||
name: String(name).trim(),
|
||||
description: descriptionHtml || null,
|
||||
logo_url: logoUrl || null,
|
||||
guaranteed,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Upewniamy się, że tabela docelowa istnieje
|
||||
*/
|
||||
function ensureSchema(db) {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS jambox_package_channels (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
package_id INTEGER NOT NULL,
|
||||
number INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
logo_url TEXT,
|
||||
guaranteed INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE (package_id, number),
|
||||
FOREIGN KEY (package_id)
|
||||
REFERENCES jambox_base_packages (id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Znajdowanie id pakietu po (source, slug)
|
||||
*/
|
||||
function getPackageId(db, source, slug) {
|
||||
const row = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT id
|
||||
FROM jambox_base_packages
|
||||
WHERE source = ?
|
||||
AND slug = ?;
|
||||
`
|
||||
)
|
||||
.get(source, slug);
|
||||
|
||||
return row?.id ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert kanałów dla danego pakietu
|
||||
*/
|
||||
function upsertChannelsForPackage(db, packageId, channels) {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO jambox_package_channels
|
||||
(package_id, number, name, description, logo_url, guaranteed, updated_at)
|
||||
VALUES
|
||||
(@package_id, @number, @name, @description, @logo_url, @guaranteed, @updated_at)
|
||||
ON CONFLICT(package_id, number) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
description = excluded.description,
|
||||
logo_url = excluded.logo_url,
|
||||
guaranteed = excluded.guaranteed,
|
||||
updated_at = excluded.updated_at;
|
||||
`);
|
||||
|
||||
const tx = db.transaction((rows) => {
|
||||
let count = 0;
|
||||
for (const ch of rows) {
|
||||
stmt.run({
|
||||
package_id: packageId,
|
||||
number: ch.number,
|
||||
name: ch.name,
|
||||
description: ch.description,
|
||||
logo_url: ch.logo_url,
|
||||
guaranteed: ch.guaranteed,
|
||||
updated_at: now,
|
||||
});
|
||||
count++;
|
||||
}
|
||||
console.log(` → zapisano/zmieniono ${count} kanałów dla package_id=${packageId}`);
|
||||
});
|
||||
|
||||
tx(channels);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`Używam bazy: ${DB_PATH}`);
|
||||
const db = new Database(DB_PATH);
|
||||
|
||||
ensureSchema(db);
|
||||
|
||||
for (const feed of FEEDS) {
|
||||
console.log(
|
||||
`\n=== Przetwarzam ${feed.url} (source=${feed.source}, slug=${feed.slug}) ===`
|
||||
);
|
||||
|
||||
const packageId = getPackageId(db, feed.source, feed.slug);
|
||||
if (!packageId) {
|
||||
console.warn(
|
||||
`⚠ Brak pakietu w jambox_base_packages dla source=${feed.source}, slug="${feed.slug}". Pomijam ten feed.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const xml = await fetchXml(feed.url);
|
||||
const nodes = parseNodes(xml);
|
||||
|
||||
console.log(`📦 Znaleziono ${nodes.length} node'ów (kanałów) w XML-u.`);
|
||||
|
||||
const channels = [];
|
||||
for (const node of nodes) {
|
||||
const mapped = mapChannelNode(node);
|
||||
if (!mapped) continue;
|
||||
channels.push(mapped);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`🧮 Po zmapowaniu pozostaje ${channels.length} poprawnych kanałów.`
|
||||
);
|
||||
|
||||
upsertChannelsForPackage(db, packageId, channels);
|
||||
} catch (err) {
|
||||
console.error(`❌ Błąd przy przetwarzaniu ${feed.url}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
db.close();
|
||||
console.log("\n🎉 Import list kanałów JAMBOX zakończony.");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("❌ Krytyczny błąd:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
127
src/scripts/importJamboxTvAddons.js
Normal file
127
src/scripts/importJamboxTvAddons.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import Database from "better-sqlite3";
|
||||
import { XMLParser } from "fast-xml-parser";
|
||||
|
||||
const DB_PATH = process.env.FUZ_DB_PATH || "./src/data/ServicesRange.db";
|
||||
|
||||
const SOURCES = [
|
||||
{
|
||||
kind: "premium",
|
||||
url: "https://www.jambox.pl/xml/slownik-pakietypremium.xml",
|
||||
},
|
||||
{
|
||||
kind: "tematyczne_plus",
|
||||
url: "https://www.jambox.pl/xml/slownik-pakietytematyczneplus.xml",
|
||||
},
|
||||
];
|
||||
|
||||
async function fetchXml(url) {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Błąd pobierania XML z ${url}: ${res.status} ${res.statusText}`
|
||||
);
|
||||
}
|
||||
return await res.text();
|
||||
}
|
||||
|
||||
function parseNodesFromXml(xmlText) {
|
||||
const parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
attributeNamePrefix: "@_",
|
||||
});
|
||||
|
||||
const json = parser.parse(xmlText);
|
||||
let nodes = json.xml?.node ?? json.node ?? [];
|
||||
|
||||
if (!nodes) return [];
|
||||
if (!Array.isArray(nodes)) nodes = [nodes];
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function ensureSchema(db) {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS jambox_tv_addons (
|
||||
tid INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
function upsertTvAddons(db, kind, nodes) {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const upsert = db.prepare(`
|
||||
INSERT INTO jambox_tv_addons (tid, name, kind, is_active, updated_at)
|
||||
VALUES (@tid, @name, @kind, 1, @updated_at)
|
||||
ON CONFLICT(tid) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
kind = excluded.kind,
|
||||
is_active = excluded.is_active,
|
||||
updated_at = excluded.updated_at;
|
||||
`);
|
||||
|
||||
const tx = db.transaction((rows) => {
|
||||
let ok = 0;
|
||||
|
||||
for (const node of rows) {
|
||||
const name = node.name ?? node.nazwa ?? node["#text"] ?? "";
|
||||
const tidRaw = node.tid ?? node.id ?? null;
|
||||
|
||||
if (!tidRaw || !name) {
|
||||
console.warn(`⚠ Pomijam node bez tid/name (kind=${kind}):`, node);
|
||||
continue;
|
||||
}
|
||||
|
||||
const tid = Number(tidRaw);
|
||||
if (!Number.isFinite(tid) || tid <= 0) {
|
||||
console.warn(`⚠ Pomijam node z niepoprawnym tid (kind=${kind}):`, node);
|
||||
continue;
|
||||
}
|
||||
|
||||
upsert.run({
|
||||
tid,
|
||||
name: String(name).trim(),
|
||||
kind,
|
||||
updated_at: now,
|
||||
});
|
||||
|
||||
ok++;
|
||||
}
|
||||
|
||||
console.log(`✅ ${kind}: zapisano/zmieniono ${ok} rekordów.`);
|
||||
});
|
||||
|
||||
tx(nodes);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`Używam bazy: ${DB_PATH}`);
|
||||
const db = new Database(DB_PATH);
|
||||
|
||||
ensureSchema(db);
|
||||
|
||||
for (const { kind, url } of SOURCES) {
|
||||
console.log(`\n=== Przetwarzam ${kind} (${url}) ===`);
|
||||
|
||||
try {
|
||||
const xml = await fetchXml(url);
|
||||
const nodes = parseNodesFromXml(xml);
|
||||
|
||||
console.log(`📦 Znaleziono ${nodes.length} node'ów (kind=${kind})`);
|
||||
upsertTvAddons(db, kind, nodes);
|
||||
} catch (err) {
|
||||
console.error(`❌ Błąd przy imporcie ${kind}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
db.close();
|
||||
console.log("\n🎉 Import słowników pakietów dodatkowych zakończony.");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("❌ Krytyczny błąd:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
239
src/scripts/updateJamboxChannels.js
Normal file
239
src/scripts/updateJamboxChannels.js
Normal file
@@ -0,0 +1,239 @@
|
||||
// scripts/updateJamboxChannels.js
|
||||
import Database from "better-sqlite3";
|
||||
import { XMLParser } from "fast-xml-parser";
|
||||
|
||||
const DB_PATH =
|
||||
process.env.FUZ_DB_PATH || "./src/data/ServicesRange.db";
|
||||
|
||||
const JAMBOX_NUMBERS_URL = "https://www.jambox.pl/xml/jamboxwliczbach.xml";
|
||||
|
||||
/**
|
||||
* Prosty helper do pobrania XML-a
|
||||
*/
|
||||
async function fetchXml(url) {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Błąd pobierania XML z ${url}: ${res.status} ${res.statusText}`
|
||||
);
|
||||
}
|
||||
return await res.text();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsowanie XML -> pojedynczy node z licznikami
|
||||
*/
|
||||
function parseCountsNode(xmlText) {
|
||||
const parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
attributeNamePrefix: "@_",
|
||||
});
|
||||
|
||||
const json = parser.parse(xmlText);
|
||||
|
||||
// dokładnie jak w podglądzie: <xml><node>...</node></xml>
|
||||
const node = json.xml?.node ?? json.node ?? null;
|
||||
|
||||
if (!node) {
|
||||
throw new Error("Nie znaleziono <node> w jamboxwliczbach.xml");
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
function getInt(node, key) {
|
||||
const raw = node[key];
|
||||
const n = Number(raw);
|
||||
if (!Number.isFinite(n)) {
|
||||
console.warn(`⚠ Pole ${key} ma dziwną wartość:`, raw);
|
||||
return 0;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pobierz ID cech "Liczba Kanałów" i "Liczba Kanałów HD"
|
||||
* żeby nie polegać na tym, że to zawsze 7 i 8.
|
||||
*/
|
||||
function getChannelFeatureIds(db) {
|
||||
const rows = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT id, label
|
||||
FROM internet_features
|
||||
WHERE label IN ('Liczba Kanałów', 'Liczba Kanałów HD');
|
||||
`
|
||||
)
|
||||
.all();
|
||||
|
||||
let channelsId = null;
|
||||
let channelsHdId = null;
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.label === "Liczba Kanałów") channelsId = row.id;
|
||||
if (row.label === "Liczba Kanałów HD") channelsHdId = row.id;
|
||||
}
|
||||
|
||||
if (!channelsId || !channelsHdId) {
|
||||
throw new Error(
|
||||
`Brak wymaganych cech w internet_features (Liczba Kanałów / Liczba Kanałów HD).`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`ℹ Feature IDs: "Liczba Kanałów" = ${channelsId}, "Liczba Kanałów HD" = ${channelsHdId}`
|
||||
);
|
||||
|
||||
return { channelsId, channelsHdId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Zwraca mapę (source, slug) -> package_id z jambox_base_packages
|
||||
*/
|
||||
function getPackageIdLookup(db) {
|
||||
const rows = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT id, source, slug
|
||||
FROM jambox_base_packages;
|
||||
`
|
||||
)
|
||||
.all();
|
||||
|
||||
const map = new Map();
|
||||
|
||||
for (const row of rows) {
|
||||
if (!row.source || !row.slug) continue;
|
||||
const key = `${row.source}::${row.slug}`;
|
||||
map.set(key, row.id);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualizacja rekordów w jambox_package_feature_values
|
||||
* dla "Liczba Kanałów" i "Liczba Kanałów HD"
|
||||
*/
|
||||
function updateChannelFeatureValues(db, packages, featureIds, packageIdMap) {
|
||||
const { channelsId, channelsHdId } = featureIds;
|
||||
|
||||
const upsertStmt = db.prepare(`
|
||||
INSERT INTO jambox_package_feature_values (package_id, feature_id, value)
|
||||
VALUES (@package_id, @feature_id, @value)
|
||||
ON CONFLICT(package_id, feature_id) DO UPDATE SET
|
||||
value = excluded.value;
|
||||
`);
|
||||
|
||||
const tx = db.transaction((rows) => {
|
||||
let total = 0;
|
||||
|
||||
for (const pkg of rows) {
|
||||
const key = `${pkg.source}::${pkg.slug}`;
|
||||
const packageId = packageIdMap.get(key);
|
||||
|
||||
if (!packageId) {
|
||||
console.warn(
|
||||
`⚠ Brak pakietu w jambox_base_packages dla: source=${pkg.source}, slug="${pkg.slug}"`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Liczba Kanałów
|
||||
upsertStmt.run({
|
||||
package_id: packageId,
|
||||
feature_id: channelsId,
|
||||
value: String(pkg.canals),
|
||||
});
|
||||
|
||||
// Liczba Kanałów HD
|
||||
upsertStmt.run({
|
||||
package_id: packageId,
|
||||
feature_id: channelsHdId,
|
||||
value: String(pkg.canalshd),
|
||||
});
|
||||
|
||||
total += 2;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ Zaktualizowano / wstawiono ${total} wartości cech w jambox_package_feature_values.`
|
||||
);
|
||||
});
|
||||
|
||||
tx(packages);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`Używam bazy: ${DB_PATH}`);
|
||||
const db = new Database(DB_PATH); // tu normalne R/W połączenie
|
||||
|
||||
console.log(`\n=== Pobieram JAMBOX "w liczbach" ===`);
|
||||
const xml = await fetchXml(JAMBOX_NUMBERS_URL);
|
||||
const node = parseCountsNode(xml);
|
||||
|
||||
// 🔗 Ręczne mapowanie z ich XML-a na nasze pakiety (source + slug)
|
||||
const packages = [
|
||||
// EVIO
|
||||
{
|
||||
source: "EVIO",
|
||||
slug: "smart",
|
||||
canals: getInt(node, "ilosc_kanalow_eviosmart"),
|
||||
canalshd: getInt(node, "ilosc_kanalow_hd_eviosmart"),
|
||||
},
|
||||
{
|
||||
source: "EVIO",
|
||||
slug: "optimum",
|
||||
canals: getInt(node, "ilosc_kanalow_eviooptimum"),
|
||||
canalshd: getInt(node, "ilosc_kanalow_hd_eviooptimum"),
|
||||
},
|
||||
{
|
||||
source: "EVIO",
|
||||
slug: "platinum",
|
||||
canals: getInt(node, "ilosc_kanalow_evioplatinum"),
|
||||
canalshd: getInt(node, "ilosc_kanalow_hd_evioplatinum"),
|
||||
},
|
||||
|
||||
// PLUS
|
||||
{
|
||||
source: "PLUS",
|
||||
slug: "podstawowy",
|
||||
canals: getInt(node, "ilosc_kanalow_podstawowy"),
|
||||
canalshd: getInt(node, "ilosc_kanalow_hd_podstawowy"),
|
||||
},
|
||||
{
|
||||
source: "PLUS",
|
||||
slug: "korzystny",
|
||||
canals: getInt(node, "ilosc_kanalow_korzystny"),
|
||||
canalshd: getInt(node, "ilosc_kanalow_hd_korzystny"),
|
||||
},
|
||||
{
|
||||
source: "PLUS",
|
||||
slug: "bogaty",
|
||||
canals: getInt(node, "ilosc_kanalow_bogaty"),
|
||||
canalshd: getInt(node, "ilosc_kanalow_hd_bogaty"),
|
||||
},
|
||||
];
|
||||
|
||||
console.log("🧮 Pakiety do aktualizacji:");
|
||||
packages.forEach((p) => {
|
||||
console.log(
|
||||
` - ${p.source} / ${p.slug}: ${p.canals} kanałów, ${p.canalshd} HD`
|
||||
);
|
||||
});
|
||||
|
||||
// 🔹 Pobierz ID cech i mapę pakietów
|
||||
const featureIds = getChannelFeatureIds(db);
|
||||
const packageIdMap = getPackageIdLookup(db);
|
||||
|
||||
// 🔹 Aktualizacja tabeli jambox_package_feature_values
|
||||
updateChannelFeatureValues(db, packages, featureIds, packageIdMap);
|
||||
|
||||
db.close();
|
||||
console.log("\n🎉 Aktualizacja liczby kanałów zakończona.");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("❌ Krytyczny błąd:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -139,3 +139,188 @@
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
obszar ze scrollem wewnątrz modala
|
||||
.jmb-channels-scroll {
|
||||
margin-top: 1rem;
|
||||
padding-right: 0.25rem;
|
||||
max-height: 72vh;
|
||||
overflow-y: auto;
|
||||
} */
|
||||
|
||||
/* dużo kart w wierszu */
|
||||
.jmb-channels-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
/* pojedyncza karta kanału */
|
||||
.jmb-channel-card {
|
||||
background: var(--fuz-bg);
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.75rem 0.5rem;
|
||||
text-align: center;
|
||||
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* logo kanału */
|
||||
.jmb-channel-logo {
|
||||
max-width: 90px;
|
||||
max-height: 60px;
|
||||
margin: 0 auto 0.5rem auto;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* podpisy pod logo */
|
||||
.jmb-channel-name {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
|
||||
.jmb-channel-number {
|
||||
font-size: 0.75rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* badge "gwarantowany" */
|
||||
.jmb-channel-tag {
|
||||
margin-top: 0.35rem;
|
||||
font-size: 0.7rem;
|
||||
display: inline-block;
|
||||
padding: 0.1rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
background: rgba(34, 197, 94, 0.08);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* === FIX: stabilny grid + flip (nie rozjeżdża kart) === */
|
||||
|
||||
.jmb-channels-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 0.9rem;
|
||||
align-items: stretch; /* ważne: równe wysokości w wierszu */
|
||||
}
|
||||
|
||||
.jmb-channel-card {
|
||||
position: relative;
|
||||
perspective: 1000px;
|
||||
|
||||
/* zostawiamy Twój wygląd */
|
||||
background: var(--fuz-bg);
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.15);
|
||||
|
||||
/* kluczowe: karta ma “ramę” */
|
||||
min-height: 170px; /* dopasuj: 160–200 */
|
||||
height: 100%;
|
||||
padding: 0; /* padding przenosimy na face */
|
||||
overflow: hidden; /* żeby back nie wystawał */
|
||||
}
|
||||
|
||||
.jmb-channel-inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 0.6s ease;
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.jmb-channel-card:hover .jmb-channel-inner {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* front + back */
|
||||
.jmb-channel-face {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 0.75rem 0.5rem; /* to był Twój padding z card */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
text-align: center;
|
||||
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.jmb-channel-front {
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
|
||||
.jmb-channel-back {
|
||||
transform: rotateY(180deg);
|
||||
text-align: left;
|
||||
justify-content: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.jmb-channel-back-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.jmb-channel-desc {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.35;
|
||||
opacity: 0.9;
|
||||
overflow: auto; /* długi opis nie rozwala karty */
|
||||
}
|
||||
|
||||
|
||||
.jmb-search {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
margin: 0.75rem 0 0.25rem;
|
||||
}
|
||||
|
||||
.jmb-search-input {
|
||||
flex: 1;
|
||||
padding: 0.6rem 0.75rem;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||
background: var(--fuz-bg);
|
||||
color: var(--fuz-text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.jmb-search-input:focus {
|
||||
border-color: rgba(59, 130, 246, 0.55);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.jmb-search-clear {
|
||||
padding: 0.55rem 0.7rem;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.jmb-search-meta {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.75;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
Reference in New Issue
Block a user