Przebudowa stron na indywidualne karty , pobierane z bazy danych
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user