Zmiany w modalach

This commit is contained in:
dm
2025-12-13 11:25:11 +01:00
parent 99b83b7773
commit 32d77ac5ad
7 changed files with 704 additions and 703 deletions

View File

@@ -124,14 +124,12 @@ export default function InternetAddonsModal({ isOpen, onClose, plan }) {
}); });
}; };
const togglePhoneOpen = (id) => {
setOpenPhoneId((prev) => (prev === id ? null : id));
};
return ( return (
<div class="fuz-modal-overlay" onClick={onClose}> <div class="f-modal-overlay" onClick={onClose}>
<button <button
class="fuz-modal-close" class="f-modal-close"
type="button"
aria-label="Zamknij"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onClose(); onClose();
@@ -141,33 +139,35 @@ export default function InternetAddonsModal({ isOpen, onClose, plan }) {
</button> </button>
<div <div
class="fuz-modal-panel fuz-modal-panel--compact" class="f-modal-panel f-modal-panel--compact"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<div class="fuz-modal-inner"> <div class="f-modal-inner">
<h2 class="fuz-modal-title">Konfiguracja usług dodatkowych</h2> <h2 class="f-modal-title">Konfiguracja usług dodatkowych</h2>
{/* INTERNET (fiber) jako akordeon */} {/* INTERNET (fiber) jako akordeon */}
<div class="fuz-modal-section"> <div class="f-modal-section">
<div class={`fuz-accordion-item ${baseOpen ? "is-open" : ""}`}> <div class={`f-accordion-item ${baseOpen ? "is-open" : ""}`}>
<button <button
type="button" type="button"
class="fuz-accordion-header" class="f-accordion-header"
onClick={() => setBaseOpen((prev) => !prev)} onClick={() => setBaseOpen((prev) => !prev)}
> >
<span class="fuz-modal-phone-name">{plan.name}</span> <span class="f-modal-phone-name">{plan.name}</span>
<span class="fuz-modal-phone-price"> <span class="f-modal-phone-price">
{basePrice.toFixed(2)} /mies. {basePrice.toFixed(2)} /mies.
</span> </span>
</button> </button>
{baseOpen && plan.features && plan.features.length > 0 && ( {baseOpen && plan.features && plan.features.length > 0 && (
<div class="fuz-accordion-body"> <div class="f-accordion-body">
<ul class="f-card-features"> <ul class="f-card-features">
{plan.features.map((f, idx) => ( {plan.features.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">{formatFeatureValue(f.value)}</span> <span class="f-card-value">
{formatFeatureValue(f.value)}
</span>
</li> </li>
))} ))}
</ul> </ul>
@@ -181,21 +181,22 @@ export default function InternetAddonsModal({ isOpen, onClose, plan }) {
{!loading && !error && ( {!loading && !error && (
<> <>
{/* Sekcja: wybór telefonu (akordeon + opcja bez telefonu) */} {/* Telefon */}
<div class="fuz-modal-section"> <div class="f-modal-section">
<h3>Usługa telefoniczna</h3> <h3>Usługa telefoniczna</h3>
{phonePlans.length === 0 ? ( {phonePlans.length === 0 ? (
<p>Brak dostępnych pakietów telefonicznych.</p> <p>Brak dostępnych pakietów telefonicznych.</p>
) : ( ) : (
<div class="fuz-modal-phone-list fuz-accordion"> <div class="f-modal-phone-list f-accordion">
{/* OPCJA: brak telefonu */} {/* brak telefonu */}
<div class="fuz-accordion-item fuz-accordion-item--no-phone"> <div class="f-accordion-item f-accordion-item--no-phone">
<button <button
type="button" type="button"
class="fuz-accordion-header" class="f-accordion-header"
onClick={() => handlePhoneSelect(null)} onClick={() => handlePhoneSelect(null)}
> >
<span class="fuz-accordion-header-left"> <span class="f-accordion-header-left">
<input <input
type="radio" type="radio"
name="phone-plan" name="phone-plan"
@@ -206,32 +207,30 @@ export default function InternetAddonsModal({ isOpen, onClose, plan }) {
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
/> />
<span class="fuz-modal-phone-name"> <span class="f-modal-phone-name">
Nie potrzebuję telefonu Nie potrzebuję telefonu
</span> </span>
</span> </span>
<span class="fuz-modal-phone-price">0,00 /mies.</span> <span class="f-modal-phone-price">0,00 /mies.</span>
</button> </button>
</div> </div>
{/* lista pakietów telefonu */}
{/* LISTA PAKIETÓW TELEFONICZNYCH */}
{phonePlans.map((p) => { {phonePlans.map((p) => {
const isSelected = selectedPhoneId === p.id; const isSelected = selectedPhoneId === p.id;
const isOpen = openPhoneId === p.id; const isOpen = openPhoneId === p.id;
return ( return (
<div <div
class={`fuz-accordion-item ${isOpen ? "is-open" : "" class={`f-accordion-item ${isOpen ? "is-open" : ""}`}
}`}
key={p.id} key={p.id}
> >
<button <button
type="button" type="button"
class="fuz-accordion-header" class="f-accordion-header"
onClick={() => handlePhoneSelect(p.id)} onClick={() => handlePhoneSelect(p.id)}
> >
<span class="fuz-accordion-header-left"> <span class="f-accordion-header-left">
<input <input
type="radio" type="radio"
name="phone-plan" name="phone-plan"
@@ -242,36 +241,29 @@ export default function InternetAddonsModal({ isOpen, onClose, plan }) {
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
/> />
<span class="fuz-modal-phone-name"> <span class="f-modal-phone-name">{p.name}</span>
{p.name}
</span>
</span> </span>
<span class="fuz-modal-phone-price">
<span class="f-modal-phone-price">
{p.price_monthly.toFixed(2)} /mies. {p.price_monthly.toFixed(2)} /mies.
</span> </span>
</button> </button>
{isOpen && ( {isOpen && (
<div class="fuz-accordion-body"> <div class="f-accordion-body">
{p.features && p.features.length > 0 && ( {p.features && p.features.length > 0 && (
<ul class="f-card-features"> <ul class="f-card-features">
{p.features {p.features
.filter( .filter(
(f) => (f) =>
!String( !String(f.label || "")
f.label || ""
)
.toLowerCase() .toLowerCase()
.includes("aktyw") .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"> <span class="f-card-label">{f.label}</span>
{f.label} <span class="f-card-value">{f.value}</span>
</span>
<span class="f-card-value">
{f.value}
</span>
</li> </li>
))} ))}
</ul> </ul>
@@ -285,47 +277,39 @@ export default function InternetAddonsModal({ isOpen, onClose, plan }) {
)} )}
</div> </div>
{/* Sekcja: dodatki internetowe */} {/* Dodatki internetowe */}
<div class="fuz-modal-section"> <div class="f-modal-section">
<h3>Dodatkowe usługi</h3> <h3>Dodatkowe usługi</h3>
{addons.length === 0 ? ( {addons.length === 0 ? (
<p>Brak dodatkowych usług.</p> <p>Brak dodatkowych usług.</p>
) : ( ) : (
<div class="fuz-addon-list"> <div class="f-addon-list">
{addons.map((addon) => {addons.map((addon) =>
addon.options.map((opt) => { addon.options.map((opt) => {
const checked = selectedAddons.some( const checked = selectedAddons.some(
(x) => (x) => x.addonId === addon.id && x.optionId === opt.id
x.addonId === addon.id &&
x.optionId === opt.id
); );
return ( return (
<label <label class="f-addon-item" key={`${addon.id}-${opt.id}`}>
class="fuz-addon-item" <div class="f-addon-checkbox">
key={`${addon.id}-${opt.id}`}
>
<div class="fuz-addon-checkbox">
<input <input
type="checkbox" type="checkbox"
checked={checked} checked={checked}
onChange={() => onChange={() => handleAddonToggle(addon.id, opt.id)}
handleAddonToggle(addon.id, opt.id)
}
/> />
</div> </div>
<div class="fuz-addon-main"> <div class="f-addon-main">
<div class="fuz-addon-name">{addon.name}</div> <div class="f-addon-name">{addon.name}</div>
{addon.description && ( {addon.description && (
<div class="fuz-addon-desc"> <div class="f-addon-desc">{addon.description}</div>
{addon.description}
</div>
)} )}
</div> </div>
<div class="fuz-addon-price"> <div class="f-addon-price">
{opt.price.toFixed(2)} /mies. {opt.price.toFixed(2)} {opt.currency}
</div> </div>
</label> </label>
); );
@@ -336,34 +320,30 @@ export default function InternetAddonsModal({ isOpen, onClose, plan }) {
</div> </div>
{/* Podsumowanie */} {/* Podsumowanie */}
<div class="fuz-modal-section fuz-summary"> <div class="f-modal-section f-summary">
<h3>Podsumowanie miesięczne</h3> <h3>Podsumowanie miesięczne</h3>
<div class="fuz-summary-list"> <div class="f-summary-list">
<div class="fuz-summary-row"> <div class="f-summary-row">
<span>Internet</span> <span>Internet</span>
<span>{basePrice.toFixed(2)} /mies.</span> <span>{basePrice.toFixed(2)} /mies.</span>
</div> </div>
<div class="fuz-summary-row"> <div class="f-summary-row">
<span>Telefon</span> <span>Telefon</span>
<span> <span>
{phonePrice {phonePrice ? `${phonePrice.toFixed(2)} zł/mies.` : "—"}
? `${phonePrice.toFixed(2)} zł/mies.`
: "—"}
</span> </span>
</div> </div>
<div class="fuz-summary-row"> <div class="f-summary-row">
<span>Dodatki</span> <span>Dodatki</span>
<span> <span>
{addonsPrice {addonsPrice ? `${addonsPrice.toFixed(2)} zł/mies.` : "—"}
? `${addonsPrice.toFixed(2)} zł/mies.`
: "—"}
</span> </span>
</div> </div>
<div class="fuz-summary-total"> <div class="f-summary-total">
<span>Łącznie</span> <span>Łącznie</span>
<span>{totalMonthly.toFixed(2)} /mies.</span> <span>{totalMonthly.toFixed(2)} /mies.</span>
</div> </div>

View File

@@ -3,408 +3,386 @@ import "../../styles/modal.css";
import "../../styles/offers/offers-table.css"; import "../../styles/offers/offers-table.css";
export default function JamboxAddonsModal({ isOpen, onClose, pkg }) { export default function JamboxAddonsModal({ isOpen, onClose, pkg }) {
const [phonePlans, setPhonePlans] = useState([]); const [phonePlans, setPhonePlans] = useState([]);
const [addons, setAddons] = useState([]); const [addons, setAddons] = useState([]);
const [tvAddons, setTvAddons] = useState([]); const [tvAddons, setTvAddons] = useState([]);
const [selectedTvAddonTids, setSelectedTvAddonTids] = useState([]); const [selectedTvAddonTids, setSelectedTvAddonTids] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [selectedPhoneId, setSelectedPhoneId] = useState(null); const [selectedPhoneId, setSelectedPhoneId] = useState(null);
const [selectedAddonIds, setSelectedAddonIds] = useState([]); const [selectedAddonIds, setSelectedAddonIds] = useState([]);
// akordeony // akordeony
const [openPhoneId, setOpenPhoneId] = useState(null); const [openPhoneId, setOpenPhoneId] = useState(null);
const [baseOpen, setBaseOpen] = useState(true); const [baseOpen, setBaseOpen] = useState(true);
const formatFeatureValue = (val) => { const formatFeatureValue = (val) => {
if (val === true || val === "true") return "✓"; if (val === true || val === "true") return "✓";
if (val === false || val === "false" || val == null) return "✕"; if (val === false || val === "false" || val == null) return "✕";
return val; return val;
}; };
const handlePhoneSelect = (id) => { const handlePhoneSelect = (id) => {
if (id === null) { if (id === null) {
setSelectedPhoneId(null); setSelectedPhoneId(null);
setOpenPhoneId(null); setOpenPhoneId(null);
return; return;
}
setSelectedPhoneId(id);
setOpenPhoneId((prev) => (prev === id ? null : id));
};
const toggleAddon = (addonId) => {
setSelectedAddonIds((prev) =>
prev.includes(addonId) ? prev.filter((x) => x !== addonId) : [...prev, addonId]
);
};
// reset po otwarciu / zmianie pakietu
useEffect(() => {
if (!isOpen) return;
setSelectedPhoneId(null);
setSelectedAddonIds([]);
setOpenPhoneId(null);
setBaseOpen(true);
setError("");
setSelectedTvAddonTids([]);
}, [isOpen, pkg?.id]);
// load danych
useEffect(() => {
if (!isOpen || !pkg?.id) 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 JAMBOX (dla pakietu)
const addonsRes = await fetch(`/api/jambox/addons?packageId=${pkg.id}`);
if (!addonsRes.ok) throw new Error(`HTTP ${addonsRes.status} (addons)`);
const addonsJson = await addonsRes.json();
const addonsData = Array.isArray(addonsJson.data) ? addonsJson.data : [];
// pakiety TV
const tvRes = await fetch(`/api/jambox/tv-addons?packageId=${pkg.id}`);
if (!tvRes.ok) throw new Error(`HTTP ${tvRes.status} (tv-addons)`);
const tvJson = await tvRes.json();
const tvData = Array.isArray(tvJson.data) ? tvJson.data : [];
if (!cancelled) {
setPhonePlans(phoneData);
setAddons(addonsData);
setTvAddons(tvData);
} }
setSelectedPhoneId(id); } catch (err) {
setOpenPhoneId((prev) => (prev === id ? null : id)); console.error("❌ Błąd ładowania danych do JamboxAddonsModal:", err);
if (!cancelled) setError("Nie udało się załadować danych dodatkowych usług.");
} finally {
if (!cancelled) setLoading(false);
}
}
loadData();
return () => {
cancelled = true;
}; };
}, [isOpen, pkg?.id]);
const toggleAddon = (addonId) => { if (!isOpen || !pkg) return null;
setSelectedAddonIds((prev) =>
prev.includes(addonId)
? prev.filter((x) => x !== addonId)
: [...prev, addonId]
);
};
// reset po otwarciu / zmianie pakietu const basePrice = Number(pkg.price_monthly || 0);
useEffect(() => {
if (!isOpen) return;
setSelectedPhoneId(null);
setSelectedAddonIds([]);
setOpenPhoneId(null);
setBaseOpen(true);
setError("");
setSelectedTvAddonTids([]);
}, [isOpen, pkg?.id]);
// load danych const phonePrice = useMemo(() => {
useEffect(() => { if (!selectedPhoneId) return 0;
if (!isOpen || !pkg?.id) return; const p = phonePlans.find((x) => x.id === selectedPhoneId);
return Number(p?.price_monthly || 0);
}, [selectedPhoneId, phonePlans]);
let cancelled = false; // backend może zwrócić { id, price } albo { addon_id, price }
const addonsPrice = useMemo(() => {
return selectedAddonIds.reduce((sum, addonId) => {
const a = addons.find((x) => (x.id ?? x.addon_id) === addonId);
return sum + Number(a?.price || 0);
}, 0);
}, [selectedAddonIds, addons]);
async function loadData() { const tvAddonsPrice = useMemo(() => {
setLoading(true); return selectedTvAddonTids.reduce((sum, tid) => {
setError(""); const a = tvAddons.find((x) => Number(x.tid) === Number(tid));
return sum + Number(a?.price || 0);
}, 0);
}, [selectedTvAddonTids, tvAddons]);
try { const totalMonthly = basePrice + phonePrice + addonsPrice + tvAddonsPrice;
// 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 JAMBOX (dla pakietu) const toggleTvAddon = (tid) => {
const addonsRes = await fetch(`/api/jambox/addons?packageId=${pkg.id}`); const t = Number(tid);
if (!addonsRes.ok) throw new Error(`HTTP ${addonsRes.status} (addons)`); setSelectedTvAddonTids((prev) =>
const addonsJson = await addonsRes.json(); prev.includes(t) ? prev.filter((x) => x !== t) : [...prev, t]
const addonsData = Array.isArray(addonsJson.data) ? addonsJson.data : []; );
};
const tvRes = await fetch(`/api/jambox/tv-addons?packageId=${pkg.id}`); return (
if (!tvRes.ok) throw new Error(`HTTP ${tvRes.status} (tv-addons)`); <div class="f-modal-overlay" onClick={onClose}>
<button
class="f-modal-close"
type="button"
aria-label="Zamknij"
onClick={(e) => {
e.stopPropagation();
onClose();
}}
>
</button>
const tvJson = await tvRes.json(); <div class="f-modal-panel f-modal-panel--compact" onClick={(e) => e.stopPropagation()}>
const tvData = Array.isArray(tvJson.data) ? tvJson.data : []; <div class="f-modal-inner">
<h2 class="f-modal-title">Konfiguracja usług dodatkowych</h2>
if (!cancelled) { {/* PAKIET JAMBOX jako akordeon */}
setPhonePlans(phoneData); <div class="f-modal-section">
setAddons(addonsData); <div class={`f-accordion-item ${baseOpen ? "is-open" : ""}`}>
setTvAddons(tvData); <button
} type="button"
} catch (err) { class="f-accordion-header"
console.error("❌ Błąd ładowania danych do JamboxAddonsModal:", err); onClick={() => setBaseOpen((prev) => !prev)}
if (!cancelled) setError("Nie udało się załadować danych dodatkowych usług."); >
} finally { <span class="f-modal-phone-name">{pkg.name}</span>
if (!cancelled) setLoading(false); <span class="f-modal-phone-price">
} {basePrice ? `${basePrice.toFixed(2)} zł/mies.` : "—"}
} </span>
</button>
loadData(); {baseOpen && pkg.features && pkg.features.length > 0 && (
return () => { <div class="f-accordion-body">
cancelled = true; <ul class="f-card-features">
}; {pkg.features.map((f, idx) => (
}, [isOpen, pkg?.id]); <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>
if (!isOpen || !pkg) return null; {loading && <p>Ładowanie danych...</p>}
{error && <p class="text-red-600">{error}</p>}
const basePrice = Number(pkg.price_monthly || 0); {!loading && !error && (
<>
{/* TV ADDONS */}
<div class="f-modal-section">
<h3>Pakiety dodatkowe TV</h3>
const phonePrice = useMemo(() => { {tvAddons.length === 0 ? (
if (!selectedPhoneId) return 0; <p>Brak pakietów dodatkowych TV dla tego pakietu.</p>
const p = phonePlans.find((x) => x.id === selectedPhoneId); ) : (
return Number(p?.price_monthly || 0); <div class="f-addon-list">
}, [selectedPhoneId, phonePlans]); {tvAddons.map((a) => {
const tid = Number(a.tid);
const checked = selectedTvAddonTids.includes(tid);
const priceNum = Number(a.price || 0);
// UWAGA: backend może zwrócić { id, price } albo { addon_id, price } return (
const addonsPrice = useMemo(() => { <label class="f-addon-item" key={`tv-${tid}`}>
return selectedAddonIds.reduce((sum, addonId) => { <div class="f-addon-checkbox">
const a = addons.find((x) => (x.id ?? x.addon_id) === addonId); <input
return sum + Number(a?.price || 0); type="checkbox"
}, 0); checked={checked}
}, [selectedAddonIds, addons]); onChange={() => toggleTvAddon(tid)}
/>
</div>
<div class="f-addon-main">
<div class="f-addon-name">{a.name}</div>
<div class="f-addon-desc">{a.description}</div>
</div>
<div class="f-addon-price">
{Number.isFinite(priceNum) ? `${priceNum.toFixed(2)} zł/mies.` : "—"}
</div>
</label>
);
})}
</div>
)}
</div>
const tvAddonsPrice = useMemo(() => { {/* TELEFON */}
return selectedTvAddonTids.reduce((sum, tid) => { <div class="f-modal-section">
const a = tvAddons.find((x) => Number(x.tid) === Number(tid)); <h3>Usługa telefoniczna</h3>
return sum + Number(a?.price || 0);
}, 0);
}, [selectedTvAddonTids, tvAddons]);
const totalMonthly = basePrice + phonePrice + addonsPrice + tvAddonsPrice; {phonePlans.length === 0 ? (
<p>Brak dostępnych pakietów telefonicznych.</p>
const toggleTvAddon = (tid) => { ) : (
const t = Number(tid); <div class="f-modal-phone-list f-accordion">
setSelectedTvAddonTids((prev) => {/* brak telefonu */}
prev.includes(t) ? prev.filter((x) => x !== t) : [...prev, t] <div class="f-accordion-item f-accordion-item--no-phone">
); <button
}; type="button"
class="f-accordion-header"
return ( onClick={() => handlePhoneSelect(null)}
<div class="fuz-modal-overlay" onClick={onClose}> >
<button <span class="f-accordion-header-left">
class="fuz-modal-close" <input
onClick={(e) => { type="radio"
e.stopPropagation(); name="phone-plan"
onClose(); checked={selectedPhoneId === null}
}} onChange={(e) => {
> e.stopPropagation();
handlePhoneSelect(null);
</button> }}
onClick={(e) => e.stopPropagation()}
<div />
class="fuz-modal-panel fuz-modal-panel--compact" <span class="f-modal-phone-name">Nie potrzebuję telefonu</span>
onClick={(e) => e.stopPropagation()} </span>
> <span class="f-modal-phone-price">0,00 /mies.</span>
<div class="fuz-modal-inner"> </button>
<h2 class="fuz-modal-title">Konfiguracja usług dodatkowych</h2>
{/* PAKIET JAMBOX jako akordeon (jak internet w Twoim modalu) */}
<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">{pkg.name}</span>
<span class="fuz-modal-phone-price">
{basePrice ? `${basePrice.toFixed(2)} zł/mies.` : "—"}
</span>
</button>
{baseOpen && pkg.features && pkg.features.length > 0 && (
<div class="fuz-accordion-body">
<ul class="f-card-features">
{pkg.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> </div>
{loading && <p>Ładowanie danych...</p>} {/* pakiety telefonu */}
{error && <p class="text-red-600">{error}</p>} {phonePlans.map((p) => {
const isSelected = selectedPhoneId === p.id;
const isOpen = openPhoneId === p.id;
{!loading && !error && ( return (
<> <div class={`f-accordion-item ${isOpen ? "is-open" : ""}`} key={p.id}>
<button
type="button"
class="f-accordion-header"
onClick={() => handlePhoneSelect(p.id)}
>
<span class="f-accordion-header-left">
<input
type="radio"
name="phone-plan"
checked={isSelected}
onChange={(e) => {
e.stopPropagation();
handlePhoneSelect(p.id);
}}
onClick={(e) => e.stopPropagation()}
/>
<span class="f-modal-phone-name">{p.name}</span>
</span>
<span class="f-modal-phone-price">
{Number(p.price_monthly || 0).toFixed(2)} /mies.
</span>
</button>
<div class="fuz-modal-section"> {isOpen && (
<h3>Pakiety dodatkowe TV</h3> <div class="f-accordion-body">
{p.features && p.features.length > 0 && (
{tvAddons.length === 0 ? ( <ul class="f-card-features">
<p>Brak pakietów dodatkowych TV dla tego pakietu.</p> {p.features
) : ( .filter(
<div class="fuz-addon-list"> (f) =>
{tvAddons.map((a) => { !String(f.label || "").toLowerCase().includes("aktyw")
const tid = Number(a.tid); )
const checked = selectedTvAddonTids.includes(tid); .map((f, idx) => (
const priceNum = Number(a.price || 0); <li class="f-card-row" key={idx}>
<span class="f-card-label">{f.label}</span>
return ( <span class="f-card-value">{f.value}</span>
<label class="fuz-addon-item" key={`tv-${tid}`}> </li>
<div class="fuz-addon-checkbox"> ))}
<input </ul>
type="checkbox" )}
checked={checked}
onChange={() => toggleTvAddon(tid)}
/>
</div>
<div class="fuz-addon-main">
<div class="fuz-addon-name">{a.name}</div>
{/* jeśli chcesz pokazać typ/kind */}
<div class="fuz-addon-desc">{a.description}</div>
</div>
<div class="fuz-addon-price">
{Number.isFinite(priceNum) ? `${priceNum.toFixed(2)} zł/mies.` : "—"}
</div>
</label>
);
})}
</div>
)}
</div> </div>
)}
</div>
);
})}
</div>
)}
</div>
{/* TELEFON (identycznie jak w InternetAddonsModal) */} {/* DODATKI JAMBOX */}
<div class="fuz-modal-section"> <div class="f-modal-section">
<h3>Usługa telefoniczna</h3> <h3>Dodatkowe usługi</h3>
{phonePlans.length === 0 ? ( {addons.length === 0 ? (
<p>Brak dostępnych pakietów telefonicznych.</p> <p>Brak usług dodatkowych dla tego pakietu.</p>
) : ( ) : (
<div class="fuz-modal-phone-list fuz-accordion"> <div class="f-addon-list">
{/* OPCJA: brak telefonu */} {addons.map((a) => {
<div class="fuz-accordion-item fuz-accordion-item--no-phone"> const addonId = a.id ?? a.addon_id;
<button const checked = selectedAddonIds.includes(addonId);
type="button" const priceNum = Number(a.price || 0);
class="fuz-accordion-header"
onClick={() => handlePhoneSelect(null)}
>
<span class="fuz-accordion-header-left">
<input
type="radio"
name="phone-plan"
checked={selectedPhoneId === null}
onChange={(e) => {
e.stopPropagation();
handlePhoneSelect(null);
}}
onClick={(e) => e.stopPropagation()}
/>
<span class="fuz-modal-phone-name">
Nie potrzebuję telefonu
</span>
</span>
<span class="fuz-modal-phone-price">0,00 /mies.</span>
</button>
</div>
{/* PAKIETY TELEFONU */} return (
{phonePlans.map((p) => { <label class="f-addon-item" key={addonId}>
const isSelected = selectedPhoneId === p.id; <div class="f-addon-checkbox">
const isOpen = openPhoneId === p.id; <input
type="checkbox"
checked={checked}
onChange={() => toggleAddon(addonId)}
/>
</div>
return ( <div class="f-addon-main">
<div <div class="f-addon-name">{a.name}</div>
class={`fuz-accordion-item ${isOpen ? "is-open" : ""}`} {a.description && <div class="f-addon-desc">{a.description}</div>}
key={p.id} </div>
>
<button
type="button"
class="fuz-accordion-header"
onClick={() => handlePhoneSelect(p.id)}
>
<span class="fuz-accordion-header-left">
<input
type="radio"
name="phone-plan"
checked={isSelected}
onChange={(e) => {
e.stopPropagation();
handlePhoneSelect(p.id);
}}
onClick={(e) => e.stopPropagation()}
/>
<span class="fuz-modal-phone-name">{p.name}</span>
</span>
<span class="fuz-modal-phone-price">
{Number(p.price_monthly || 0).toFixed(2)} /mies.
</span>
</button>
{isOpen && ( <div class="f-addon-price">{priceNum.toFixed(2)} /mies.</div>
<div class="fuz-accordion-body"> </label>
{p.features && p.features.length > 0 && ( );
<ul class="f-card-features"> })}
{p.features </div>
.filter( )}
(f) => </div>
!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>
{/* DODATKI JAMBOX (checkbox, cena z jambox_package_addon_options.price) */} {/* PODSUMOWANIE */}
<div class="fuz-modal-section"> <div class="f-modal-section f-summary">
<h3>Dodatkowe usługi</h3> <h3>Podsumowanie miesięczne</h3>
{addons.length === 0 ? ( <div class="f-summary-list">
<p>Brak usług dodatkowych dla tego pakietu.</p> <div class="f-summary-row">
) : ( <span>Pakiet</span>
<div class="fuz-addon-list"> <span>{basePrice ? `${basePrice.toFixed(2)} zł/mies.` : "—"}</span>
{addons.map((a) => { </div>
const addonId = a.id ?? a.addon_id; // defensywnie
const checked = selectedAddonIds.includes(addonId);
return ( <div class="f-summary-row">
<label class="fuz-addon-item" key={addonId}> <span>Pakiety TV</span>
<div class="fuz-addon-checkbox"> <span>{tvAddonsPrice ? `${tvAddonsPrice.toFixed(2)} zł/mies.` : "—"}</span>
<input </div>
type="checkbox"
checked={checked}
onChange={() => toggleAddon(addonId)}
/>
</div>
<div class="fuz-addon-main"> <div class="f-summary-row">
<div class="fuz-addon-name">{a.name}</div> <span>Telefon</span>
{a.description && ( <span>{phonePrice ? `${phonePrice.toFixed(2)} zł/mies.` : "—"}</span>
<div class="fuz-addon-desc">{a.description}</div> </div>
)}
</div>
<div class="fuz-addon-price"> <div class="f-summary-row">
{Number(a.price || 0).toFixed(2)} /mies. <span>Dodatki</span>
</div> <span>{addonsPrice ? `${addonsPrice.toFixed(2)} zł/mies.` : "—"}</span>
</label> </div>
);
})}
</div>
)}
</div>
{/* PODSUMOWANIE */} <div class="f-summary-total">
<div class="fuz-modal-section fuz-summary"> <span>Łącznie</span>
<h3>Podsumowanie miesięczne</h3> <span>{totalMonthly.toFixed(2)} /mies.</span>
</div>
<div class="fuz-summary-list">
<div class="fuz-summary-row">
<span>Pakiet</span>
<span>
{basePrice ? `${basePrice.toFixed(2)} zł/mies.` : "—"}
</span>
</div>
<div class="fuz-summary-row">
<span>Pakiety TV</span>
<span>
{tvAddonsPrice ? `${tvAddonsPrice.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)} /mies.</span>
</div>
</div>
</div>
</>
)}
</div> </div>
</div> </div>
</>
)}
</div> </div>
); </div>
</div>
);
} }

View File

@@ -1,146 +1,167 @@
// src/islands/JamboxChannelsModal.jsx import { useEffect, useMemo, useState } from "preact/hooks";
import { useEffect, useState } from "preact/hooks";
import "../../styles/modal.css"; import "../../styles/modal.css";
import "../../styles/offers/offers-table.css"; import "../../styles/offers/offers-table.css";
import "../../styles/channels-search.css"; // żeby input miał identyczny styl
export default function JamboxChannelsModal({ isOpen, onClose, pkg }) { export default function JamboxChannelsModal({ isOpen, onClose, pkg }) {
const [channels, setChannels] = useState([]); const [channels, setChannels] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [query, setQuery] = useState("");
const [query, setQuery] = useState(""); const q = query.trim().toLowerCase();
const q = query.trim().toLowerCase(); const filtered = !q
? channels
: channels.filter((ch) => (ch.name || "").toLowerCase().includes(q));
const filtered = !q const meta = useMemo(() => {
? channels if (loading) return "Ładowanie…";
: channels.filter((ch) => if (error) return error;
(ch.name || "").toLowerCase().includes(q) if (!query.trim()) return `Wyniki: ${filtered.length} / ${channels.length}`;
); return `Wyniki: ${filtered.length} / ${channels.length}`;
}, [loading, error, query, filtered.length, channels.length]);
useEffect(() => {
if (!isOpen || !pkg?.id) return;
useEffect(() => { let cancelled = false;
if (!isOpen || !pkg?.id) return;
let cancelled = false; async function loadChannels() {
setLoading(true);
setError("");
setChannels([]);
setQuery("");
async function loadChannels() { try {
setLoading(true); const params = new URLSearchParams({ packageId: String(pkg.id) });
setError(""); const res = await fetch(`/api/jambox/channels?${params.toString()}`);
setChannels([]);
setQuery("");
try { if (!res.ok) throw new Error(`HTTP ${res.status}`);
const params = new URLSearchParams({ packageId: String(pkg.id) });
const res = await fetch(`/api/jambox/channels?${params.toString()}`);
if (!res.ok) { const json = await res.json();
throw new Error(`HTTP ${res.status}`); if (!cancelled) setChannels(Array.isArray(json.data) ? json.data : []);
} } catch (err) {
console.error("❌ Błąd pobierania listy kanałów:", err);
if (!cancelled) setError("Nie udało się załadować listy kanałów.");
} finally {
if (!cancelled) setLoading(false);
}
}
const json = await res.json(); loadChannels();
if (!cancelled) { return () => {
setChannels(Array.isArray(json.data) ? json.data : []); cancelled = true;
} };
} catch (err) { }, [isOpen, pkg?.id]);
console.error("❌ Błąd pobierania listy kanałów:", err);
if (!cancelled) {
setError("Nie udało się załadować listy kanałów.");
}
} finally {
if (!cancelled) setLoading(false);
}
}
loadChannels(); if (!isOpen || !pkg) return null;
return () => {
cancelled = true;
};
}, [isOpen, pkg?.id]);
if (!isOpen || !pkg) return null; return (
<div class="f-modal-overlay" onClick={onClose}>
<button
class="f-modal-close"
onClick={(e) => {
e.stopPropagation();
onClose();
}}
aria-label="Zamknij"
type="button"
>
</button>
return ( <div
<div class="fuz-modal-overlay" onClick={onClose}> class="f-modal-panel f-modal-panel--channels"
<button onClick={(e) => e.stopPropagation()}
class="fuz-modal-close" role="dialog"
onClick={(e) => { aria-modal="true"
e.stopPropagation(); aria-label={`Kanały w pakiecie ${pkg.name}`}
onClose(); >
}} <div class="max-w-8xl mx-auto px-3 md:px-6">
> <h2 class="f-modal-title">Kanały w pakiecie {pkg.name}</h2>
</button>
<div {/* INPUT jak w wyszukiwarce (z własnym X) */}
class="fuz-modal-panel fuz-modal-panel--channels" <div class="f-chsearch__top">
onClick={(e) => e.stopPropagation()} <div class="f-chsearch__inputwrap">
> <input
<div class="max-w-8xl mx-auto px-3 md:px-6"> class="f-chsearch__input"
<h2 class="fuz-modal-title">Kanały w pakiecie {pkg.name}</h2> type="search"
value={query}
onInput={(e) => setQuery(e.currentTarget.value)}
placeholder="Szukaj kanału po nazwie…"
aria-label="Szukaj kanału po nazwie"
/>
<div class="fuz-chsearch__top"> {query && (
<input <button
class="fuz-chsearch__input" type="button"
type="search" class="f-chsearch__clear"
value={query} aria-label="Wyczyść wyszukiwanie"
onInput={(e) => setQuery(e.currentTarget.value)} onClick={() => setQuery("")}
placeholder="Szukaj kanału po nazwie…" onMouseDown={(e) => e.preventDefault()} // nie zabieraj focusa inputowi
aria-label="Szukaj kanału po nazwie" >
/>
</div> </button>
)}
{!loading && !error && (
<div class="jmb-search-meta">
Wyniki: <strong>{filtered.length}</strong> / {channels.length}
</div>
)}
{loading && <p>Ładowanie kanałów...</p>}
{error && <p class="text-red-600">{error}</p>}
{!loading && !error && (
<>
{filtered.length === 0 ? (
<p>Brak kanałów spełniających kryteria.</p>
) : (
<div class="">
<div class="f-section-channel">
{filtered.map((ch) => (
<div
class="jmb-channel-card"
key={ch.number}
onClick={(e) => {
if (e.target.closest("a, button")) return;
e.currentTarget.classList.toggle("is-flipped");
}}
>
<div class="jmb-channel-inner">
<div class="jmb-channel-face jmb-channel-front">
{ch.logo_url && (
<img src={ch.logo_url} alt={ch.name} class="jmb-channel-logo" loading="lazy" />
)}
<div class="jmb-channel-name">{ch.name}</div>
<div class="jmb-channel-number">kanał {ch.number}</div>
</div>
<div class="jmb-channel-face jmb-channel-back">
<div class="jmb-channel-back-title">{ch.name}</div>
<div
class="jmb-channel-desc"
dangerouslySetInnerHTML={{ __html: ch.description || "<em>Brak opisu kanału.</em>" }}
/>
</div>
</div>
</div>
))}
</div>
</div>
)}
</>
)}
</div>
</div> </div>
<div class="f-chsearch__meta">{meta}</div>
</div>
{loading && <p>Ładowanie kanałów...</p>}
{error && !loading && <p class="text-red-600">{error}</p>}
{!loading && !error && (
<>
{filtered.length === 0 ? (
<p>Brak kanałów spełniających kryteria.</p>
) : (
<div class="f-section-channel">
{filtered.map((ch) => (
<div
class="jmb-channel-card"
key={ch.number}
onClick={(e) => {
// żeby klik w link w opisie nie flipował
if (e.target.closest("a, button")) return;
e.currentTarget.classList.toggle("is-flipped");
}}
>
<div class="jmb-channel-inner">
{/* FRONT */}
<div class="jmb-channel-face jmb-channel-front">
{ch.logo_url && (
<img
src={ch.logo_url}
alt={ch.name}
class="jmb-channel-logo"
loading="lazy"
/>
)}
<div class="jmb-channel-name">{ch.name}</div>
<div class="jmb-channel-number">kanał {ch.number}</div>
</div>
{/* BACK */}
<div class="jmb-channel-face jmb-channel-back">
<div class="jmb-channel-back-title">{ch.name}</div>
<div
class="jmb-channel-desc"
dangerouslySetInnerHTML={{
__html: ch.description || "<em>Brak opisu kanału.</em>",
}}
/>
</div>
</div>
</div>
))}
</div>
)}
</>
)}
</div> </div>
); </div>
</div>
);
} }

View File

@@ -31,10 +31,13 @@ export default function JamboxChannelsSearch() {
params.set("q", qq); params.set("q", qq);
params.set("limit", "80"); params.set("limit", "80");
const res = await fetch(`/api/jambox/channels-search?${params.toString()}`, { const res = await fetch(
signal: ac.signal, `/api/jambox/channels-search?${params.toString()}`,
headers: { Accept: "application/json" }, {
}); signal: ac.signal,
headers: { Accept: "application/json" },
}
);
const json = await res.json(); const json = await res.json();
if (!res.ok || !json.ok) throw new Error(json?.error || "API_ERROR"); if (!res.ok || !json.ok) throw new Error(json?.error || "API_ERROR");
@@ -68,76 +71,81 @@ export default function JamboxChannelsSearch() {
el.scrollIntoView({ behavior: "smooth", block: "start" }); el.scrollIntoView({ behavior: "smooth", block: "start" });
el.classList.add("is-target"); el.classList.add("is-target");
setTimeout(() => el.classList.remove("is-target"), 1200); window.setTimeout(() => el.classList.remove("is-target"), 1200);
} }
return ( return (
<div class="fuz-chsearch"> <div class="f-chsearch">
<h1 class="f-section-title">Wyszukiwanie kanałów w pakietach telewizji</h1> <h1 class="f-section-title">Wyszukiwanie kanałów w pakietach telewizji</h1>
<div class="fuz-chsearch__top">
<input
class="fuz-chsearch__input"
type="search"
value={q}
onInput={(e) => setQ(e.currentTarget.value)}
placeholder="Szukaj kanału po nazwie…"
aria-label="Szukaj kanału po nazwie"
/>
<div class="fuz-chsearch__meta"> <div class="f-chsearch__top">
{meta} <div class="f-chsearch__inputwrap">
<input
class="f-chsearch__input"
type="search"
value={q}
onInput={(e) => setQ(e.currentTarget.value)}
placeholder="Szukaj kanału po nazwie…"
aria-label="Szukaj kanału po nazwie"
/>
{q && (
<button
type="button"
class="f-chsearch__clear"
aria-label="Wyczyść wyszukiwanie"
onClick={() => setQ("")}
>
</button>
)}
</div> </div>
<div class="f-chsearch__meta">{meta}</div>
</div> </div>
<div class="fuz-chsearch__list" role="list"> <div class="f-chsearch__list" role="list">
{items.map((c) => ( {items.map((c) => (
<div class="fuz-chsearch__row" role="listitem" key={`${c.name}-${c.logo_url || ""}`}> <div
class="f-chsearch__row"
role="listitem"
key={`${c.name}-${c.logo_url || ""}`}
>
{/* kolumna 1 */} {/* kolumna 1 */}
<div class="fuz-chsearch__left"> <div class="f-chsearch__left">
{c.logo_url && ( {c.logo_url && (
<img <img
src={c.logo_url} src={c.logo_url}
alt={c.name} alt={c.name}
class="fuz-chsearch__logo" class="f-chsearch__logo"
loading="lazy" loading="lazy"
/> />
)} )}
<div class="fuz-chsearch__channel-name"> <div class="f-chsearch__channel-name">{c.name}</div>
{c.name}
</div>
{/* <div class="fuz-chsearch__channel-number">
kanał {c.min_number || "—"}
</div> */}
</div> </div>
{/* kolumna 2 */} {/* kolumna 2 */}
<div class="fuz-chsearch__right"> <div class="f-chsearch__right">
<div <div
class="fuz-chsearch__desc fuz-chsearch__desc--html" class="f-chsearch__desc f-chsearch__desc--html"
dangerouslySetInnerHTML={{ __html: c.description || "<em>—</em>" }} dangerouslySetInnerHTML={{
__html: c.description || "<em>—</em>",
}}
/> />
{Array.isArray(c.packages) && c.packages.length > 0 && ( {Array.isArray(c.packages) && c.packages.length > 0 && (
<div class="fuz-chsearch__packages"> <div class="f-chsearch__packages">
Dostępny w:&nbsp; Dostępny w:&nbsp;
{c.packages.map((p, i) => ( {c.packages.map((p, i) => (
// <span class="fuz-chsearch__pkg" key={p.id}>
// {p.name}{" "}
// <span class="fuz-chsearch__pkgnum">(kanał {p.number})</span>
// {i < c.packages.length - 1 ? ", " : ""}
// </span>
<button <button
type="button" type="button"
class="fuz-chsearch__pkg" class="f-chsearch__pkg"
key={p.id} key={p.id}
onClick={() => scrollToPackage(p.id)} onClick={() => scrollToPackage(p.id)}
> >
{p.name}{" "} {p.name}{" "}
<span class="fuz-chsearch__pkgnum">(kanał {p.number})</span> <span class="f-chsearch__pkgnum">(kanał {p.number})</span>
{i < c.packages.length - 1 ? ", " : ""} {i < c.packages.length - 1 ? ", " : ""}
</button> </button>
))} ))}
@@ -148,7 +156,7 @@ export default function JamboxChannelsSearch() {
))} ))}
{q.trim().length >= 2 && !loading && items.length === 0 && ( {q.trim().length >= 2 && !loading && items.length === 0 && (
<div class="fuz-chsearch__empty"> <div class="f-chsearch__empty">
Brak wyników dla: <strong>{q}</strong> Brak wyników dla: <strong>{q}</strong>
</div> </div>
)} )}

View File

@@ -1,94 +1,127 @@
.fuz-chsearch { .f-chsearch {
@apply mt-6; @apply mt-6;
} }
.fuz-chsearch__top { .f-chsearch__top {
@apply flex flex-col gap-1 mb-4; @apply flex flex-col gap-1 mb-4;
} }
.fuz-chsearch__input { /* wrapper dla X w środku inputa */
@apply w-full md:flex-1 px-4 py-3 rounded-xl border border-[--f-input-border] bg-[--f-background] text-[--f-text] outline-none focus:ring-2 focus:ring-[--btn-background]; .f-chsearch__inputwrap {
@apply relative;
} }
.fuz-chsearch__meta { .f-chsearch__input {
@apply w-full md:flex-1 px-4 py-3 rounded-xl border border-[--f-input-border] bg-[--f-background] text-[--f-text] outline-none focus:ring-2 focus:ring-[--btn-background];
padding-right: 2.75rem; /* miejsce na X */
}
.f-chsearch__clear {
@apply absolute right-2 top-1/2 -translate-y-1/2 grid place-items-center rounded-full border-0 bg-transparent text-[--f-text] opacity-70;
width: 2rem;
height: 2rem;
cursor: pointer;
}
.f-chsearch__clear:hover {
@apply opacity-100;
}
.f-chsearch__meta {
@apply text-sm opacity-70 pl-1; @apply text-sm opacity-70 pl-1;
} }
/* ========================= /* =========================
List + Row layout List + Row layout
========================== */ ========================== */
.fuz-chsearch__list { .f-chsearch__list {
@apply flex flex-col gap-2; @apply flex flex-col gap-2;
} }
/* ✅ węższa pierwsza kolumna */ .f-chsearch__row {
.fuz-chsearch__row {
@apply grid grid-cols-1 md:grid-cols-[220px_1fr] gap-6 rounded-2xl border border-[--f-input-border] bg-[--f-background] p-4; @apply grid grid-cols-1 md:grid-cols-[220px_1fr] gap-6 rounded-2xl border border-[--f-input-border] bg-[--f-background] p-4;
} }
/* ========================= /* =========================
Column 1: Channel card Column 1: Channel card
========================== */ ========================== */
.fuz-chsearch__left { .f-chsearch__left {
@apply flex flex-col items-center text-center gap-1 px-2; @apply flex flex-col items-center text-center gap-1 px-2;
} }
.fuz-chsearch__logo { .f-chsearch__logo {
@apply w-14 h-14 object-contain bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl p-1 mb-1; @apply w-14 h-14 object-contain bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl p-1 mb-1;
} }
.fuz-chsearch__channel-name { .f-chsearch__channel-name {
@apply font-semibold text-[--fuz-header] leading-tight; @apply font-semibold text-[--fuz-header] leading-tight;
} }
.fuz-chsearch__channel-number {
@apply text-sm opacity-70;
}
/* ========================= /* =========================
Column 2: Description + Packages Column 2: Description + Packages
========================== */ ========================== */
.fuz-chsearch__right { .f-chsearch__right {
@apply flex flex-col gap-2; @apply flex flex-col gap-2;
} }
/* opis NIE ucinany */ .f-chsearch__desc {
.fuz-chsearch__desc {
@apply text-sm md:text-base opacity-90; @apply text-sm md:text-base opacity-90;
overflow-wrap: anywhere; overflow-wrap: anywhere;
word-break: break-word; word-break: break-word;
} }
/* HTML jak w modalu */ .f-chsearch__desc--html :global(p) {
.fuz-chsearch__desc--html :global(p) {
@apply mb-2; @apply mb-2;
} }
.fuz-chsearch__desc--html :global(ol), .f-chsearch__desc--html :global(ol),
.fuz-chsearch__desc--html :global(ul) { .f-chsearch__desc--html :global(ul) {
@apply pl-6 mb-2; @apply pl-6 mb-2;
} }
.fuz-chsearch__desc--html :global(li) { .f-chsearch__desc--html :global(li) {
@apply mb-1; @apply mb-1;
} }
/* pakiety (bez gwarantowanych) */ .f-chsearch__packages {
.fuz-chsearch__packages {
@apply text-sm opacity-80; @apply text-sm opacity-80;
} }
.fuz-chsearch__pkg { /* klikalny pakiet jak link */
@apply inline text-[--btn-background]; .f-chsearch__pkg {
@apply inline;
background: transparent;
border: 0;
padding: 0;
cursor: pointer;
font: inherit;
color: var(--f-link, var(--btn-background));
text-decoration: underline;
} }
.fuz-chsearch__pkgnum { .f-chsearch__pkg:hover {
text-decoration-thickness: 2px;
}
.f-chsearch__pkgnum {
@apply opacity-70; @apply opacity-70;
} }
/* ========================= /* =========================
Empty state Empty state
========================== */ ========================== */
.fuz-chsearch__empty { .f-chsearch__empty {
@apply mt-2 p-4 rounded-2xl border border-slate-200 dark:border-slate-700 opacity-80; @apply mt-2 p-4 rounded-2xl border border-slate-200 dark:border-slate-700 opacity-80;
} }
.f-chsearch__input::-webkit-search-cancel-button {
-webkit-appearance: none;
}
.f-chsearch__input::-ms-clear {
display: none;
}
.f-chsearch__input[type="search"] {
appearance: none;
}

View File

@@ -2,98 +2,93 @@
MODAL — FULLSCREEN OVERLAY MODAL — FULLSCREEN OVERLAY
=========================== */ =========================== */
.fuz-modal-overlay { .f-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;
} }
.fuz-modal-close { .f-modal-close {
@apply absolute top-4 right-6 text-3xl font-bold cursor-pointer transition-opacity; @apply absolute top-4 right-6 text-3xl font-bold cursor-pointer transition-opacity;
@apply text-[--f-text] opacity-70; @apply text-[--f-text] opacity-70;
} }
.fuz-modal-close:hover { .f-modal-close:hover {
@apply opacity-100; @apply opacity-100;
} }
/* panel pełny ekran, ale treść centrowana max-w */ /* panel pełny ekran, ale treść centrowana max-w */
.fuz-modal-panel { .f-modal-panel {
@apply w-full h-full overflow-y-auto 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; @apply px-6 py-8 md:px-12 md:py-12;
} }
/* wersja "kompaktowa" z mniejszym max-width (używana w dodatkach) */ /* wersja "kompaktowa" z mniejszym max-width (używana w dodatkach) */
.fuz-modal-panel.fuz-modal-panel--compact { .f-modal-panel.f-modal-panel--compact {
@apply flex justify-center items-start; @apply flex justify-center items-start;
} }
.fuz-modal-inner { .f-modal-inner {
@apply w-full max-w-4xl mx-auto; @apply w-full max-w-4xl mx-auto;
} }
.fuz-modal-title { .f-modal-title {
@apply text-3xl md: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 { .f-modal-content p {
@apply leading-relaxed text-2xl text-center; @apply leading-relaxed text-2xl text-center;
} }
.fuz-modal-content p img { .f-modal-content p img {
@apply mt-2; @apply mt-2;
} }
@keyframes fadeIn { @keyframes fadeIn {
from { from { opacity: 0; }
opacity: 0; to { opacity: 1; }
}
to {
opacity: 1;
}
} }
/* =========================== /* ===========================
TELEFON — AKORDEON TELEFON — AKORDEON
=========================== */ =========================== */
.fuz-modal-phone-list.fuz-accordion { .f-modal-phone-list.f-accordion {
@apply flex flex-col gap-2; @apply flex flex-col gap-2;
} }
.fuz-accordion-item { .f-accordion-item {
@apply rounded-xl border overflow-hidden bg-[--f-background]; @apply rounded-xl border overflow-hidden bg-[--f-background];
border-color: rgba(148, 163, 184, 0.6); /* neutralna szarość — ok w obu motywach */ border-color: rgba(148, 163, 184, 0.6);
} }
.fuz-accordion-header { .f-accordion-header {
@apply w-full flex items-center justify-between gap-4 px-4 py-2 cursor-pointer; @apply w-full flex items-center justify-between gap-4 px-4 py-2 cursor-pointer;
background: rgba(148, 163, 184, 0.06); background: rgba(148, 163, 184, 0.06);
border: none; border: none;
outline: none; outline: none;
} }
.fuz-accordion-header-left { .f-accordion-header-left {
@apply flex items-center gap-1; @apply flex items-center gap-1;
} }
.fuz-modal-phone-name { .f-modal-phone-name {
@apply font-medium ml-2; @apply font-medium ml-2;
} }
.fuz-modal-phone-price { .f-modal-phone-price {
@apply font-semibold whitespace-nowrap; @apply font-semibold whitespace-nowrap;
} }
.fuz-accordion-body { .f-accordion-body {
@apply px-4 pt-2 pb-3; @apply px-4 pt-2 pb-3;
border-top: 1px solid rgba(148, 163, 184, 0.4); border-top: 1px solid rgba(148, 163, 184, 0.4);
} }
/* wyróżnienie otwartego pakietu lekki „accent wash” na tle */ /* wyróżnienie otwartego pakietu */
.fuz-accordion-item.is-open .fuz-accordion-header { .f-accordion-item.is-open .f-accordion-header {
background: color-mix(in srgb, var(--fuz-accent, #2563eb) 8%, transparent); background: color-mix(in srgb, var(--fuz-accent, #2563eb) 8%, transparent);
} }
@@ -101,94 +96,96 @@
DODATKI — KOLUMNOWA LISTA DODATKI — KOLUMNOWA LISTA
=========================== */ =========================== */
.fuz-addon-list { .f-addon-list {
@apply flex flex-col gap-2; @apply flex flex-col gap-2;
} }
.fuz-addon-item { .f-addon-item {
@apply grid items-center gap-3 px-3 py-2 rounded-xl border cursor-pointer; @apply grid items-center gap-3 px-3 py-2 rounded-xl border cursor-pointer;
grid-template-columns: auto 1fr auto; /* [checkbox] [opis] [cena] */ 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);
} }
/* kliknięcie w środek też zaznacza checkboxa */ .f-addon-item input[type="checkbox"] {
.fuz-addon-item input[type="checkbox"] {
@apply cursor-pointer; @apply cursor-pointer;
} }
.fuz-addon-checkbox { .f-addon-checkbox {
@apply flex items-center justify-center; @apply flex items-center justify-center;
} }
.fuz-addon-main { .f-addon-main {
@apply flex flex-col gap-0.5; @apply flex flex-col gap-0.5;
} }
.fuz-addon-name { .f-addon-name {
@apply font-medium; @apply font-medium;
} }
.fuz-addon-desc { .f-addon-desc {
@apply text-sm opacity-85; @apply text-sm opacity-85;
} }
.fuz-addon-price { .f-addon-price {
@apply font-semibold whitespace-nowrap; @apply font-semibold whitespace-nowrap;
} }
/* lekkie podświetlenie przy hover */ .f-addon-item:hover {
.fuz-addon-item:hover { border-color: color-mix(
border-color: color-mix(in srgb, var(--fuz-accent, #2563eb) 70%, rgba(148, 163, 184, 0.5) 30%); in srgb,
var(--fuz-accent, #2563eb) 70%,
rgba(148, 163, 184, 0.5) 30%
);
} }
/* =========================== /* ===========================
PODSUMOWANIE MIESIĘCZNE PODSUMOWANIE MIESIĘCZNE
=========================== */ =========================== */
.fuz-summary { .f-summary {
@apply pt-2; @apply pt-2;
} }
.fuz-summary-list { .f-summary-list {
@apply flex flex-col gap-1 mt-2 p-4 rounded-xl; @apply flex flex-col gap-1 mt-2 p-4 rounded-xl;
background: rgba(148, 163, 184, 0.07); background: rgba(148, 163, 184, 0.07);
} }
.fuz-summary-row, .f-summary-row,
.fuz-summary-total { .f-summary-total {
@apply flex items-center justify-between; @apply flex items-center justify-between;
} }
.fuz-summary-row span:last-child { .f-summary-row span:last-child {
@apply font-medium whitespace-nowrap; @apply font-medium whitespace-nowrap;
} }
.fuz-summary-total { .f-summary-total {
@apply mt-1 pt-2; @apply mt-1 pt-2;
border-top: 1px solid rgba(148, 163, 184, 0.4); border-top: 1px solid rgba(148, 163, 184, 0.4);
} }
.fuz-summary-total span:last-child { .f-summary-total span:last-child {
@apply font-bold; @apply font-bold;
font-size: 1.25rem; font-size: 1.25rem;
color: var(--fuz-accent, #2563eb); color: var(--fuz-accent, #2563eb);
} }
.fuz-modal-section { .f-modal-section {
@apply mb-6; @apply mb-6;
} }
.fuz-modal-section h3 { .f-modal-section h3 {
@apply text-xl md:text-2xl font-semibold mb-3; @apply text-xl md:text-2xl font-semibold mb-3;
} }
/* opcja "bez telefonu" — trochę lżejsze tło */ /* opcja "bez telefonu" */
.fuz-accordion-item--no-phone .fuz-accordion-header { .f-accordion-item--no-phone .f-accordion-header {
background: rgba(148, 163, 184, 0.03); background: rgba(148, 163, 184, 0.03);
} }
.fuz-accordion-header-left input[type="radio"] { .f-accordion-header-left input[type="radio"] {
width: 1.05rem; width: 1.05rem;
height: 1.05rem; height: 1.05rem;
transform: scale(1.05); transform: scale(1.05);
@@ -196,7 +193,7 @@
cursor: pointer; cursor: pointer;
} }
.fuz-addon-checkbox input[type="checkbox"] { .f-addon-checkbox input[type="checkbox"] {
width: 1.05rem; width: 1.05rem;
height: 1.05rem; height: 1.05rem;
transform: scale(1.05); transform: scale(1.05);
@@ -204,10 +201,7 @@
cursor: pointer; cursor: pointer;
} }
.fuz-accordion-header-left { .f-accordion-header-left,
align-items: center; .f-addon-checkbox {
}
.fuz-addon-checkbox {
align-items: center; align-items: center;
} }

View File

@@ -331,16 +331,3 @@ obszar ze scrollem wewnątrz modala
outline-offset: 4px; outline-offset: 4px;
} }
.fuz-chsearch__pkg {
background: transparent;
border: 0;
padding: 0;
cursor: pointer;
font: inherit;
color: var(--f-link, var(--f-link));
text-decoration: underline;
}
.fuz-chsearch__pkg:hover {
text-decoration-thickness: 2px;
}