Przebudowa stron na indywidualne karty , pobierane z bazy danych
This commit is contained in:
@@ -5,7 +5,8 @@
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
"preview": "astro preview",
|
||||
"update:jambox:base": "node src/scripts/update-jambox-base.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^9.5.1",
|
||||
@@ -13,6 +14,7 @@
|
||||
"@preact/signals": "^2.5.1",
|
||||
"astro": "^5.16.0",
|
||||
"better-sqlite3": "^12.4.6",
|
||||
"fast-xml-parser": "^5.3.2",
|
||||
"globby": "^16.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsdom": "^27.2.0",
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
title:
|
||||
- "Internet światłowodowy"
|
||||
subtitle:
|
||||
- Internet bez kompromisów
|
||||
- Szybkie i stabilne łącze
|
||||
- Optymalna oferta
|
||||
- Lokalny operator, znamy Twoją okolicę
|
||||
|
||||
# description: |
|
||||
# Szybki i stabilny Internet światłowodowy w Wyszkowie oraz okolicach.
|
||||
# Sprawdź zasięg usług i wybierz najlepsze łącze dla swojego domu.
|
||||
imageUrl: "section-fiber.webp"
|
||||
ctas:
|
||||
- label: "Zobacz ofertę Telewizji"
|
||||
href: "/internet-telewizja"
|
||||
title: "Przejdź do oferty Internet + Telewizja w FUZ"
|
||||
primary: false
|
||||
|
||||
- label: "Zobacz ofertę Telefonu "
|
||||
href: "/telefon"
|
||||
primary: false
|
||||
title: "Przejdź do oferty telefonu"
|
||||
|
||||
# - label: "Sprawdź dostępność usługi"
|
||||
# href: "/mapa-zasiegu"
|
||||
# title: "Sprawdź zasięg Internetu światłowodowego FUZ"
|
||||
# primary: false
|
||||
@@ -1,153 +0,0 @@
|
||||
przelaczniki:
|
||||
- id: "budynek"
|
||||
etykieta: "Rodzaj budynku"
|
||||
domyslny: "jednorodzinny"
|
||||
title: "Zmień rodzaj budynku by zobaczyć odpowiednie ceny"
|
||||
opcje:
|
||||
- id: "jednorodzinny"
|
||||
nazwa: "Jednorodzinny"
|
||||
|
||||
- id: "wielorodzinny"
|
||||
nazwa: "Wielorodzinny"
|
||||
|
||||
- id: "umowa"
|
||||
etykieta: "Okres umowy"
|
||||
domyslny: "24m"
|
||||
title: "Wybierz okres umowy by zobaczyć odpowiednie ceny"
|
||||
opcje:
|
||||
- id: "24m"
|
||||
nazwa: "24 miesiące"
|
||||
|
||||
- id: "bezterminowa"
|
||||
nazwa: "Bezterminowa"
|
||||
|
||||
funkcje:
|
||||
- id: "pobieranie"
|
||||
etykieta: "Prędkość pobierania"
|
||||
|
||||
- id: "wysylanie"
|
||||
etykieta: "Prędkość wysyłania"
|
||||
|
||||
- id: "router"
|
||||
etykieta: "Router Wi-Fi"
|
||||
|
||||
- id: "adres_ip"
|
||||
etykieta: "Adres IP"
|
||||
|
||||
- id: "umowa_info"
|
||||
etykieta: "Umowa"
|
||||
|
||||
- id: "instalacja"
|
||||
etykieta: "Aktywacja"
|
||||
|
||||
plany:
|
||||
- id: "fiber100"
|
||||
nazwa: "FIBER 100"
|
||||
popularny: false
|
||||
|
||||
ceny:
|
||||
jednorodzinny:
|
||||
24m: 64
|
||||
bezterminowa: 84
|
||||
wielorodzinny:
|
||||
24m: 54
|
||||
bezterminowa: 74
|
||||
|
||||
koszty:
|
||||
instalacja:
|
||||
jednorodzinny:
|
||||
24m: 149
|
||||
bezterminowa: 199
|
||||
wielorodzinny:
|
||||
24m: 99
|
||||
bezterminowa: 149
|
||||
|
||||
funkcje:
|
||||
pobieranie: "do 100 Mb/s"
|
||||
wysylanie: "do 50 Mb/s"
|
||||
router: true
|
||||
adres_ip: "Dynamiczny"
|
||||
umowa_info: "12 / 24 / bez umowy"
|
||||
|
||||
- id: "fiber300"
|
||||
nazwa: "FIBER 300"
|
||||
popularny: true
|
||||
|
||||
ceny:
|
||||
jednorodzinny:
|
||||
24m: 75
|
||||
bezterminowa: 95
|
||||
wielorodzinny:
|
||||
24m: 65
|
||||
bezterminowa: 85
|
||||
|
||||
koszty:
|
||||
instalacja:
|
||||
jednorodzinny:
|
||||
24m: 149
|
||||
bezterminowa: 199
|
||||
wielorodzinny:
|
||||
24m: 99
|
||||
bezterminowa: 149
|
||||
|
||||
funkcje:
|
||||
pobieranie: "do 300 Mb/s"
|
||||
wysylanie: "do 150 Mb/s"
|
||||
router: true
|
||||
adres_ip: "Dynamiczny"
|
||||
umowa_info: "12 / 24 / bez umowy"
|
||||
|
||||
- id: "fiber600"
|
||||
nazwa: "FIBER 600"
|
||||
popularny: false
|
||||
|
||||
ceny:
|
||||
jednorodzinny:
|
||||
24m: 85
|
||||
bezterminowa: 105
|
||||
wielorodzinny:
|
||||
24m: 75
|
||||
bezterminowa: 95
|
||||
|
||||
koszty:
|
||||
instalacja:
|
||||
jednorodzinny:
|
||||
24m: 149
|
||||
bezterminowa: 199
|
||||
wielorodzinny:
|
||||
24m: 99
|
||||
bezterminowa: 149
|
||||
|
||||
funkcje:
|
||||
pobieranie: "do 600 Mb/s"
|
||||
wysylanie: "do 300 Mb/s"
|
||||
router: true
|
||||
adres_ip: "Dynamiczny"
|
||||
umowa_info: "24 / bez umowy"
|
||||
|
||||
addons:
|
||||
- id: "public_ip"
|
||||
nazwa: "Publiczny adres IP"
|
||||
typ: "checkbox"
|
||||
cena: 18.45
|
||||
|
||||
- id: "telefon"
|
||||
nazwa: "Telefon VoIP"
|
||||
typ: "select"
|
||||
opis: "Wybierz pakiet telefonii"
|
||||
opcje:
|
||||
- id: "brak"
|
||||
nazwa: "Bez telefonu"
|
||||
cena: 0
|
||||
- id: "tele30"
|
||||
nazwa: "TELE 30"
|
||||
cena: 9.90
|
||||
- id: "tele100"
|
||||
nazwa: "TELE 100"
|
||||
cena: 15.00
|
||||
- id: "tele300"
|
||||
nazwa: "TELE 300"
|
||||
cena: 29.00
|
||||
- id: "tele500"
|
||||
nazwa: "TELE 500"
|
||||
cena: 44.00
|
||||
@@ -1,22 +0,0 @@
|
||||
title:
|
||||
- INTERNET ŚWIATŁOWODOWY
|
||||
|
||||
paragraphs:
|
||||
- title:
|
||||
content: |
|
||||
Wybierz rodzaj budynku i czas trwania umowy
|
||||
|
||||
# Gwarantują błyskawiczną prędkość, stabilność i niezawodność bez względu na warunki.
|
||||
|
||||
# Dedykowane pasmo,pełna prędkość tylko dla Ciebie.
|
||||
|
||||
# Stała jakość, pogoda ani liczba użytkowników w sieci nie mają znaczenia.
|
||||
|
||||
# Wiele urządzeń jednocześnie, komputer, telefon, tablet, konsola wszystko działa płynnie.
|
||||
|
||||
# Światłowód to również dostęp do telewizji i telefonu w najwyższej jakości.
|
||||
|
||||
# Sprawdź naszą pełną ofertę i wybierz rozwiązanie dopasowane do Twoich potrzeb.
|
||||
|
||||
# - title:
|
||||
# content:
|
||||
@@ -1,21 +1,18 @@
|
||||
sections:
|
||||
- title: Sprawdź dostępność usługi
|
||||
image: section-range.webp
|
||||
button:
|
||||
text: "Sprawdź dostępność pod Twoim adresem →"
|
||||
url: "/mapa-zasiegu"
|
||||
title: "Sprawdź zasięg Internetu światłowodowego FUZ"
|
||||
content: |
|
||||
Naszą sieć światłowodową systematycznie rozbudowujemy, ale infrastruktura nie dociera jeszcze do wszystkich adresów.
|
||||
|
||||
Proces budowy wymaga czasu może jednak akurat Twoja lokalizacja jest już podłączona?
|
||||
|
||||
[Sprawdź](/mapa-zasiegu "Sprawdź zasięg naszego Internetu") na interaktywnej mapie, czy internet światłowodowy jest już dostępny pod Twoim adresem.
|
||||
# - title: Sprawdź dostępność usługi
|
||||
# image:
|
||||
# button:
|
||||
# text: "Sprawdź dostępność pod Twoim adresem →"
|
||||
# url: "/mapa-zasiegu"
|
||||
# title: "Sprawdź zasięg Internetu światłowodowego FUZ"
|
||||
# content: |
|
||||
# Naszą sieć światłowodową systematycznie rozbudowujemy, ale infrastruktura nie dociera jeszcze do wszystkich adresów.
|
||||
# Sprawdź zasięg naszego Internetu na interaktywnej mapie, czy internet światłowodowy jest już dostępny pod Twoim adresem.
|
||||
|
||||
- title: Router WiFi HL-4BX3V-F
|
||||
image: "HL-4BX3V-F.webp"
|
||||
content: |
|
||||
Nowoczesny router marki HALNy to urządzenie stworzone z myślą o wymagających użytkownikach.
|
||||
W ramach instalacji otrzymujesz nowoczesny router marki HALNy to urządzenie stworzone z myślą o wymagających użytkownikach.
|
||||
|
||||
Znajdziesz w nim nowoczesny standard WiFi 6, porty 2,5 Gb/s oraz 1 Gb/s, wsparcie dla sieci Mesh i VoIP. Stabilność, niezawodność i pełne wykorzystanie łącza – w całym Twoim domu.
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
title:
|
||||
- Telefon usługa dodatkowa
|
||||
subtitle:
|
||||
- Niezawodna łączność głosowa dzięki technologii VoIP
|
||||
- Atrakcyjne ceny
|
||||
- Bez telefonu jak bez ręki
|
||||
|
||||
# - Wszystko, czego potrzebujesz w jednym miejscu
|
||||
# description: |
|
||||
# Dziś dla wielu to niezbędne narzędzie pełne funkcji – „bez telefonu jak bez ręki".
|
||||
imageUrl: section-telefon.webp
|
||||
ctas:
|
||||
- label: "Zobacz ofertę Internetu"
|
||||
href: "/internet-swiatlowodowy"
|
||||
title: "Przejdź do oferty Internetu światłowodowego"
|
||||
primary: false
|
||||
|
||||
- label: "Zobacz ofertę Telewizji"
|
||||
href: "/internet-telewizja"
|
||||
title: "Przejdź do oferty Internet + Telewizja w FUZ"
|
||||
primary: false
|
||||
|
||||
# - label: "Sprawdź dostępność usługi "
|
||||
# href: "/mapa-zasiegu"
|
||||
# title: "Sprawdź zasięg Internetu światłowodowego FUZ"
|
||||
# primary: false
|
||||
@@ -1,55 +0,0 @@
|
||||
funkcje:
|
||||
- id: "minuty_darmowe"
|
||||
etykieta: "Darmowe minuty"
|
||||
|
||||
- id: "cena_minuta_stacjonarna"
|
||||
etykieta: "Połączenia do krajowych sieci stacjonarnych"
|
||||
|
||||
- id: "cena_minuta_komorkowe"
|
||||
etykieta: "Połączenia do krajowych sieci komórkowych"
|
||||
|
||||
- id: "instalacja"
|
||||
etykieta: "Aktywacja"
|
||||
|
||||
plany:
|
||||
- id: "tele30"
|
||||
nazwa: "TELE 30"
|
||||
popularny: false
|
||||
cena: 9.90
|
||||
funkcje:
|
||||
minuty_darmowe: "30"
|
||||
cena_minuta_stacjonarna: "0,07 zł / min."
|
||||
cena_minuta_komorkowe: "0,19 zł / min"
|
||||
instalacja: "1,23 zł"
|
||||
|
||||
- id: "tele100"
|
||||
nazwa: "TELE 100"
|
||||
popularny: false
|
||||
cena: 15.00
|
||||
funkcje:
|
||||
minuty_darmowe: "100"
|
||||
cena_minuta_stacjonarna: "0,07 zł / min."
|
||||
cena_minuta_komorkowe: "0,19 zł / min"
|
||||
instalacja: "1,23 zł"
|
||||
|
||||
- id: "tele300"
|
||||
nazwa: "TELE 300"
|
||||
popularny: false
|
||||
cena: 29.00
|
||||
funkcje:
|
||||
minuty_darmowe: "300"
|
||||
cena_minuta_stacjonarna: "0,07 zł / min."
|
||||
cena_minuta_komorkowe: "0,19 zł / min"
|
||||
instalacja: "1,23 zł"
|
||||
|
||||
- id: "tele500"
|
||||
nazwa: "TELE 500"
|
||||
popularny: false
|
||||
cena: 44.00
|
||||
funkcje:
|
||||
minuty_darmowe: "500"
|
||||
cena_minuta_stacjonarna: "0,07 zł / min."
|
||||
cena_minuta_komorkowe: "0,19 zł / min"
|
||||
instalacja: "1,23 zł"
|
||||
|
||||
addons: []
|
||||
@@ -1,27 +0,0 @@
|
||||
title:
|
||||
- USŁUGA TELEFONU
|
||||
paragraphs:
|
||||
- title:
|
||||
# content: |
|
||||
# Od czasów Aleksandra Bella telefon przeszedł niesamowitą ewolucję.
|
||||
|
||||
# Dziś dla wielu to niezbędne narzędzie pełne funkcji – „bez telefonu jak bez ręki".
|
||||
|
||||
# Ale są też tacy, którzy używają go po prostu do rozmów. I to w zupełności wystarczy.
|
||||
|
||||
# Stworzyliśmy zróżnicowaną ofertę, abyś mógł wybrać rozwiązanie idealnie dopasowane do swoich potrzeb.
|
||||
|
||||
# Od prostej linii telefonicznej po rozbudowane opcje.
|
||||
|
||||
# Zapoznaj się z naszymi propozycjami i wybierz to, co jest dla Ciebie najlepsze.
|
||||
|
||||
- title:
|
||||
content: |
|
||||
Usługa telefonu stacjonarnego dostępna jest wyłącznie w pakiecie z Internetem światłowodowym lub Internetem z telewizją.
|
||||
|
||||
Dzięki temu możesz cieszyć się niezawodną łącznością głosową dzięki technologii VoIP, jednocześnie korzystając z szybkiego i stabilnego dostępu do Internetu oraz bogatej oferty telewizyjnej.
|
||||
|
||||
Wybierz naszą usługę telefonu stacjonarnego z [internetem światłowodowym](/internet-swiatlowodowy "Przejdź do oferty Internetu światłowodowego") lub [internetem + telewizją](/internet-telewizja "Przejdź do oferty Internet + Telewizja w FUZ") i zyskaj kompleksowe rozwiązanie komunikacyjne dostosowane do Twoich potrzeb.
|
||||
|
||||
|
||||
# Kolejne sekcje mozna dodawać poja wiać się bedą pod tabela produktów
|
||||
@@ -1,4 +1,12 @@
|
||||
sections:
|
||||
- title:
|
||||
content: |
|
||||
Usługa telefonu stacjonarnego dostępna jest wyłącznie w pakiecie z Internetem światłowodowym lub Internetem z telewizją.
|
||||
|
||||
Dzięki temu możesz cieszyć się niezawodną łącznością głosową dzięki technologii VoIP, jednocześnie korzystając z szybkiego i stabilnego dostępu do Internetu oraz bogatej oferty telewizyjnej.
|
||||
|
||||
Wybierz naszą usługę telefonu stacjonarnego z [internetem światłowodowym](/internet-swiatlowodowy "Przejdź do oferty Internetu światłowodowego") lub [internetem + telewizją](/internet-telewizja "Przejdź do oferty Internet + Telewizja w FUZ") i zyskaj kompleksowe rozwiązanie komunikacyjne dostosowane do Twoich potrzeb.
|
||||
|
||||
- title: Zachowaj dotychczasowy numer telefonu
|
||||
image: "przeniesienie.png"
|
||||
content: |
|
||||
|
||||
Binary file not shown.
193
src/islands/Offers/JamboxBasePackages.jsx
Normal file
193
src/islands/Offers/JamboxBasePackages.jsx
Normal file
@@ -0,0 +1,193 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,141 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
// import "../../styles/offers/offers-switches.css";
|
||||
|
||||
export default function OffersSwitches({ switches, selected, onSwitch }) {
|
||||
if (!switches.length) return null;
|
||||
function buildLabels(switches, selected) {
|
||||
const out = {};
|
||||
for (const sw of switches || []) {
|
||||
const currentId = selected[sw.id];
|
||||
const opt = sw.opcje?.find((op) => String(op.id) === String(currentId));
|
||||
if (opt) out[sw.id] = opt.nazwa;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export default function OffersSwitches(props) {
|
||||
const { switches, selected, onSwitch } = props || {};
|
||||
|
||||
const isControlled =
|
||||
Array.isArray(switches) &&
|
||||
switches.length > 0 &&
|
||||
typeof onSwitch === "function";
|
||||
|
||||
const [autoSwitches, setAutoSwitches] = useState([]);
|
||||
const [autoSelected, setAutoSelected] = useState({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
// AUTO: pobieramy konfigurację z API
|
||||
useEffect(() => {
|
||||
if (isControlled) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/internet");
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
const json = await res.json();
|
||||
const sws = Array.isArray(json.data) ? json.data : [];
|
||||
if (cancelled) return;
|
||||
|
||||
const initial = {};
|
||||
for (const sw of sws) {
|
||||
if (sw.domyslny != null) initial[sw.id] = sw.domyslny;
|
||||
else if (sw.opcje?.length) initial[sw.id] = sw.opcje[0].id;
|
||||
}
|
||||
|
||||
const labels = buildLabels(sws, initial);
|
||||
|
||||
setAutoSwitches(sws);
|
||||
setAutoSelected(initial);
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("fuz:switch-change", {
|
||||
detail: {
|
||||
id: null,
|
||||
value: null,
|
||||
selected: initial,
|
||||
labels, // tu lecą etykiety z DB
|
||||
},
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd pobierania switchy:", err);
|
||||
if (!cancelled) setError("Nie udało się załadować przełączników.");
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isControlled]);
|
||||
|
||||
const effectiveSwitches = isControlled ? switches : autoSwitches;
|
||||
const effectiveSelected = isControlled ? selected || {} : autoSelected;
|
||||
|
||||
const handleClick = (id, value) => {
|
||||
if (isControlled) {
|
||||
onSwitch(id, value);
|
||||
} else {
|
||||
setAutoSelected((prev) => {
|
||||
const next = { ...prev, [id]: value };
|
||||
const labels = buildLabels(autoSwitches, next);
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("fuz:switch-change", {
|
||||
detail: {
|
||||
id,
|
||||
value,
|
||||
selected: next,
|
||||
labels, // etykiety po kliknięciu
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!isControlled && loading) {
|
||||
return (
|
||||
<div class="f-switches-wrapper">
|
||||
<p>Ładowanie opcji przełączników...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isControlled && error) {
|
||||
return (
|
||||
<div class="f-switches-wrapper">
|
||||
<p class="text-red-600">{error}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!effectiveSwitches.length) return null;
|
||||
|
||||
return (
|
||||
<div class="f-switches-wrapper">
|
||||
{switches.map((sw) => (
|
||||
{effectiveSwitches.map((sw) => (
|
||||
<div class="f-switch-box">
|
||||
<div class="f-switch-group">
|
||||
{sw.opcje.map((op) => (
|
||||
<button
|
||||
type="button"
|
||||
class={`f-switch ${selected[sw.id] === op.id ? "active" : ""
|
||||
}`}
|
||||
onClick={() => onSwitch(sw.id, op.id)}
|
||||
class={`f-switch ${
|
||||
String(effectiveSelected[sw.id]) === String(op.id)
|
||||
? "active"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => handleClick(sw.id, op.id)}
|
||||
title={sw.title}
|
||||
>
|
||||
{op.nazwa}
|
||||
|
||||
138
src/islands/OffersInternetCards.jsx
Normal file
138
src/islands/OffersInternetCards.jsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "../styles/offers/offers-table.css";
|
||||
|
||||
export default function InternetDbOffersCards({
|
||||
title = "Oferty Internetu FUZ",
|
||||
}) {
|
||||
const [selected, setSelected] = useState({});
|
||||
const [labels, setLabels] = useState({});
|
||||
const [plans, setPlans] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
// nasłuchuj globalnego eventu z OffersSwitches
|
||||
useEffect(() => {
|
||||
function handler(e) {
|
||||
const detail = e.detail || {};
|
||||
if (detail.selected) {
|
||||
setSelected(detail.selected);
|
||||
}
|
||||
if (detail.labels) {
|
||||
setLabels(detail.labels);
|
||||
}
|
||||
}
|
||||
|
||||
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({
|
||||
building: String(buildingCode),
|
||||
contract: String(contractCode),
|
||||
});
|
||||
|
||||
const res = await fetch(`/api/internet/plans?${params.toString()}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
const json = await res.json();
|
||||
if (!cancelled) {
|
||||
setPlans(Array.isArray(json.data) ? json.data : []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Błąd pobierania planów internetu:", err);
|
||||
if (!cancelled) setError("Nie udało się załadować ofert.");
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [buildingCode, contractCode]);
|
||||
|
||||
const contractLabel = labels.umowa || "";
|
||||
|
||||
return (
|
||||
<section class="f-offers">
|
||||
{loading && <p>Ładowanie ofert...</p>}
|
||||
{error && <p class="text-red-600">{error}</p>}
|
||||
|
||||
{!loading && !error && (
|
||||
<div class={`f-offers-grid f-count-${plans.length || 1}`}>
|
||||
{plans.map((plan) => (
|
||||
<OfferCard
|
||||
key={plan.id}
|
||||
plan={plan}
|
||||
contractLabel={contractLabel}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function OfferCard({ plan, contractLabel }) {
|
||||
const basePrice = plan.price_monthly;
|
||||
const installPrice = plan.price_installation;
|
||||
|
||||
const featureRows = (plan.features || []).filter(
|
||||
(f) => f.id !== "umowa_info" && f.id !== "instalacja"
|
||||
);
|
||||
|
||||
return (
|
||||
<div class={`f-card ${plan.popular ? "f-card-popular" : ""}`}>
|
||||
{/* {plan.popular && <div class="f-card-badge">Najczęściej wybierany</div>} */}
|
||||
|
||||
<div class="f-card-header">
|
||||
<div class="f-card-name">{plan.name}</div>
|
||||
<div class="f-card-price">{basePrice} zł/mies.</div>
|
||||
</div>
|
||||
|
||||
<ul class="f-card-features">
|
||||
{featureRows.map((f) => {
|
||||
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">
|
||||
<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">Umowa</span>
|
||||
<span class="f-card-value">{contractLabel}</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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
import OffersSwitches from "./Offers/OffersSwitches.jsx";
|
||||
import OffersCards from "./Offers/OffersTable.jsx"; // <-- WAŻNE!!
|
||||
import OffersCards from "./Offers/OffersCards.jsx"; // <-- WAŻNE!!
|
||||
import OffersExtraServices from "./Offers/OffersExtraServices.jsx";
|
||||
|
||||
export default function OffersIsland({ data }) {
|
||||
|
||||
77
src/islands/OffersPhoneCards.jsx
Normal file
77
src/islands/OffersPhoneCards.jsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "../styles/offers/offers-table.css";
|
||||
|
||||
export default function PhoneDbOffersCards({
|
||||
title = "Telefonia stacjonarna FUZ",
|
||||
}) {
|
||||
const [plans, setPlans] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/phone/plans");
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
const json = await res.json();
|
||||
if (!cancelled) {
|
||||
setPlans(Array.isArray(json.data) ? json.data : []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd pobierania planów telefonii:", err);
|
||||
if (!cancelled) {
|
||||
setError("Nie udało się załadować pakietów telefonicznych.");
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section class="f-offers">
|
||||
{loading && <p>Ładowanie pakietów telefonicznych...</p>}
|
||||
{error && <p class="text-red-600">{error}</p>}
|
||||
|
||||
{!loading && !error && (
|
||||
<div class={`f-offers-grid f-count-${plans.length || 1}`}>
|
||||
{plans.map((plan) => (
|
||||
<PhoneOfferCard key={plan.id} plan={plan} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function PhoneOfferCard({ plan }) {
|
||||
return (
|
||||
<div class={`f-card ${plan.popular ? "f-card-popular" : ""}`}>
|
||||
{plan.popular && <div class="f-card-badge">Najczęściej wybierany</div>}
|
||||
|
||||
<div class="f-card-header">
|
||||
<div class="f-card-name">{plan.name}</div>
|
||||
<div class="f-card-price">{plan.price_monthly} zł/mies.</div>
|
||||
</div>
|
||||
|
||||
<ul class="f-card-features">
|
||||
{plan.features.map((f) => (
|
||||
<li class="f-card-row">
|
||||
<span class="f-card-label">{f.label}</span>
|
||||
<span class="f-card-value">{f.value}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
src/pages/api/internet.js
Normal file
71
src/pages/api/internet.js
Normal file
@@ -0,0 +1,71 @@
|
||||
// src/pages/api/switches/internet.js
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const DB_PATH = "./src/data/ServicesRange.db";
|
||||
|
||||
function getDb() {
|
||||
return new Database(DB_PATH, { readonly: true });
|
||||
}
|
||||
|
||||
export function GET() {
|
||||
const db = getDb();
|
||||
|
||||
try {
|
||||
const buildingTypes = db
|
||||
.prepare("SELECT code, label FROM jambox_building_types ORDER BY code")
|
||||
.all();
|
||||
|
||||
const contractTypes = db
|
||||
.prepare(
|
||||
"SELECT code, label FROM jambox_contract_types ORDER BY code"
|
||||
)
|
||||
.all();
|
||||
|
||||
const switches = [
|
||||
{
|
||||
id: "budynek",
|
||||
etykieta: "Rodzaj budynku",
|
||||
domyslny: buildingTypes[0]?.code ?? 1,
|
||||
title: "Zmień rodzaj budynku by zobaczyć odpowiednie ceny",
|
||||
opcje: buildingTypes.map((b) => ({
|
||||
id: b.code, // 1,2,...
|
||||
nazwa: b.label,
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "umowa",
|
||||
etykieta: "Okres umowy",
|
||||
domyslny: contractTypes[0]?.code ?? 1,
|
||||
title: "Wybierz okres umowy by zobaczyć odpowiednie ceny",
|
||||
opcje: contractTypes.map((c) => ({
|
||||
id: c.code, // 1,2,...
|
||||
nazwa: c.label,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ ok: true, data: switches }),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Cache-Control": "public, max-age=60",
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd w /api/switches/internet:", 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();
|
||||
}
|
||||
}
|
||||
110
src/pages/api/internet/plans.js
Normal file
110
src/pages/api/internet/plans.js
Normal file
@@ -0,0 +1,110 @@
|
||||
// src/pages/api/internet/plans.js
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const DB_PATH = "./src/data/ServicesRange.db";
|
||||
|
||||
function getDb() {
|
||||
return new Database(DB_PATH, { readonly: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/internet/plans?building=1|2&contract=1|2
|
||||
*/
|
||||
export function GET({ url }) {
|
||||
const buildingParam = url.searchParams.get("building");
|
||||
const contractParam = url.searchParams.get("contract");
|
||||
|
||||
const building = buildingParam ? Number(buildingParam) : 1;
|
||||
const contract = contractParam ? Number(contractParam) : 1;
|
||||
|
||||
const db = getDb();
|
||||
|
||||
try {
|
||||
const stmt = db.prepare(
|
||||
`
|
||||
SELECT
|
||||
p.id AS plan_id,
|
||||
p.name AS plan_name,
|
||||
p.popular AS plan_popular,
|
||||
|
||||
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
|
||||
|
||||
FROM internet_plans p
|
||||
LEFT JOIN internet_plan_prices pr
|
||||
ON pr.plan_id = p.id
|
||||
AND pr.building_type = ?
|
||||
AND pr.contract_type = ?
|
||||
|
||||
LEFT JOIN internet_plan_feature_values fv
|
||||
ON fv.plan_id = p.id
|
||||
|
||||
LEFT JOIN internet_features f
|
||||
ON f.id = fv.feature_id
|
||||
|
||||
ORDER BY p.id ASC, f.id ASC;
|
||||
`.trim()
|
||||
);
|
||||
|
||||
const rows = stmt.all(building, contract);
|
||||
|
||||
// grupowanie do struktury: jeden plan = jedna karta
|
||||
const byPlan = new Map();
|
||||
|
||||
for (const row of rows) {
|
||||
if (!byPlan.has(row.plan_id)) {
|
||||
byPlan.set(row.plan_id, {
|
||||
id: row.plan_id,
|
||||
code: row.plan_code,
|
||||
name: row.plan_name,
|
||||
popular: !!row.plan_popular,
|
||||
price_monthly: row.price_monthly,
|
||||
price_installation: row.price_installation,
|
||||
features: [], // później wypełniamy
|
||||
});
|
||||
}
|
||||
|
||||
if (row.feature_id) {
|
||||
byPlan.get(row.plan_id).features.push({
|
||||
id: row.feature_id,
|
||||
label: row.feature_label,
|
||||
value: row.feature_value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const data = Array.from(byPlan.values());
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
building,
|
||||
contract,
|
||||
count: data.length,
|
||||
data,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Cache-Control": "public, max-age=30",
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd w /api/internet/plans:", err);
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: "DB_ERROR" }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
152
src/pages/api/jambox/base-packages.js
Normal file
152
src/pages/api/jambox/base-packages.js
Normal file
@@ -0,0 +1,152 @@
|
||||
// src/pages/api/jambox/base-packages.js
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const DB_PATH = "./src/data/ServicesRange.db"; // dostosuj, jeśli masz gdzie indziej
|
||||
|
||||
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)
|
||||
*/
|
||||
export function GET({ url }) {
|
||||
const sourceParam = url.searchParams.get("source");
|
||||
const source = sourceParam ? sourceParam.toUpperCase() : null;
|
||||
|
||||
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 db = getDb();
|
||||
|
||||
try {
|
||||
let rows = [];
|
||||
const hasVariant =
|
||||
Number.isInteger(building) && Number.isInteger(contract);
|
||||
|
||||
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;
|
||||
`.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);
|
||||
}
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
source: source ?? "ALL",
|
||||
building,
|
||||
contract,
|
||||
count: rows.length,
|
||||
data: rows,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Cache-Control": "public, max-age=60",
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd odczytu z bazy jambox_base_packages:", err);
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: "DB_ERROR" }),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
85
src/pages/api/phone/plans.js
Normal file
85
src/pages/api/phone/plans.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const DB_PATH = "./src/data/ServicesRange.db";
|
||||
|
||||
export async function GET() {
|
||||
const db = new Database(DB_PATH, { readonly: true });
|
||||
|
||||
try {
|
||||
const stmt = db.prepare(`
|
||||
SELECT
|
||||
p.id AS plan_id,
|
||||
p.name AS plan_name,
|
||||
IFNULL(p.popular, 0) AS plan_popular,
|
||||
p.price_monthly AS price_monthly,
|
||||
p.currency AS currency,
|
||||
|
||||
f.id AS feature_id,
|
||||
f.label AS feature_label,
|
||||
fv.value AS feature_value
|
||||
|
||||
FROM phone_plans p
|
||||
LEFT JOIN phone_plan_feature_values fv
|
||||
ON fv.plan_id = p.id
|
||||
LEFT JOIN phone_features f
|
||||
ON f.id = fv.feature_id
|
||||
ORDER BY p.id ASC, f.id ASC
|
||||
`);
|
||||
|
||||
const rows = stmt.all();
|
||||
|
||||
const byPlan = new Map();
|
||||
|
||||
for (const row of rows) {
|
||||
if (!byPlan.has(row.plan_id)) {
|
||||
byPlan.set(row.plan_id, {
|
||||
id: row.plan_id,
|
||||
code: row.plan_code,
|
||||
name: row.plan_name,
|
||||
popular: !!row.plan_popular,
|
||||
price_monthly: row.price_monthly,
|
||||
currency: row.currency || "PLN",
|
||||
features: [],
|
||||
});
|
||||
}
|
||||
|
||||
if (row.feature_id) {
|
||||
byPlan.get(row.plan_id).features.push({
|
||||
id: row.feature_id,
|
||||
label: row.feature_label,
|
||||
value: row.feature_value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const data = Array.from(byPlan.values());
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
count: data.length,
|
||||
data,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd w /api/phone/plans:", err);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
ok: false,
|
||||
error: err.message || "DB_ERROR",
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||
import Hero from "../../components/hero/Hero.astro";
|
||||
import OffersSwitches from "../../islands/Offers/OffersSwitches.jsx";
|
||||
import InternetDbOffersCards from "../../islands/OffersInternetCards.jsx";
|
||||
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
||||
import Markdown from "../../islands/Markdown.jsx";
|
||||
import OffersIsland from "../../islands/OffersIsland.jsx";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import fs from "fs";
|
||||
@@ -11,52 +10,19 @@ import fs from "fs";
|
||||
const seo = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-swiatlowodowy/seo.yaml", "utf8"),
|
||||
);
|
||||
const hero = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-swiatlowodowy/hero.yaml", "utf8"),
|
||||
);
|
||||
const page = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-swiatlowodowy/page.yaml", "utf8"),
|
||||
);
|
||||
|
||||
type Paragraph = {
|
||||
title?: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
const data = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-swiatlowodowy/offers.yaml", "utf8"),
|
||||
);
|
||||
const first = page.paragraphs[0];
|
||||
const rest = page.paragraphs.slice(1);
|
||||
---
|
||||
|
||||
<DefaultLayout seo={seo}>
|
||||
<Hero {...hero} />
|
||||
|
||||
<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} />
|
||||
<h1 class="f-section-title">Internet światłowodowy</h1>
|
||||
<div class="fuz-markdown max-w-none">
|
||||
<p>Wybierz rodzaj budynku i czas trwania umowy</p>
|
||||
</div>
|
||||
<OffersSwitches client:load />
|
||||
<InternetDbOffersCards client:load />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="f-section">
|
||||
<div class="f-section-grid-single md:grid-cols-1">
|
||||
<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">
|
||||
{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/internet-swiatlowodowy/section.yaml" />
|
||||
</DefaultLayout>
|
||||
|
||||
@@ -6,6 +6,7 @@ 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";
|
||||
@@ -48,12 +49,18 @@ const rest = page.paragraphs.slice(1);
|
||||
|
||||
<section class="f-section">
|
||||
<div class="f-section-grid-single md:grid-cols-1 max-w-6xl mx-auto">
|
||||
|
||||
<JamboxBasePackages
|
||||
client:load
|
||||
source=""
|
||||
title=""/>
|
||||
|
||||
<OffersIsland client:load data={data} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- <OffersIsland client:load data={data} /> -->
|
||||
|
||||
{
|
||||
rest.map((p: Paragraph) => (
|
||||
<section class="f-section">
|
||||
|
||||
@@ -146,7 +146,7 @@ const mapStyleId = "8e0a97af9476f2d3";
|
||||
<div class="f-info-header">
|
||||
<div class="f-info-heading">
|
||||
${
|
||||
result.availableFiber
|
||||
result.available
|
||||
? `<span class="ok">✔</span> Internet światłowodowy dostępny`
|
||||
: `<span class="no">✖</span> Światłowód niedostępny`
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
---
|
||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||
import Hero from "../../components/hero/Hero.astro";
|
||||
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
||||
import Markdown from "../../islands/Markdown.jsx";
|
||||
import OffersIsland from "../../islands/OffersIsland.jsx";
|
||||
import OffersPhoneCards from "../../islands/OffersPhoneCards.jsx";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import fs from "fs";
|
||||
@@ -11,44 +9,18 @@ import fs from "fs";
|
||||
const seo = yaml.load(
|
||||
fs.readFileSync("./src/content/telefon/seo.yaml", "utf8"),
|
||||
);
|
||||
const hero = yaml.load(
|
||||
fs.readFileSync("./src/content/telefon/hero.yaml", "utf8"),
|
||||
);
|
||||
const page = yaml.load(
|
||||
fs.readFileSync("./src/content/telefon/page.yaml", "utf8"),
|
||||
);
|
||||
|
||||
type Paragraph = {
|
||||
title?: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
const data = yaml.load(
|
||||
fs.readFileSync("./src/content/telefon/offers.yaml", "utf8"),
|
||||
);
|
||||
const first = page.paragraphs[0];
|
||||
const rest = page.paragraphs.slice(1);
|
||||
---
|
||||
|
||||
<DefaultLayout seo={seo}>
|
||||
<Hero {...hero} />
|
||||
|
||||
<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 class="f-section">
|
||||
<div class="f-section-grid-single md:grid-cols-1">
|
||||
<OffersIsland client:load data={data} />
|
||||
<h1 class="f-section-title">Usługa telefonu</h1>
|
||||
<OffersPhoneCards client: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">
|
||||
@@ -57,7 +29,7 @@ const rest = page.paragraphs.slice(1);
|
||||
</div>
|
||||
</section>
|
||||
))
|
||||
}
|
||||
} -->
|
||||
|
||||
<SectionRenderer src="./src/content/telefon/section.yaml" />
|
||||
</DefaultLayout>
|
||||
|
||||
134
src/scripts/update-jambox-base.js
Normal file
134
src/scripts/update-jambox-base.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import Database from "better-sqlite3";
|
||||
import { XMLParser } from "fast-xml-parser";
|
||||
|
||||
const DB_PATH = "./src/data/ServicesRange.db";
|
||||
|
||||
const SOURCES = [
|
||||
{
|
||||
source: "EVIO",
|
||||
url: "https://www.jambox.pl/xml/slownik-pakietypodstawoweevio.xml",
|
||||
},
|
||||
{
|
||||
source: "PLUS",
|
||||
url: "https://www.jambox.pl/xml/slownik-pakietypodstawoweplus.xml",
|
||||
},
|
||||
];
|
||||
|
||||
function slugify(str) {
|
||||
if (!str) return null;
|
||||
return str
|
||||
.toString()
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
}
|
||||
|
||||
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);
|
||||
const nodes = json.xml?.node ?? json.node ?? [];
|
||||
|
||||
if (Array.isArray(nodes)) return nodes;
|
||||
if (!nodes) return [];
|
||||
return [nodes];
|
||||
}
|
||||
|
||||
function ensureSchema(db) {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS jambox_base_packages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source TEXT NOT NULL,
|
||||
tid INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT,
|
||||
sort_order INTEGER,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(source, tid)
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
function upsertBasePackages(db, source, nodes) {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const upsert = db.prepare(`
|
||||
INSERT INTO jambox_base_packages (source, tid, name, slug, sort_order, updated_at)
|
||||
VALUES (@source, @tid, @name, @slug, @sort_order, @updated_at)
|
||||
ON CONFLICT(source, tid) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
slug = excluded.slug,
|
||||
sort_order = excluded.sort_order,
|
||||
updated_at = excluded.updated_at;
|
||||
`);
|
||||
|
||||
const tx = db.transaction((rows) => {
|
||||
rows.forEach((node, index) => {
|
||||
const name = node.name ?? node["#text"] ?? "";
|
||||
const tidRaw = node.tid ?? node.id ?? null;
|
||||
|
||||
if (!tidRaw || !name) {
|
||||
console.warn(`⚠ Pomijam node bez tid/name (source=${source}):`, node);
|
||||
return;
|
||||
}
|
||||
|
||||
const tid = Number(tidRaw);
|
||||
|
||||
const record = {
|
||||
source,
|
||||
tid,
|
||||
name: String(name).trim(),
|
||||
slug: slugify(String(name)),
|
||||
sort_order: index + 1,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
upsert.run(record);
|
||||
});
|
||||
});
|
||||
|
||||
tx(nodes);
|
||||
|
||||
console.log(`✅ ${source}: zapisano/zmieniono ${nodes.length} pakietów.`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`Używam bazy: ${DB_PATH}`);
|
||||
const db = new Database(DB_PATH);
|
||||
ensureSchema(db);
|
||||
|
||||
for (const { source, url } of SOURCES) {
|
||||
console.log(`\n=== Przetwarzam ${source} (${url}) ===`);
|
||||
try {
|
||||
const xml = await fetchXml(url);
|
||||
const nodes = parseNodesFromXml(xml);
|
||||
|
||||
console.log(`📦 Znaleziono ${nodes.length} node'ów dla ${source}`);
|
||||
upsertBasePackages(db, source, nodes);
|
||||
} catch (err) {
|
||||
console.error(`❌ Błąd przy źródle ${source}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
db.close();
|
||||
console.log("\n🎉 Import pakietów podstawowych zakończony.");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("❌ Krytyczny błąd:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -29,21 +29,10 @@ body {
|
||||
color: var(--f-text);
|
||||
}
|
||||
|
||||
/* Theme Toggle */
|
||||
/* .theme-toggle-btn {
|
||||
@apply text-xl p-2 rounded-full cursor-pointer transition-colors;
|
||||
color: var(--f-text);
|
||||
} */
|
||||
|
||||
.theme-toggle-btn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.grecaptcha-badge {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
@apply text-[--f-link-text];
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
.f-section-grid-single {
|
||||
@apply grid items-center gap-5 max-w-6xl mx-auto;
|
||||
@apply grid items-center gap-5 max-w-7xl mx-auto;
|
||||
}
|
||||
|
||||
.f-section-grid-single-center {
|
||||
|
||||
Reference in New Issue
Block a user