Porządkowanie styli Card, addons
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import useDraggableFloating from "../hooks/useDraggableFloating.js";
|
||||
import "../../styles/modal.css";
|
||||
import "../../styles/addons.css";
|
||||
|
||||
@@ -73,6 +74,7 @@ export default function InternetAddonsModal({
|
||||
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]);
|
||||
|
||||
@@ -484,7 +486,13 @@ export default function InternetAddonsModal({
|
||||
</SectionAccordion>
|
||||
</div>
|
||||
|
||||
<div class="f-floating-total" onClick={(e) => e.stopPropagation()}>
|
||||
<div
|
||||
ref={floating.ref}
|
||||
class="f-floating-total"
|
||||
style={floating.style}
|
||||
{...floating.handlers}
|
||||
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>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from "preact/hooks";
|
||||
import Markdown from "../Markdown.jsx";
|
||||
import OffersSwitches from "../Switches.jsx";
|
||||
import InternetAddonsModal from "./InternetAddonsModal.jsx";
|
||||
import "../../styles/addons.css";
|
||||
import "../../styles/cards.css";
|
||||
|
||||
function formatMoney(amount, currency = "PLN") {
|
||||
if (typeof amount !== "number" || Number.isNaN(amount)) return "";
|
||||
|
||||
140
src/islands/hooks/useDraggableFloating.js
Normal file
140
src/islands/hooks/useDraggableFloating.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
function clamp(v, min, max) {
|
||||
return Math.max(min, Math.min(max, v));
|
||||
}
|
||||
|
||||
export default function useDraggableFloating(storageKey, opts = {}) {
|
||||
const margin = Number.isFinite(opts.margin) ? opts.margin : 12;
|
||||
|
||||
const ref = useRef(null);
|
||||
|
||||
const [pos, setPos] = useState(() => {
|
||||
if (typeof window === "undefined") return { x: null, y: null };
|
||||
try {
|
||||
const raw = localStorage.getItem(storageKey);
|
||||
return raw ? JSON.parse(raw) : { x: null, y: null };
|
||||
} catch {
|
||||
return { x: null, y: null };
|
||||
}
|
||||
});
|
||||
|
||||
const stateRef = useRef({
|
||||
dragging: false,
|
||||
pointerId: null,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
originX: 0,
|
||||
originY: 0,
|
||||
});
|
||||
|
||||
function getBounds(el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const maxX = window.innerWidth - rect.width - margin;
|
||||
const maxY = window.innerHeight - rect.height - margin;
|
||||
return { maxX, maxY };
|
||||
}
|
||||
|
||||
function persist(next) {
|
||||
try {
|
||||
localStorage.setItem(storageKey, JSON.stringify(next));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function onPointerDown(e) {
|
||||
if (e.pointerType === "mouse" && e.button !== 0) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
el.setPointerCapture?.(e.pointerId);
|
||||
|
||||
const st = stateRef.current;
|
||||
st.dragging = true;
|
||||
st.pointerId = e.pointerId;
|
||||
st.startX = e.clientX;
|
||||
st.startY = e.clientY;
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
st.originX = pos.x == null ? rect.left : pos.x;
|
||||
st.originY = pos.y == null ? rect.top : pos.y;
|
||||
}
|
||||
|
||||
function onPointerMove(e) {
|
||||
const st = stateRef.current;
|
||||
if (!st.dragging || st.pointerId !== e.pointerId) return;
|
||||
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
const dx = e.clientX - st.startX;
|
||||
const dy = e.clientY - st.startY;
|
||||
|
||||
const { maxX, maxY } = getBounds(el);
|
||||
|
||||
const next = {
|
||||
x: clamp(st.originX + dx, margin, maxX),
|
||||
y: clamp(st.originY + dy, margin, maxY),
|
||||
};
|
||||
|
||||
setPos(next);
|
||||
}
|
||||
|
||||
function onPointerUp(e) {
|
||||
const st = stateRef.current;
|
||||
if (!st.dragging || st.pointerId !== e.pointerId) return;
|
||||
|
||||
st.dragging = false;
|
||||
st.pointerId = null;
|
||||
|
||||
// zapis po puszczeniu
|
||||
persist(pos);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
const next = { x: null, y: null };
|
||||
setPos(next);
|
||||
try {
|
||||
localStorage.removeItem(storageKey);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// docinanie na resize
|
||||
useEffect(() => {
|
||||
function onResize() {
|
||||
const el = ref.current;
|
||||
if (!el || pos.x == null || pos.y == null) return;
|
||||
|
||||
const { maxX, maxY } = getBounds(el);
|
||||
const next = {
|
||||
x: clamp(pos.x, margin, maxX),
|
||||
y: clamp(pos.y, margin, maxY),
|
||||
};
|
||||
|
||||
if (next.x !== pos.x || next.y !== pos.y) {
|
||||
setPos(next);
|
||||
persist(next);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
}, [pos.x, pos.y]);
|
||||
|
||||
const style =
|
||||
pos.x == null || pos.y == null
|
||||
? undefined
|
||||
: `left:${pos.x}px; top:${pos.y}px; right:auto; bottom:auto;`;
|
||||
|
||||
const handlers = {
|
||||
onPointerDown,
|
||||
onPointerMove,
|
||||
onPointerUp,
|
||||
onPointerCancel: onPointerUp,
|
||||
};
|
||||
|
||||
return { ref, style, handlers, pos, setPos, reset };
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import useDraggableFloating from "../hooks/useDraggableFloating.js";
|
||||
|
||||
import "../../styles/modal.css";
|
||||
import "../../styles/addons.css";
|
||||
|
||||
@@ -181,6 +183,7 @@ export default function JamboxAddonsModal({
|
||||
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 [];
|
||||
@@ -415,130 +418,130 @@ export default function JamboxAddonsModal({
|
||||
// ---------
|
||||
const LS_KEY = "fuz_offer_config_v1";
|
||||
|
||||
function buildOfferPayload() {
|
||||
const phone = selectedPhoneId
|
||||
? phonePlans.find((p) => String(p.id) === String(selectedPhoneId))
|
||||
: null;
|
||||
function buildOfferPayload() {
|
||||
const phone = selectedPhoneId
|
||||
? phonePlans.find((p) => String(p.id) === String(selectedPhoneId))
|
||||
: null;
|
||||
|
||||
const decoder = selectedDecoderId
|
||||
? decodersList.find((d) => String(d.id) === String(selectedDecoderId))
|
||||
: null;
|
||||
const decoder = selectedDecoderId
|
||||
? decodersList.find((d) => String(d.id) === String(selectedDecoderId))
|
||||
: null;
|
||||
|
||||
const tvChosen = tvAddonsVisible
|
||||
.map((a) => {
|
||||
const qty = Number(selectedQty[a.id] || 0);
|
||||
if (qty <= 0) return null;
|
||||
const tvChosen = tvAddonsVisible
|
||||
.map((a) => {
|
||||
const qty = Number(selectedQty[a.id] || 0);
|
||||
if (qty <= 0) return null;
|
||||
|
||||
const termPricing = hasTvTermPricing(a, pkg);
|
||||
const term = tvTerm[a.id] || "12m";
|
||||
const unit = getAddonUnitPrice(a, pkg, termPricing ? term : null);
|
||||
const termPricing = hasTvTermPricing(a, pkg);
|
||||
const term = tvTerm[a.id] || "12m";
|
||||
const unit = getAddonUnitPrice(a, pkg, termPricing ? term : null);
|
||||
|
||||
return {
|
||||
id: a.id,
|
||||
nazwa: a.nazwa,
|
||||
qty,
|
||||
term: termPricing ? term : null,
|
||||
unit,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
return {
|
||||
id: a.id,
|
||||
nazwa: a.nazwa,
|
||||
qty,
|
||||
term: termPricing ? term : null,
|
||||
unit,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const addonsChosen = addonsList
|
||||
.map((a) => {
|
||||
const qty = Number(selectedQty[a.id] || 0);
|
||||
if (qty <= 0) return null;
|
||||
const addonsChosen = addonsList
|
||||
.map((a) => {
|
||||
const qty = Number(selectedQty[a.id] || 0);
|
||||
if (qty <= 0) return null;
|
||||
|
||||
const unit = getAddonUnitPrice(a, pkg, null);
|
||||
return { id: a.id, nazwa: a.nazwa, qty, unit };
|
||||
})
|
||||
.filter(Boolean);
|
||||
const unit = getAddonUnitPrice(a, pkg, null);
|
||||
return { id: a.id, nazwa: a.nazwa, qty, unit };
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
createdAt: new Date().toISOString(),
|
||||
pkg: { id: pkg?.id ?? null, name: pkg?.name ?? "", price: basePrice },
|
||||
phone: phone ? { id: phone.id, name: phone.name, price: phone.price_monthly } : null,
|
||||
decoder: decoder ? { id: decoder.id, name: decoder.nazwa, price: decoder.cena } : null,
|
||||
tvAddons: tvChosen,
|
||||
addons: addonsChosen,
|
||||
totals: {
|
||||
base: basePrice,
|
||||
phone: phonePrice,
|
||||
decoder: decoderPrice,
|
||||
tv: tvAddonsPrice,
|
||||
addons: addonsOnlyPrice,
|
||||
total: totalMonthly,
|
||||
currencyLabel: cenaOpis,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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)})`);
|
||||
return {
|
||||
createdAt: new Date().toISOString(),
|
||||
pkg: { id: pkg?.id ?? null, name: pkg?.name ?? "", price: basePrice },
|
||||
phone: phone ? { id: phone.id, name: phone.name, price: phone.price_monthly } : null,
|
||||
decoder: decoder ? { id: decoder.id, name: decoder.nazwa, price: decoder.cena } : null,
|
||||
tvAddons: tvChosen,
|
||||
addons: addonsChosen,
|
||||
totals: {
|
||||
base: basePrice,
|
||||
phone: phonePrice,
|
||||
decoder: decoderPrice,
|
||||
tv: tvAddonsPrice,
|
||||
addons: addonsOnlyPrice,
|
||||
total: totalMonthly,
|
||||
currencyLabel: cenaOpis,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (payload?.decoder) {
|
||||
lines.push("");
|
||||
lines.push(`Dekoder: ${payload.decoder.name} (${moneyWithLabel(payload.decoder.price)})`);
|
||||
function saveOfferToLocalStorage() {
|
||||
try {
|
||||
const payload = buildOfferPayload();
|
||||
localStorage.setItem(LS_KEY, JSON.stringify(payload));
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if (Array.isArray(payload?.tvAddons) && payload.tvAddons.length) {
|
||||
//-- dopisane
|
||||
function moneyWithLabel(v) {
|
||||
return `${money(v)} ${cenaOpis}`;
|
||||
}
|
||||
|
||||
function buildOfferMessage(payload) {
|
||||
const lines = [];
|
||||
|
||||
// nagłówek
|
||||
lines.push(`Wybrana oferta: ${payload?.pkg?.name || "—"}`);
|
||||
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)}`
|
||||
);
|
||||
|
||||
// ✅ 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 (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)}`);
|
||||
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");
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function saveOfferToLocalStorage() {
|
||||
try {
|
||||
const payload = buildOfferPayload();
|
||||
payload.message = buildOfferMessage(payload); // ✅ gotowy tekst
|
||||
localStorage.setItem(LS_KEY, JSON.stringify(payload));
|
||||
} catch {}
|
||||
}
|
||||
function saveOfferToLocalStorage() {
|
||||
try {
|
||||
const payload = buildOfferPayload();
|
||||
payload.message = buildOfferMessage(payload); // ✅ gotowy tekst
|
||||
localStorage.setItem(LS_KEY, JSON.stringify(payload));
|
||||
} catch { }
|
||||
}
|
||||
|
||||
|
||||
// ---------
|
||||
@@ -596,38 +599,38 @@ function saveOfferToLocalStorage() {
|
||||
open={openSections.decoder}
|
||||
onToggle={() => toggleSection("decoder")}
|
||||
>
|
||||
|
||||
{decodersList.length === 0 ? (
|
||||
<p>Brak dostępnych dekoderów.</p>
|
||||
) : (
|
||||
<div class="f-radio-list">
|
||||
{decodersList.map((d) => {
|
||||
const isSelected = String(selectedDecoderId) === String(d.id);
|
||||
|
||||
return (
|
||||
<label class={`f-radio-item ${isSelected ? "is-selected" : ""}`} key={d.id}>
|
||||
<div class="f-radio-check">
|
||||
<input
|
||||
type="radio"
|
||||
name="decoder"
|
||||
checked={isSelected}
|
||||
onChange={() => setSelectedDecoderId(String(d.id))}
|
||||
/>
|
||||
</div>
|
||||
{decodersList.length === 0 ? (
|
||||
<p>Brak dostępnych dekoderów.</p>
|
||||
) : (
|
||||
<div class="f-radio-list">
|
||||
{decodersList.map((d) => {
|
||||
const isSelected = String(selectedDecoderId) === String(d.id);
|
||||
|
||||
<div class="f-radio-main">
|
||||
<div class="f-radio-name">{d.nazwa}</div>
|
||||
{d.opis && <div class="f-addon-desc">{d.opis}</div>}
|
||||
</div>
|
||||
return (
|
||||
<label class={`f-radio-item ${isSelected ? "is-selected" : ""}`} key={d.id}>
|
||||
<div class="f-radio-check">
|
||||
<input
|
||||
type="radio"
|
||||
name="decoder"
|
||||
checked={isSelected}
|
||||
onChange={() => setSelectedDecoderId(String(d.id))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="f-radio-price">
|
||||
{money(d.cena)} {cenaOpis}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div class="f-radio-main">
|
||||
<div class="f-radio-name">{d.nazwa}</div>
|
||||
{d.opis && <div class="f-addon-desc">{d.opis}</div>}
|
||||
</div>
|
||||
|
||||
<div class="f-radio-price">
|
||||
{money(d.cena)} {cenaOpis}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
@@ -666,77 +669,77 @@ function saveOfferToLocalStorage() {
|
||||
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>
|
||||
{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-main">
|
||||
<div class="f-radio-name">Nie potrzebuję telefonu</div>
|
||||
</div>
|
||||
|
||||
<div class="f-radio-price">0,00 {cenaOpis}</div>
|
||||
</label>
|
||||
<div class="f-radio-price">0,00 {cenaOpis}</div>
|
||||
</label>
|
||||
|
||||
{/* pakiety */}
|
||||
{phonePlans.map((p) => {
|
||||
const isSelected = String(selectedPhoneId) === String(p.id);
|
||||
{/* 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>
|
||||
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-main">
|
||||
<div class="f-radio-name">{p.name}</div>
|
||||
</div>
|
||||
|
||||
<div class="f-radio-price">
|
||||
{money(p.price_monthly)} {cenaOpis}
|
||||
</div>
|
||||
</label>
|
||||
<div class="f-radio-price">
|
||||
{money(p.price_monthly)} {cenaOpis}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* ✅ detale ZAWSZE widoczne */}
|
||||
{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>
|
||||
{/* ✅ detale ZAWSZE widoczne */}
|
||||
{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>
|
||||
@@ -799,16 +802,16 @@ function saveOfferToLocalStorage() {
|
||||
<div class="f-summary-total">
|
||||
<span>Łącznie</span>
|
||||
<span>{money(totalMonthly)} {cenaOpis}</span>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="/kontakt"
|
||||
class="btn btn-primary w-full mt-4"
|
||||
onClick={() => saveOfferToLocalStorage()}
|
||||
>
|
||||
Wyślij zapytanie z tym wyborem
|
||||
</a>
|
||||
href="/kontakt"
|
||||
class="btn btn-primary w-full mt-4"
|
||||
onClick={() => saveOfferToLocalStorage()}
|
||||
>
|
||||
Wyślij zapytanie z tym wyborem
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -824,19 +827,25 @@ function saveOfferToLocalStorage() {
|
||||
</span>
|
||||
</div>
|
||||
</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
|
||||
ref={floating.ref}
|
||||
class="f-floating-total"
|
||||
style={floating.style}
|
||||
{...floating.handlers}
|
||||
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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "../../styles/addons.css";
|
||||
import "../../styles/cards.css";
|
||||
|
||||
import OffersSwitches from "../Switches.jsx";
|
||||
import JamboxChannelsModal from "./JamboxChannelsModal.jsx";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import "../../styles/modal.css";
|
||||
import "../../styles/addons.css";
|
||||
import "../../styles/channels-search.css";
|
||||
import "../../styles/jambox-modal-channel.css";
|
||||
import "../../styles/jambox-search.css";
|
||||
|
||||
export default function JamboxChannelsModal({ isOpen, onClose, pkg }) {
|
||||
const [channels, setChannels] = useState([]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import "../../styles/channels-search.css";
|
||||
import "../../styles/jambox-search.css";
|
||||
|
||||
export default function JamboxChannelsSearch() {
|
||||
const [q, setQ] = useState("");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from "preact/hooks";
|
||||
import { marked } from "marked";
|
||||
import "../../styles/channels-search.css";
|
||||
import "../../styles/jambox-search.css";
|
||||
|
||||
function norm(s) {
|
||||
return String(s || "")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Markdown from "../../islands/Markdown.jsx";
|
||||
import "../../styles/addons.css";
|
||||
import "../../styles/cards.css";
|
||||
|
||||
/**
|
||||
* @typedef {{ klucz: string, label: string, value: (string|number) }} PhoneParam
|
||||
|
||||
Reference in New Issue
Block a user