Przebudowa stron na indywidualne karty , pobierane z bazy danych
This commit is contained in:
@@ -5,7 +5,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview"
|
"preview": "astro preview",
|
||||||
|
"update:jambox:base": "node src/scripts/update-jambox-base.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/node": "^9.5.1",
|
"@astrojs/node": "^9.5.1",
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
"@preact/signals": "^2.5.1",
|
"@preact/signals": "^2.5.1",
|
||||||
"astro": "^5.16.0",
|
"astro": "^5.16.0",
|
||||||
"better-sqlite3": "^12.4.6",
|
"better-sqlite3": "^12.4.6",
|
||||||
|
"fast-xml-parser": "^5.3.2",
|
||||||
"globby": "^16.0.0",
|
"globby": "^16.0.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsdom": "^27.2.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:
|
sections:
|
||||||
- title: Sprawdź dostępność usługi
|
# - title: Sprawdź dostępność usługi
|
||||||
image: section-range.webp
|
# image:
|
||||||
button:
|
# button:
|
||||||
text: "Sprawdź dostępność pod Twoim adresem →"
|
# text: "Sprawdź dostępność pod Twoim adresem →"
|
||||||
url: "/mapa-zasiegu"
|
# url: "/mapa-zasiegu"
|
||||||
title: "Sprawdź zasięg Internetu światłowodowego FUZ"
|
# title: "Sprawdź zasięg Internetu światłowodowego FUZ"
|
||||||
content: |
|
# content: |
|
||||||
Naszą sieć światłowodową systematycznie rozbudowujemy, ale infrastruktura nie dociera jeszcze do wszystkich adresów.
|
# 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.
|
||||||
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: Router WiFi HL-4BX3V-F
|
- title: Router WiFi HL-4BX3V-F
|
||||||
image: "HL-4BX3V-F.webp"
|
image: "HL-4BX3V-F.webp"
|
||||||
content: |
|
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.
|
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:
|
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
|
- title: Zachowaj dotychczasowy numer telefonu
|
||||||
image: "przeniesienie.png"
|
image: "przeniesienie.png"
|
||||||
content: |
|
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";
|
// import "../../styles/offers/offers-switches.css";
|
||||||
|
|
||||||
export default function OffersSwitches({ switches, selected, onSwitch }) {
|
function buildLabels(switches, selected) {
|
||||||
if (!switches.length) return null;
|
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 (
|
return (
|
||||||
<div class="f-switches-wrapper">
|
<div class="f-switches-wrapper">
|
||||||
{switches.map((sw) => (
|
{effectiveSwitches.map((sw) => (
|
||||||
<div class="f-switch-box">
|
<div class="f-switch-box">
|
||||||
<div class="f-switch-group">
|
<div class="f-switch-group">
|
||||||
{sw.opcje.map((op) => (
|
{sw.opcje.map((op) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`f-switch ${selected[sw.id] === op.id ? "active" : ""
|
class={`f-switch ${
|
||||||
}`}
|
String(effectiveSelected[sw.id]) === String(op.id)
|
||||||
onClick={() => onSwitch(sw.id, op.id)}
|
? "active"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
onClick={() => handleClick(sw.id, op.id)}
|
||||||
title={sw.title}
|
title={sw.title}
|
||||||
>
|
>
|
||||||
{op.nazwa}
|
{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 { useState } from "preact/hooks";
|
||||||
|
|
||||||
import OffersSwitches from "./Offers/OffersSwitches.jsx";
|
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";
|
import OffersExtraServices from "./Offers/OffersExtraServices.jsx";
|
||||||
|
|
||||||
export default function OffersIsland({ data }) {
|
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 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 SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
||||||
import Markdown from "../../islands/Markdown.jsx";
|
|
||||||
import OffersIsland from "../../islands/OffersIsland.jsx";
|
|
||||||
|
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
@@ -11,52 +10,19 @@ import fs from "fs";
|
|||||||
const seo = yaml.load(
|
const seo = yaml.load(
|
||||||
fs.readFileSync("./src/content/internet-swiatlowodowy/seo.yaml", "utf8"),
|
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}>
|
<DefaultLayout seo={seo}>
|
||||||
<Hero {...hero} />
|
|
||||||
|
|
||||||
<section class="f-section">
|
<section class="f-section">
|
||||||
<div class="f-section-grid-single md:grid-cols-1">
|
<div class="f-section-grid-single md:grid-cols-1">
|
||||||
{page.title.map((line: any) => <h1 class="f-section-title">{line}</h1>)}
|
<h1 class="f-section-title">Internet światłowodowy</h1>
|
||||||
{first.title && <h3>{first.title}</h3>}
|
<div class="fuz-markdown max-w-none">
|
||||||
<Markdown text={first.content} />
|
<p>Wybierz rodzaj budynku i czas trwania umowy</p>
|
||||||
|
</div>
|
||||||
|
<OffersSwitches client:load />
|
||||||
|
<InternetDbOffersCards client:load />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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" />
|
<SectionRenderer src="./src/content/internet-swiatlowodowy/section.yaml" />
|
||||||
</DefaultLayout>
|
</DefaultLayout>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Markdown from "../../islands/Markdown.jsx";
|
|||||||
import Modal from "../../islands/Modal.jsx";
|
import Modal from "../../islands/Modal.jsx";
|
||||||
import OffersIsland from "../../islands/OffersIsland.jsx";
|
import OffersIsland from "../../islands/OffersIsland.jsx";
|
||||||
import JamboxMozliwosci from "../../components/sections/SectionJamboxMozliwosci.astro";
|
import JamboxMozliwosci from "../../components/sections/SectionJamboxMozliwosci.astro";
|
||||||
|
import JamboxBasePackages from "../../islands/Offers/JamboxBasePackages.jsx";
|
||||||
|
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
@@ -48,12 +49,18 @@ const rest = page.paragraphs.slice(1);
|
|||||||
|
|
||||||
<section class="f-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 max-w-6xl mx-auto">
|
||||||
|
|
||||||
|
<JamboxBasePackages
|
||||||
|
client:load
|
||||||
|
source=""
|
||||||
|
title=""/>
|
||||||
|
|
||||||
<OffersIsland client:load data={data} />
|
<OffersIsland client:load data={data} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
<!-- <OffersIsland client:load data={data} /> -->
|
|
||||||
{
|
{
|
||||||
rest.map((p: Paragraph) => (
|
rest.map((p: Paragraph) => (
|
||||||
<section class="f-section">
|
<section class="f-section">
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ const mapStyleId = "8e0a97af9476f2d3";
|
|||||||
<div class="f-info-header">
|
<div class="f-info-header">
|
||||||
<div class="f-info-heading">
|
<div class="f-info-heading">
|
||||||
${
|
${
|
||||||
result.availableFiber
|
result.available
|
||||||
? `<span class="ok">✔</span> Internet światłowodowy dostępny`
|
? `<span class="ok">✔</span> Internet światłowodowy dostępny`
|
||||||
: `<span class="no">✖</span> Światłowód niedostępny`
|
: `<span class="no">✖</span> Światłowód niedostępny`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
---
|
---
|
||||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||||
import Hero from "../../components/hero/Hero.astro";
|
|
||||||
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
||||||
import Markdown from "../../islands/Markdown.jsx";
|
import OffersPhoneCards from "../../islands/OffersPhoneCards.jsx";
|
||||||
import OffersIsland from "../../islands/OffersIsland.jsx";
|
|
||||||
|
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
@@ -11,44 +9,18 @@ import fs from "fs";
|
|||||||
const seo = yaml.load(
|
const seo = yaml.load(
|
||||||
fs.readFileSync("./src/content/telefon/seo.yaml", "utf8"),
|
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}>
|
<DefaultLayout seo={seo}>
|
||||||
<Hero {...hero} />
|
|
||||||
|
|
||||||
<section class="f-section">
|
<section class="f-section">
|
||||||
<div class="f-section-grid-single md:grid-cols-1">
|
<div class="f-section-grid-single md:grid-cols-1">
|
||||||
{page.title.map((line: any) => <h1 class="f-section-title">{line}</h1>)}
|
<h1 class="f-section-title">Usługa telefonu</h1>
|
||||||
{first.title && <h3>{first.title}</h3>}
|
<OffersPhoneCards client:load />
|
||||||
<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} />
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- <OffersIsland client:load data={data} /> -->
|
<!-- <OffersIsland client:load data={data} /> -->
|
||||||
{
|
<!-- {
|
||||||
rest.map((p: Paragraph) => (
|
rest.map((p: Paragraph) => (
|
||||||
<section class="f-section">
|
<section class="f-section">
|
||||||
<div class="f-section-grid-single md:grid-cols-1">
|
<div class="f-section-grid-single md:grid-cols-1">
|
||||||
@@ -57,7 +29,7 @@ const rest = page.paragraphs.slice(1);
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
))
|
))
|
||||||
}
|
} -->
|
||||||
|
|
||||||
<SectionRenderer src="./src/content/telefon/section.yaml" />
|
<SectionRenderer src="./src/content/telefon/section.yaml" />
|
||||||
</DefaultLayout>
|
</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);
|
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 {
|
.grecaptcha-badge {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@apply text-[--f-link-text];
|
@apply text-[--f-link-text];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.f-section-grid-single {
|
.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 {
|
.f-section-grid-single-center {
|
||||||
|
|||||||
Reference in New Issue
Block a user