Kolejne przeróbki,

This commit is contained in:
dm
2025-12-13 10:13:20 +01:00
parent 5822237745
commit 4655554ff2
17 changed files with 197 additions and 218 deletions

View File

@@ -2,7 +2,7 @@
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import type { ImageMetadata } from "astro"; import type { ImageMetadata } from "astro";
import Markdown from "../../islands/Markdown.jsx"; import Markdown from "../../islands/Markdown.jsx";
import TvChannelsSearch from "../../islands/jambox/JamboxChannelsSearch.jsx"; import JamboxChannelsSearch from "../../islands/jambox/JamboxChannelsSearch.jsx";
const props = Astro.props ?? {}; const props = Astro.props ?? {};
const section = props.section ?? {}; const section = props.section ?? {};
@@ -44,7 +44,7 @@ if (section.image) {
{section.title && <h2 class="f-section-title">{section.title}</h2>} {section.title && <h2 class="f-section-title">{section.title}</h2>}
{section.content && <Markdown text={section.content} />} {section.content && <Markdown text={section.content} />}
<TvChannelsSearch client:load /> <JamboxChannelsSearch client:load />
{section.button && ( {section.button && (
<div class="f-section-nav"> <div class="f-section-nav">

View File

@@ -1,4 +1,4 @@
title: SKONTAKTUJ SIĘ Z NAMI title: Skontaktuj się z nami
description: | description: |
<h3>FUZ ADAM ROJEK</h3> <h3>FUZ ADAM ROJEK</h3>
<h4>ul. Świętojańska 46</h4> <h4>ul. Świętojańska 46</h4>

View File

@@ -1,31 +1,75 @@
import { useState } from "preact/hooks"; import { useEffect, useRef, useState } from "preact/hooks";
/** /**
* @typedef {{ name: string; href: string }} MenuLink * @typedef {{ name: string; href: string }} MenuLink
* @param {{ links: MenuLink[] }} props * @param {{ links: MenuLink[] }} props
*/ */
export default function MobileMenu({ links = [] }) { export default function MobileMenu({ links = [] }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const menuRef = useRef(null);
const btnRef = useRef(null);
const toggle = () => setOpen(o => !o); const close = () => setOpen(false);
const toggle = (e) => {
e?.stopPropagation?.();
setOpen((o) => !o);
};
useEffect(() => {
const onKey = (e) => e.key === "Escape" && close();
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, []);
useEffect(() => {
if (!open) return;
const onDocClick = (e) => {
const menuEl = menuRef.current;
const btnEl = btnRef.current;
if (menuEl && menuEl.contains(e.target)) return;
if (btnEl && btnEl.contains(e.target)) return;
close();
};
document.addEventListener("mousedown", onDocClick);
document.addEventListener("touchstart", onDocClick, { passive: true });
return () => {
document.removeEventListener("mousedown", onDocClick);
document.removeEventListener("touchstart", onDocClick);
};
}, [open]);
return ( return (
<> <>
<button <button
ref={btnRef}
onClick={toggle} onClick={toggle}
class="f-mobile-toggle md:hidden" class="f-mobile-toggle md:hidden"
aria-label="Menu mobilne" aria-label="Menu mobilne"
aria-expanded={open}
aria-controls="mobile-menu"
> >
{open ? "✖" : "☰"} {open ? "✖" : "☰"}
</button> </button>
<div class={`f-mobile-menu ${open ? "open" : ""}`}> <div
{links.map(link => ( class={`f-mobile-backdrop ${open ? "open" : ""}`}
<a onClick={close}
href={link.href} aria-hidden="true"
class="f-mobile-link" />
onClick={() => setOpen(false)}
> <div
id="mobile-menu"
ref={menuRef}
class={`f-mobile-menu ${open ? "open" : ""}`}
onClick={(e) => e.stopPropagation()}
>
{links.map((link) => (
<a href={link.href} class="f-mobile-link" onClick={close}>
{link.name} {link.name}
</a> </a>
))} ))}

View File

@@ -23,7 +23,6 @@ export default function OffersSwitches(props) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
// 🔹 AUTO mode sam pobieram /api/switches + wysyłam event
useEffect(() => { useEffect(() => {
if (isControlled) return; if (isControlled) return;
@@ -52,7 +51,6 @@ export default function OffersSwitches(props) {
setAutoSwitches(sws); setAutoSwitches(sws);
setAutoSelected(initial); setAutoSelected(initial);
// 🔥 zapisz globalny stan
window.fuzSwitchState = { window.fuzSwitchState = {
selected: initial, selected: initial,
labels, labels,
@@ -69,7 +67,7 @@ export default function OffersSwitches(props) {
}), }),
); );
} catch (err) { } catch (err) {
console.error("Błąd pobierania switchy:", err); console.error("Błąd pobierania przełączników:", err);
if (!cancelled) setError("Nie udało się załadować przełączników."); if (!cancelled) setError("Nie udało się załadować przełączników.");
} finally { } finally {
if (!cancelled) setLoading(false); if (!cancelled) setLoading(false);
@@ -93,7 +91,6 @@ export default function OffersSwitches(props) {
const next = { ...prev, [id]: value }; const next = { ...prev, [id]: value };
const labels = buildLabels(autoSwitches, next); const labels = buildLabels(autoSwitches, next);
// 🔥 aktualizuj globalny stan
window.fuzSwitchState = { window.fuzSwitchState = {
selected: next, selected: next,
labels, labels,
@@ -115,7 +112,6 @@ export default function OffersSwitches(props) {
} }
}; };
// 🔥 CONTROLLED: zsynchronizuj globalny stan + event
useEffect(() => { useEffect(() => {
if (!isControlled) return; if (!isControlled) return;
if (!Array.isArray(switches) || !switches.length) return; if (!Array.isArray(switches) || !switches.length) return;
@@ -123,7 +119,6 @@ export default function OffersSwitches(props) {
const safeSelected = selected || {}; const safeSelected = selected || {};
const labels = buildLabels(switches, safeSelected); const labels = buildLabels(switches, safeSelected);
// 🔥 globalny stan
window.fuzSwitchState = { window.fuzSwitchState = {
selected: safeSelected, selected: safeSelected,
labels, labels,
@@ -162,23 +157,20 @@ export default function OffersSwitches(props) {
return ( return (
<div class="f-switches-wrapper"> <div class="f-switches-wrapper">
{effectiveSwitches.map((sw) => ( {effectiveSwitches.map((sw) => (
<div class="f-switch-box"> <div class="f-switch-group">
<div class="f-switch-group"> {sw.opcje.map((op) => (
{sw.opcje.map((op) => ( <button
<button type="button"
type="button" class={`f-switch ${String(effectiveSelected[sw.id]) === String(op.id)
class={`f-switch ${ ? "active"
String(effectiveSelected[sw.id]) === String(op.id) : ""
? "active"
: ""
}`} }`}
onClick={() => handleClick(sw.id, op.id)} onClick={() => handleClick(sw.id, op.id)}
title={sw.title} title={sw.title}
> >
{op.nazwa} {op.nazwa}
</button> </button>
))} ))}
</div>
</div> </div>
))} ))}
</div> </div>

View File

@@ -79,20 +79,15 @@ export default function JamboxChannelsModal({ isOpen, onClose, pkg }) {
<div class="max-w-8xl mx-auto px-3 md:px-6"> <div class="max-w-8xl mx-auto px-3 md:px-6">
<h2 class="fuz-modal-title">Kanały w pakiecie {pkg.name}</h2> <h2 class="fuz-modal-title">Kanały w pakiecie {pkg.name}</h2>
<div class="jmb-search"> <div class="fuz-chsearch__top">
<input <input
class="jmb-search-input" class="fuz-chsearch__input"
type="search" type="search"
value={query} value={query}
onInput={(e) => setQuery(e.currentTarget.value)} onInput={(e) => setQuery(e.currentTarget.value)}
placeholder="Szukaj kanału po nazwie…" placeholder="Szukaj kanału po nazwie…"
aria-label="Szukaj kanału po nazwie" aria-label="Szukaj kanału po nazwie"
/> />
{query && (
<button class="jmb-search-clear" type="button" onClick={() => setQuery("")}>
Wyczyść
</button>
)}
</div> </div>
{!loading && !error && ( {!loading && !error && (
@@ -112,35 +107,32 @@ export default function JamboxChannelsModal({ isOpen, onClose, pkg }) {
<div class=""> <div class="">
<div class="f-section-channel"> <div class="f-section-channel">
{filtered.map((ch) => ( {filtered.map((ch) => (
<div class="jmb-channel-card" key={ch.number}> <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-inner">
{/* FRONT */}
<div class="jmb-channel-face jmb-channel-front"> <div class="jmb-channel-face jmb-channel-front">
{ch.logo_url && ( {ch.logo_url && (
<img <img src={ch.logo_url} alt={ch.name} class="jmb-channel-logo" loading="lazy" />
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-name">{ch.name}</div>
<div class="jmb-channel-number">kanał {ch.number}</div> <div class="jmb-channel-number">kanał {ch.number}</div>
</div> </div>
{/* BACK */}
<div class="jmb-channel-face jmb-channel-back"> <div class="jmb-channel-face jmb-channel-back">
<div class="jmb-channel-back-title">{ch.name}</div> <div class="jmb-channel-back-title">{ch.name}</div>
<div <div
class="jmb-channel-desc" class="jmb-channel-desc"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{ __html: ch.description || "<em>Brak opisu kanału.</em>" }}
__html: ch.description || "<em>Brak opisu kanału.</em>",
}}
/> />
</div> </div>
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div> </div>

View File

@@ -97,9 +97,9 @@ export default function JamboxChannelsSearch() {
{c.name} {c.name}
</div> </div>
<div class="fuz-chsearch__channel-number"> {/* <div class="fuz-chsearch__channel-number">
kanał {c.min_number || "—"} kanał {c.min_number || "—"}
</div> </div> */}
</div> </div>
@@ -116,7 +116,7 @@ export default function JamboxChannelsSearch() {
{c.packages.map((p, i) => ( {c.packages.map((p, i) => (
<span class="fuz-chsearch__pkg" key={p.id}> <span class="fuz-chsearch__pkg" key={p.id}>
{p.name}{" "} {p.name}{" "}
<span class="fuz-chsearch__pkgnum">({p.number})</span> <span class="fuz-chsearch__pkgnum">(kanał {p.number})</span>
{i < c.packages.length - 1 ? ", " : ""} {i < c.packages.length - 1 ? ", " : ""}
</span> </span>
))} ))}

View File

@@ -3,7 +3,7 @@ import DefaultLayout from "../../layouts/DefaultLayout.astro";
import OffersSwitches from "../../islands/OffersSwitches.jsx"; import OffersSwitches from "../../islands/OffersSwitches.jsx";
import JamboxCards from "../../islands/jambox/JamboxCards.jsx"; import JamboxCards from "../../islands/jambox/JamboxCards.jsx";
import SectionRenderer from "../../components/sections/SectionRenderer.astro"; import SectionRenderer from "../../components/sections/SectionRenderer.astro";
import SectionChannelsSearch from "../../components/sections/SectionChannelsSearch.astro" import SectionChannelsSearch from "../../components/sections/SectionChannelsSearch.astro";
import yaml from "js-yaml"; import yaml from "js-yaml";
import fs from "fs"; import fs from "fs";
@@ -24,7 +24,6 @@ const seo = yaml.load(
<JamboxCards client:load /> <JamboxCards client:load />
</div> </div>
</section> </section>
<SectionChannelsSearch />
<SectionRenderer src="./src/content/internet-telewizja/section.yaml" /> <SectionRenderer src="./src/content/internet-telewizja/section.yaml" />
<SectionChannelsSearch/>
</DefaultLayout> </DefaultLayout>

View File

@@ -18,83 +18,85 @@ const form = data.form;
--- ---
<DefaultLayout seo={seo}> <DefaultLayout seo={seo}>
<section id="kontakt" class="f-section"> <section class="f-section">
<div class="f-contact-grid"> <div class="f-section-grid md:grid-cols-2 gap-10 items-start">
<!-- Kolumna lewa --> <div>
<div class="f-contact-col-1"> <h2 class="f-section-title">{data.title}</h2>
<h2>{data.title}</h2> <div class="f-contact-item" set:html={data.description} />
<div class="f-contact-item" set:html={data.description} /> </div>
</div>
<div class="f-contact-col-2">
<h2>{data.contactFormTitle}</h2>
<form id="contactForm" class="f-contact-form">
<div class="f-contact-form-inner">
<input
type="text"
name="firstName"
placeholder={form.firstName.placeholder}
class="f-input"
required
/>
<input
type="text"
name="lastName"
placeholder={form.lastName.placeholder}
class="f-input"
required
/>
</div>
<div class="grid grid-cols-2 gap-4">
<input
type="email"
name="email"
placeholder={form.email.placeholder}
class="f-input"
required
autocomplete="email"
/>
<input
type="tel"
name="phone"
placeholder={form.phone.placeholder}
class="f-input"
required
autocomplete="tel"
/>
</div>
<div>
<h2 class="f-section-title">{data.contactFormTitle}</h2>
<form id="contactForm" class="f-contact-form">
<div class="f-contact-form-inner">
<input <input
type="text" type="text"
name="subject" name="firstName"
placeholder={form.subject.placeholder} placeholder={form.firstName.placeholder}
class="f-input" class="f-input"
required required
/> />
<input
<textarea type="text"
name="message" name="lastName"
rows={form.message.rows} placeholder={form.lastName.placeholder}
placeholder={form.message.placeholder}
class="f-input" class="f-input"
required></textarea> required
/>
</div>
<label class="f-rodo"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<input type="checkbox" name="rodo" required /> <input
<span> type="email"
{form.rodo.label} name="email"
<a href={form.rodo.policyLink} title={form.rodo.policyTitle} placeholder={form.email.placeholder}
>{form.rodo.policyText}</a class="f-input"
>. required
</span> autocomplete="email"
</label> />
<input
type="tel"
name="phone"
placeholder={form.phone.placeholder}
class="f-input"
required
autocomplete="tel"
/>
</div>
<button title={form.submit.title}>{form.submit.label}</button> <input
</form> type="text"
</div> name="subject"
placeholder={form.subject.placeholder}
class="f-input"
required
/>
<textarea
name="message"
rows={form.message.rows}
placeholder={form.message.placeholder}
class="f-input"
required></textarea>
<label class="f-rodo">
<input type="checkbox" name="rodo" required />
<span>
{form.rodo.label}
<a href={form.rodo.policyLink} title={form.rodo.policyTitle}>
{form.rodo.policyText}
</a>.
</span>
</label>
<button type="submit" class="btn btn-primary w-full py-3" title={form.submit.title}>
{form.submit.label}
</button>
</form>
</div> </div>
</div>
<div class="mt-10">
<div class="f-contact-map"> <div class="f-contact-map">
<MapGoogle <MapGoogle
apiKey={apiKey} apiKey={apiKey}
@@ -108,9 +110,10 @@ const form = data.form;
mapStyleId={data.maps.mapId} mapStyleId={data.maps.mapId}
/> />
</div> </div>
</div>
<div id="toast" class="f-toast"></div> <div id="toast" class="f-toast"></div>
</section> </section>
<!-- ReCaptcha v3 --> <!-- ReCaptcha v3 -->
<script <script

View File

@@ -12,7 +12,6 @@
@import "./footer.css"; @import "./footer.css";
@import "./cookie.css"; @import "./cookie.css";
@import "./contact.css"; @import "./contact.css";
@import "./offers/offers-main.css";
@import "./offers/offers-switches.css"; @import "./offers/offers-switches.css";
html { html {

View File

@@ -1,56 +1,49 @@
.btn { .btn {
@apply inline-flex items-center justify-center font-semibold rounded-lg px-6 py-3 text-base transition-all duration-200 cursor-pointer select-none; @apply inline-flex items-center justify-center gap-2 font-semibold rounded-lg px-6 py-3 text-base transition-all duration-200 cursor-pointer select-none focus:outline-none focus-visible:ring-2 focus-visible:ring-[--f-header] focus-visible:ring-offset-2 focus-visible:ring-offset-[--f-background];
} }
.btn-primary { .btn-primary {
@apply border-none bg-[--btn-background] text-[--btn-text]; @apply border border-transparent bg-[--btn-background] text-[--btn-text];
} }
.btn-primary:hover { .btn-primary:hover {
@apply bg-[--btn-background-hover] text-[--btn-text-hover]; @apply bg-[--btn-background-hover] text-[--btn-text-hover];
} }
.btn-primary:disabled {
@apply opacity-60 cursor-not-allowed;
}
.f-input { .f-input {
@apply w-full py-3 px-4 rounded-xl border border-[--f-input-border] bg-[--f-background] text-[--f-text] transition-all duration-200; @apply w-full py-3 px-4 rounded-xl border border-[--f-input-border] bg-[--f-background] text-[--f-text] transition-all duration-200;
} }
.f-input:hover { .f-input:hover {
@apply border-[--f-text] opacity-[0.9]; @apply border-[--f-text] opacity-[0.4];
} }
.f-input:focus { .f-input:focus {
@apply outline-none border-[--f-header]; @apply outline-none border-[--f-header];
box-shadow: 0 0 0 3px color-mix(in srgb, var(--f-header) 40%, transparent);
} }
.fuz-input::placeholder { .f-input::placeholder {
color: color-mix(in srgb, var(--f-text) 40%, transparent); color: color-mix(in srgb, var(--f-text) 40%, transparent);
} }
.fuz-input-error { .f-input-error {
border-color: #ff4d4f !important; @apply border-red-500 ring-2 ring-red-500/30;
box-shadow: 0 0 0 3px rgba(255, 77, 79, 0.3) !important;
}
.fuz-input:disabled {
opacity: 0.6;
cursor: not-allowed;
background-color: color-mix(in srgb, var(--f-background) 80%, #888);
} }
.autocomplete-wrapper { .autocomplete-wrapper {
position: relative; @apply relative;
} }
.autocomplete-open { .autocomplete-open {
border-bottom-left-radius: 0 !important; @apply rounded-b-none border-b-0;
border-bottom-right-radius: 0 !important;
border-bottom-width: 0 !important;
} }
.autocomplete-list { .autocomplete-list {
@apply absolute left-0 right-0 z-50 bg-[--f-background] text-[--f-text] border border-gray-300 dark:border-slate-700 rounded-b-xl shadow-xl max-h-56 overflow-auto; @apply absolute left-0 right-0 z-50 bg-[--f-background] text-[--f-text] border border-[--f-input-border] rounded-b-xl shadow-xl max-h-56 overflow-auto;
border-top: none; border-top: none;
animation: fadeIn 0.12s ease-out; animation: fadeIn 0.12s ease-out;

View File

@@ -1,30 +1,5 @@
@tailwind base; @tailwind base;
.f-contact-grid {
@apply grid md:grid-cols-2 gap-10 items-start max-w-7xl mx-auto;
}
.f-contact-col-1 {
h1,
h2,
h3,
h4 {
@apply f-section-header;
}
}
.f-contact-col-2 {
@apply bg-[--f-background] text-[--f-text];
h1,
h2,
h3,
h4 {
@apply f-section-header;
}
}
.f-contact-item { .f-contact-item {
@apply space-y-1; @apply space-y-1;
@@ -55,10 +30,6 @@
@apply mt-2 h-4 w-4; @apply mt-2 h-4 w-4;
} }
} }
button {
@apply btn btn-primary w-full py-3 text-lg;
}
} }
.f-contact-map { .f-contact-map {

View File

@@ -44,10 +44,11 @@
.fuz-markdown button.modal-link { .fuz-markdown button.modal-link {
@apply no-underline hover:no-underline mt-2 bg-[--f-background] text-[--f-link-text]; @apply no-underline hover:no-underline mt-2 bg-[--f-background] text-[--f-link-text];
} }
.fuz-markdown blockquote { .fuz-markdown blockquote {
@apply border-l-4 border-gray-300 dark:border-gray-700 pl-4 italic mb-4; @apply border-l-4 border-gray-300 dark:border-gray-700 pl-4 italic mb-4;
} }
.fuz-markdown strong { .fuz-markdown strong {
@apply font-semibold; @apply font-semibold;
} }

View File

@@ -15,21 +15,39 @@
} }
.f-mobile-toggle { .f-mobile-toggle {
@apply text-3xl p-2 text-[--f-text]; @apply text-3xl p-2 text-[--f-text] relative z-[70];
} }
.f-mobile-menu { .f-mobile-menu {
@apply fixed top-0 right-0 h-full w-64 bg-[--f-background] shadow-lg transform translate-x-full transition-transform duration-300 flex flex-col gap-4 p-6; @apply fixed right-4 bg-[--f-background] border border-[--f-border-color] p-4 flex flex-col gap-2 opacity-0 scale-95 -translate-y-2 pointer-events-none transition duration-200 divide-y divide-[--f-border-color];
top: calc(var(--f-navbar-height, 64px) + 0.75rem);
width: 18rem;
max-width: calc(100vw - 2rem);
z-index: 70;
max-height: calc(100vh - var(--f-navbar-height, 64px) - 2rem);
overflow-y: auto;
} }
.f-mobile-menu.open { .f-mobile-menu.open {
@apply translate-x-0; @apply opacity-100 scale-100 translate-y-0 pointer-events-auto;
} }
.f-mobile-link { .f-mobile-link {
@apply text-lg py-2 border-b text-[--f-navbar-link] hover:text-[--f-navbar-link-hover] border-[--f-border-color]; @apply text-base py-2 px-2
text-[--f-navbar-link]
hover:text-[--f-navbar-link-hover]
hover:bg-black/5 dark:hover:bg-white/5
transition;
} }
.f-navbar-logo { .f-navbar-logo {
@apply w-[70] h-[36]; @apply w-[70] h-[36];
}
.f-mobile-backdrop {
@apply fixed inset-0 bg-black/30 opacity-0 pointer-events-none transition-opacity duration-200 z-[60];
}
.f-mobile-backdrop.open {
@apply opacity-100 pointer-events-auto;
} }

View File

@@ -1,22 +0,0 @@
.f-extra-services {
@apply mt-6 max-w-6xl mx-auto;
}
.f-services-title {
@apply text-2xl font-semibold mb-4 ml-10;
}
.f-services-body {
@apply text-lg font-semibold mb-4 ml-10;
}
.f-expand-details {
@apply px-4 py-4;
}
.f-feature-link {
@apply cursor-pointer w-full h-full text-[--f-link-text] text-lg;
}
.f-feature-link:hover {
@apply text-[--f-link-text-hover];
}

View File

@@ -1,11 +0,0 @@
.fuz-offers-section {
@apply py-6;
}
.f-offers-container {
@apply max-w-7xl mx-auto px-6;
}
.f-offers-description {
@apply mb-10 text-base leading-relaxed;
}

View File

@@ -7,7 +7,7 @@
} }
.f-switch { .f-switch {
@apply px-6 py-2 text-sm font-semibold cursor-pointer select-none transition-all; @apply px-6 py-3 text-sm font-semibold cursor-pointer select-none transition-all;
} }
.f-switch.active { .f-switch.active {

View File

@@ -78,7 +78,7 @@
/* var(--link-color-light); */ /* var(--link-color-light); */
--btn-background-hover: var(--surface4-light); --btn-background-hover: var(--surface4-light);
/* var(--link-color-light); */ /* var(--link-color-light); */
--btn-text-hover: var(--link-color-light); --btn-text-hover: var(--link-color-light);
/* var(--surface4-light); */ /* var(--surface4-light); */
--f-background-toast: var(--surface2-dark); --f-background-toast: var(--surface2-dark);