diff --git a/src/islands/Internet/InternetAddonsModal.jsx b/src/islands/Internet/InternetAddonsModal.jsx
index 28bcdcc..4f991f0 100644
--- a/src/islands/Internet/InternetAddonsModal.jsx
+++ b/src/islands/Internet/InternetAddonsModal.jsx
@@ -1,85 +1,31 @@
import { useEffect, useMemo, useState } from "preact/hooks";
-import useDraggableFloating from "../hooks/useDraggableFloating.js";
+import OfferModalShell from "../modals/OfferModalShell.jsx";
+
+import PlanSection from "../modals/sections/PlanSection.jsx";
+import PhoneSection from "../modals/sections/PhoneSection.jsx";
+import AddonsSection from "../modals/sections/AddonsSection.jsx";
+import SummarySection from "../modals/sections/SummarySection.jsx";
+import FloatingTotal from "../modals/sections/FloatingTotal.jsx";
+
+import { mapPhoneYamlToPlans, normalizeAddons } from "../../lib/offer-normalize.js";
+import { saveOfferToLocalStorage } from "../../lib/offer-payload.js";
+
import "../../styles/modal.css";
import "../../styles/addons.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(".", ",");
-}
-
-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,
- })),
- }));
-}
-
-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),
- }));
-}
-
-function SectionAccordion({ title, right, open, onToggle, children }) {
- return (
-
-
-
- {open &&
{children}
}
-
- );
-}
-
export default function InternetAddonsModal({
isOpen,
onClose,
plan,
+
phoneCards = [],
addons = [],
+
cenaOpis = "zł / mies.",
}) {
- const floating = useDraggableFloating("fuz_floating_total_pos_internet_v1");
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({});
@@ -93,19 +39,12 @@ export default function InternetAddonsModal({
const toggleSection = (key) => {
setOpenSections((prev) => {
const nextOpen = !prev[key];
- return {
- internet: false,
- phone: false,
- addons: false,
- summary: false,
- [key]: nextOpen,
- };
+ return { internet: false, phone: false, addons: false, summary: false, [key]: nextOpen };
});
};
useEffect(() => {
if (!isOpen) return;
- setError("");
setSelectedPhoneId(null);
setSelectedQty({});
setOpenSections({ internet: true, phone: false, addons: false, summary: false });
@@ -124,36 +63,13 @@ export default function InternetAddonsModal({
const addonsPrice = useMemo(() => {
return addonsList.reduce((sum, a) => {
const qty = Number(selectedQty[a.id] || 0);
- return sum + qty * Number(a.cena || 0);
+ const unit = Number(a.cena || 0);
+ return sum + qty * unit;
}, 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 wyborów
- const LS_KEY = "fuz_offer_config_v1";
-
function buildOfferPayload() {
const phone = selectedPhoneId
? phonePlans.find((p) => String(p.id) === String(selectedPhoneId))
@@ -170,11 +86,8 @@ export default function InternetAddonsModal({
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,
@@ -185,322 +98,62 @@ export default function InternetAddonsModal({
};
}
- 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 { }
- }
-
+ const onSend = () => {
+ const payload = buildOfferPayload();
+ saveOfferToLocalStorage(payload, cenaOpis);
+ };
return (
-
-
+
+ toggleSection("internet")}
+ price={basePrice}
+ cenaOpis={cenaOpis}
+ features={plan.features || []}
+ />
- e.stopPropagation()}>
-
-
{plan.name} — konfiguracja usług
+
toggleSection("phone")}
+ cenaOpis={cenaOpis}
+ phonePlans={phonePlans}
+ selectedPhoneId={selectedPhoneId}
+ setSelectedPhoneId={setSelectedPhoneId}
+ phonePrice={phonePrice}
+ />
- {error && {error}
}
+ toggleSection("addons")}
+ cenaOpis={cenaOpis}
+ addonsList={addonsList}
+ selectedQty={selectedQty}
+ setSelectedQty={setSelectedQty}
+ addonsPrice={addonsPrice}
+ getUnitPrice={(a) => Number(a.cena || 0)}
+ />
- {/* INTERNET */}
-
-
{money(basePrice)} {cenaOpis}}
- open={openSections.internet}
- onToggle={() => toggleSection("internet")}
- >
- {plan.features?.length ? (
-
- {plan.features.map((f, idx) => (
- -
- {f.label}
- {formatFeatureValue(f.value)}
-
- ))}
-
- ) : (
- Brak szczegółów.
- )}
-
-
+ toggleSection("summary")}
+ cenaOpis={cenaOpis}
+ totalMonthly={totalMonthly}
+ ctaHref="/kontakt#form"
+ onSend={onSend}
+ rows={[
+ { label: "Pakiet", value: basePrice, showDashIfZero: false },
+ { label: "Telefon", value: phonePrice, showDashIfZero: true },
+ { label: "Dodatkowe usługi", value: addonsPrice, showDashIfZero: true },
+ ]}
+ />
- {/* TELEFON */}
-
-
- {phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"}
-
- }
- open={openSections.phone}
- onToggle={() => toggleSection("phone")}
- >
- {phonePlans.length === 0 ? (
- Brak dostępnych pakietów telefonicznych.
- ) : (
-
-
-
- {phonePlans.map((p) => {
- const isSelected = String(selectedPhoneId) === String(p.id);
-
- return (
-
-
-
- {/* Szczegóły telefony rozwinięte */}
- {p.features?.length > 0 && (
-
-
- {p.features
- .filter(
- (f) => !String(f.label || "").toLowerCase().includes("aktyw"),
- )
- .map((f, idx) => (
- -
- {f.label}
- {formatFeatureValue(f.value)}
-
- ))}
-
-
- )}
-
- );
- })}
-
- )}
-
-
-
- {/* USLUGI DODATKOWE */}
-
-
- {addonsPrice ? `${money(addonsPrice)} ${cenaOpis}` : "—"}
-
- }
- open={openSections.addons}
- onToggle={() => toggleSection("addons")}
- >
- {addonsList.length === 0 ? (
- Brak dodatkowych usług.
- ) : (
-
- {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 (
-
- );
- }
-
- // 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);
-
- return (
-
-
-
-
-
{a.nazwa}
- {a.opis &&
{a.opis}
}
-
-
-
e.stopPropagation()}>
-
-
- {qty}
-
-
-
-
-
-
- {money(a.cena)} {cenaOpis}
-
-
- {qty > 0 ? `${money(lineTotal)} ${cenaOpis}` : "—"}
-
-
-
- );
- })}
-
- )}
-
-
-
- {/* PODSUMOWANIE */}
-
-
{money(totalMonthly)} {cenaOpis}}
- open={openSections.summary}
- onToggle={() => toggleSection("summary")}
- >
-
-
-
- Pakiet
- {money(basePrice)} {cenaOpis}
-
-
-
- Telefon
- {phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"}
-
-
-
- Dodatkowe usługi
- {addonsPrice ? `${money(addonsPrice)} ${cenaOpis}` : "—"}
-
-
-
- Łącznie
- {money(totalMonthly)} {cenaOpis}
-
-
-
saveOfferToLocalStorage()}
- >
- Wyślij zapytanie z tym wyborem
-
-
-
-
-
-
- e.stopPropagation()}
- >
-
- Razem
- {money(totalMonthly)}
- {cenaOpis}
-
-
-
-
-
+
+
);
}
diff --git a/src/islands/jambox/JamboxAddonsModal.jsx b/src/islands/jambox/JamboxAddonsModal.jsx
index 2b225bf..185f82c 100644
--- a/src/islands/jambox/JamboxAddonsModal.jsx
+++ b/src/islands/jambox/JamboxAddonsModal.jsx
@@ -1,171 +1,21 @@
import { useEffect, useMemo, useState } from "preact/hooks";
-import useDraggableFloating from "../hooks/useDraggableFloating.js";
+import OfferModalShell from "../modals/OfferModalShell.jsx";
+
+import PlanSection from "../modals/sections/PlanSection.jsx";
+import DecoderSection from "../modals/sections/DecoderSection.jsx";
+import TvAddonsSection from "../modals/sections/TvAddonsSection.jsx";
+import PhoneSection from "../modals/sections/PhoneSection.jsx";
+import AddonsSection from "../modals/sections/AddonsSection.jsx";
+import SummarySection from "../modals/sections/SummarySection.jsx";
+import FloatingTotal from "../modals/sections/FloatingTotal.jsx";
+
+import { mapPhoneYamlToPlans, normalizeAddons, normalizeDecoders } from "../../lib/offer-normalize.js";
+import { isTvAddonAvailableForPkg, hasTvTermPricing, getAddonUnitPrice } from "../../lib/offer-pricing.js";
+import { saveOfferToLocalStorage } from "../../lib/offer-payload.js";
import "../../styles/modal.css";
import "../../styles/addons.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(".", ",");
-}
-
-/** telefon z YAML (phone/cards.yaml -> cards[]) => { id, name, price_monthly, features[] } */
-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,
- })),
- }));
-}
-
-/** dekodery z YAML */
-function normalizeDecoders(list) {
- const arr = Array.isArray(list) ? list : [];
- return arr
- .filter((d) => d?.id && d?.nazwa)
- .map((d) => ({
- id: String(d.id),
- nazwa: String(d.nazwa),
- opis: d.opis ? String(d.opis) : "",
- cena: Number(d.cena ?? 0),
- }));
-}
-
-/** dodatki z YAML (tv-addons.yaml / addons.yaml) */
-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: a.cena ?? 0,
- tid: String(a.tid),
- }));
-}
-
-function normKey(s) {
- return String(s || "").trim().toLowerCase().replace(/\s+/g, " ");
-}
-
-/** TV: wybór wariantu ceny po pkg.name, albo fallback "*" */
-function pickTvVariant(addon, pkgName) {
- const c = addon?.cena;
- if (!Array.isArray(c)) return null;
-
- const wanted = normKey(pkgName);
-
- for (const row of c) {
- const pk = row?.pakiety;
- if (!Array.isArray(pk)) continue;
- if (pk.some((p) => normKey(p) === wanted)) return row;
- }
-
- for (const row of c) {
- const pk = row?.pakiety;
- if (!Array.isArray(pk)) continue;
- if (pk.some((p) => String(p).trim() === "*")) return row;
- }
-
- return null;
-}
-
-function isTvAddonAvailableForPkg(addon, pkg) {
- if (!pkg) return false;
- const v = pickTvVariant(addon, String(pkg?.name ?? ""));
- return !!v;
-}
-
-function hasTvTermPricing(addon, pkg) {
- const c = addon?.cena;
- if (!Array.isArray(c)) return false;
-
- const v = pickTvVariant(addon, String(pkg?.name ?? ""));
- if (!v || typeof v !== "object") return false;
-
- return v["12m"] != null && v.bezterminowo != null;
-}
-
-/**
- * ✅ cena jednostkowa:
- * - addons.yaml: number / string / legacy {default, by_name}
- * - tv-addons.yaml: tablica wariantów
- */
-function getAddonUnitPrice(addon, pkg, term /* "12m"|"bezterminowo"|null */) {
- const c = addon?.cena;
-
- if (typeof c === "number") return c;
- if (typeof c === "string" && c.trim() !== "" && !Number.isNaN(Number(c))) return Number(c);
-
- if (Array.isArray(c)) {
- const v = pickTvVariant(addon, String(pkg?.name ?? ""));
- if (!v) return 0;
-
- const t = term || "12m";
- if (v[t] != null) return Number(v[t]) || 0;
-
- if (v.bezterminowo != null) return Number(v.bezterminowo) || 0;
- if (v["12m"] != null) return Number(v["12m"]) || 0;
- return 0;
- }
-
- if (c && typeof c === "object") {
- const name = String(pkg?.name ?? "");
- const wanted = normKey(name);
-
- const byName = c.by_name || c.byName || c.by_nazwa || c.byNazwa;
- if (byName && typeof byName === "object" && name) {
- for (const k of Object.keys(byName)) {
- if (normKey(k) === wanted) return Number(byName[k]) || 0;
- }
- }
-
- if (c.default != null) return Number(c.default) || 0;
- }
-
- return 0;
-}
-
-/** ✅ Sekcja-akordeon (jak w internet modal) */
-function SectionAccordion({ title, right, open, onToggle, children }) {
- return (
-
-
-
- {open &&
{children}
}
-
- );
-}
-
export default function JamboxAddonsModal({
isOpen,
onClose,
@@ -179,29 +29,20 @@ export default function JamboxAddonsModal({
cenaOpis = "zł/mies.",
}) {
const phonePlans = useMemo(() => mapPhoneYamlToPlans(phoneCards), [phoneCards]);
-
const tvAddonsList = useMemo(() => normalizeAddons(tvAddons), [tvAddons]);
const addonsList = useMemo(() => normalizeAddons(addons), [addons]);
-
const decodersList = useMemo(() => normalizeDecoders(decoders), [decoders]);
- const floating = useDraggableFloating("fuz_floating_total_pos_tv_v1");
const tvAddonsVisible = useMemo(() => {
if (!pkg) return [];
return tvAddonsList.filter((a) => isTvAddonAvailableForPkg(a, pkg));
}, [tvAddonsList, pkg]);
- // wybory
const [selectedPhoneId, setSelectedPhoneId] = useState(null);
- const [openPhoneId, setOpenPhoneId] = useState(null);
-
const [selectedDecoderId, setSelectedDecoderId] = useState(null);
-
const [selectedQty, setSelectedQty] = useState({});
-
const [tvTerm, setTvTerm] = useState({}); // { [id]: "12m" | "bezterminowo" }
- // ✅ sekcje (jedna otwarta naraz)
const [openSections, setOpenSections] = useState({
base: true,
decoder: false,
@@ -226,16 +67,18 @@ export default function JamboxAddonsModal({
});
};
- // reset po otwarciu / zmianie pakietu
useEffect(() => {
if (!isOpen) return;
setSelectedPhoneId(null);
- setOpenPhoneId(null);
- setSelectedDecoderId(null);
setSelectedQty({});
setTvTerm({});
+ const d0 =
+ (Array.isArray(decodersList) && decodersList.find((d) => Number(d.cena) === 0)) ||
+ (Array.isArray(decodersList) ? decodersList[0] : null);
+ setSelectedDecoderId(d0 ? String(d0.id) : null);
+
setOpenSections({
base: true,
decoder: false,
@@ -244,12 +87,6 @@ export default function JamboxAddonsModal({
addons: false,
summary: false,
});
-
- const d0 =
- (Array.isArray(decodersList) && decodersList.find((d) => Number(d.cena) === 0)) ||
- (Array.isArray(decodersList) ? decodersList[0] : null);
-
- setSelectedDecoderId(d0 ? String(d0.id) : null);
}, [isOpen, pkg?.id, decodersList]);
if (!isOpen || !pkg) return null;
@@ -289,150 +126,7 @@ export default function JamboxAddonsModal({
}, 0);
}, [selectedQty, addonsList, pkg]);
- const addonsPrice = tvAddonsPrice + addonsOnlyPrice;
- const totalMonthly = basePrice + phonePrice + decoderPrice + 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) => {
- 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 }));
- };
-
- const renderAddonRow = (a, isTv = false) => {
- const qty = Number(selectedQty[a.id] || 0);
- const isQty = a.typ === "quantity" || a.ilosc === true;
-
- const termPricing = isTv && hasTvTermPricing(a, pkg);
- const term = tvTerm[a.id] || "12m";
- const unit = getAddonUnitPrice(a, pkg, termPricing ? term : null);
-
- if (!isQty) {
- return (
-
- );
- }
-
- 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 * unit;
-
- return (
-
-
-
-
-
{a.nazwa}
- {a.opis &&
{a.opis}
}
-
-
-
e.stopPropagation()}>
-
-
- {qty}
-
-
-
-
-
-
- {money(unit)} {cenaOpis}
-
-
{qty > 0 ? `${money(lineTotal)} ${cenaOpis}` : "—"}
-
-
- );
- };
-
- // ---------
- const LS_KEY = "fuz_offer_config_v1";
+ const totalMonthly = basePrice + phonePrice + decoderPrice + tvAddonsPrice + addonsOnlyPrice;
function buildOfferPayload() {
const phone = selectedPhoneId
@@ -491,371 +185,87 @@ export default function JamboxAddonsModal({
};
}
- function saveOfferToLocalStorage() {
- try {
- const payload = buildOfferPayload();
- localStorage.setItem(LS_KEY, JSON.stringify(payload));
- } catch { }
- }
-
- //-- dopisane
- function moneyWithLabel(v) {
- return `${money(v)} ${cenaOpis}`;
- }
-
- function buildOfferMessage(payload) {
- const lines = [];
-
- // nagłówek
- lines.push(`Wybrana oferta: ${payload?.pkg?.name || "—"}`);
- lines.push("");
-
- // ✅ WSZYSTKIE linie jak w podsumowaniu
- lines.push(`Pakiet: ${moneyWithLabel(payload?.totals?.base ?? 0)}`);
- lines.push(`Telefon: ${payload?.phone ? moneyWithLabel(payload.totals.phone) : "—"}`);
- lines.push(`Dekoder: ${payload?.decoder ? moneyWithLabel(payload.totals.decoder) : "—"}`);
- lines.push(`Dodatki TV: ${payload?.tvAddons?.length ? moneyWithLabel(payload.totals.tv) : "—"}`);
- lines.push(`Dodatkowe usługi: ${payload?.addons?.length ? moneyWithLabel(payload.totals.addons) : "—"}`);
- lines.push(`Łącznie: ${moneyWithLabel(payload?.totals?.total ?? 0)}`);
-
- // szczegóły (pozycje)
- if (payload?.phone) {
- lines.push("");
- lines.push(`Telefon: ${payload.phone.name} (${moneyWithLabel(payload.phone.price)})`);
- }
-
- if (payload?.decoder) {
- lines.push("");
- lines.push(`Dekoder: ${payload.decoder.name} (${moneyWithLabel(payload.decoder.price)})`);
- }
-
- if (Array.isArray(payload?.tvAddons) && payload.tvAddons.length) {
- lines.push("");
- lines.push("Pakiety dodatkowe TV:");
- for (const it of payload.tvAddons) {
- const termTxt = it.term ? `, ${it.term}` : "";
- lines.push(
- `- ${it.nazwa} x${it.qty}${termTxt} @ ${moneyWithLabel(it.unit)}`
- );
- }
- }
-
- 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); // ✅ gotowy tekst
- localStorage.setItem(LS_KEY, JSON.stringify(payload));
- } catch { }
- }
-
-
- // ---------
+ const onSend = () => {
+ const payload = buildOfferPayload();
+ saveOfferToLocalStorage(payload, cenaOpis);
+ };
return (
-
-
+
+ toggleSection("base")}
+ price={basePrice}
+ cenaOpis={cenaOpis}
+ features={pkg.features || []}
+ />
- e.stopPropagation()}>
-
-
{pkg.name} — konfiguracja usług
+
toggleSection("decoder")}
+ cenaOpis={cenaOpis}
+ decoders={decodersList}
+ selectedDecoderId={selectedDecoderId}
+ setSelectedDecoderId={setSelectedDecoderId}
+ decoderPrice={decoderPrice}
+ />
- {/* ✅ PAKIET (sekcja) */}
-
-
{money(basePrice)} {cenaOpis}}
- open={openSections.base}
- onToggle={() => toggleSection("base")}
- >
- {pkg.features?.length ? (
-
- {pkg.features.map((f, idx) => (
- -
- {f.label}
- {formatFeatureValue(f.value)}
-
- ))}
-
- ) : (
- Brak szczegółów pakietu.
- )}
-
-
+ toggleSection("tv")}
+ cenaOpis={cenaOpis}
+ pkg={pkg}
+ tvAddonsVisible={tvAddonsVisible}
+ selectedQty={selectedQty}
+ setSelectedQty={setSelectedQty}
+ tvTerm={tvTerm}
+ setTvTerm={setTvTerm}
+ tvAddonsPrice={tvAddonsPrice}
+ />
- {/* DEKODER */}
-
-
- {decoderPrice ? `${money(decoderPrice)} ${cenaOpis}` : "—"}
-
- }
- open={openSections.decoder}
- onToggle={() => toggleSection("decoder")}
- >
+ toggleSection("phone")}
+ cenaOpis={cenaOpis}
+ phonePlans={phonePlans}
+ selectedPhoneId={selectedPhoneId}
+ setSelectedPhoneId={setSelectedPhoneId}
+ phonePrice={phonePrice}
+ />
- {decodersList.length === 0 ? (
- Brak dostępnych dekoderów.
- ) : (
-
- {decodersList.map((d) => {
- const isSelected = String(selectedDecoderId) === String(d.id);
+
toggleSection("addons")}
+ cenaOpis={cenaOpis}
+ addonsList={addonsList}
+ selectedQty={selectedQty}
+ setSelectedQty={setSelectedQty}
+ addonsPrice={addonsOnlyPrice}
+ getUnitPrice={(a) => getAddonUnitPrice(a, pkg, null)}
+ />
- return (
-
-
- setSelectedDecoderId(String(d.id))}
- />
-
+ toggleSection("summary")}
+ cenaOpis={cenaOpis}
+ totalMonthly={totalMonthly}
+ ctaHref="/kontakt"
+ onSend={onSend}
+ rows={[
+ { label: "Pakiet", value: basePrice, showDashIfZero: false },
+ { label: "Telefon", value: phonePrice, showDashIfZero: true },
+ { label: "Dekoder", value: decoderPrice, showDashIfZero: true },
+ { label: "Dodatki TV", value: tvAddonsPrice, showDashIfZero: true },
+ { label: "Dodatkowe usługi", value: addonsOnlyPrice, showDashIfZero: true },
+ ]}
+ />
-
-
{d.nazwa}
- {d.opis &&
{d.opis}
}
-
-
-
- {money(d.cena)} {cenaOpis}
-
-
- );
- })}
-
- )}
-
-
-
-
-
-
- {/* TV ADDONS */}
-
-
- {tvAddonsPrice ? `${money(tvAddonsPrice)} ${cenaOpis}` : "—"}
-
- }
- open={openSections.tv}
- onToggle={() => toggleSection("tv")}
- >
- {tvAddonsVisible.length === 0 ? (
- Brak pakietów dodatkowych TV.
- ) : (
- {tvAddonsVisible.map((a) => renderAddonRow(a, true))}
- )}
-
-
-
- {/* TELEFON */}
-
-
- {phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"}
-
- }
- open={openSections.phone}
- onToggle={() => toggleSection("phone")}
- >
- {phonePlans.length === 0 ? (
- Brak dostępnych pakietów telefonicznych.
- ) : (
-
- {/* brak telefonu */}
-
-
- handlePhoneSelect(null)}
- />
-
-
-
-
Nie potrzebuję telefonu
-
-
- 0,00 {cenaOpis}
-
-
- {/* pakiety */}
- {phonePlans.map((p) => {
- const isSelected = String(selectedPhoneId) === String(p.id);
-
- return (
-
-
-
- handlePhoneSelect(p.id)}
- />
-
-
-
-
-
- {money(p.price_monthly)} {cenaOpis}
-
-
-
- {/* ✅ detale ZAWSZE widoczne */}
- {p.features?.length > 0 && (
-
-
- {p.features
- .filter(
- (f) =>
- !String(f.label || "").toLowerCase().includes("aktyw"),
- )
- .map((f, idx) => (
- -
- {f.label}
- {formatFeatureValue(f.value)}
-
- ))}
-
-
- )}
-
- );
- })}
-
-
- )}
-
-
-
-
- {/* DODATKI */}
-
-
- {addonsOnlyPrice ? `${money(addonsOnlyPrice)} ${cenaOpis}` : "—"}
-
- }
- open={openSections.addons}
- onToggle={() => toggleSection("addons")}
- >
- {addonsList.length === 0 ? (
- Brak usług dodatkowych.
- ) : (
- {addonsList.map((a) => renderAddonRow(a, false))}
- )}
-
-
-
- {/* PODSUMOWANIE */}
-
-
{money(totalMonthly)} {cenaOpis}}
- open={openSections.summary}
- onToggle={() => toggleSection("summary")}
- >
-
-
-
- Pakiet
- {money(basePrice)} {cenaOpis}
-
-
-
- Telefon
- {phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"}
-
-
-
- Dekoder
- {decoderPrice ? `${money(decoderPrice)} ${cenaOpis}` : "—"}
-
-
-
- Dodatki TV
- {tvAddonsPrice ? `${money(tvAddonsPrice)} ${cenaOpis}` : "—"}
-
-
-
- Dodatkowe usługi
- {addonsOnlyPrice ? `${money(addonsOnlyPrice)} ${cenaOpis}` : "—"}
-
-
-
- Łącznie
- {money(totalMonthly)} {cenaOpis}
-
-
-
-
saveOfferToLocalStorage()}
- >
- Wyślij zapytanie z tym wyborem
-
-
-
-
-
-
-
- e.stopPropagation()}
- >
-
-
- Razem
-
-
- {money(totalMonthly)}
-
-
- {cenaOpis}
-
-
-
-
-
-
-
+
+
);
}
diff --git a/src/islands/modals/OfferModalShell.jsx b/src/islands/modals/OfferModalShell.jsx
new file mode 100644
index 0000000..c81330f
--- /dev/null
+++ b/src/islands/modals/OfferModalShell.jsx
@@ -0,0 +1,26 @@
+export default function OfferModalShell({ isOpen, onClose, title, children }) {
+ if (!isOpen) return null;
+
+ return (
+
+
+
+
e.stopPropagation()}>
+
+
{title}
+ {children}
+
+
+
+ );
+}
diff --git a/src/islands/modals/sections/AddonsSection.jsx b/src/islands/modals/sections/AddonsSection.jsx
new file mode 100644
index 0000000..7b6d16f
--- /dev/null
+++ b/src/islands/modals/sections/AddonsSection.jsx
@@ -0,0 +1,123 @@
+import SectionAccordion from "./SectionAccordion.jsx";
+import { money } from "../../../lib/money.js";
+
+export default function AddonsSection({
+ open,
+ onToggle,
+ title = "Dodatkowe usługi",
+ cenaOpis,
+ addonsList = [],
+ selectedQty,
+ setSelectedQty,
+ addonsPrice,
+ // pricing:
+ getUnitPrice, // (addon) => number
+}) {
+ 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 }));
+ };
+
+ const renderRow = (a) => {
+ const qty = Number(selectedQty[a.id] || 0);
+ const isQty = a.typ === "quantity" || a.ilosc === true;
+
+ const unit = Number(getUnitPrice?.(a) ?? 0);
+
+ if (!isQty) {
+ return (
+
+
+ 0}
+ onChange={() => toggleCheckboxAddon(a.id)}
+ />
+
+
+
+
+ {money(unit)} {cenaOpis}
+
+ {/* ✅ opis jako osobny wiersz na pełną szerokość */}
+ {a.opis ? (
+ e.stopPropagation()}>
+
{a.opis}
+
+ ) : null}
+
+ );
+ }
+
+
+ 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 * unit;
+
+ return (
+
+
+
+
+
{a.nazwa}
+ {a.opis &&
{a.opis}
}
+
+
+
e.stopPropagation()}>
+
+
+ {qty}
+
+
+
+
+
+
{money(unit)} {cenaOpis}
+
{qty > 0 ? `${money(lineTotal)} ${cenaOpis}` : "—"}
+
+
+ );
+ };
+
+ return (
+
+
{addonsPrice ? `${money(addonsPrice)} ${cenaOpis}` : "—"}}
+ open={open}
+ onToggle={onToggle}
+ >
+ {addonsList.length === 0 ? (
+ Brak usług dodatkowych.
+ ) : (
+ {addonsList.map(renderRow)}
+ )}
+
+
+ );
+}
diff --git a/src/islands/modals/sections/DecoderSection.jsx b/src/islands/modals/sections/DecoderSection.jsx
new file mode 100644
index 0000000..8fb9d60
--- /dev/null
+++ b/src/islands/modals/sections/DecoderSection.jsx
@@ -0,0 +1,59 @@
+import SectionAccordion from "./SectionAccordion.jsx";
+import { money } from "../../../lib/money.js";
+
+export default function DecoderSection({
+ open,
+ onToggle,
+ cenaOpis,
+ decoders = [],
+ selectedDecoderId,
+ setSelectedDecoderId,
+ decoderPrice,
+}) {
+ return (
+
+
{decoderPrice ? `${money(decoderPrice)} ${cenaOpis}` : "—"}}
+ open={open}
+ onToggle={onToggle}
+ >
+ {decoders.length === 0 ? (
+ Brak dostępnych dekoderów.
+ ) : (
+
+ {decoders.map((d) => {
+ const isSelected = String(selectedDecoderId) === String(d.id);
+
+ return (
+
+
+ setSelectedDecoderId(String(d.id))}
+ />
+
+
+
+
+ {money(d.cena)} {cenaOpis}
+
+ {d.opis ? (
+
+ ) : null}
+
+
+ );
+ })}
+
+ )}
+
+
+ );
+}
diff --git a/src/islands/modals/sections/FloatingTotal.jsx b/src/islands/modals/sections/FloatingTotal.jsx
new file mode 100644
index 0000000..fcedde9
--- /dev/null
+++ b/src/islands/modals/sections/FloatingTotal.jsx
@@ -0,0 +1,22 @@
+import useDraggableFloating from "../../hooks/useDraggableFloating.js";
+import { money } from "../../../lib/money.js";
+
+export default function FloatingTotal({ storageKey, totalMonthly, cenaOpis }) {
+ const floating = useDraggableFloating(storageKey);
+
+ return (
+ e.stopPropagation()}
+ >
+
+ Razem
+ {money(totalMonthly)}
+ {cenaOpis}
+
+
+ );
+}
diff --git a/src/islands/modals/sections/PhoneSection.jsx b/src/islands/modals/sections/PhoneSection.jsx
new file mode 100644
index 0000000..077ceee
--- /dev/null
+++ b/src/islands/modals/sections/PhoneSection.jsx
@@ -0,0 +1,100 @@
+import SectionAccordion from "./SectionAccordion.jsx";
+import { money } from "../../../lib/money.js";
+
+function formatFeatureValue(val) {
+ if (val === true || val === "true") return "✓";
+ if (val === false || val === "false" || val == null) return "✕";
+ return val;
+}
+
+export default function PhoneSection({
+ open,
+ onToggle,
+ cenaOpis,
+ phonePlans = [],
+ selectedPhoneId,
+ setSelectedPhoneId,
+ phonePrice,
+}) {
+ const handlePhoneSelect = (id) => {
+ if (id === null) {
+ setSelectedPhoneId(null);
+ return;
+ }
+ setSelectedPhoneId(id);
+ };
+
+ return (
+
+
{phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"}}
+ open={open}
+ onToggle={onToggle}
+ >
+ {phonePlans.length === 0 ? (
+ Brak dostępnych pakietów telefonicznych.
+ ) : (
+
+
+
+ handlePhoneSelect(null)}
+ />
+
+
+
+
Nie potrzebuję telefonu
+
+
+ 0,00 {cenaOpis}
+
+
+ {phonePlans.map((p) => {
+ const isSelected = String(selectedPhoneId) === String(p.id);
+
+ return (
+
+
+
+ handlePhoneSelect(p.id)}
+ />
+
+
+
+
+ {money(p.price_monthly)} {cenaOpis}
+
+
+ {p.features?.length > 0 && (
+
+
+ {p.features
+ .filter((f) => !String(f.label || "").toLowerCase().includes("aktyw"))
+ .map((f, idx) => (
+ -
+ {f.label}
+ {formatFeatureValue(f.value)}
+
+ ))}
+
+
+ )}
+
+ );
+ })}
+
+ )}
+
+
+ );
+}
diff --git a/src/islands/modals/sections/PlanSection.jsx b/src/islands/modals/sections/PlanSection.jsx
new file mode 100644
index 0000000..5280a6b
--- /dev/null
+++ b/src/islands/modals/sections/PlanSection.jsx
@@ -0,0 +1,34 @@
+import SectionAccordion from "./SectionAccordion.jsx";
+import { money } from "../../../lib/money.js";
+
+function formatFeatureValue(val) {
+ if (val === true || val === "true") return "✓";
+ if (val === false || val === "false" || val == null) return "✕";
+ return val;
+}
+
+export default function PlanSection({ title, open, onToggle, price, cenaOpis, features = [] }) {
+ return (
+
+
{money(price)} {cenaOpis}}
+ open={open}
+ onToggle={onToggle}
+ >
+ {features?.length ? (
+
+ {features.map((f, idx) => (
+ -
+ {f.label}
+ {formatFeatureValue(f.value)}
+
+ ))}
+
+ ) : (
+ Brak szczegółów.
+ )}
+
+
+ );
+}
diff --git a/src/islands/modals/sections/SectionAccordion.jsx b/src/islands/modals/sections/SectionAccordion.jsx
new file mode 100644
index 0000000..a19d3ea
--- /dev/null
+++ b/src/islands/modals/sections/SectionAccordion.jsx
@@ -0,0 +1,19 @@
+export default function SectionAccordion({ title, right, open, onToggle, children }) {
+ return (
+
+
+
+ {open &&
{children}
}
+
+ );
+}
diff --git a/src/islands/modals/sections/SummarySection.jsx b/src/islands/modals/sections/SummarySection.jsx
new file mode 100644
index 0000000..cf5a589
--- /dev/null
+++ b/src/islands/modals/sections/SummarySection.jsx
@@ -0,0 +1,52 @@
+import SectionAccordion from "./SectionAccordion.jsx";
+import { money } from "../../../lib/money.js";
+
+export default function SummarySection({
+ open,
+ onToggle,
+ cenaOpis,
+ rows = [], // [{label, valueNumberOrNull, showDashIfZero?}]
+ totalMonthly,
+ ctaHref = "/kontakt#form",
+ onSend,
+ ctaLabel = "Wyślij zapytanie z tym wyborem",
+}) {
+ return (
+
+
{money(totalMonthly)} {cenaOpis}}
+ open={open}
+ onToggle={onToggle}
+ >
+
+
+ {rows.map((r) => {
+ const v = Number(r.value ?? 0);
+ const showDash = r.showDashIfZero !== false; // domyślnie true
+ return (
+
+ {r.label}
+ {(v > 0 || !showDash) ? `${money(v)} ${cenaOpis}` : "—"}
+
+ );
+ })}
+
+
+ Łącznie
+ {money(totalMonthly)} {cenaOpis}
+
+
+
onSend?.()}
+ >
+ {ctaLabel}
+
+
+
+
+
+ );
+}
diff --git a/src/islands/modals/sections/TvAddonsSection.jsx b/src/islands/modals/sections/TvAddonsSection.jsx
new file mode 100644
index 0000000..08269bf
--- /dev/null
+++ b/src/islands/modals/sections/TvAddonsSection.jsx
@@ -0,0 +1,163 @@
+import SectionAccordion from "./SectionAccordion.jsx";
+import { money } from "../../../lib/money.js";
+import { hasTvTermPricing, getAddonUnitPrice } from "../../../lib/offer-pricing.js";
+
+export default function TvAddonsSection({
+ open,
+ onToggle,
+ cenaOpis,
+ pkg,
+ tvAddonsVisible = [],
+ selectedQty,
+ setSelectedQty,
+ tvTerm,
+ setTvTerm,
+ tvAddonsPrice,
+}) {
+ 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 }));
+ };
+
+ const renderRow = (a) => {
+ const qty = Number(selectedQty[a.id] || 0);
+ const isQty = a.typ === "quantity" || a.ilosc === true;
+
+ const termPricing = hasTvTermPricing(a, pkg);
+ const term = tvTerm[a.id] || "12m";
+ const unit = getAddonUnitPrice(a, pkg, termPricing ? term : null);
+
+ if (!isQty) {
+ return (
+
+
+ 0}
+ onChange={() => toggleCheckboxAddon(a.id)}
+ />
+
+
+
+
+ {money(unit)} {cenaOpis}
+
+ {/* ✅ osobny wiersz na pełną szerokość */}
+ {(a.tid || a.opis) && (
+
+ )}
+
+
+ );
+ }
+
+ 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 * unit;
+
+ return (
+
+
+
+
+
{a.nazwa}
+ {a.opis &&
{a.opis}
}
+
+
+
e.stopPropagation()}>
+
+
+ {qty}
+
+
+
+
+
+
{money(unit)} {cenaOpis}
+
{qty > 0 ? `${money(lineTotal)} ${cenaOpis}` : "—"}
+
+
+ );
+ };
+
+ return (
+
+
{tvAddonsPrice ? `${money(tvAddonsPrice)} ${cenaOpis}` : "—"}}
+ open={open}
+ onToggle={onToggle}
+ >
+ {tvAddonsVisible.length === 0 ? (
+ Brak pakietów dodatkowych TV.
+ ) : (
+ {tvAddonsVisible.map(renderRow)}
+ )}
+
+
+ );
+}
diff --git a/src/lib/money.js b/src/lib/money.js
new file mode 100644
index 0000000..0f37fbf
--- /dev/null
+++ b/src/lib/money.js
@@ -0,0 +1,8 @@
+export function money(amount) {
+ const n = Number(amount || 0);
+ return n.toFixed(2).replace(".", ",");
+}
+
+export function moneyWithLabel(v, cenaOpis) {
+ return `${money(v)} ${cenaOpis}`;
+}
diff --git a/src/lib/offer-normalize.js b/src/lib/offer-normalize.js
new file mode 100644
index 0000000..8257d84
--- /dev/null
+++ b/src/lib/offer-normalize.js
@@ -0,0 +1,47 @@
+/** telefon z YAML (phone/cards.yaml -> cards[]) => { id, name, price_monthly, features[] } */
+export 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,
+ })),
+ }));
+}
+
+/** dekodery z YAML */
+export function normalizeDecoders(list) {
+ const arr = Array.isArray(list) ? list : [];
+ return arr
+ .filter((d) => d?.id && d?.nazwa)
+ .map((d) => ({
+ id: String(d.id),
+ nazwa: String(d.nazwa),
+ opis: d.opis ? String(d.opis) : "",
+ cena: Number(d.cena ?? 0),
+ }));
+}
+
+/** dodatki z YAML (tv-addons.yaml / addons.yaml) */
+export 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: a.cena ?? 0,
+ tid: a.tid != null ? String(a.tid) : "",
+ }));
+}
diff --git a/src/lib/offer-payload.js b/src/lib/offer-payload.js
new file mode 100644
index 0000000..b6ba45e
--- /dev/null
+++ b/src/lib/offer-payload.js
@@ -0,0 +1,57 @@
+import { moneyWithLabel } from "./money.js";
+
+export const LS_KEY = "fuz_offer_config_v1";
+
+export function buildOfferMessage(payload, cenaOpis) {
+ const m = (v) => moneyWithLabel(v, cenaOpis);
+ const lines = [];
+
+ lines.push(`Wybrana oferta: ${payload?.pkg?.name || "—"}`);
+ lines.push("");
+
+ const t = payload?.totals || {};
+ lines.push(`Pakiet: ${m(t.base ?? 0)}`);
+ lines.push(`Telefon: ${payload?.phone ? m(t.phone ?? 0) : "—"}`);
+
+ if ("decoder" in t) lines.push(`Dekoder: ${payload?.decoder ? m(t.decoder ?? 0) : "—"}`);
+ if ("tv" in t) lines.push(`Dodatki TV: ${payload?.tvAddons?.length ? m(t.tv ?? 0) : "—"}`);
+
+ lines.push(`Dodatkowe usługi: ${payload?.addons?.length ? m(t.addons ?? 0) : "—"}`);
+ lines.push(`Łącznie: ${m(t.total ?? 0)}`);
+
+ if (payload?.phone) {
+ lines.push("");
+ lines.push(`Telefon: ${payload.phone.name} (${m(payload.phone.price)})`);
+ }
+
+ if (payload?.decoder) {
+ lines.push("");
+ lines.push(`Dekoder: ${payload.decoder.name} (${m(payload.decoder.price)})`);
+ }
+
+ if (Array.isArray(payload?.tvAddons) && payload.tvAddons.length) {
+ lines.push("");
+ lines.push("Pakiety dodatkowe TV:");
+ for (const it of payload.tvAddons) {
+ const termTxt = it.term ? `, ${it.term}` : "";
+ lines.push(`- ${it.nazwa} x${it.qty}${termTxt} @ ${m(it.unit)}`);
+ }
+ }
+
+ 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} @ ${m(it.unit)}`);
+ }
+ }
+
+ return lines.join("\n");
+}
+
+export function saveOfferToLocalStorage(payload, cenaOpis) {
+ try {
+ payload.message = buildOfferMessage(payload, cenaOpis);
+ localStorage.setItem(LS_KEY, JSON.stringify(payload));
+ } catch {}
+}
diff --git a/src/lib/offer-pricing.js b/src/lib/offer-pricing.js
new file mode 100644
index 0000000..d1fa1f5
--- /dev/null
+++ b/src/lib/offer-pricing.js
@@ -0,0 +1,83 @@
+function normKey(s) {
+ return String(s || "").trim().toLowerCase().replace(/\s+/g, " ");
+}
+
+/** TV: wybór wariantu ceny po pkg.name, albo fallback "*" */
+export function pickTvVariant(addon, pkgName) {
+ const c = addon?.cena;
+ if (!Array.isArray(c)) return null;
+
+ const wanted = normKey(pkgName);
+
+ for (const row of c) {
+ const pk = row?.pakiety;
+ if (!Array.isArray(pk)) continue;
+ if (pk.some((p) => normKey(p) === wanted)) return row;
+ }
+
+ for (const row of c) {
+ const pk = row?.pakiety;
+ if (!Array.isArray(pk)) continue;
+ if (pk.some((p) => String(p).trim() === "*")) return row;
+ }
+
+ return null;
+}
+
+export function isTvAddonAvailableForPkg(addon, pkg) {
+ if (!pkg) return false;
+ const v = pickTvVariant(addon, String(pkg?.name ?? ""));
+ return !!v;
+}
+
+export function hasTvTermPricing(addon, pkg) {
+ const c = addon?.cena;
+ if (!Array.isArray(c)) return false;
+
+ const v = pickTvVariant(addon, String(pkg?.name ?? ""));
+ if (!v || typeof v !== "object") return false;
+
+ return v["12m"] != null && v.bezterminowo != null;
+}
+
+/**
+ * ✅ cena jednostkowa:
+ * - addons.yaml: number / string / legacy {default, by_name}
+ * - tv-addons.yaml: tablica wariantów
+ */
+export function getAddonUnitPrice(addon, pkg, term /* "12m"|"bezterminowo"|null */) {
+ const c = addon?.cena;
+
+ if (typeof c === "number") return c;
+ if (typeof c === "string" && c.trim() !== "" && !Number.isNaN(Number(c))) return Number(c);
+
+ // tv-addons.yaml
+ if (Array.isArray(c)) {
+ const v = pickTvVariant(addon, String(pkg?.name ?? ""));
+ if (!v) return 0;
+
+ const t = term || "12m";
+ if (v[t] != null) return Number(v[t]) || 0;
+
+ if (v.bezterminowo != null) return Number(v.bezterminowo) || 0;
+ if (v["12m"] != null) return Number(v["12m"]) || 0;
+ return 0;
+ }
+
+ // legacy object
+ if (c && typeof c === "object") {
+ const name = String(pkg?.name ?? "");
+ const wanted = normKey(name);
+
+ const byName = c.by_name || c.byName || c.by_nazwa || c.byNazwa;
+ if (byName && typeof byName === "object" && name) {
+ for (const k of Object.keys(byName)) {
+ if (normKey(k) === wanted) return Number(byName[k]) || 0;
+ }
+ }
+
+ if (c.default != null) return Number(c.default) || 0;
+ }
+
+ return 0;
+}
diff --git a/src/styles/addons.css b/src/styles/addons.css
index 19df908..77ab1c5 100644
--- a/src/styles/addons.css
+++ b/src/styles/addons.css
@@ -1,466 +1,352 @@
- .f-section-acc .f-accordion-header {
- @apply flex items-center justify-between gap-3;
- }
+.f-section-acc .f-accordion-header {
+ @apply flex items-center justify-between gap-3;
+}
- .f-accordion-header-right {
- @apply flex items-center gap-3;
- }
+.f-accordion-header-right {
+ @apply flex items-center gap-3;
+}
- .f-acc-chevron {
- @apply opacity-60 text-sm;
- }
+.f-acc-chevron {
+ @apply opacity-60 text-sm;
+}
+
+.f-accordion-item {
+ @apply rounded-xl border overflow-hidden bg-[--f-background];
+ border-color: rgba(148, 163, 184, 0.6);
+}
+
+.f-accordion-header {
+ @apply w-full flex items-center justify-between gap-4 px-4 py-2 cursor-pointer;
+ background: rgba(148, 163, 184, 0.06);
+ border: none;
+ outline: none;
+}
+
+.f-accordion-header-left {
+ @apply flex items-center gap-1;
+}
+
+.f-modal-phone-name {
+ @apply font-medium ml-2;
+}
+
+.f-modal-phone-price {
+ @apply font-semibold whitespace-nowrap;
+}
+
+.f-accordion-body {
+ @apply px-4 pt-2 pb-3;
+ border-top: 1px solid rgba(148, 163, 184, 0.4);
+}
+
+.f-accordion-item.is-open .f-accordion-header {
+ background: color-mix(in srgb, var(--fuz-accent, #2563eb) 8%, transparent);
+}
+
+.f-modal-section {
+ @apply mb-6;
+}
+
+.f-radio-item {
+ @apply grid items-start gap-3 px-3 py-2 cursor-pointer;
+ grid-template-columns: auto 1fr auto;
+ grid-template-areas:
+ "check main price"
+ "below below below";
+ border-bottom: 1px solid rgba(148, 163, 184, 0.4);
+ background: var(--f-background);
+}
- .f-floating-total {
- @apply fixed bottom-5 right-5 z-[10000];
- @apply pointer-events-auto;
- @apply select-none;
- touch-action: none;
- }
+.f-radio-item:last-child {
+ border-bottom: none;
+}
- /* kółko */
- .f-floating-total-circle {
- @apply w-24 h-24 md:w-32 md:h-32 rounded-full;
- @apply flex flex-col items-center justify-center text-center;
- @apply shadow-xl;
- @apply bg-[--f-addons-background];
- @apply backdrop-blur-md;
- }
+.f-radio-check input {
+ @apply mt-1;
+}
- /* kwota */
- .f-floating-total-amount {
- @apply text-lg md:text-xl font-bold leading-none text-[--f-addons-text];
- }
+.f-radio-name {
+ @apply font-medium;
+}
- /* jednostka */
- .f-floating-total-unit {
- @apply my-1 text-xs md:text-sm opacity-70 text-[--f-addons-text];
- }
+.f-radio-price {
+ @apply whitespace-nowrap font-semibold;
+}
+.f-radio-details {
+ @apply pl-10 pr-3 pb-3 -mt-1 text-sm;
+}
- @keyframes fuz-bounce {
- 0%, 100% { transform: translateY(0); }
- 50% { transform: translateY(-6px); }
- }
+.f-addon-list {
+ @apply flex flex-col gap-2;
+}
- .f-floating-total:hover .f-floating-total-circle {
- animation: fuz-bounce 420ms ease-in-out;
- }
+.f-addon-item {
+ @apply grid items-start gap-3 px-3 py-2 cursor-pointer;
+ border-bottom: 1px solid rgba(148, 163, 184, 0.4);
+ grid-template-columns: auto 1fr auto;
+ background: var(--f-background);
+}
- .f-floating-total:active .f-floating-total-circle {
- transform: translateY(-4px) scale(1.02);
- }
+.f-addon-item:last-child {
+ border-bottom: none;
+}
-/* 3D */
-.f-floating-total-circle {
- @apply relative overflow-hidden;
- @apply shadow-xl;
- @apply bg-[--f-addons-background];
- @apply backdrop-blur-md;
+.f-addon-checkbox {
+ @apply flex items-center justify-center;
+ align-items: center;
+ margin-top: 0.1rem;
+}
- /* 3D feel */
- box-shadow:
- 0 18px 35px hsla(221 47% 11% / 0.28),
- 0 6px 14px hsla(221 47% 11% / 0.18),
- inset 0 1px 0 hsla(0 0% 100% / 0.22),
- inset 0 -10px 18px hsla(221 47% 11% / 0.25);
+.f-addon-checkbox input[type="checkbox"] {
+ width: 1.05rem;
+ height: 1.05rem;
+ transform: scale(1.05);
+ accent-color: var(--fuz-accent, #2563eb);
+ cursor: pointer;
+}
- /* subtelna “kopuła” */
- background-image:
- radial-gradient(120% 120% at 30% 20%, hsla(0 0% 100% / 0.22) 0%, transparent 55%),
- radial-gradient(140% 140% at 70% 80%, hsla(221 47% 11% / 0.22) 0%, transparent 60%);
- }
+.f-addon-main {
+ @apply flex flex-col gap-0.5;
+ min-width: 0;
+}
- /* połysk */
- .f-floating-total-circle::before {
- content: "";
- position: absolute;
- inset: -30% -30% auto -30%;
- height: 70%;
- border-radius: 9999px;
- background: radial-gradient(
- closest-side,
- hsla(0 0% 100% / 0.28),
- transparent 70%
- );
- transform: rotate(-12deg);
- pointer-events: none;
- }
+.f-addon-name {
+ @apply font-medium;
+}
- /* “rim”/krawędź */
- .f-floating-total-circle::after {
- content: "";
- position: absolute;
- inset: 0;
- border-radius: 9999px;
- border: 1px solid hsla(0 0% 100% / 0.16);
- box-shadow: inset 0 0 0 1px hsla(221 47% 11% / 0.18);
- pointer-events: none;
- }
+.f-addon-desc {
+ @apply text-sm opacity-85;
+}
- /* lekka reakcja 3D na hover */
- @media (hover: hover) and (pointer: fine) {
- .f-floating-total:hover .f-floating-total-circle {
- transform: translateY(-2px) scale(1.02);
- box-shadow:
- 0 22px 44px hsla(221 47% 11% / 0.32),
- 0 8px 18px hsla(221 47% 11% / 0.20),
- inset 0 1px 0 hsla(0 0% 100% / 0.24),
- inset 0 -12px 20px hsla(221 47% 11% / 0.28);
- transition: transform 180ms ease, box-shadow 180ms ease;
- }
- }
+.f-addon-more {
+ @apply text-sm underline opacity-80;
+}
+.f-addon-price {
+ @apply font-semibold whitespace-nowrap;
+ justify-self: end;
+ text-align: right;
+ min-width: 140px;
+}
+.f-addon-price-total {
+ margin-top: 0.15rem;
+ font-size: 0.9em;
+ font-weight: 600;
+ opacity: 0.85;
+ white-space: nowrap;
+ color: var(--fuz-accent, #2563eb);
+}
+.f-addon-item--qty {
+ grid-template-columns: auto 1fr auto auto;
+ align-items: start;
+}
+.f-addon-item--qty .f-addon-checkbox {
+ visibility: hidden;
+ width: 1.05rem;
+ height: 1.05rem;
+ transform: scale(1.05);
+}
- /* ----------- Uporządkować ------ */
- /* ===========================
- TELEFON — AKORDEON
- =========================== */
+.f-addon-item--qty .f-addon-qty {
+ justify-self: end;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-top: 0.05rem;
+}
- .f-modal-phone-list.f-accordion {
- @apply flex flex-col gap-2;
- }
+.f-addon-qty-value {
+ min-width: 2ch;
+ text-align: center;
+}
- .f-accordion-item {
- @apply rounded-xl border overflow-hidden bg-[--f-background];
- border-color: rgba(148, 163, 184, 0.6);
- }
-
- .f-accordion-header {
- @apply w-full flex items-center justify-between gap-4 px-4 py-2 cursor-pointer;
- background: rgba(148, 163, 184, 0.06);
- border: none;
- outline: none;
- }
-
- .f-accordion-header-left {
- @apply flex items-center gap-1;
- }
-
- .f-modal-phone-name {
- @apply font-medium ml-2;
- }
-
- .f-modal-phone-price {
- @apply font-semibold whitespace-nowrap;
- }
-
- .f-accordion-body {
- @apply px-4 pt-2 pb-3;
- border-top: 1px solid rgba(148, 163, 184, 0.4);
- }
-
- /* wyróżnienie otwartego pakietu */
- .f-accordion-item.is-open .f-accordion-header {
- background: color-mix(in srgb, var(--fuz-accent, #2563eb) 8%, transparent);
- }
-
- /* ===========================
- DODATKI — KOLUMNOWA LISTA (GRID)
- checkbox: checkbox | main | price
- quantity: slot | main | qty | price
- =========================== */
-
- .f-addon-list {
- @apply flex flex-col gap-2;
- }
-
- /* BAZA: checkbox | main | price */
- .f-addon-item {
- @apply grid items-start gap-3 px-3 py-2;
- border-bottom: 1px solid rgba(148, 163, 184, 0.4);
- /* rounded-xl border cursor-pointer; */
- grid-template-columns: auto 1fr auto;
- /* border-color: rgba(148, 163, 184, 0.5); */
- background: var(--f-background);
- @apply cursor-pointer;
- }
-
- .f-addon-item * {
- @apply cursor-pointer;
- }
-
- .f-addon-item:last-child {
- border-bottom: none;
- }
-
- .f-addon-item:hover {
- /* border-color: color-mix(
- in srgb,
- var(--fuz-accent, #2563eb) 70%,
- rgba(148, 163, 184, 0.5) 30%
- ); */
- }
-
- .f-addon-item input[type="checkbox"] {
- @apply cursor-pointer;
- }
-
- /* kolumna 1 */
- .f-addon-checkbox {
- @apply flex items-center justify-center;
- align-items: center;
- margin-top: 0.1rem;
- }
-
- .f-addon-checkbox input[type="checkbox"] {
- width: 1.05rem;
- height: 1.05rem;
- transform: scale(1.05);
- accent-color: var(--fuz-accent, #2563eb);
- cursor: pointer;
- }
-
- /* kolumna 2 */
- .f-addon-main {
- @apply flex flex-col gap-0.5;
- min-width: 0;
- }
-
- .f-addon-name {
- @apply font-medium;
- }
-
- .f-addon-desc {
- @apply text-sm opacity-85;
- }
-
- /* kolumna 3 (cena) */
- .f-addon-price {
- @apply font-semibold whitespace-nowrap;
- justify-self: end;
- text-align: right;
- min-width: 140px;
- /* stała kolumna cen */
- }
-
- /* suma pod ceną (quantity) */
- .f-addon-price-total {
- margin-top: 0.15rem;
- font-size: 0.9em;
- font-weight: 600;
- opacity: 0.85;
- white-space: nowrap;
- color: var(--fuz-accent, #2563eb);
- }
-
- /* WARIANT: quantity -> slot | main | qty | price */
+@media (max-width: 640px) {
.f-addon-item--qty {
- grid-template-columns: auto 1fr auto auto;
- align-items: start;
- }
-
- /* “pusty” slot w kolumnie 1 (żeby wyrównać do checkboxa) */
- .f-addon-item--qty .f-addon-checkbox {
- visibility: hidden;
- /* zajmuje miejsce, ale nie widać */
- width: 1.05rem;
- height: 1.05rem;
- transform: scale(1.05);
- }
-
- /* kolumna qty (3) – bliżej prawej */
- .f-addon-item--qty .f-addon-qty {
- justify-self: end;
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
- margin-top: 0.05rem;
- }
-
- /* wartość qty */
- .f-addon-qty-value {
- min-width: 2ch;
- text-align: center;
- }
-
- /* mobile: w razie ciasnoty przenosimy qty pod main, cena zostaje po prawej */
- @media (max-width: 640px) {
- .f-addon-item--qty {
- grid-template-columns: auto 1fr auto;
- grid-template-areas:
- "slot main price"
- "slot qty price";
- }
-
- .f-addon-item--qty .f-addon-checkbox {
- grid-area: slot;
- }
-
- .f-addon-item--qty .f-addon-main {
- grid-area: main;
- }
-
- .f-addon-item--qty .f-addon-qty {
- grid-area: qty;
- justify-self: start;
- }
-
- .f-addon-item--qty .f-addon-price {
- grid-area: price;
- }
- }
-
-
- /* ===========================
- PODSUMOWANIE MIESIĘCZNE
- =========================== */
-
- .f-summary {
- @apply pt-2;
- }
-
- .f-summary-list {
- @apply flex flex-col gap-1 mt-2 p-4 rounded-xl;
- background: rgba(148, 163, 184, 0.07);
- }
-
- .f-summary-row,
- .f-summary-total {
- @apply flex items-center justify-between;
- }
-
- .f-summary-row span:last-child {
- @apply font-medium whitespace-nowrap;
- }
-
- .f-summary-total {
- @apply mt-1 pt-2;
- border-top: 1px solid rgba(148, 163, 184, 0.4);
- }
-
- .f-summary-total span:last-child {
- @apply font-bold;
- font-size: 1.25rem;
- color: var(--fuz-accent, #2563eb);
- }
-
- .f-modal-section {
- @apply mb-6;
- }
-
- .f-modal-section h3 {
- @apply text-xl md:text-2xl font-semibold mb-3;
- }
-
- /* opcja "bez telefonu" */
- .f-accordion-item--no-phone .f-accordion-header {
- background: rgba(148, 163, 184, 0.03);
- }
-
- .f-accordion-header-left input[type="radio"] {
- width: 1.05rem;
- height: 1.05rem;
- transform: scale(1.05);
- accent-color: var(--fuz-accent, #2563eb);
- cursor: pointer;
- }
-
- .f-addon-checkbox input[type="checkbox"] {
- width: 1.05rem;
- height: 1.05rem;
- transform: scale(1.05);
- accent-color: var(--fuz-accent, #2563eb);
- cursor: pointer;
- }
-
- .f-accordion-header-left,
- .f-addon-checkbox {
- align-items: center;
- }
-
-
- /* ===========================
- FLOATING TOTAL (dymek jak czat)
- =========================== */
-
- .f-floating-total {
- position: fixed;
- right: 1rem;
- bottom: 1rem;
- z-index: 10000;
- /* wyżej niż overlay (9999) */
- pointer-events: auto;
- }
-
- .f-floating-total-inner {
- @apply flex items-center gap-3;
- @apply px-4 py-3 rounded-2xl shadow-xl border;
- border-color: rgba(148, 163, 184, 0.5);
- background: color-mix(in srgb, var(--f-background) 92%, transparent);
- backdrop-filter: blur(10px);
- }
-
- .f-floating-total-label {
- @apply text-sm opacity-80;
- }
-
- .f-floating-total-value {
- @apply font-bold whitespace-nowrap;
- font-size: 1.1rem;
- color: var(--fuz-accent, #2563eb);
- }
-
- /* na bardzo małych ekranach lekko mniejszy dymek */
- @media (max-width: 420px) {
- .f-floating-total-inner {
- @apply px-3 py-2;
- }
-
- .f-floating-total-value {
- font-size: 1rem;
- }
- }
-
- .f-addon-price-total {
- margin-top: 0.15rem;
- font-size: 0.9em;
- font-weight: 600;
- opacity: 0.85;
- white-space: nowrap;
- color: var(--fuz-accent, #2563eb);
- }
-
-
-
-
- /* -------------------------- */
- .f-radio-item {
- @apply grid items-start gap-3 px-3 py-2 cursor-pointer;
grid-template-columns: auto 1fr auto;
- border-bottom: 1px solid rgba(148, 163, 184, 0.4);
- background: var(--f-background);
+ grid-template-areas:
+ "slot main price"
+ "slot qty price";
}
- .f-radio-item:last-child {
- border-bottom: none;
+ .f-addon-item--qty .f-addon-checkbox {
+ grid-area: slot;
}
- .f-radio-check input {
- @apply mt-1;
+ .f-addon-item--qty .f-addon-main {
+ grid-area: main;
}
- .f-radio-name {
- @apply font-medium;
+ .f-addon-item--qty .f-addon-qty {
+ grid-area: qty;
+ justify-self: start;
}
- .f-radio-price {
- @apply whitespace-nowrap font-semibold;
+ .f-addon-item--qty .f-addon-price {
+ grid-area: price;
+ }
+}
+
+
+.f-summary {
+ @apply pt-2;
+}
+
+.f-summary-list {
+ @apply flex flex-col gap-1 mt-2 p-4 rounded-xl;
+ background: rgba(148, 163, 184, 0.07);
+}
+
+.f-summary-row,
+.f-summary-total {
+ @apply flex items-center justify-between;
+}
+
+.f-summary-row span:last-child {
+ @apply font-medium whitespace-nowrap;
+}
+
+.f-summary-total {
+ @apply mt-1 pt-2;
+ border-top: 1px solid rgba(148, 163, 184, 0.4);
+}
+
+.f-summary-total span:last-child {
+ @apply font-bold;
+ font-size: 1.25rem;
+ color: var(--fuz-accent, #2563eb);
+}
+
+
+.f-floating-total {
+ @apply fixed bottom-5 right-5 z-[10000];
+ @apply pointer-events-auto select-none;
+ touch-action: none;
+}
+
+.f-floating-total-circle {
+ @apply w-24 h-24 md:w-32 md:h-32 rounded-full;
+ @apply flex flex-col items-center justify-center text-center;
+ @apply relative overflow-hidden;
+ @apply bg-[--f-addons-background];
+ @apply backdrop-blur-md;
+
+ box-shadow:
+ 0 18px 35px hsla(221 47% 11% / 0.28),
+ 0 6px 14px hsla(221 47% 11% / 0.18),
+ inset 0 1px 0 hsla(0 0% 100% / 0.22),
+ inset 0 -10px 18px hsla(221 47% 11% / 0.25);
+
+ background-image:
+ radial-gradient(120% 120% at 30% 20%, hsla(0 0% 100% / 0.22) 0%, transparent 55%),
+ radial-gradient(140% 140% at 70% 80%, hsla(221 47% 11% / 0.22) 0%, transparent 60%);
+}
+
+.f-floating-total-circle::before {
+ content: "";
+ position: absolute;
+ inset: -30% -30% auto -30%;
+ height: 70%;
+ border-radius: 9999px;
+ background: radial-gradient(closest-side, hsla(0 0% 100% / 0.28), transparent 70%);
+ transform: rotate(-12deg);
+ pointer-events: none;
+}
+
+.f-floating-total-circle::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ border-radius: 9999px;
+ border: 1px solid hsla(0 0% 100% / 0.16);
+ box-shadow: inset 0 0 0 1px hsla(221 47% 11% / 0.18);
+ pointer-events: none;
+}
+
+.f-floating-total-amount {
+ @apply text-lg md:text-xl font-bold leading-none text-[--f-addons-text];
+}
+
+.f-floating-total-unit {
+ @apply my-1 text-xs md:text-sm opacity-70 text-[--f-addons-text];
+}
+
+@keyframes fuz-bounce {
+
+ 0%,
+ 100% {
+ transform: translateY(0);
}
- .f-radio-item.is-selected {
- /* delikatne wyróżnienie wybranego */
- /* @apply rounded-xl; */
- /* background: rgba(148, 163, 184, 0.12); */
+ 50% {
+ transform: translateY(-6px);
+ }
+}
+
+.f-floating-total:hover .f-floating-total-circle {
+ animation: fuz-bounce 420ms ease-in-out;
+}
+
+.f-floating-total:active .f-floating-total-circle {
+ transform: translateY(-4px) scale(1.02);
+}
+
+@media (hover: hover) and (pointer: fine) {
+ .f-floating-total:hover .f-floating-total-circle {
+ transform: translateY(-2px) scale(1.02);
+ box-shadow:
+ 0 22px 44px hsla(221 47% 11% / 0.32),
+ 0 8px 18px hsla(221 47% 11% / 0.20),
+ inset 0 1px 0 hsla(0 0% 100% / 0.24),
+ inset 0 -12px 20px hsla(221 47% 11% / 0.28);
+ transition: transform 180ms ease, box-shadow 180ms ease;
+ }
+}
+
+.f-addon-below {
+ grid-column: 1 / -1; /* pełna szerokość */
+ @apply pt-1;
+}
+
+.f-addon-below {
+ grid-column: 1 / -1; /* od kolumny main */
+}
+
+.f-radio-check { grid-area: check; }
+.f-radio-main { grid-area: main; min-width: 0; }
+.f-radio-price { grid-area: price; justify-self: end; text-align: right; }
+.f-radio-below {
+ grid-area: below;
+ @apply text-sm opacity-85;
+ justify-self: start;
+ text-align: left;
+}
+
+.f-addon-price {
+ @apply font-semibold whitespace-nowrap;
+ justify-self: end;
+ text-align: right;
+ min-width: 140px;
+}
+
+/* ✅ DLA QTY — nie trzymaj 140px, bo na mobile wypycha */
+.f-addon-item--qty .f-addon-price {
+ min-width: 110px; /* było 140px */
+}
+
+/* ✅ DLA QTY na małych ekranach jeszcze ciaśniej */
+@media (max-width: 640px) {
+ .f-addon-item--qty .f-addon-price {
+ min-width: 96px;
}
- .f-radio-details {
- @apply pl-10 pr-3 pb-3 -mt-1 text-sm;
- /* pl-10 = przesunięcie w prawo (radio + gap) */
+ /* opcjonalnie: minimalnie mniejsza czcionka w cenie w QTY */
+ .f-addon-item--qty .f-addon-price,
+ .f-addon-item--qty .f-addon-price-total {
+ font-size: 0.95em;
}
-
-
-
\ No newline at end of file
+}
\ No newline at end of file