Przebudowa strony
This commit is contained in:
Binary file not shown.
365
src/islands/Internet/InternetAddonsModal.jsx
Normal file
365
src/islands/Internet/InternetAddonsModal.jsx
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import "../../styles/modal.css";
|
||||||
|
import "../../styles/offers/offers-table.css";
|
||||||
|
|
||||||
|
export default function InternetAddonsModal({ isOpen, onClose, plan }) {
|
||||||
|
const [phonePlans, setPhonePlans] = useState([]);
|
||||||
|
const [addons, setAddons] = useState([]);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const [selectedPhoneId, setSelectedPhoneId] = useState(null);
|
||||||
|
const [selectedAddons, setSelectedAddons] = useState([]);
|
||||||
|
|
||||||
|
// który pakiet telefoniczny jest rozwinięty
|
||||||
|
const [openPhoneId, setOpenPhoneId] = useState(null);
|
||||||
|
|
||||||
|
// czy akordeon internetu (fiber) jest rozwinięty
|
||||||
|
const [baseOpen, setBaseOpen] = useState(true);
|
||||||
|
|
||||||
|
// reset wyborów po otwarciu nowego planu
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
setSelectedPhoneId(null);
|
||||||
|
setSelectedAddons([]);
|
||||||
|
setOpenPhoneId(null);
|
||||||
|
setBaseOpen(true);
|
||||||
|
}, [isOpen, plan]);
|
||||||
|
|
||||||
|
// ładowanie danych
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// telefon
|
||||||
|
const phoneRes = await fetch("/api/phone/plans");
|
||||||
|
if (!phoneRes.ok) throw new Error(`HTTP ${phoneRes.status} (phone)`);
|
||||||
|
|
||||||
|
const phoneJson = await phoneRes.json();
|
||||||
|
const phoneData = Array.isArray(phoneJson.data) ? phoneJson.data : [];
|
||||||
|
|
||||||
|
// dodatki
|
||||||
|
const addonsRes = await fetch("/api/internet/addons");
|
||||||
|
if (!addonsRes.ok) throw new Error(`HTTP ${addonsRes.status} (addons)`);
|
||||||
|
|
||||||
|
const addonsJson = await addonsRes.json();
|
||||||
|
const addonsData = Array.isArray(addonsJson.data) ? addonsJson.data : [];
|
||||||
|
|
||||||
|
if (!cancelled) {
|
||||||
|
setPhonePlans(phoneData);
|
||||||
|
setAddons(addonsData);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Błąd ładowania danych do InternetAddonsModal:", err);
|
||||||
|
if (!cancelled) {
|
||||||
|
setError("Nie udało się załadować danych dodatkowych usług.");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
if (!isOpen || !plan) return null;
|
||||||
|
|
||||||
|
const basePrice = plan.price_monthly || 0;
|
||||||
|
|
||||||
|
const phonePrice = (() => {
|
||||||
|
if (!selectedPhoneId) return 0;
|
||||||
|
const p = phonePlans.find((p) => p.id === selectedPhoneId);
|
||||||
|
return p?.price_monthly || 0;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const addonsPrice = selectedAddons.reduce((sum, sel) => {
|
||||||
|
const addon = addons.find((a) => a.id === sel.addonId);
|
||||||
|
if (!addon) return sum;
|
||||||
|
const opt = addon.options.find((o) => o.id === sel.optionId);
|
||||||
|
if (!opt) return sum;
|
||||||
|
return sum + (opt.price || 0);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const totalMonthly = basePrice + phonePrice + addonsPrice;
|
||||||
|
|
||||||
|
const handleAddonToggle = (addonId, optionId) => {
|
||||||
|
setSelectedAddons((prev) => {
|
||||||
|
const exists = prev.some(
|
||||||
|
(x) => x.addonId === addonId && x.optionId === optionId
|
||||||
|
);
|
||||||
|
if (exists) {
|
||||||
|
return prev.filter(
|
||||||
|
(x) => !(x.addonId === addonId && x.optionId === optionId)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return [...prev, { addonId, optionId }];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const togglePhoneOpen = (id) => {
|
||||||
|
setOpenPhoneId((prev) => (prev === id ? null : id));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="fuz-modal-overlay" onClick={onClose}>
|
||||||
|
<button
|
||||||
|
class="fuz-modal-close"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="fuz-modal-panel fuz-modal-panel--compact"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div class="fuz-modal-inner">
|
||||||
|
<h2 class="fuz-modal-title">Konfiguracja usług dodatkowych</h2>
|
||||||
|
|
||||||
|
{/* INTERNET (fiber) jako akordeon */}
|
||||||
|
<div class="fuz-modal-section">
|
||||||
|
<div class={`fuz-accordion-item ${baseOpen ? "is-open" : ""}`}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="fuz-accordion-header"
|
||||||
|
onClick={() => setBaseOpen((prev) => !prev)}
|
||||||
|
>
|
||||||
|
<span class="fuz-modal-phone-name">{plan.name}</span>
|
||||||
|
<span class="fuz-modal-phone-price">
|
||||||
|
{basePrice.toFixed(2)} zł/mies.
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{baseOpen && plan.features && plan.features.length > 0 && (
|
||||||
|
<div class="fuz-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">{f.value}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading && <p>Ładowanie danych...</p>}
|
||||||
|
{error && <p class="text-red-600">{error}</p>}
|
||||||
|
|
||||||
|
{!loading && !error && (
|
||||||
|
<>
|
||||||
|
{/* Sekcja: wybór telefonu (akordeon + opcja bez telefonu) */}
|
||||||
|
<div class="fuz-modal-section">
|
||||||
|
<h3>Usługa telefoniczna</h3>
|
||||||
|
{phonePlans.length === 0 ? (
|
||||||
|
<p>Brak dostępnych pakietów telefonicznych.</p>
|
||||||
|
) : (
|
||||||
|
<div class="fuz-modal-phone-list fuz-accordion">
|
||||||
|
{/* OPCJA: brak telefonu */}
|
||||||
|
<div class="fuz-accordion-item fuz-accordion-item--no-phone">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="fuz-accordion-header"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedPhoneId(null);
|
||||||
|
setOpenPhoneId(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="fuz-accordion-header-left">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="phone-plan"
|
||||||
|
checked={selectedPhoneId === null}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSelectedPhoneId(null);
|
||||||
|
setOpenPhoneId(null);
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
<span class="fuz-modal-phone-name">
|
||||||
|
Nie potrzebuję telefonu
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="fuz-modal-phone-price">0,00 zł/mies.</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* LISTA PAKIETÓW TELEFONICZNYCH */}
|
||||||
|
{phonePlans.map((p) => {
|
||||||
|
const isSelected = selectedPhoneId === p.id;
|
||||||
|
const isOpen = openPhoneId === p.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={`fuz-accordion-item ${
|
||||||
|
isOpen ? "is-open" : ""
|
||||||
|
}`}
|
||||||
|
key={p.id}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="fuz-accordion-header"
|
||||||
|
onClick={() => togglePhoneOpen(p.id)}
|
||||||
|
>
|
||||||
|
<span class="fuz-accordion-header-left">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="phone-plan"
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSelectedPhoneId(p.id);
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
<span class="fuz-modal-phone-name">
|
||||||
|
{p.name}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="fuz-modal-phone-price">
|
||||||
|
{p.price_monthly.toFixed(2)} zł/mies.
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div class="fuz-accordion-body">
|
||||||
|
{p.features && p.features.length > 0 && (
|
||||||
|
<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">
|
||||||
|
{f.value}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sekcja: dodatki internetowe */}
|
||||||
|
<div class="fuz-modal-section">
|
||||||
|
<h3>Dodatkowe usługi</h3>
|
||||||
|
{addons.length === 0 ? (
|
||||||
|
<p>Brak dodatkowych usług.</p>
|
||||||
|
) : (
|
||||||
|
<div class="fuz-addon-list">
|
||||||
|
{addons.map((addon) =>
|
||||||
|
addon.options.map((opt) => {
|
||||||
|
const checked = selectedAddons.some(
|
||||||
|
(x) =>
|
||||||
|
x.addonId === addon.id &&
|
||||||
|
x.optionId === opt.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
class="fuz-addon-item"
|
||||||
|
key={`${addon.id}-${opt.id}`}
|
||||||
|
>
|
||||||
|
<div class="fuz-addon-checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
onChange={() =>
|
||||||
|
handleAddonToggle(addon.id, opt.id)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fuz-addon-main">
|
||||||
|
<div class="fuz-addon-name">{addon.name}</div>
|
||||||
|
{addon.description && (
|
||||||
|
<div class="fuz-addon-desc">
|
||||||
|
{addon.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fuz-addon-price">
|
||||||
|
{opt.price.toFixed(2)} zł/mies.
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Podsumowanie */}
|
||||||
|
<div class="fuz-modal-section fuz-summary">
|
||||||
|
<h3>Podsumowanie miesięczne</h3>
|
||||||
|
|
||||||
|
<div class="fuz-summary-list">
|
||||||
|
<div class="fuz-summary-row">
|
||||||
|
<span>Internet</span>
|
||||||
|
<span>{basePrice.toFixed(2)} zł/mies.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fuz-summary-row">
|
||||||
|
<span>Telefon</span>
|
||||||
|
<span>
|
||||||
|
{phonePrice
|
||||||
|
? `${phonePrice.toFixed(2)} zł/mies.`
|
||||||
|
: "—"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fuz-summary-row">
|
||||||
|
<span>Dodatki</span>
|
||||||
|
<span>
|
||||||
|
{addonsPrice
|
||||||
|
? `${addonsPrice.toFixed(2)} zł/mies.`
|
||||||
|
: "—"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fuz-summary-total">
|
||||||
|
<span>Łącznie</span>
|
||||||
|
<span>{totalMonthly.toFixed(2)} zł/mies.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import "../styles/offers/offers-table.css";
|
import "../../styles/offers/offers-table.css";
|
||||||
|
import InternetAddonsModal from "./InternetAddonsModal.jsx"; // 🔹 dostosuj ścieżkę, jeśli inna
|
||||||
|
|
||||||
export default function InternetDbOffersCards({
|
export default function InternetDbOffersCards({
|
||||||
title = "Oferty Internetu FUZ",
|
title = "Oferty Internetu FUZ",
|
||||||
@@ -10,6 +11,10 @@ export default function InternetDbOffersCards({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
// 🔹 stan modala z dodatkami
|
||||||
|
const [addonsModalOpen, setAddonsModalOpen] = useState(false);
|
||||||
|
const [activePlan, setActivePlan] = useState(null);
|
||||||
|
|
||||||
// nasłuchuj globalnego eventu z OffersSwitches
|
// nasłuchuj globalnego eventu z OffersSwitches
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handler(e) {
|
function handler(e) {
|
||||||
@@ -69,6 +74,7 @@ export default function InternetDbOffersCards({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section class="f-offers">
|
<section class="f-offers">
|
||||||
|
|
||||||
{loading && <p>Ładowanie ofert...</p>}
|
{loading && <p>Ładowanie ofert...</p>}
|
||||||
{error && <p class="text-red-600">{error}</p>}
|
{error && <p class="text-red-600">{error}</p>}
|
||||||
|
|
||||||
@@ -79,26 +85,45 @@ export default function InternetDbOffersCards({
|
|||||||
key={plan.id}
|
key={plan.id}
|
||||||
plan={plan}
|
plan={plan}
|
||||||
contractLabel={contractLabel}
|
contractLabel={contractLabel}
|
||||||
|
onConfigureAddons={() => {
|
||||||
|
setActivePlan(plan);
|
||||||
|
setAddonsModalOpen(true);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 🔹 Modal z usługami dodatkowymi (internet + telefon + addon’y) */}
|
||||||
|
<InternetAddonsModal
|
||||||
|
isOpen={addonsModalOpen}
|
||||||
|
onClose={() => setAddonsModalOpen(false)}
|
||||||
|
plan={activePlan}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function OfferCard({ plan, contractLabel }) {
|
function OfferCard({ plan, contractLabel, onConfigureAddons }) {
|
||||||
const basePrice = plan.price_monthly;
|
const basePrice = plan.price_monthly;
|
||||||
const installPrice = plan.price_installation;
|
const installPrice = plan.price_installation;
|
||||||
|
|
||||||
const featureRows = (plan.features || []).filter(
|
const allFeatures = plan.features || [];
|
||||||
|
|
||||||
|
// 🔹 to są inne cechy (bez umowy i instalacji)
|
||||||
|
const featureRows = allFeatures.filter(
|
||||||
(f) => f.id !== "umowa_info" && f.id !== "instalacja"
|
(f) => f.id !== "umowa_info" && f.id !== "instalacja"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 🔹 cecha opisująca umowę (z backendu)
|
||||||
|
const contractFeature = allFeatures.find((f) => f.id === "umowa_info");
|
||||||
|
|
||||||
|
// 🔹 tekst, który faktycznie pokażemy w wierszu "Umowa"
|
||||||
|
const effectiveContract =
|
||||||
|
contractLabel || contractFeature?.value || contractFeature?.label || "—";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={`f-card ${plan.popular ? "f-card-popular" : ""}`}>
|
<div class={`f-card ${plan.popular ? "f-card-popular" : ""}`}>
|
||||||
{/* {plan.popular && <div class="f-card-badge">Najczęściej wybierany</div>} */}
|
|
||||||
|
|
||||||
<div class="f-card-header">
|
<div class="f-card-header">
|
||||||
<div class="f-card-name">{plan.name}</div>
|
<div class="f-card-name">{plan.name}</div>
|
||||||
<div class="f-card-price">{basePrice} zł/mies.</div>
|
<div class="f-card-price">{basePrice} zł/mies.</div>
|
||||||
@@ -123,7 +148,7 @@ function OfferCard({ plan, contractLabel }) {
|
|||||||
|
|
||||||
<li class="f-card-row">
|
<li class="f-card-row">
|
||||||
<span class="f-card-label">Umowa</span>
|
<span class="f-card-label">Umowa</span>
|
||||||
<span class="f-card-value">{contractLabel}</span>
|
<span class="f-card-value">{effectiveContract}</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="f-card-row">
|
<li class="f-card-row">
|
||||||
@@ -133,6 +158,14 @@ function OfferCard({ plan, contractLabel }) {
|
|||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary mt-4"
|
||||||
|
onClick={onConfigureAddons}
|
||||||
|
>
|
||||||
|
Skonfiguruj usługi dodatkowe
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
82
src/pages/api/internet/addons.js
Normal file
82
src/pages/api/internet/addons.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import Database from "better-sqlite3";
|
||||||
|
|
||||||
|
const DB_PATH = "./src/data/ServicesRange.db";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const db = new Database(DB_PATH, { readonly: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const addonsRows = db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT id, name, type, description
|
||||||
|
FROM internet_addons
|
||||||
|
ORDER BY id
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.all();
|
||||||
|
|
||||||
|
const optionsRows = db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT id, addon_id, code, name, price
|
||||||
|
FROM internet_addon_options
|
||||||
|
ORDER BY addon_id, id
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.all();
|
||||||
|
|
||||||
|
const byAddon = new Map();
|
||||||
|
|
||||||
|
for (const addon of addonsRows) {
|
||||||
|
byAddon.set(addon.id, {
|
||||||
|
id: addon.id,
|
||||||
|
name: addon.name,
|
||||||
|
type: addon.type, // 'checkbox' / 'select'
|
||||||
|
description: addon.description || "",
|
||||||
|
options: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const opt of optionsRows) {
|
||||||
|
const parent = byAddon.get(opt.addon_id);
|
||||||
|
if (!parent) continue;
|
||||||
|
parent.options.push({
|
||||||
|
id: opt.id,
|
||||||
|
code: opt.code,
|
||||||
|
name: opt.name,
|
||||||
|
price: opt.price,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = Array.from(byAddon.values());
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
count: data.length,
|
||||||
|
data,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Błąd w /api/internet/addons:", err);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
ok: false,
|
||||||
|
error: err.message || "DB_ERROR",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||||
import OffersSwitches from "../../islands/Offers/OffersSwitches.jsx";
|
import OffersSwitches from "../../islands/Offers/OffersSwitches.jsx";
|
||||||
import InternetDbOffersCards from "../../islands/OffersInternetCards.jsx";
|
import InternetDbOffersCards from "../../islands/Internet/OffersInternetCards.jsx";
|
||||||
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
||||||
|
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
|
|||||||
@@ -1,47 +1,189 @@
|
|||||||
/* MODAL — FULLSCREEN OVERLAY */
|
/* ===========================
|
||||||
|
MODAL — FULLSCREEN OVERLAY
|
||||||
|
=========================== */
|
||||||
|
|
||||||
.fuz-modal-overlay {
|
.fuz-modal-overlay {
|
||||||
@apply fixed inset-0 z-[9999] flex flex-col;
|
@apply fixed inset-0 z-[9999] flex flex-col;
|
||||||
background: rgba(0, 0, 0, 0.65);
|
background: rgba(0, 0, 0, 0.65);
|
||||||
backdrop-filter: blur(6px);
|
backdrop-filter: blur(6px);
|
||||||
animation: fadeIn 0.25s ease-out forwards;
|
animation: fadeIn 0.25s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CLOSE BUTTON */
|
|
||||||
.fuz-modal-close {
|
.fuz-modal-close {
|
||||||
@apply absolute top-4 right-6 text-3xl font-bold cursor-pointer transition-opacity text-[--f-text] opacity-[0.7];
|
@apply absolute top-4 right-6 text-3xl font-bold cursor-pointer transition-opacity;
|
||||||
|
@apply text-[--f-text] opacity-70;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fuz-modal-close:hover {
|
.fuz-modal-close:hover {
|
||||||
@apply opacity-100;
|
@apply opacity-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* panel – pełny ekran, ale treść centrowana max-w */
|
||||||
.fuz-modal-panel {
|
.fuz-modal-panel {
|
||||||
@apply w-full h-full overflow-y-auto px-6 py-8 md:px-12 md:py-12 bg-[--f-background] text-[--f-text];
|
@apply w-full h-full overflow-y-auto bg-[--f-background] text-[--f-text];
|
||||||
|
@apply px-6 py-8 md:px-12 md:py-12;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* wersja "kompaktowa" z mniejszym max-width (używana w dodatkach) */
|
||||||
|
.fuz-modal-panel.fuz-modal-panel--compact {
|
||||||
|
@apply flex justify-center items-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fuz-modal-inner {
|
.fuz-modal-inner {
|
||||||
@apply max-w-4xl mx-auto;
|
@apply w-full max-w-4xl mx-auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fuz-modal-title {
|
.fuz-modal-title {
|
||||||
@apply text-4xl font-bold mb-8 text-center text-[--f-text];
|
@apply text-3xl md:text-4xl font-bold mb-8 text-center text-[--f-text];
|
||||||
}
|
}
|
||||||
|
|
||||||
.fuz-modal-content p {
|
.fuz-modal-content p {
|
||||||
@apply leading-relaxed text-2xl text-center;
|
@apply leading-relaxed text-2xl text-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fuz-modal-content p img {
|
.fuz-modal-content p img {
|
||||||
@apply mt-2 leading-relaxed;
|
@apply mt-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 2;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
TELEFON — AKORDEON
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
.fuz-modal-phone-list.fuz-accordion {
|
||||||
|
@apply flex flex-col gap-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuz-accordion-item {
|
||||||
|
@apply rounded-xl border overflow-hidden bg-[--f-background];
|
||||||
|
border-color: rgba(148, 163, 184, 0.6); /* neutralna szarość — ok w obu motywach */
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuz-accordion-header {
|
||||||
|
@apply w-full flex items-center justify-between gap-4 px-4 py-3 cursor-pointer;
|
||||||
|
background: rgba(148, 163, 184, 0.06);
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuz-accordion-header-left {
|
||||||
|
@apply flex items-center gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuz-modal-phone-name {
|
||||||
|
@apply font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuz-modal-phone-price {
|
||||||
|
@apply font-semibold whitespace-nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuz-accordion-body {
|
||||||
|
@apply px-4 pt-2 pb-3;
|
||||||
|
border-top: 1px solid rgba(148, 163, 184, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* wyróżnienie otwartego pakietu – lekki „accent wash” na tle */
|
||||||
|
.fuz-accordion-item.is-open .fuz-accordion-header {
|
||||||
|
background: color-mix(in srgb, var(--fuz-accent, #2563eb) 8%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
DODATKI — KOLUMNOWA LISTA
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
.fuz-addon-list {
|
||||||
|
@apply flex flex-col gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuz-addon-item {
|
||||||
|
@apply grid items-center gap-3 px-3 py-2 rounded-xl border cursor-pointer;
|
||||||
|
grid-template-columns: auto 1fr auto; /* [checkbox] [opis] [cena] */
|
||||||
|
border-color: rgba(148, 163, 184, 0.5);
|
||||||
|
background: var(--f-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* kliknięcie w środek też zaznacza checkboxa */
|
||||||
|
.fuz-addon-item input[type="checkbox"] {
|
||||||
|
@apply cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuz-addon-checkbox {
|
||||||
|
@apply flex items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuz-addon-main {
|
||||||
|
@apply flex flex-col gap-0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuz-addon-name {
|
||||||
|
@apply font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuz-addon-desc {
|
||||||
|
@apply text-sm opacity-85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuz-addon-price {
|
||||||
|
@apply font-semibold whitespace-nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* lekkie podświetlenie przy hover */
|
||||||
|
.fuz-addon-item:hover {
|
||||||
|
border-color: color-mix(in srgb, var(--fuz-accent, #2563eb) 70%, rgba(148, 163, 184, 0.5) 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
PODSUMOWANIE MIESIĘCZNE
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
.fuz-summary {
|
||||||
|
@apply pt-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuz-summary-list {
|
||||||
|
@apply flex flex-col gap-1 mt-2 p-4 rounded-xl;
|
||||||
|
background: rgba(148, 163, 184, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuz-summary-row,
|
||||||
|
.fuz-summary-total {
|
||||||
|
@apply flex items-center justify-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuz-summary-row span:last-child {
|
||||||
|
@apply font-medium whitespace-nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuz-summary-total {
|
||||||
|
@apply mt-1 pt-2;
|
||||||
|
border-top: 1px solid rgba(148, 163, 184, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuz-summary-total span:last-child {
|
||||||
|
@apply font-bold;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: var(--fuz-accent, #2563eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuz-modal-section {
|
||||||
|
@apply mb-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fuz-modal-section h3 {
|
||||||
|
@apply text-xl md:text-2xl font-semibold mb-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* opcja "bez telefonu" — trochę lżejsze tło */
|
||||||
|
.fuz-accordion-item--no-phone .fuz-accordion-header {
|
||||||
|
background: rgba(148, 163, 184, 0.03);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user