Stylizacja hero, zmiany w InternetAddonsModal

This commit is contained in:
dm
2025-12-16 07:16:47 +01:00
parent 6c91584fe1
commit 116a915cac
11 changed files with 488 additions and 944 deletions

View File

@@ -1,7 +1,7 @@
title: title:
- "Internet i Telewizja FUZ" - "Internet i Telewizja FUZ"
subtitle: subtitle:
- W Wyszkowie i okolicach - Doskanały internet światłowodowy w Wyszkowie i okolicach,
- "Lokalny operator, znamy Twoją okolicę," - "Lokalny operator, znamy Twoją okolicę,"
- "Realny serwis, szybkie wsparcie," - "Realny serwis, szybkie wsparcie,"
- "Stabilna infrastruktura światłowodowa," - "Stabilna infrastruktura światłowodowa,"

View File

@@ -1,7 +1,7 @@
tytul: "Dodatkowe usługi" tytul: "Dodatkowe usługi"
opis: "Wybierz usługi dodatkowe do internetu." opis: "Wybierz usługi dodatkowe do internetu."
cena_opis: "zł/mies." cena_opis: "zł / mies."
dodatki: dodatki:
- id: "public_ip" - id: "public_ip"
@@ -10,13 +10,3 @@ dodatki:
ilosc: false ilosc: false
opis: "Otrzymujesz unikalny, publiczny adres IP przypisany na stałe do Twojego łącza." opis: "Otrzymujesz unikalny, publiczny adres IP przypisany na stałe do Twojego łącza."
cena: 18.45 cena: 18.45
# - id: "ip_v4_extra"
# nazwa: "Dodatkowy publiczny adres IP"
# typ: "quantity"
# ilosc: true
# min: 0
# max: 4
# krok: 1
# opis: "Możesz dodać kilka dodatkowych adresów IP. Cena za sztukę."
# cena: 18.45

View File

@@ -2,7 +2,7 @@ sections:
- title: Router WiFi HL-4BX3V-F - title: Router WiFi HL-4BX3V-F
image: "HL-4BX3V-F.webp" image: "HL-4BX3V-F.webp"
content: | content: |
W ramach instalacji otrzymujesz nowoczesny router marki HALNy - urządzenie stworzone z myślą o wymagających użytkownikach.. W ramach instalacji otrzymujesz nowoczesny router marki HALNy - urządzenie stworzone z myślą o wymagających użytkownikach.
Znajdziesz w nim nowoczesny standard WiFi 6, porty 2,5Gb/s oraz 1 Gb/s, wsparcie dla sieci Mesh i VoIP. Stabilność, niezawodność i pełne wykorzystanie łącza w całym Twoim domu. Znajdziesz w nim nowoczesny standard WiFi 6, porty 2,5Gb/s oraz 1 Gb/s, wsparcie dla sieci Mesh i VoIP. Stabilność, niezawodność i pełne wykorzystanie łącza w całym Twoim domu.

View File

@@ -14,7 +14,7 @@ company:
country: "PL" country: "PL"
lat: 52.597385 lat: 52.597385
lon: 21.456797 lon: 21.456797
logo: "/images/logo-fuz.webp" logo: "/logo.webp"
page: page:
title: "FUZ Internet światłowodowy w Wyszkowie" title: "FUZ Internet światłowodowy w Wyszkowie"

View File

@@ -13,10 +13,6 @@ function money(amount) {
return n.toFixed(2).replace(".", ","); 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) { function mapPhoneYamlToPlans(phoneCards) {
const list = Array.isArray(phoneCards) ? phoneCards : []; const list = Array.isArray(phoneCards) ? phoneCards : [];
return list return list
@@ -32,10 +28,6 @@ function mapPhoneYamlToPlans(phoneCards) {
})); }));
} }
/**
* Dodatki z YAML:
* { id, nazwa, typ, ilosc, min, max, krok, opis, cena }
*/
function normalizeAddons(addons) { function normalizeAddons(addons) {
const list = Array.isArray(addons) ? addons : []; const list = Array.isArray(addons) ? addons : [];
return list return list
@@ -43,7 +35,7 @@ function normalizeAddons(addons) {
.map((a) => ({ .map((a) => ({
id: String(a.id), id: String(a.id),
nazwa: String(a.nazwa), nazwa: String(a.nazwa),
typ: String(a.typ || "checkbox"), typ: String(a.typ ?? a.type ?? "checkbox"),
ilosc: !!a.ilosc, ilosc: !!a.ilosc,
min: a.min != null ? Number(a.min) : 0, min: a.min != null ? Number(a.min) : 0,
max: a.max != null ? Number(a.max) : 10, 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({ export default function InternetAddonsModal({
isOpen, isOpen,
onClose, onClose,
plan, plan,
phoneCards = [],
// ✅ nowe: z YAML addons = [],
phoneCards = [], // telefon/cards.yaml -> cards[] cenaOpis = "zł / mies.",
addons = [], // internet-swiatlowodowy/addons.yaml -> dodatki[]
cenaOpis = "zł/mies.",
}) { }) {
const phonePlans = useMemo(() => mapPhoneYamlToPlans(phoneCards), [phoneCards]); const phonePlans = useMemo(() => mapPhoneYamlToPlans(phoneCards), [phoneCards]);
const addonsList = useMemo(() => normalizeAddons(addons), [addons]); const addonsList = useMemo(() => normalizeAddons(addons), [addons]);
@@ -69,51 +79,61 @@ export default function InternetAddonsModal({
const [error, setError] = useState(""); const [error, setError] = useState("");
const [selectedPhoneId, setSelectedPhoneId] = useState(null); const [selectedPhoneId, setSelectedPhoneId] = useState(null);
// zamiast selectedAddons (DB) -> mapka ilości
// { public_ip: 1, ip_v4_extra: 3 }
const [selectedQty, setSelectedQty] = useState({}); const [selectedQty, setSelectedQty] = useState({});
// akordeony const [openSections, setOpenSections] = useState({
const [openPhoneId, setOpenPhoneId] = useState(null); internet: true,
const [baseOpen, setBaseOpen] = useState(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(() => { useEffect(() => {
if (!isOpen) return; if (!isOpen) return;
setError(""); setError("");
setSelectedPhoneId(null); setSelectedPhoneId(null);
setSelectedQty({}); setSelectedQty({});
setOpenPhoneId(null); setOpenSections({ internet: true, phone: false, addons: false, summary: false });
setBaseOpen(true); }, [isOpen, plan?.id]);
}, [isOpen, plan]);
if (!isOpen || !plan) return null; if (!isOpen || !plan) return null;
const basePrice = Number(plan.price_monthly || 0); const basePrice = Number(plan.price_monthly || 0);
const phonePrice = (() => { const phonePrice = useMemo(() => {
if (!selectedPhoneId) return 0; 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); return Number(p?.price_monthly || 0);
})(); }, [selectedPhoneId, phonePlans]);
const addonsPrice = addonsList.reduce((sum, a) => { const addonsPrice = useMemo(() => {
const qty = Number(selectedQty[a.id] || 0); return addonsList.reduce((sum, a) => {
return sum + qty * Number(a.cena || 0); const qty = Number(selectedQty[a.id] || 0);
}, 0); return sum + qty * Number(a.cena || 0);
}, 0);
}, [selectedQty, addonsList]);
const totalMonthly = basePrice + phonePrice + addonsPrice; const totalMonthly = basePrice + phonePrice + addonsPrice;
const handlePhoneSelect = (id) => { const handlePhoneSelect = (id) => {
if (id === null) { if (id === null) {
setSelectedPhoneId(null); setSelectedPhoneId(null);
setOpenPhoneId(null);
return; return;
} }
setSelectedPhoneId(id); setSelectedPhoneId(id);
setOpenPhoneId((prev) => (String(prev) === String(id) ? null : id));
}; };
const toggleCheckboxAddon = (id) => { const toggleCheckboxAddon = (id) => {
@@ -129,6 +149,77 @@ export default function InternetAddonsModal({
setSelectedQty((prev) => ({ ...prev, [id]: safe })); 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 ( return (
<div class="f-modal-overlay" onClick={onClose}> <div class="f-modal-overlay" onClick={onClose}>
<button <button
@@ -143,269 +234,261 @@ export default function InternetAddonsModal({
</button> </button>
<div <div class="f-modal-panel f-modal-panel--compact" onClick={(e) => e.stopPropagation()}>
class="f-modal-panel f-modal-panel--compact"
onClick={(e) => e.stopPropagation()}
>
<div class="f-modal-inner"> <div class="f-modal-inner">
<h2 class="f-modal-title">{plan.name} konfiguracja usług</h2> <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>} {error && <p class="text-red-600">{error}</p>}
{/* Telefon */} {/* INTERNET */}
<div class="f-modal-section"> <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 ? ( {/* TELEFON */}
<p>Brak dostępnych pakietów telefonicznych.</p> <div class="f-modal-section">
) : ( <SectionAccordion
<div class="f-modal-phone-list f-accordion"> title="Usługa telefoniczna"
{/* brak telefonu */} right={
<div class="f-accordion-item f-accordion-item--no-phone"> <span class="f-modal-phone-price">
<button {phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"}
type="button" </span>
class="f-accordion-header" }
onClick={() => handlePhoneSelect(null)} open={openSections.phone}
> onToggle={() => toggleSection("phone")}
<span class="f-accordion-header-left"> >
{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 <input
type="radio" type="radio"
name="phone-plan" name="phone-plan"
checked={selectedPhoneId === null} checked={selectedPhoneId === null}
onChange={(e) => { onChange={() => handlePhoneSelect(null)}
e.stopPropagation();
handlePhoneSelect(null);
}}
onClick={(e) => e.stopPropagation()}
/> />
<span class="f-modal-phone-name">Nie potrzebuję telefonu</span> </div>
</span>
<span class="f-modal-phone-price">0,00 {cenaOpis}</span>
</button>
</div>
{phonePlans.map((p) => { <div class="f-radio-main">
const isSelected = String(selectedPhoneId) === String(p.id); <div class="f-radio-name">Nie potrzebuję telefonu</div>
const isOpen = String(openPhoneId) === String(p.id); </div>
return ( <div class="f-radio-price">0,00 {cenaOpis}</div>
<div </label>
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>
<span class="f-modal-phone-price"> {phonePlans.map((p) => {
{money(p.price_monthly)} {cenaOpis} const isSelected = String(selectedPhoneId) === String(p.id);
</span>
</button>
{isOpen && ( return (
<div class="f-accordion-body"> <div class="f-radio-block" key={p.id}>
{p.features && p.features.length > 0 && ( <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"> <ul class="f-card-features">
{p.features {p.features
.filter( .filter(
(f) => (f) => !String(f.label || "").toLowerCase().includes("aktyw"),
!String(f.label || "")
.toLowerCase()
.includes("aktyw"),
) )
.map((f, idx) => ( .map((f, idx) => (
<li class="f-card-row" key={idx}> <li class="f-card-row" key={idx}>
<span class="f-card-label">{f.label}</span> <span class="f-card-label">{f.label}</span>
<span class="f-card-value"> <span class="f-card-value">{formatFeatureValue(f.value)}</span>
{formatFeatureValue(f.value)}
</span>
</li> </li>
))} ))}
</ul> </ul>
)} </div>
</div> )}
)} </div>
</div> );
); })}
})} </div>
</div> )}
)} </SectionAccordion>
</div> </div>
{/* Dodatki internetowe */} {/* USLUGI DODATKOWE */}
<div class="f-modal-section"> <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 ? ( if (!isQty) {
<p>Brak dodatkowych usług.</p> const checked = qty > 0;
) : ( return (
<div class="f-addon-list"> <label class="f-addon-item" key={a.id}>
{addonsList.map((a) => { <div class="f-addon-checkbox">
const qty = Number(selectedQty[a.id] || 0); <input
const isQty = a.typ === "quantity" || a.ilosc === true; 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 ( return (
<label class="f-addon-item" key={a.id}> <div class="f-addon-item f-addon-item--qty" key={a.id}>
<div class="f-addon-checkbox"> <div class="f-addon-checkbox" aria-hidden="true"></div>
<input
type="checkbox"
checked={checked}
onChange={() => toggleCheckboxAddon(a.id)}
/>
</div>
<div class="f-addon-main"> <div class="f-addon-main">
<div class="f-addon-name">{a.nazwa}</div> <div class="f-addon-name">{a.nazwa}</div>
{a.opis && <div class="f-addon-desc">{a.opis}</div>} {a.opis && <div class="f-addon-desc">{a.opis}</div>}
</div> </div>
<div class="f-addon-price"> <div class="f-addon-qty" onClick={(e) => e.stopPropagation()}>
{money(a.cena)} {cenaOpis} <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>
</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>
); );
} })}
</div>
)}
// quantity </SectionAccordion>
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> </div>
{/* Podsumowanie */} {/* PODSUMOWANIE */}
<div class="f-modal-section f-summary"> <div class="f-modal-section">
<h3>Podsumowanie miesięczne</h3> <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">
<div class="f-summary-row"> <span>Telefon</span>
<span>Internet</span> <span>{phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"}</span>
<span>{money(basePrice)} {cenaOpis}</span> </div>
</div>
<div class="f-summary-row"> <div class="f-summary-row">
<span>Telefon</span> <span>Dodatkowe usługi</span>
<span>{phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"}</span> <span>{addonsPrice ? `${money(addonsPrice)} ${cenaOpis}` : "—"}</span>
</div> </div>
<div class="f-summary-row"> <div class="f-summary-total">
<span>Dodatki</span> <span>Łącznie</span>
<span>{addonsPrice ? `${money(addonsPrice)} ${cenaOpis}` : "—"}</span> <span>{money(totalMonthly)} {cenaOpis}</span>
</div> </div>
<div class="f-summary-total"> <a
<span>Łącznie</span> href="/kontakt#form"
<span>{money(totalMonthly)} {cenaOpis}</span> class="btn btn-primary w-full mt-4"
onClick={() => saveOfferToLocalStorage()}
>
Wyślij zapytanie z tym wyborem
</a>
</div>
</div> </div>
</div> </SectionAccordion>
</div> </div>
<div class="f-floating-total" onClick={(e) => e.stopPropagation()}> <div class="f-floating-total" onClick={(e) => e.stopPropagation()}>
<div class="f-floating-total-inner"> <div class="f-floating-total-circle" role="status" aria-label="Cena miesięczna">
<span class="f-floating-total-label">Suma</span> <span class="f-floating-total-unit">Razem</span>
<span class="f-floating-total-value"> <span class="f-floating-total-amount">{money(totalMonthly)}</span>
{money(totalMonthly)} {cenaOpis} <span class="f-floating-total-unit">{cenaOpis}</span>
</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -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>
);
}

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import Markdown from "../Markdown.jsx"; import Markdown from "../Markdown.jsx";
import OffersSwitches from "../OffersSwitches.jsx"; import OffersSwitches from "../OffersSwitches.jsx";
import InternetAddonsModal from "./InternetAddonsModalCompact.jsx"; import InternetAddonsModal from "./InternetAddonsModal.jsx";
import "../../styles/offers-table.css"; import "../../styles/offers-table.css";
function formatMoney(amount, currency = "PLN") { function formatMoney(amount, currency = "PLN") {

View File

@@ -1,125 +1,135 @@
.f-hero { @layer components {
@apply relative min-h-[600px] md:min-h-[700px] lg:min-h-[700px]; .f-hero {
@apply flex items-center overflow-hidden; @apply relative flex items-center overflow-hidden;
} @apply min-h-[600px] md:min-h-[700px] lg:min-h-[700px];
.f-hero-background {
@apply absolute inset-0 w-full h-full object-cover z-0;
object-position: center center;
}
.f-hero-overlay {
@apply absolute inset-0 z-[1] bg-black/5;
}
.f-hero--right .f-hero-overlay {
@apply md:bg-gradient-to-r;
background-image: linear-gradient(
to right,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.1) 15%,
rgba(0, 0, 0, 0.1) 30%,
rgba(0, 0, 0, 0.5) 45%,
rgba(0, 0, 0, 0.5) 55%,
rgba(0, 0, 0, 0.5) 65%,
rgba(0, 0, 0, 0.5) 75%,
rgba(0, 0, 0, 0.75) 100%
);
}
.f-hero--left .f-hero-overlay {
@apply md:bg-gradient-to-l;
background-image: linear-gradient(
to left,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.1) 50%,
rgba(0, 0, 0, 0.5) 60%,
rgba(0, 0, 0, 0.75) 100%
);
}
.f-hero--center .f-hero-overlay {
@apply bg-black/50;
}
.f-hero-container {
@apply relative z-[2] w-full max-w-[1400px] mx-auto;
@apply px-6 md:px-8 py-8 md:py-1;
@apply grid grid-cols-1 gap-4;
}
.f-hero--right .f-hero-container {
@apply md:grid-cols-2;
}
.f-hero--right .f-hero-content {
@apply md:col-start-2;
}
.f-hero--left .f-hero-container {
@apply md:grid-cols-2;
}
.f-hero--left .f-hero-content {
@apply md:col-start-1;
}
.f-hero--center .f-hero-container {
@apply md:grid-cols-1 md:max-w-4xl md:text-center;
}
.f-hero-content {
@apply flex flex-col gap-6 text-[--f-hero-text] mx-1;
}
.f-hero-titles {
@apply space-y-2;
}
.f-hero-title {
@apply text-4xl md:text-5xl lg:text-6xl text-[--f-hero-header];
@apply font-bold leading-tight m-0;
@apply drop-shadow-[0_2px_8px_rgba(0,0,0,0.8)];
}
.f-hero-subtitles {
@apply space-y-4;
}
.f-hero-subtitle {
@apply text-lg md:text-xl lg:text-2xl pl-9;
@apply leading-relaxed m-0;
@apply opacity-0 animate-fade-in-up;
@apply drop-shadow-[0_1px_4px_rgba(0,0,0,0.8)];
animation-delay: calc(var(--delay, 0) * 150ms);
}
.f-hero-description {
@apply text-base md:text-lg leading-relaxed;
@apply opacity-90 max-w-2xl m-0;
@apply drop-shadow-[0_1px_4px_rgba(0,0,0,0.8)];
}
.f-hero-ctas {
@apply flex flex-wrap gap-4 mt-2 justify-center;
}
.f-hero-ctas a {
@apply w-full md:w-auto md:min-w-[280px] md:flex-1 md:max-w-[400px] justify-center;
}
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(20px);
} }
to {
opacity: 1; .f-hero-background {
transform: translateY(0); @apply absolute inset-0 z-0 w-full h-full object-cover;
object-position: center top;
}
.f-hero-overlay {
@apply absolute inset-0 z-[1] bg-black/5;
}
.f-hero--right .f-hero-overlay {
background-image: linear-gradient(
to right,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.1) 15%,
rgba(0, 0, 0, 0.1) 30%,
rgba(0, 0, 0, 0.5) 45%,
rgba(0, 0, 0, 0.5) 55%,
rgba(0, 0, 0, 0.5) 65%,
rgba(0, 0, 0, 0.5) 75%,
rgba(0, 0, 0, 0.75) 100%
);
}
.f-hero--left .f-hero-overlay {
background-image: linear-gradient(
to left,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.1) 50%,
rgba(0, 0, 0, 0.5) 60%,
rgba(0, 0, 0, 0.75) 100%
);
}
.f-hero--center .f-hero-overlay {
@apply bg-black/50;
background-image: none;
}
.f-hero-container {
@apply relative z-[2] w-full max-w-[1400px] mx-auto;
@apply px-6 md:px-8 py-8 md:py-1;
@apply grid grid-cols-1 gap-4;
}
.f-hero--right .f-hero-container,
.f-hero--left .f-hero-container {
@apply md:grid-cols-2;
}
.f-hero--right .f-hero-content {
@apply md:col-start-2;
}
.f-hero--left .f-hero-content {
@apply md:col-start-1;
}
.f-hero--center .f-hero-container {
@apply md:grid-cols-1 md:max-w-4xl md:text-center;
}
.f-hero-content {
@apply mx-1 flex flex-col gap-6;
@apply text-[--f-hero-text];
}
.f-hero-title {
@apply m-0 font-bold leading-tight;
@apply text-4xl md:text-6xl lg:text-7xl text-center;
@apply text-[--f-hero-header];
@apply drop-shadow-[0_2px_8px_rgba(0,0,0,0.8)];
}
.f-hero-subtitles {
@apply space-y-4;
}
.f-hero-subtitle {
@apply m-0 leading-relaxed;
@apply text-xl md:text-2xl lg:text-3xl;
@apply pl-9 opacity-0;
@apply drop-shadow-[0_1px_4px_rgba(0,0,0,0.8)];
animation: fade-in-up 0.6s ease-out forwards;
animation-delay: calc(var(--delay, 0) * 150ms);
}
.f-hero--center .f-hero-subtitle {
@apply pl-0;
}
.f-hero-description {
@apply m-0 max-w-2xl opacity-90;
@apply text-base md:text-lg leading-relaxed;
@apply drop-shadow-[0_1px_4px_rgba(0,0,0,0.8)];
}
.f-hero-ctas {
@apply mt-2 flex flex-wrap gap-4;
}
.f-hero--left .f-hero-ctas {
@apply justify-start;
}
.f-hero--right .f-hero-ctas {
@apply justify-end;
}
.f-hero--center .f-hero-ctas {
@apply justify-center;
}
.f-hero-ctas a {
@apply w-full justify-center;
@apply md:w-auto md:min-w-[280px] md:flex-1 md:max-w-[400px];
}
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
} }
.animate-fade-in-up {
animation: fade-in-up 0.6s ease-out forwards;
}

View File

@@ -102,7 +102,7 @@
@apply flex flex-col gap-2; @apply flex flex-col gap-2;
} }
/* BAZA: checkbox | main | price */ /* BAZA: checkbox | main | price */
.f-addon-item { .f-addon-item {
@apply grid items-start gap-3 px-3 py-2; @apply grid items-start gap-3 px-3 py-2;
border-bottom: 1px solid rgba(148, 163, 184, 0.4); border-bottom: 1px solid rgba(148, 163, 184, 0.4);
@@ -110,7 +110,9 @@
grid-template-columns: auto 1fr auto; grid-template-columns: auto 1fr auto;
/* border-color: rgba(148, 163, 184, 0.5); */ /* border-color: rgba(148, 163, 184, 0.5); */
background: var(--f-background); background: var(--f-background);
@apply cursor-pointer;
} }
.f-addon-item * { @apply cursor-pointer; }
.f-addon-item:last-child { .f-addon-item:last-child {
border-bottom: none; border-bottom: none;

View File

@@ -36,7 +36,7 @@
} }
.f-card-name { .f-card-name {
@apply text-xl font-semibold; @apply text-3xl font-semibold;
} }
.f-card-price { .f-card-price {
@@ -57,11 +57,11 @@
} }
.f-card-label { .f-card-label {
@apply text-sm font-medium opacity-80; @apply text-base font-medium opacity-80;
} }
.f-card-value { .f-card-value {
@apply text-sm font-semibold text-right; @apply text-base font-semibold text-right;
} }
.f-card-value.yes { .f-card-value.yes {

View File

@@ -10,22 +10,16 @@
--link-color-light: hsla(210, 100%, 40%, 1); --link-color-light: hsla(210, 100%, 40%, 1);
--link-hover-light: hsla(165, 80%, 25%, 1); --link-hover-light: hsla(165, 80%, 25%, 1);
--text1-light: hsl(var(--brand-hue) var(--brand-saturation) 10%); --text1-light: hsla(200, 100%, 10%, 1);
--text2-light: hsl(var(--brand-hue) 30% 30%); --text2-light: hsla(200, 30%, 30%, 1);
--text3-light: hsl(var(--brand-hue) 15% 85%); --text3-light: hsla(200, 15%, 85%, 1);
/*
--surface1-light: hsl(var(--brand-hue) 25% 90%);
--surface2-light: hsl(var(--brand-hue) 20% 99%);
--surface3-light: hsl(var(--brand-hue) 20% 92%);
--surface4-light: hsl(var(--brand-hue) 20% 85%);
--surface5-light: hsla(217, 70%, 26%, 1);
*/
--surface1-light: hsla(200, 25%, 90%, 1); --surface1-light: hsla(200, 25%, 90%, 1);
--surface2-light: hsla(200, 20%, 99%, 1); --surface2-light: hsla(200, 20%, 99%, 1);
--surface3-light: hsla(200, 20%, 92%, 1); --surface3-light: hsla(200, 20%, 92%, 1);
--surface4-light: hsla(200, 20%, 85%, 1); --surface4-light: hsla(200, 20%, 85%, 1);
--surface5-light: hsla(217, 70%, 26%, 1); --surface5-light: hsla(217, 70%, 26%, 1);
--surface6-light: hsla(200, 5%, 75%, 1);
--surface-shadow-light: var(--brand-hue) 10% 20%; --surface-shadow-light: var(--brand-hue) 10% 20%;
@@ -34,8 +28,10 @@
--brand-dark: hsl(var(--brand-hue) calc(var(--brand-saturation) / 0.2) calc(var(--brand-lightness) / 1.5)); --brand-dark: hsl(var(--brand-hue) calc(var(--brand-saturation) / 0.2) calc(var(--brand-lightness) / 1.5));
--text1-dark: hsl(var(--brand-hue) 15% 85%);
--text2-dark: hsl(var(--brand-hue) 5% 65%); --text1-dark: hsla(200, 15%, 85%, 1);
--text2-dark: hsla(200, 5%, 65%, 1);
--text3-dark: hsla(200, 5%, 100%, 1);
--surface1-dark: hsla(200, 10%, 10%, 1); --surface1-dark: hsla(200, 10%, 10%, 1);
--surface2-dark: hsla(200, 10%, 15%, 1); --surface2-dark: hsla(200, 10%, 15%, 1);
@@ -60,16 +56,16 @@
} }
:root { :root {
/* --- Hero --- */ --f-hero-text: var(--text3-dark);
--f-hero-text: var(--text1-dark); --f-hero-header: var(--text3-dark);
--f-hero-header: var(--text1-dark);
/* --- Background and Text --- */ /* --- Background and Text --- */
--f-background: var(--surface3-light); --f-background: var(--surface3-light);
--f-text: var(--text1-light); --f-text: var(--text1-light);
--f-header: var(--text1-light); --f-header: var(--text1-light);
--f-header-items: (var(--text1-light)); --f-header-items: (var(--text1-light));
/*--- Navbar --- */ /*--- Navbar --- */
--f-navbar-background: var(--surface4-light); --f-navbar-background: var(--surface6-light);
--f-navbar-link: var(--link-color-light); --f-navbar-link: var(--link-color-light);
--f-navbar-link-hover: var(--link-hover-light); --f-navbar-link-hover: var(--link-hover-light);
/*--- Footer --- */ /*--- Footer --- */
@@ -121,9 +117,9 @@
} }
:root.dark { :root.dark {
/* --- Hero --- */ --f-hero-text: var(--text3-dark);
--f-hero-text: var(--text1-dark); --f-hero-header: var(--text3-dark);
--f-hero-header: var(--text1-dark);
/* --- Background and Text --- */ /* --- Background and Text --- */
--f-background: var(--surface1-dark); --f-background: var(--surface1-dark);
--f-text: var(--text1-dark); --f-text: var(--text1-dark);