Stylizacja hero, zmiany w InternetAddonsModal
This commit is contained in:
@@ -13,10 +13,6 @@ function money(amount) {
|
||||
return n.toFixed(2).replace(".", ",");
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapuje YAML telefonu (cards.yaml) na format używany w modalu:
|
||||
* { id, name, price_monthly, features: [{label, value}] }
|
||||
*/
|
||||
function mapPhoneYamlToPlans(phoneCards) {
|
||||
const list = Array.isArray(phoneCards) ? phoneCards : [];
|
||||
return list
|
||||
@@ -32,10 +28,6 @@ function mapPhoneYamlToPlans(phoneCards) {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dodatki z YAML:
|
||||
* { id, nazwa, typ, ilosc, min, max, krok, opis, cena }
|
||||
*/
|
||||
function normalizeAddons(addons) {
|
||||
const list = Array.isArray(addons) ? addons : [];
|
||||
return list
|
||||
@@ -43,7 +35,7 @@ function normalizeAddons(addons) {
|
||||
.map((a) => ({
|
||||
id: String(a.id),
|
||||
nazwa: String(a.nazwa),
|
||||
typ: String(a.typ || "checkbox"),
|
||||
typ: String(a.typ ?? a.type ?? "checkbox"),
|
||||
ilosc: !!a.ilosc,
|
||||
min: a.min != null ? Number(a.min) : 0,
|
||||
max: a.max != null ? Number(a.max) : 10,
|
||||
@@ -53,15 +45,33 @@ function normalizeAddons(addons) {
|
||||
}));
|
||||
}
|
||||
|
||||
function SectionAccordion({ title, right, open, onToggle, children }) {
|
||||
return (
|
||||
<div class={`f-accordion-item f-section-acc ${open ? "is-open" : ""}`}>
|
||||
<button type="button" class="f-accordion-header" onClick={onToggle}>
|
||||
<span class="f-accordion-header-left">
|
||||
<span class="f-modal-phone-name">{title}</span>
|
||||
</span>
|
||||
<span class="f-accordion-header-right">
|
||||
{right}
|
||||
<span class="f-acc-chevron" aria-hidden="true">
|
||||
{open ? "▲" : "▼"}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{open && <div class="f-accordion-body">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InternetAddonsModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
plan,
|
||||
|
||||
// ✅ nowe: z YAML
|
||||
phoneCards = [], // telefon/cards.yaml -> cards[]
|
||||
addons = [], // internet-swiatlowodowy/addons.yaml -> dodatki[]
|
||||
cenaOpis = "zł/mies.",
|
||||
phoneCards = [],
|
||||
addons = [],
|
||||
cenaOpis = "zł / mies.",
|
||||
}) {
|
||||
const phonePlans = useMemo(() => mapPhoneYamlToPlans(phoneCards), [phoneCards]);
|
||||
const addonsList = useMemo(() => normalizeAddons(addons), [addons]);
|
||||
@@ -69,51 +79,61 @@ export default function InternetAddonsModal({
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const [selectedPhoneId, setSelectedPhoneId] = useState(null);
|
||||
|
||||
// zamiast selectedAddons (DB) -> mapka ilości
|
||||
// { public_ip: 1, ip_v4_extra: 3 }
|
||||
const [selectedQty, setSelectedQty] = useState({});
|
||||
|
||||
// akordeony
|
||||
const [openPhoneId, setOpenPhoneId] = useState(null);
|
||||
const [baseOpen, setBaseOpen] = useState(true);
|
||||
const [openSections, setOpenSections] = useState({
|
||||
internet: true,
|
||||
phone: false,
|
||||
addons: false,
|
||||
summary: false,
|
||||
});
|
||||
|
||||
const toggleSection = (key) => {
|
||||
setOpenSections((prev) => {
|
||||
const nextOpen = !prev[key];
|
||||
return {
|
||||
internet: false,
|
||||
phone: false,
|
||||
addons: false,
|
||||
summary: false,
|
||||
[key]: nextOpen,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// reset wyborów po otwarciu / zmianie planu
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
setError("");
|
||||
setSelectedPhoneId(null);
|
||||
setSelectedQty({});
|
||||
setOpenPhoneId(null);
|
||||
setBaseOpen(true);
|
||||
}, [isOpen, plan]);
|
||||
setOpenSections({ internet: true, phone: false, addons: false, summary: false });
|
||||
}, [isOpen, plan?.id]);
|
||||
|
||||
if (!isOpen || !plan) return null;
|
||||
|
||||
const basePrice = Number(plan.price_monthly || 0);
|
||||
|
||||
const phonePrice = (() => {
|
||||
const phonePrice = useMemo(() => {
|
||||
if (!selectedPhoneId) return 0;
|
||||
const p = phonePlans.find((p) => String(p.id) === String(selectedPhoneId));
|
||||
const p = phonePlans.find((x) => String(x.id) === String(selectedPhoneId));
|
||||
return Number(p?.price_monthly || 0);
|
||||
})();
|
||||
}, [selectedPhoneId, phonePlans]);
|
||||
|
||||
const addonsPrice = addonsList.reduce((sum, a) => {
|
||||
const qty = Number(selectedQty[a.id] || 0);
|
||||
return sum + qty * Number(a.cena || 0);
|
||||
}, 0);
|
||||
const addonsPrice = useMemo(() => {
|
||||
return addonsList.reduce((sum, a) => {
|
||||
const qty = Number(selectedQty[a.id] || 0);
|
||||
return sum + qty * Number(a.cena || 0);
|
||||
}, 0);
|
||||
}, [selectedQty, addonsList]);
|
||||
|
||||
const totalMonthly = basePrice + phonePrice + addonsPrice;
|
||||
|
||||
const handlePhoneSelect = (id) => {
|
||||
if (id === null) {
|
||||
setSelectedPhoneId(null);
|
||||
setOpenPhoneId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedPhoneId(id);
|
||||
setOpenPhoneId((prev) => (String(prev) === String(id) ? null : id));
|
||||
};
|
||||
|
||||
const toggleCheckboxAddon = (id) => {
|
||||
@@ -129,6 +149,77 @@ export default function InternetAddonsModal({
|
||||
setSelectedQty((prev) => ({ ...prev, [id]: safe }));
|
||||
};
|
||||
|
||||
// Zapis do localStorage wyborów
|
||||
const LS_KEY = "fuz_offer_config_v1";
|
||||
|
||||
function buildOfferPayload() {
|
||||
const phone = selectedPhoneId
|
||||
? phonePlans.find((p) => String(p.id) === String(selectedPhoneId))
|
||||
: null;
|
||||
|
||||
const addonsChosen = addonsList
|
||||
.map((a) => {
|
||||
const qty = Number(selectedQty[a.id] || 0);
|
||||
if (qty <= 0) return null;
|
||||
return { id: a.id, nazwa: a.nazwa, qty, unit: Number(a.cena || 0) };
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
createdAt: new Date().toISOString(),
|
||||
pkg: { id: plan?.id ?? null, name: plan?.name ?? "", price: basePrice },
|
||||
|
||||
phone: phone ? { id: phone.id, name: phone.name, price: phone.price_monthly } : null,
|
||||
|
||||
addons: addonsChosen,
|
||||
|
||||
totals: {
|
||||
base: basePrice,
|
||||
phone: phonePrice,
|
||||
addons: addonsPrice,
|
||||
total: totalMonthly,
|
||||
currencyLabel: cenaOpis,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function moneyWithLabel(v) {
|
||||
return `${money(v)} ${cenaOpis}`;
|
||||
}
|
||||
|
||||
function buildOfferMessage(payload) {
|
||||
const lines = [];
|
||||
|
||||
lines.push(`Internet światłowodowy ${payload?.pkg?.name}: ${moneyWithLabel(payload?.totals?.base ?? 0)}`);
|
||||
lines.push(`Usługa Telefon: ${payload?.phone ? moneyWithLabel(payload.totals.phone) : "—"}`);
|
||||
lines.push(`Dodatkowe usługi: ${payload?.addons?.length ? moneyWithLabel(payload.totals.addons) : "—"}`);
|
||||
lines.push(`Łącznie: ${moneyWithLabel(payload?.totals?.total ?? 0)}`);
|
||||
|
||||
if (payload?.phone) {
|
||||
lines.push("");
|
||||
lines.push(`Telefon: ${payload.phone.name} (${moneyWithLabel(payload.phone.price)})`);
|
||||
}
|
||||
|
||||
if (Array.isArray(payload?.addons) && payload.addons.length) {
|
||||
lines.push("");
|
||||
lines.push("Dodatkowe usługi:");
|
||||
for (const it of payload.addons) {
|
||||
lines.push(`- ${it.nazwa} x${it.qty} @ ${moneyWithLabel(it.unit)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function saveOfferToLocalStorage() {
|
||||
try {
|
||||
const payload = buildOfferPayload();
|
||||
payload.message = buildOfferMessage(payload);
|
||||
localStorage.setItem(LS_KEY, JSON.stringify(payload));
|
||||
} catch { }
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div class="f-modal-overlay" onClick={onClose}>
|
||||
<button
|
||||
@@ -143,269 +234,261 @@ export default function InternetAddonsModal({
|
||||
✕
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="f-modal-panel f-modal-panel--compact"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div class="f-modal-panel f-modal-panel--compact" onClick={(e) => e.stopPropagation()}>
|
||||
<div class="f-modal-inner">
|
||||
<h2 class="f-modal-title">{plan.name} — konfiguracja usług</h2>
|
||||
|
||||
{/* INTERNET (fiber) jako akordeon */}
|
||||
<div class="f-modal-section">
|
||||
<div class={`f-accordion-item ${baseOpen ? "is-open" : ""}`}>
|
||||
<button
|
||||
type="button"
|
||||
class="f-accordion-header"
|
||||
onClick={() => setBaseOpen((prev) => !prev)}
|
||||
>
|
||||
<span class="f-modal-phone-name">{plan.name}</span>
|
||||
<span class="f-modal-phone-price">
|
||||
{money(basePrice)} {cenaOpis}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{baseOpen && plan.features && plan.features.length > 0 && (
|
||||
<div class="f-accordion-body">
|
||||
<ul class="f-card-features">
|
||||
{plan.features.map((f, idx) => (
|
||||
<li class="f-card-row" key={idx}>
|
||||
<span class="f-card-label">{f.label}</span>
|
||||
<span class="f-card-value">
|
||||
{formatFeatureValue(f.value)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <p class="text-red-600">{error}</p>}
|
||||
|
||||
{/* Telefon */}
|
||||
{/* INTERNET */}
|
||||
<div class="f-modal-section">
|
||||
<h3>Usługa telefoniczna</h3>
|
||||
<SectionAccordion
|
||||
title={plan.name}
|
||||
right={<span class="f-modal-phone-price">{money(basePrice)} {cenaOpis}</span>}
|
||||
open={openSections.internet}
|
||||
onToggle={() => toggleSection("internet")}
|
||||
>
|
||||
{plan.features?.length ? (
|
||||
<ul class="f-card-features">
|
||||
{plan.features.map((f, idx) => (
|
||||
<li class="f-card-row" key={idx}>
|
||||
<span class="f-card-label">{f.label}</span>
|
||||
<span class="f-card-value">{formatFeatureValue(f.value)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p class="opacity-80">Brak szczegółów.</p>
|
||||
)}
|
||||
</SectionAccordion>
|
||||
</div>
|
||||
|
||||
{phonePlans.length === 0 ? (
|
||||
<p>Brak dostępnych pakietów telefonicznych.</p>
|
||||
) : (
|
||||
<div class="f-modal-phone-list f-accordion">
|
||||
{/* brak telefonu */}
|
||||
<div class="f-accordion-item f-accordion-item--no-phone">
|
||||
<button
|
||||
type="button"
|
||||
class="f-accordion-header"
|
||||
onClick={() => handlePhoneSelect(null)}
|
||||
>
|
||||
<span class="f-accordion-header-left">
|
||||
{/* TELEFON */}
|
||||
<div class="f-modal-section">
|
||||
<SectionAccordion
|
||||
title="Usługa telefoniczna"
|
||||
right={
|
||||
<span class="f-modal-phone-price">
|
||||
{phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"}
|
||||
</span>
|
||||
}
|
||||
open={openSections.phone}
|
||||
onToggle={() => toggleSection("phone")}
|
||||
>
|
||||
{phonePlans.length === 0 ? (
|
||||
<p>Brak dostępnych pakietów telefonicznych.</p>
|
||||
) : (
|
||||
<div class="f-radio-list">
|
||||
<label class={`f-radio-item ${selectedPhoneId === null ? "is-selected" : ""}`}>
|
||||
<div class="f-radio-check">
|
||||
<input
|
||||
type="radio"
|
||||
name="phone-plan"
|
||||
checked={selectedPhoneId === null}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePhoneSelect(null);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={() => handlePhoneSelect(null)}
|
||||
/>
|
||||
<span class="f-modal-phone-name">Nie potrzebuję telefonu</span>
|
||||
</span>
|
||||
<span class="f-modal-phone-price">0,00 {cenaOpis}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{phonePlans.map((p) => {
|
||||
const isSelected = String(selectedPhoneId) === String(p.id);
|
||||
const isOpen = String(openPhoneId) === String(p.id);
|
||||
<div class="f-radio-main">
|
||||
<div class="f-radio-name">Nie potrzebuję telefonu</div>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div
|
||||
class={`f-accordion-item ${isOpen ? "is-open" : ""}`}
|
||||
key={p.id}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="f-accordion-header"
|
||||
onClick={() => handlePhoneSelect(p.id)}
|
||||
>
|
||||
<span class="f-accordion-header-left">
|
||||
<input
|
||||
type="radio"
|
||||
name="phone-plan"
|
||||
checked={isSelected}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePhoneSelect(p.id);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<span class="f-modal-phone-name">{p.name}</span>
|
||||
</span>
|
||||
<div class="f-radio-price">0,00 {cenaOpis}</div>
|
||||
</label>
|
||||
|
||||
<span class="f-modal-phone-price">
|
||||
{money(p.price_monthly)} {cenaOpis}
|
||||
</span>
|
||||
</button>
|
||||
{phonePlans.map((p) => {
|
||||
const isSelected = String(selectedPhoneId) === String(p.id);
|
||||
|
||||
{isOpen && (
|
||||
<div class="f-accordion-body">
|
||||
{p.features && p.features.length > 0 && (
|
||||
return (
|
||||
<div class="f-radio-block" key={p.id}>
|
||||
<label class={`f-radio-item ${isSelected ? "is-selected" : ""}`}>
|
||||
<div class="f-radio-check">
|
||||
<input
|
||||
type="radio"
|
||||
name="phone-plan"
|
||||
checked={isSelected}
|
||||
onChange={() => handlePhoneSelect(p.id)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="f-radio-main">
|
||||
<div class="f-radio-name">{p.name}</div>
|
||||
</div>
|
||||
|
||||
<div class="f-radio-price">
|
||||
{money(p.price_monthly)} {cenaOpis}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* Szczegóły telefony rozwinięte */}
|
||||
{p.features?.length > 0 && (
|
||||
<div class="f-radio-details">
|
||||
<ul class="f-card-features">
|
||||
{p.features
|
||||
.filter(
|
||||
(f) =>
|
||||
!String(f.label || "")
|
||||
.toLowerCase()
|
||||
.includes("aktyw"),
|
||||
(f) => !String(f.label || "").toLowerCase().includes("aktyw"),
|
||||
)
|
||||
.map((f, idx) => (
|
||||
<li class="f-card-row" key={idx}>
|
||||
<span class="f-card-label">{f.label}</span>
|
||||
<span class="f-card-value">
|
||||
{formatFeatureValue(f.value)}
|
||||
</span>
|
||||
<span class="f-card-value">{formatFeatureValue(f.value)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</SectionAccordion>
|
||||
</div>
|
||||
|
||||
{/* Dodatki internetowe */}
|
||||
{/* USLUGI DODATKOWE */}
|
||||
<div class="f-modal-section">
|
||||
<h3>Dodatkowe usługi</h3>
|
||||
<SectionAccordion
|
||||
title="Dodatkowe usługi"
|
||||
right={
|
||||
<span class="f-modal-phone-price">
|
||||
{addonsPrice ? `${money(addonsPrice)} ${cenaOpis}` : "—"}
|
||||
</span>
|
||||
}
|
||||
open={openSections.addons}
|
||||
onToggle={() => toggleSection("addons")}
|
||||
>
|
||||
{addonsList.length === 0 ? (
|
||||
<p>Brak dodatkowych usług.</p>
|
||||
) : (
|
||||
<div class="f-addon-list">
|
||||
{addonsList.map((a) => {
|
||||
const qty = Number(selectedQty[a.id] || 0);
|
||||
const isQty = a.typ === "quantity" || a.ilosc === true;
|
||||
|
||||
{addonsList.length === 0 ? (
|
||||
<p>Brak dodatkowych usług.</p>
|
||||
) : (
|
||||
<div class="f-addon-list">
|
||||
{addonsList.map((a) => {
|
||||
const qty = Number(selectedQty[a.id] || 0);
|
||||
const isQty = a.typ === "quantity" || a.ilosc === true;
|
||||
if (!isQty) {
|
||||
const checked = qty > 0;
|
||||
return (
|
||||
<label class="f-addon-item" key={a.id}>
|
||||
<div class="f-addon-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={() => toggleCheckboxAddon(a.id)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="f-addon-main">
|
||||
<div class="f-addon-name">{a.nazwa}</div>
|
||||
{a.opis && <div class="f-addon-desc">{a.opis}</div>}
|
||||
</div>
|
||||
|
||||
<div class="f-addon-price">
|
||||
{money(a.cena)} {cenaOpis}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
// Usługa z ilośćią
|
||||
const min = Number.isFinite(a.min) ? a.min : 0;
|
||||
const max = Number.isFinite(a.max) ? a.max : 10;
|
||||
const step = Number.isFinite(a.krok) ? a.krok : 1;
|
||||
const lineTotal = qty * Number(a.cena || 0);
|
||||
|
||||
if (!isQty) {
|
||||
const checked = qty > 0;
|
||||
return (
|
||||
<label class="f-addon-item" key={a.id}>
|
||||
<div class="f-addon-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={() => toggleCheckboxAddon(a.id)}
|
||||
/>
|
||||
</div>
|
||||
<div class="f-addon-item f-addon-item--qty" key={a.id}>
|
||||
<div class="f-addon-checkbox" aria-hidden="true"></div>
|
||||
|
||||
<div class="f-addon-main">
|
||||
<div class="f-addon-name">{a.nazwa}</div>
|
||||
{a.opis && <div class="f-addon-desc">{a.opis}</div>}
|
||||
</div>
|
||||
|
||||
<div class="f-addon-price">
|
||||
{money(a.cena)} {cenaOpis}
|
||||
<div class="f-addon-qty" onClick={(e) => e.stopPropagation()}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline"
|
||||
onClick={() => setQtyAddon(a.id, qty - step, min, max)}
|
||||
disabled={qty <= min}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
|
||||
<span class="f-addon-qty-value">{qty}</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline"
|
||||
onClick={() => setQtyAddon(a.id, qty + step, min, max)}
|
||||
disabled={qty >= max}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div class="f-addon-price">
|
||||
<div>
|
||||
{money(a.cena)} {cenaOpis}
|
||||
</div>
|
||||
<div class="f-addon-price-total">
|
||||
{qty > 0 ? `${money(lineTotal)} ${cenaOpis}` : "—"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// quantity
|
||||
const min = Number.isFinite(a.min) ? a.min : 0;
|
||||
const max = Number.isFinite(a.max) ? a.max : 10;
|
||||
const step = Number.isFinite(a.krok) ? a.krok : 1;
|
||||
|
||||
const lineTotal = qty * Number(a.cena || 0);
|
||||
|
||||
return (
|
||||
<div class="f-addon-item f-addon-item--qty" key={a.id}>
|
||||
{/* slot na checkbox (dla wyrównania kolumn) */}
|
||||
<div class="f-addon-checkbox" aria-hidden="true"></div>
|
||||
|
||||
<div class="f-addon-main">
|
||||
<div class="f-addon-name">{a.nazwa}</div>
|
||||
{a.opis && <div class="f-addon-desc">{a.opis}</div>}
|
||||
</div>
|
||||
|
||||
{/* licznik ilości bliżej prawej */}
|
||||
<div class="f-addon-qty" onClick={(e) => e.stopPropagation()}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline"
|
||||
onClick={() => setQtyAddon(a.id, qty - step, min, max)}
|
||||
disabled={qty <= min}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
|
||||
<span class="f-addon-qty-value">{qty}</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline"
|
||||
onClick={() => setQtyAddon(a.id, qty + step, min, max)}
|
||||
disabled={qty >= max}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* cena po prawej + suma pod spodem */}
|
||||
<div class="f-addon-price">
|
||||
<div>
|
||||
{money(a.cena)} {cenaOpis}
|
||||
</div>
|
||||
<div class="f-addon-price-total">
|
||||
{qty > 0 ? `${money(lineTotal)} ${cenaOpis}` : "—"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</SectionAccordion>
|
||||
</div>
|
||||
|
||||
{/* Podsumowanie */}
|
||||
<div class="f-modal-section f-summary">
|
||||
<h3>Podsumowanie miesięczne</h3>
|
||||
{/* PODSUMOWANIE */}
|
||||
<div class="f-modal-section">
|
||||
<SectionAccordion
|
||||
title="Podsumowanie miesięczne"
|
||||
right={<span class="f-modal-phone-price">{money(totalMonthly)} {cenaOpis}</span>}
|
||||
open={openSections.summary}
|
||||
onToggle={() => toggleSection("summary")}
|
||||
>
|
||||
<div class="f-summary">
|
||||
<div class="f-summary-list">
|
||||
<div class="f-summary-row">
|
||||
<span>Pakiet</span>
|
||||
<span>{money(basePrice)} {cenaOpis}</span>
|
||||
</div>
|
||||
|
||||
<div class="f-summary-list">
|
||||
<div class="f-summary-row">
|
||||
<span>Internet</span>
|
||||
<span>{money(basePrice)} {cenaOpis}</span>
|
||||
</div>
|
||||
<div class="f-summary-row">
|
||||
<span>Telefon</span>
|
||||
<span>{phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"}</span>
|
||||
</div>
|
||||
|
||||
<div class="f-summary-row">
|
||||
<span>Telefon</span>
|
||||
<span>{phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"}</span>
|
||||
</div>
|
||||
<div class="f-summary-row">
|
||||
<span>Dodatkowe usługi</span>
|
||||
<span>{addonsPrice ? `${money(addonsPrice)} ${cenaOpis}` : "—"}</span>
|
||||
</div>
|
||||
|
||||
<div class="f-summary-row">
|
||||
<span>Dodatki</span>
|
||||
<span>{addonsPrice ? `${money(addonsPrice)} ${cenaOpis}` : "—"}</span>
|
||||
</div>
|
||||
<div class="f-summary-total">
|
||||
<span>Łącznie</span>
|
||||
<span>{money(totalMonthly)} {cenaOpis}</span>
|
||||
</div>
|
||||
|
||||
<div class="f-summary-total">
|
||||
<span>Łącznie</span>
|
||||
<span>{money(totalMonthly)} {cenaOpis}</span>
|
||||
<a
|
||||
href="/kontakt#form"
|
||||
class="btn btn-primary w-full mt-4"
|
||||
onClick={() => saveOfferToLocalStorage()}
|
||||
>
|
||||
Wyślij zapytanie z tym wyborem
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SectionAccordion>
|
||||
</div>
|
||||
|
||||
<div class="f-floating-total" onClick={(e) => e.stopPropagation()}>
|
||||
<div class="f-floating-total-inner">
|
||||
<span class="f-floating-total-label">Suma</span>
|
||||
<span class="f-floating-total-value">
|
||||
{money(totalMonthly)} {cenaOpis}
|
||||
</span>
|
||||
<div class="f-floating-total-circle" role="status" aria-label="Cena miesięczna">
|
||||
<span class="f-floating-total-unit">Razem</span>
|
||||
<span class="f-floating-total-amount">{money(totalMonthly)}</span>
|
||||
<span class="f-floating-total-unit">{cenaOpis}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,537 +0,0 @@
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import "../../styles/modal.css";
|
||||
import "../../styles/offers-table.css";
|
||||
|
||||
function formatFeatureValue(val) {
|
||||
if (val === true || val === "true") return "✓";
|
||||
if (val === false || val === "false" || val == null) return "✕";
|
||||
return val;
|
||||
}
|
||||
|
||||
function money(amount) {
|
||||
const n = Number(amount || 0);
|
||||
return n.toFixed(2).replace(".", ",");
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapuje YAML telefonu (cards.yaml) na format używany w modalu:
|
||||
* { id, name, price_monthly, features: [{label, value}] }
|
||||
*/
|
||||
function mapPhoneYamlToPlans(phoneCards) {
|
||||
const list = Array.isArray(phoneCards) ? phoneCards : [];
|
||||
return list
|
||||
.filter((c) => c?.widoczny !== false)
|
||||
.map((c, idx) => ({
|
||||
id: String(c?.id ?? c?.nazwa ?? idx),
|
||||
name: c?.nazwa ?? "—",
|
||||
price_monthly: Number(c?.cena?.wartosc ?? 0),
|
||||
features: (Array.isArray(c?.parametry) ? c.parametry : []).map((p) => ({
|
||||
label: p.label,
|
||||
value: p.value,
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dodatki z YAML:
|
||||
* { id, nazwa, typ, ilosc, min, max, krok, opis, cena }
|
||||
*/
|
||||
function normalizeAddons(addons) {
|
||||
const list = Array.isArray(addons) ? addons : [];
|
||||
return list
|
||||
.filter((a) => a?.id && a?.nazwa)
|
||||
.map((a) => ({
|
||||
id: String(a.id),
|
||||
nazwa: String(a.nazwa),
|
||||
typ: String(a.typ ?? a.type ?? "checkbox"),
|
||||
ilosc: !!a.ilosc,
|
||||
min: a.min != null ? Number(a.min) : 0,
|
||||
max: a.max != null ? Number(a.max) : 10,
|
||||
krok: a.krok != null ? Number(a.krok) : 1,
|
||||
opis: a.opis ? String(a.opis) : "",
|
||||
cena: Number(a.cena ?? 0),
|
||||
}));
|
||||
}
|
||||
|
||||
/** ✅ Sekcja-akordeon (jak w internet modal) */
|
||||
function SectionAccordion({ title, right, open, onToggle, children }) {
|
||||
return (
|
||||
<div class={`f-accordion-item f-section-acc ${open ? "is-open" : ""}`}>
|
||||
<button type="button" class="f-accordion-header" onClick={onToggle}>
|
||||
<span class="f-accordion-header-left">
|
||||
<span class="f-modal-phone-name">{title}</span>
|
||||
</span>
|
||||
<span class="f-accordion-header-right">
|
||||
{right}
|
||||
<span class="f-acc-chevron" aria-hidden="true">
|
||||
{open ? "▲" : "▼"}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{open && <div class="f-accordion-body">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InternetAddonsModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
plan,
|
||||
|
||||
phoneCards = [],
|
||||
addons = [],
|
||||
cenaOpis = "zł/mies.",
|
||||
}) {
|
||||
const phonePlans = useMemo(() => mapPhoneYamlToPlans(phoneCards), [phoneCards]);
|
||||
const addonsList = useMemo(() => normalizeAddons(addons), [addons]);
|
||||
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const [selectedPhoneId, setSelectedPhoneId] = useState(null);
|
||||
const [selectedQty, setSelectedQty] = useState({});
|
||||
|
||||
// ✅ sekcje jako akordeony (jedna otwarta naraz)
|
||||
const [openSections, setOpenSections] = useState({
|
||||
internet: true,
|
||||
phone: false,
|
||||
addons: false,
|
||||
summary: false,
|
||||
});
|
||||
|
||||
const toggleSection = (key) => {
|
||||
setOpenSections((prev) => {
|
||||
const nextOpen = !prev[key];
|
||||
return {
|
||||
internet: false,
|
||||
phone: false,
|
||||
addons: false,
|
||||
summary: false,
|
||||
[key]: nextOpen,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// reset wyborów po otwarciu / zmianie planu
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
setError("");
|
||||
setSelectedPhoneId(null);
|
||||
setSelectedQty({});
|
||||
setOpenSections({ internet: true, phone: false, addons: false, summary: false });
|
||||
}, [isOpen, plan?.id]);
|
||||
|
||||
if (!isOpen || !plan) return null;
|
||||
|
||||
const basePrice = Number(plan.price_monthly || 0);
|
||||
|
||||
const phonePrice = useMemo(() => {
|
||||
if (!selectedPhoneId) return 0;
|
||||
const p = phonePlans.find((x) => String(x.id) === String(selectedPhoneId));
|
||||
return Number(p?.price_monthly || 0);
|
||||
}, [selectedPhoneId, phonePlans]);
|
||||
|
||||
const addonsPrice = useMemo(() => {
|
||||
return addonsList.reduce((sum, a) => {
|
||||
const qty = Number(selectedQty[a.id] || 0);
|
||||
return sum + qty * Number(a.cena || 0);
|
||||
}, 0);
|
||||
}, [selectedQty, addonsList]);
|
||||
|
||||
const totalMonthly = basePrice + phonePrice + addonsPrice;
|
||||
|
||||
const handlePhoneSelect = (id) => {
|
||||
if (id === null) {
|
||||
setSelectedPhoneId(null);
|
||||
return;
|
||||
}
|
||||
setSelectedPhoneId(id);
|
||||
};
|
||||
|
||||
const toggleCheckboxAddon = (id) => {
|
||||
setSelectedQty((prev) => {
|
||||
const next = { ...prev };
|
||||
next[id] = (next[id] || 0) > 0 ? 0 : 1;
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const setQtyAddon = (id, qty, min, max) => {
|
||||
const safe = Math.max(min, Math.min(max, qty));
|
||||
setSelectedQty((prev) => ({ ...prev, [id]: safe }));
|
||||
};
|
||||
|
||||
// ======================
|
||||
// ✅ zapis do localStorage (jak w Jambox)
|
||||
// ======================
|
||||
const LS_KEY = "fuz_offer_config_v1";
|
||||
|
||||
function buildOfferPayload() {
|
||||
const phone = selectedPhoneId
|
||||
? phonePlans.find((p) => String(p.id) === String(selectedPhoneId))
|
||||
: null;
|
||||
|
||||
const addonsChosen = addonsList
|
||||
.map((a) => {
|
||||
const qty = Number(selectedQty[a.id] || 0);
|
||||
if (qty <= 0) return null;
|
||||
return { id: a.id, nazwa: a.nazwa, qty, unit: Number(a.cena || 0) };
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
createdAt: new Date().toISOString(),
|
||||
// ważne: plan internetowy jako "pkg" żeby kontakt miał wspólny format
|
||||
pkg: { id: plan?.id ?? null, name: plan?.name ?? "", price: basePrice },
|
||||
|
||||
phone: phone ? { id: phone.id, name: phone.name, price: phone.price_monthly } : null,
|
||||
|
||||
// tu nie ma dekodera i tv-addons
|
||||
decoder: null,
|
||||
tvAddons: [],
|
||||
addons: addonsChosen,
|
||||
|
||||
totals: {
|
||||
base: basePrice,
|
||||
phone: phonePrice,
|
||||
decoder: 0,
|
||||
tv: 0,
|
||||
addons: addonsPrice,
|
||||
total: totalMonthly,
|
||||
currencyLabel: cenaOpis,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function moneyWithLabel(v) {
|
||||
return `${money(v)} ${cenaOpis}`;
|
||||
}
|
||||
|
||||
function buildOfferMessage(payload) {
|
||||
const lines = [];
|
||||
|
||||
lines.push(`Wybrana oferta: ${payload?.pkg?.name || "—"}`);
|
||||
lines.push("");
|
||||
|
||||
// ✅ WSZYSTKIE linie jak w podsumowaniu (wspólny standard)
|
||||
lines.push(`Pakiet: ${moneyWithLabel(payload?.totals?.base ?? 0)}`);
|
||||
lines.push(`Telefon: ${payload?.phone ? moneyWithLabel(payload.totals.phone) : "—"}`);
|
||||
lines.push(`Dekoder: —`);
|
||||
lines.push(`Dodatki TV: —`);
|
||||
lines.push(`Dodatkowe usługi: ${payload?.addons?.length ? moneyWithLabel(payload.totals.addons) : "—"}`);
|
||||
lines.push(`Łącznie: ${moneyWithLabel(payload?.totals?.total ?? 0)}`);
|
||||
|
||||
if (payload?.phone) {
|
||||
lines.push("");
|
||||
lines.push(`Telefon: ${payload.phone.name} (${moneyWithLabel(payload.phone.price)})`);
|
||||
}
|
||||
|
||||
if (Array.isArray(payload?.addons) && payload.addons.length) {
|
||||
lines.push("");
|
||||
lines.push("Dodatkowe usługi:");
|
||||
for (const it of payload.addons) {
|
||||
lines.push(`- ${it.nazwa} x${it.qty} @ ${moneyWithLabel(it.unit)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function saveOfferToLocalStorage() {
|
||||
try {
|
||||
const payload = buildOfferPayload();
|
||||
payload.message = buildOfferMessage(payload);
|
||||
localStorage.setItem(LS_KEY, JSON.stringify(payload));
|
||||
} catch {}
|
||||
}
|
||||
// ======================
|
||||
|
||||
return (
|
||||
<div class="f-modal-overlay" onClick={onClose}>
|
||||
<button
|
||||
class="f-modal-close"
|
||||
type="button"
|
||||
aria-label="Zamknij"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
<div class="f-modal-panel f-modal-panel--compact" onClick={(e) => e.stopPropagation()}>
|
||||
<div class="f-modal-inner">
|
||||
<h2 class="f-modal-title">{plan.name} — konfiguracja usług</h2>
|
||||
|
||||
{error && <p class="text-red-600">{error}</p>}
|
||||
|
||||
{/* ✅ INTERNET */}
|
||||
<div class="f-modal-section">
|
||||
<SectionAccordion
|
||||
title={plan.name}
|
||||
right={<span class="f-modal-phone-price">{money(basePrice)} {cenaOpis}</span>}
|
||||
open={openSections.internet}
|
||||
onToggle={() => toggleSection("internet")}
|
||||
>
|
||||
{plan.features?.length ? (
|
||||
<ul class="f-card-features">
|
||||
{plan.features.map((f, idx) => (
|
||||
<li class="f-card-row" key={idx}>
|
||||
<span class="f-card-label">{f.label}</span>
|
||||
<span class="f-card-value">{formatFeatureValue(f.value)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p class="opacity-80">Brak szczegółów.</p>
|
||||
)}
|
||||
</SectionAccordion>
|
||||
</div>
|
||||
|
||||
{/* ✅ TELEFON — identyczny wygląd jak w TV (f-radio-*) */}
|
||||
<div class="f-modal-section">
|
||||
<SectionAccordion
|
||||
title="Usługa telefoniczna"
|
||||
right={
|
||||
<span class="f-modal-phone-price">
|
||||
{phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"}
|
||||
</span>
|
||||
}
|
||||
open={openSections.phone}
|
||||
onToggle={() => toggleSection("phone")}
|
||||
>
|
||||
{phonePlans.length === 0 ? (
|
||||
<p>Brak dostępnych pakietów telefonicznych.</p>
|
||||
) : (
|
||||
<div class="f-radio-list">
|
||||
{/* brak telefonu */}
|
||||
<label class={`f-radio-item ${selectedPhoneId === null ? "is-selected" : ""}`}>
|
||||
<div class="f-radio-check">
|
||||
<input
|
||||
type="radio"
|
||||
name="phone-plan"
|
||||
checked={selectedPhoneId === null}
|
||||
onChange={() => handlePhoneSelect(null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="f-radio-main">
|
||||
<div class="f-radio-name">Nie potrzebuję telefonu</div>
|
||||
</div>
|
||||
|
||||
<div class="f-radio-price">0,00 {cenaOpis}</div>
|
||||
</label>
|
||||
|
||||
{/* pakiety */}
|
||||
{phonePlans.map((p) => {
|
||||
const isSelected = String(selectedPhoneId) === String(p.id);
|
||||
|
||||
return (
|
||||
<div class="f-radio-block" key={p.id}>
|
||||
<label class={`f-radio-item ${isSelected ? "is-selected" : ""}`}>
|
||||
<div class="f-radio-check">
|
||||
<input
|
||||
type="radio"
|
||||
name="phone-plan"
|
||||
checked={isSelected}
|
||||
onChange={() => handlePhoneSelect(p.id)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="f-radio-main">
|
||||
<div class="f-radio-name">{p.name}</div>
|
||||
</div>
|
||||
|
||||
<div class="f-radio-price">
|
||||
{money(p.price_monthly)} {cenaOpis}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* ✅ detale ZAWSZE widoczne (jak w TV) */}
|
||||
{p.features?.length > 0 && (
|
||||
<div class="f-radio-details">
|
||||
<ul class="f-card-features">
|
||||
{p.features
|
||||
.filter(
|
||||
(f) => !String(f.label || "").toLowerCase().includes("aktyw"),
|
||||
)
|
||||
.map((f, idx) => (
|
||||
<li class="f-card-row" key={idx}>
|
||||
<span class="f-card-label">{f.label}</span>
|
||||
<span class="f-card-value">{formatFeatureValue(f.value)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</SectionAccordion>
|
||||
</div>
|
||||
|
||||
{/* ✅ DODATKI */}
|
||||
<div class="f-modal-section">
|
||||
<SectionAccordion
|
||||
title="Dodatkowe usługi"
|
||||
right={
|
||||
<span class="f-modal-phone-price">
|
||||
{addonsPrice ? `${money(addonsPrice)} ${cenaOpis}` : "—"}
|
||||
</span>
|
||||
}
|
||||
open={openSections.addons}
|
||||
onToggle={() => toggleSection("addons")}
|
||||
>
|
||||
{addonsList.length === 0 ? (
|
||||
<p>Brak dodatkowych usług.</p>
|
||||
) : (
|
||||
<div class="f-addon-list">
|
||||
{addonsList.map((a) => {
|
||||
const qty = Number(selectedQty[a.id] || 0);
|
||||
const isQty = a.typ === "quantity" || a.ilosc === true;
|
||||
|
||||
if (!isQty) {
|
||||
const checked = qty > 0;
|
||||
return (
|
||||
<label class="f-addon-item" key={a.id}>
|
||||
<div class="f-addon-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={() => toggleCheckboxAddon(a.id)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="f-addon-main">
|
||||
<div class="f-addon-name">{a.nazwa}</div>
|
||||
{a.opis && <div class="f-addon-desc">{a.opis}</div>}
|
||||
</div>
|
||||
|
||||
<div class="f-addon-price">
|
||||
{money(a.cena)} {cenaOpis}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
// quantity
|
||||
const min = Number.isFinite(a.min) ? a.min : 0;
|
||||
const max = Number.isFinite(a.max) ? a.max : 10;
|
||||
const step = Number.isFinite(a.krok) ? a.krok : 1;
|
||||
const lineTotal = qty * Number(a.cena || 0);
|
||||
|
||||
return (
|
||||
<div class="f-addon-item f-addon-item--qty" key={a.id}>
|
||||
<div class="f-addon-checkbox" aria-hidden="true"></div>
|
||||
|
||||
<div class="f-addon-main">
|
||||
<div class="f-addon-name">{a.nazwa}</div>
|
||||
{a.opis && <div class="f-addon-desc">{a.opis}</div>}
|
||||
</div>
|
||||
|
||||
<div class="f-addon-qty" onClick={(e) => e.stopPropagation()}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline"
|
||||
onClick={() => setQtyAddon(a.id, qty - step, min, max)}
|
||||
disabled={qty <= min}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
|
||||
<span class="f-addon-qty-value">{qty}</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline"
|
||||
onClick={() => setQtyAddon(a.id, qty + step, min, max)}
|
||||
disabled={qty >= max}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="f-addon-price">
|
||||
<div>
|
||||
{money(a.cena)} {cenaOpis}
|
||||
</div>
|
||||
<div class="f-addon-price-total">
|
||||
{qty > 0 ? `${money(lineTotal)} ${cenaOpis}` : "—"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</SectionAccordion>
|
||||
</div>
|
||||
|
||||
{/* ✅ PODSUMOWANIE */}
|
||||
<div class="f-modal-section">
|
||||
<SectionAccordion
|
||||
title="Podsumowanie miesięczne"
|
||||
right={<span class="f-modal-phone-price">{money(totalMonthly)} {cenaOpis}</span>}
|
||||
open={openSections.summary}
|
||||
onToggle={() => toggleSection("summary")}
|
||||
>
|
||||
<div class="f-summary">
|
||||
<div class="f-summary-list">
|
||||
{/* ✅ WSZYSTKIE linie jak w Jambox (standard) */}
|
||||
<div class="f-summary-row">
|
||||
<span>Pakiet</span>
|
||||
<span>{money(basePrice)} {cenaOpis}</span>
|
||||
</div>
|
||||
|
||||
<div class="f-summary-row">
|
||||
<span>Telefon</span>
|
||||
<span>{phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"}</span>
|
||||
</div>
|
||||
|
||||
<div class="f-summary-row">
|
||||
<span>Dekoder</span>
|
||||
<span>—</span>
|
||||
</div>
|
||||
|
||||
<div class="f-summary-row">
|
||||
<span>Dodatki TV</span>
|
||||
<span>—</span>
|
||||
</div>
|
||||
|
||||
<div class="f-summary-row">
|
||||
<span>Dodatkowe usługi</span>
|
||||
<span>{addonsPrice ? `${money(addonsPrice)} ${cenaOpis}` : "—"}</span>
|
||||
</div>
|
||||
|
||||
<div class="f-summary-total">
|
||||
<span>Łącznie</span>
|
||||
<span>{money(totalMonthly)} {cenaOpis}</span>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="/kontakt#form"
|
||||
class="btn btn-primary w-full mt-4"
|
||||
onClick={() => saveOfferToLocalStorage()}
|
||||
>
|
||||
Wyślij zapytanie z tym wyborem
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</SectionAccordion>
|
||||
</div>
|
||||
|
||||
<div class="f-floating-total" onClick={(e) => e.stopPropagation()}>
|
||||
<div class="f-floating-total-circle" role="status" aria-label="Cena miesięczna">
|
||||
<span class="f-floating-total-unit">Razem</span>
|
||||
<span class="f-floating-total-amount">{money(totalMonthly)}</span>
|
||||
<span class="f-floating-total-unit">{cenaOpis}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import Markdown from "../Markdown.jsx";
|
||||
import OffersSwitches from "../OffersSwitches.jsx";
|
||||
import InternetAddonsModal from "./InternetAddonsModalCompact.jsx";
|
||||
import InternetAddonsModal from "./InternetAddonsModal.jsx";
|
||||
import "../../styles/offers-table.css";
|
||||
|
||||
function formatMoney(amount, currency = "PLN") {
|
||||
|
||||
Reference in New Issue
Block a user