Porządkowanie kodu, dodanie sekcji wyszukiwania kanałów
This commit is contained in:
@@ -25,16 +25,13 @@ const {
|
||||
ctas = []
|
||||
} = Astro.props;
|
||||
|
||||
// Wyciągnij nazwę bazową bez rozszerzenia
|
||||
const imageBase = imageUrl.replace(/\.(webp|png|jpg|jpeg)$/i, '');
|
||||
|
||||
// Importuj wszystkie obrazki
|
||||
const images = import.meta.glob<{ default: ImageMetadata }>(
|
||||
'/src/assets/hero/**/*.webp',
|
||||
{ eager: true }
|
||||
);
|
||||
|
||||
// Funkcja do znajdowania obrazka dla danego rozmiaru
|
||||
function findImage(folder: string): ImageMetadata | null {
|
||||
const key = `/src/assets/hero/${folder}/${imageBase}-${folder}.webp`;
|
||||
return images[key]?.default || null;
|
||||
|
||||
@@ -8,6 +8,7 @@ const links = [
|
||||
{ name: "TELEFON", href: "/telefon" },
|
||||
{ name: "ZASIĘG SIECI", href: "/mapa-zasiegu" },
|
||||
{ name: "KONTAKT", href: "/kontakt" },
|
||||
{ name: "DOKUMENTY", href: "/dokumenty" },
|
||||
{
|
||||
name: "BOK",
|
||||
href: "https://panel.fuz.pl/userpanel/auth",
|
||||
|
||||
58
src/components/sections/SectionChannelsSearch.astro
Normal file
58
src/components/sections/SectionChannelsSearch.astro
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
import { Image } from "astro:assets";
|
||||
import type { ImageMetadata } from "astro";
|
||||
import Markdown from "../../islands/Markdown.jsx";
|
||||
import TvChannelsSearch from "../../islands/jambox/JamboxChannelsSearch.jsx";
|
||||
|
||||
const props = Astro.props ?? {};
|
||||
const section = props.section ?? {};
|
||||
const index = Number(props.index ?? 0);
|
||||
|
||||
const hasImage = !!section.image;
|
||||
const reverse = index % 2 === 1;
|
||||
|
||||
const sectionImages = import.meta.glob<{ default: ImageMetadata }>(
|
||||
"/src/assets/sections/**/*.{png,jpg,jpeg,webp,avif}",
|
||||
{ eager: true }
|
||||
);
|
||||
|
||||
let sectionImage: ImageMetadata | null = null;
|
||||
|
||||
if (section.image) {
|
||||
const path = `/src/assets/sections/${section.image}`;
|
||||
const mod = sectionImages[path];
|
||||
if (mod) sectionImage = mod.default;
|
||||
}
|
||||
---
|
||||
|
||||
<section class="f-section">
|
||||
<div class={`f-section-grid ${hasImage ? "md:grid-cols-2" : "md:grid-cols-1"}`}>
|
||||
{sectionImage && (
|
||||
<Image
|
||||
src={sectionImage}
|
||||
alt={section.title ?? "Kanały TV"}
|
||||
class={`f-section-image ${reverse ? "md:order-1" : "md:order-2"} ${section.dimmed ? "f-image-dimmed" : ""}`}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
format="webp"
|
||||
widths={[480, 768, 1024, 1440]}
|
||||
sizes="100vw"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div class={`${hasImage ? (reverse ? "md:order-2" : "md:order-1") : ""}`}>
|
||||
{section.title && <h2 class="f-section-title">{section.title}</h2>}
|
||||
{section.content && <Markdown text={section.content} />}
|
||||
|
||||
<TvChannelsSearch client:load />
|
||||
|
||||
{section.button && (
|
||||
<div class="f-section-nav">
|
||||
<a href={section.button.url} class="btn btn-primary" title={section.button.title}>
|
||||
{section.button.text}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,125 +0,0 @@
|
||||
---
|
||||
import yaml from "js-yaml";
|
||||
import fs from "fs";
|
||||
import MapGoogle from "../../components/maps/MapGoogle.astro";
|
||||
|
||||
const data = yaml.load(
|
||||
fs.readFileSync("./src/content/contact/contact.yaml", "utf8")
|
||||
);
|
||||
|
||||
const apiKey = import.meta.env.PUBLIC_GOOGLE_MAPS_KEY;
|
||||
const form = data.form;
|
||||
---
|
||||
|
||||
<section id="kontakt" class="f-section">
|
||||
<div class="f-contact-grid">
|
||||
<!-- Kolumna lewa -->
|
||||
<div class="f-contact-col-1">
|
||||
<h2>{data.title}</h2>
|
||||
<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>
|
||||
|
||||
<input type="text" 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 title={form.submit.title}>{form.submit.label}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="f-contact-map">
|
||||
<MapGoogle
|
||||
apiKey={apiKey}
|
||||
lat={data.lat}
|
||||
lon={data.lng}
|
||||
zoom={16}
|
||||
title={data.markerTitle}
|
||||
description={data.markerAddress}
|
||||
showMarker={true}
|
||||
mode="contact"
|
||||
mapStyleId={data.maps.mapId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="f-toast"></div>
|
||||
</section>
|
||||
|
||||
<!-- ReCaptcha v3 -->
|
||||
<script is:inline define:vars={{ siteKey: import.meta.env.PUBLIC_RECAPTCHA_SITE_KEY }}>
|
||||
window.FUZ_RECAPTCHA_KEY = siteKey;
|
||||
|
||||
const s = document.createElement("script");
|
||||
s.src = "https://www.google.com/recaptcha/api.js?render=" + siteKey;
|
||||
s.async = true;
|
||||
document.head.appendChild(s);
|
||||
</script>
|
||||
|
||||
|
||||
<script
|
||||
is:inline
|
||||
define:vars={{
|
||||
successMsg: form.successMessage,
|
||||
errorMsg: form.errorMessage
|
||||
}}
|
||||
>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const form = document.getElementById("contactForm");
|
||||
const toast = document.getElementById("toast");
|
||||
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
if (!form.reportValidity()) return;
|
||||
e.preventDefault();
|
||||
|
||||
const data = Object.fromEntries(new FormData(form).entries());
|
||||
data.rodo = form.rodo.checked;
|
||||
|
||||
const token = await grecaptcha.execute(window.FUZ_RECAPTCHA_KEY, { action: "submit" });
|
||||
data.recaptcha = token;
|
||||
|
||||
const resp = await fetch("/api/contact", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const json = await resp.json();
|
||||
|
||||
showToast(json.ok ? successMsg : errorMsg, json.ok ? "success" : "error");
|
||||
|
||||
if (json.ok) form.reset();
|
||||
});
|
||||
|
||||
function showToast(msg, type) {
|
||||
toast.innerHTML = `<div class="f-toast-msg ${type}">${msg}</div>`;
|
||||
toast.classList.remove("visible");
|
||||
void toast.offsetWidth;
|
||||
toast.classList.add("visible");
|
||||
|
||||
setTimeout(() => toast.classList.remove("visible"), 3000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -43,11 +43,9 @@ if (section.image) {
|
||||
<div
|
||||
class={`f-section-grid ${hasImage ? (reverse ? "md:order-2" : "md:order-1") : ""}`}
|
||||
>
|
||||
<!-- TODO: Styl nagłowka powinien byc trochę niżej -->
|
||||
<h2 class="f-section-title">{section.title}</h2>
|
||||
|
||||
<Markdown text={section.content} />
|
||||
|
||||
{
|
||||
section.button && (
|
||||
<div class="f-section-nav">
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
import ChannelSwitcher from "../../islands/ChannelSwitcher.jsx";
|
||||
const { section } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="f-section">
|
||||
<div class="max-w-7xl mx-auto text-center">
|
||||
|
||||
{section.title && (
|
||||
<h2 class="f-section-title">{section.title}</h2>
|
||||
)}
|
||||
|
||||
{section.content && (
|
||||
<div class="f-markdown mb-10" set:html={section.html} />
|
||||
)}
|
||||
|
||||
{section.iframe_sets && section.iframe_sets.length > 0 && (
|
||||
<ChannelSwitcher
|
||||
client:load
|
||||
sets={section.iframe_sets}
|
||||
title={section.title}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,67 +0,0 @@
|
||||
---
|
||||
import Markdown from "../../islands/Markdown.jsx";
|
||||
|
||||
// Pobranie XML Jambox
|
||||
const url = "https://www.jambox.pl/xml/mozliwosci.xml";
|
||||
const xmlText: string = await fetch(url).then(r => r.text());
|
||||
|
||||
// Parser wszystkich <node>
|
||||
function parseNodes(xml: string) {
|
||||
return [...xml.matchAll(/<node>([\s\S]*?)<\/node>/g)].map((match) => {
|
||||
const block = match[1];
|
||||
|
||||
const get = (tag: string) => {
|
||||
const m = block.match(new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`));
|
||||
return m ? m[1].trim() : "";
|
||||
};
|
||||
|
||||
return {
|
||||
title: get("title"),
|
||||
teaser: get("teaser"),
|
||||
description: get("description"),
|
||||
icon: get("icon"),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const nodes = parseNodes(xmlText);
|
||||
---
|
||||
|
||||
<section class="f-section">
|
||||
<h2 class="f-section-title mb-10">Dodatkowe możliwości telewizji JAMBOX</h2>
|
||||
|
||||
{nodes.map((item, i) => {
|
||||
const reverse = i % 2 === 1;
|
||||
|
||||
return (
|
||||
<div class={`f-section-item py-14`}>
|
||||
<div class={`f-section-grid md:grid-cols-2`}>
|
||||
|
||||
<!-- OBRAZ -->
|
||||
<div class={`${reverse ? "md:order-2" : "md:order-1"}`}>
|
||||
<img
|
||||
src={item.icon}
|
||||
alt={item.title}
|
||||
loading="lazy"
|
||||
class="f-section-image rounded-xl shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- TEKST -->
|
||||
<div class={`f-section-grid ${reverse ? "md:order-1" : "md:order-2"}`}>
|
||||
<h3 class="f-section-title text-2xl mb-4">{item.title}</h3>
|
||||
|
||||
{item.teaser && (
|
||||
<p class="text-lg font-medium opacity-80 mb-4">
|
||||
{item.teaser}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Markdown text={item.description} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
@@ -4,7 +4,7 @@ import fs from "fs";
|
||||
import { marked } from "marked";
|
||||
|
||||
import SectionDefault from "./SectionDefault.astro";
|
||||
import SectionIframeChannels from "./SectionIframeChannels.astro";
|
||||
|
||||
|
||||
const { src } = Astro.props;
|
||||
|
||||
@@ -19,9 +19,5 @@ const sections = (data.sections as any[]).map((s: any) => ({
|
||||
{sections.map((section: any, index: number) => {
|
||||
const type = section.type || "default";
|
||||
|
||||
if (type === "iframe-channels") {
|
||||
return <SectionIframeChannels section={section} index={index} />;
|
||||
}
|
||||
|
||||
return <SectionDefault section={section} index={index} />;
|
||||
})}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
const { href, variant = "primary" } = Astro.props;
|
||||
|
||||
const base =
|
||||
"inline-flex items-center justify-center rounded-full px-4 py-2 text-sm font-medium transition";
|
||||
const variants = {
|
||||
primary: "bg-sky-600 text-white hover:bg-sky-700 dark:bg-sky-500 dark:hover:bg-sky-400",
|
||||
secondary: "bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-100"
|
||||
};
|
||||
const classes = `${base} ${variants[variant] ?? variants.primary}`;
|
||||
---
|
||||
|
||||
{href ? (
|
||||
<a href={href} class={classes}>
|
||||
<slot />
|
||||
</a>
|
||||
) : (
|
||||
<button type="button" class={classes}>
|
||||
<slot />
|
||||
</button>
|
||||
)}
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
const { message, type = "success" } = Astro.props;
|
||||
|
||||
const base = "fixed right-4 top-20 z-50 rounded-xl px-4 py-3 text-sm shadow-lg border backdrop-blur";
|
||||
const variants = {
|
||||
success: "bg-emerald-50/90 border-emerald-200 text-emerald-900 dark:bg-emerald-900/70 dark:border-emerald-700 dark:text-emerald-50",
|
||||
error: "bg-rose-50/90 border-rose-200 text-rose-900 dark:bg-rose-900/70 dark:border-rose-700 dark:text-rose-50",
|
||||
info: "bg-sky-50/90 border-sky-200 text-sky-900 dark:bg-sky-900/70 dark:border-sky-700 dark:text-sky-50"
|
||||
};
|
||||
const classes = `${base} ${variants[type] ?? variants.success}`;
|
||||
---
|
||||
|
||||
<div class={classes}>
|
||||
{message}
|
||||
</div>
|
||||
29
src/content/contact/seo.yaml
Normal file
29
src/content/contact/seo.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
site:
|
||||
name: "FUZ – Internet światłowodowy w Wyszkowie"
|
||||
description: "Stabilny i szybki internet w Wyszkowie i okolicach"
|
||||
url: "https://www.fuz.pl"
|
||||
lang: "pl"
|
||||
|
||||
company:
|
||||
name: "FUZ Adam Rojek"
|
||||
phone: "+48 (29) 643 80 55"
|
||||
email: "biuro@fuz.pl"
|
||||
street: "ul. Świętojańska 46"
|
||||
city: "Wyszków"
|
||||
postal: "07-200"
|
||||
country: "PL"
|
||||
lat: 52.597385
|
||||
lon: 21.456797
|
||||
logo: "/images/logo-fuz.webp"
|
||||
|
||||
page:
|
||||
title: "FUZ – Internet światłowodowy w Wyszkowie"
|
||||
description: "Szybki, stabilny internet światłowodowy w Wyszkowie. Lokalny operator, realny serwis, błyskawiczne wsparcie."
|
||||
image: "/images/og-home.webp"
|
||||
url: "/"
|
||||
keywords:
|
||||
- internet Wyszków
|
||||
- światłowód Wyszków
|
||||
- internet światłowodowy Wyszków
|
||||
- lokalny operator internetu
|
||||
schema: {}
|
||||
@@ -18,8 +18,3 @@ ctas:
|
||||
href: "/internet-telewizja"
|
||||
title: "Przejdź do oferty Internet + Telewizja w FUZ"
|
||||
primary: false
|
||||
|
||||
# - label: "Sprawdź dostępność usługi"
|
||||
# href: "/mapa-zasiegu"
|
||||
# title: "Sprawdź zasięg Internetu światłowodowego FUZ"
|
||||
# primary: false
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
sections:
|
||||
# - title: Sprawdź dostępność usługi
|
||||
# image:
|
||||
# button:
|
||||
# text: "Sprawdź dostępność pod Twoim adresem →"
|
||||
# url: "/mapa-zasiegu"
|
||||
# title: "Sprawdź zasięg Internetu światłowodowego FUZ"
|
||||
# content: |
|
||||
# Naszą sieć światłowodową systematycznie rozbudowujemy, ale infrastruktura nie dociera jeszcze do wszystkich adresów.
|
||||
# Sprawdź zasięg naszego Internetu na interaktywnej mapie, czy internet światłowodowy jest już dostępny pod Twoim adresem.
|
||||
|
||||
- title: Router WiFi HL-4BX3V-F
|
||||
image: "HL-4BX3V-F.webp"
|
||||
content: |
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
title:
|
||||
- Internet plus telewizja w pakiecie
|
||||
subtitle:
|
||||
- Internet bez kompromisów
|
||||
- Bogata oferta kanałów telewizyjnych
|
||||
- Wszystko, czego potrzebujesz w jednym pakiecie
|
||||
|
||||
# description: |
|
||||
# Szybki i stabilny Internet światłowodowy w pakiecie z telewizją w Wyszkowie oraz okolicach.
|
||||
# Sprawdź zasięg usług i wybierz najlepsze łącze dla swojego domu.
|
||||
imageUrl: "section-tv.webp"
|
||||
ctas:
|
||||
- label: "Zobacz ofertę Internetu"
|
||||
href: "/internet-swiatlowodowy"
|
||||
title: "Przejdź do oferty Internetu światłowodowego"
|
||||
primary: false
|
||||
|
||||
- label: "Zobacz ofertę Telefonu"
|
||||
href: "/telefon"
|
||||
title: "Przejdź do oferty telefonu"
|
||||
primary: false
|
||||
|
||||
# - label: "Sprawdź dostępność usługi"
|
||||
# href: "/mapa-zasiegu"
|
||||
# title: "Sprawdź zasięg Internetu światłowodowego FUZ"
|
||||
# primary: false
|
||||
@@ -1,50 +0,0 @@
|
||||
catchup:
|
||||
title: CATCHUP ARCHIWUM TV
|
||||
content: |
|
||||
Funkcja CatchUp pozwala na oglądanie archiwalnych audycji na wybranych kanałach do 7 dni wstecz.
|
||||
|
||||
Przegapiłeś jakiś program? Zapomniałeś nagrać?
|
||||
|
||||
Odnajdź go w EPG, cofając się w lewo na osi czasu.
|
||||
|
||||

|
||||
|
||||
nagrywarka:
|
||||
title: NAGRYWARKA
|
||||
content: |
|
||||
Nasze dekodery posiadają możliwość nagrywania wybranych lub wszystkich kanałów, w zależności od modelu dekodera.
|
||||
|
||||
Nagrywarka sieciowa pozwala na nagrywanie i oglądanie audycji bez dysku.
|
||||
|
||||
Dzięki tej wersji nagrywarki możesz zlecać w tym samym czasie 3 niezależne nagrania.
|
||||
|
||||
Korzyści jakie daje nagrywarka to między innymi możliwość oglądania nagrania od początku, nawet jak włączyłeś je w trakcie emisji na żywo.
|
||||
|
||||
Przydatna funkcja to możliwość zaprogramowania nagrywarki także z aplikacji JAMBOX go! Nagrania są dostępne na dekoderze przez 7 dni.
|
||||
|
||||
Nagrywanie w chmurze dostępne jest na dekoderach Abox M15, Hybroad Z123 oraz Arris VIP 4302.
|
||||
|
||||

|
||||
|
||||
startover:
|
||||
title: STARTOVER OGLĄDAJ OD POCZĄTKU
|
||||
content: |
|
||||
Funkcja StartOver pozwala na oglądanie od początku tych audycji, które już się rozpoczęły, lecz jeszcze nie skończyły.
|
||||
|
||||
Spóźniłeś się na emisję na żywo?
|
||||
|
||||
Nic nie szkodzi! Kliknij "Oglądaj od początku" i już nic Ci nie umknie!
|
||||
|
||||

|
||||
|
||||
nagrywanie_cykliczne:
|
||||
title: NAGRYWANIE CZASOWE/CYKLICZNE
|
||||
content: |
|
||||
Jeśli lubisz oglądać każdego dnia wiadomości, albo chcesz nagrać Twojemu dziecku wszystkie wieczorynki to możesz teraz w prosty sposób zaplanować nagrania.
|
||||
|
||||
Nagrywanie według czasu to funkcja, która umożliwia Ci zaprogramowanie nagrań powtarzających się.
|
||||
|
||||
Zaplanuj nagrania codzienne, weekendowe czy w wybranych przez Ciebie dniach. Dodatkowo możesz nagrać dowolny przedział czasu na kanale.
|
||||
|
||||
Nagrywanie czasowe jest dostępne na dekoderze wyposażonym w dysk dedykowany lub USB.
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
przelaczniki:
|
||||
- id: "budynek"
|
||||
etykieta: "Rodzaj budynku"
|
||||
domyslny: "jednorodzinny"
|
||||
title: "Zmień rodzaj budynku by zobaczyć odpowiednie ceny"
|
||||
opcje:
|
||||
- id: "jednorodzinny"
|
||||
nazwa: "Jednorodzinny"
|
||||
- id: "wielorodzinny"
|
||||
nazwa: "Wielorodzinny"
|
||||
|
||||
- id: "umowa"
|
||||
etykieta: "Okres umowy"
|
||||
domyslny: "24m"
|
||||
title: "Wybierz okres umowy by zobaczyć odpowiednie ceny"
|
||||
opcje:
|
||||
- id: "24m"
|
||||
nazwa: "24 miesiące"
|
||||
- id: "bezterminowa"
|
||||
nazwa: "Bezterminowa"
|
||||
|
||||
funkcje:
|
||||
- id: "pobieranie"
|
||||
etykieta: "Pobieranie"
|
||||
- id: "wysylanie"
|
||||
etykieta: "Wysyłanie"
|
||||
- id: "router"
|
||||
etykieta: "Router Wi-Fi"
|
||||
- id: "kanaly"
|
||||
etykieta: "Liczba kanałów"
|
||||
- id: "hd"
|
||||
etykieta: "Kanały HD"
|
||||
- id: "umowa_info"
|
||||
etykieta: "Umowa"
|
||||
- id: "instalacja"
|
||||
etykieta: "Aktywacja"
|
||||
|
||||
plany:
|
||||
- id: "pakiet-1"
|
||||
nazwa: "SMART"
|
||||
popularny: false
|
||||
|
||||
ceny:
|
||||
jednorodzinny:
|
||||
24m: 109
|
||||
bezterminowa: 129
|
||||
wielorodzinny:
|
||||
24m: 99
|
||||
bezterminowa: 119
|
||||
|
||||
koszty:
|
||||
instalacja:
|
||||
jednorodzinny:
|
||||
24m: 149
|
||||
bezterminowa: 199
|
||||
wielorodzinny:
|
||||
24m: 99
|
||||
bezterminowa: 149
|
||||
|
||||
funkcje:
|
||||
pobieranie: "300 Mb/s"
|
||||
wysylanie: "150 Mb/s"
|
||||
router: true
|
||||
kanaly: "127"
|
||||
hd: "99"
|
||||
umowa_info: "24 / Bezterminowa"
|
||||
|
||||
- id: "pakiet-2"
|
||||
nazwa: "OPTIMUM"
|
||||
popularny: false
|
||||
|
||||
ceny:
|
||||
jednorodzinny:
|
||||
24m: 125
|
||||
bezterminowa: 145
|
||||
wielorodzinny:
|
||||
24m: 115
|
||||
bezterminowa: 135
|
||||
|
||||
koszty:
|
||||
instalacja:
|
||||
jednorodzinny:
|
||||
24m: 149
|
||||
bezterminowa: 199
|
||||
wielorodzinny:
|
||||
24m: 99
|
||||
bezterminowa: 149
|
||||
|
||||
funkcje:
|
||||
pobieranie: "300 Mb/s"
|
||||
wysylanie: "150 Mb/s"
|
||||
router: true
|
||||
kanaly: "171"
|
||||
hd: "129"
|
||||
umowa_info: "24 / Bezterminowa"
|
||||
|
||||
- id: "pakiet-3"
|
||||
nazwa: "PLATINUM"
|
||||
popularny: true
|
||||
|
||||
ceny:
|
||||
jednorodzinny:
|
||||
24m: 158
|
||||
bezterminowa: 178
|
||||
wielorodzinny:
|
||||
24m: 148
|
||||
bezterminowa: 168
|
||||
|
||||
koszty:
|
||||
instalacja:
|
||||
jednorodzinny:
|
||||
24m: 149
|
||||
bezterminowa: 199
|
||||
wielorodzinny:
|
||||
24m: 99
|
||||
bezterminowa: 149
|
||||
|
||||
funkcje:
|
||||
pobieranie: "300 Mb/s"
|
||||
wysylanie: "150 Mb/s"
|
||||
router: true
|
||||
kanaly: "207"
|
||||
hd: "157"
|
||||
umowa_info: "24 / Bezterminowa"
|
||||
|
||||
- id: "pakiet-4"
|
||||
nazwa: "PODSTAWOWY"
|
||||
popularny: false
|
||||
|
||||
ceny:
|
||||
jednorodzinny:
|
||||
24m: 88
|
||||
bezterminowa: 108
|
||||
wielorodzinny:
|
||||
24m: 78
|
||||
bezterminowa: 98
|
||||
|
||||
koszty:
|
||||
instalacja:
|
||||
jednorodzinny:
|
||||
24m: 149
|
||||
bezterminowa: 199
|
||||
wielorodzinny:
|
||||
24m: 99
|
||||
bezterminowa: 149
|
||||
|
||||
funkcje:
|
||||
pobieranie: "300 Mb/s"
|
||||
wysylanie: "150 Mb/s"
|
||||
router: true
|
||||
kanaly: "83"
|
||||
hd: "63"
|
||||
umowa_info: "24 / Bezterminowa"
|
||||
|
||||
- id: "pakiet-5"
|
||||
nazwa: "KORZYSTNY"
|
||||
popularny: false
|
||||
|
||||
ceny:
|
||||
jednorodzinny:
|
||||
24m: 110
|
||||
bezterminowa: 130
|
||||
wielorodzinny:
|
||||
24m: 100
|
||||
bezterminowa: 120
|
||||
|
||||
koszty:
|
||||
instalacja:
|
||||
jednorodzinny:
|
||||
24m: 149
|
||||
bezterminowa: 199
|
||||
wielorodzinny:
|
||||
24m: 99
|
||||
bezterminowa: 149
|
||||
|
||||
funkcje:
|
||||
pobieranie: "300 Mb/s"
|
||||
wysylanie: "150 Mb/s"
|
||||
router: true
|
||||
kanaly: "142"
|
||||
hd: "107"
|
||||
umowa_info: "24 / Bezterminowa"
|
||||
|
||||
- id: "pakiet-6"
|
||||
nazwa: "BOGATY"
|
||||
popularny: false
|
||||
|
||||
ceny:
|
||||
jednorodzinny:
|
||||
24m: 120
|
||||
bezterminowa: 140
|
||||
wielorodzinny:
|
||||
24m: 110
|
||||
bezterminowa: 130
|
||||
|
||||
koszty:
|
||||
instalacja:
|
||||
jednorodzinny:
|
||||
24m: 149
|
||||
bezterminowa: 199
|
||||
wielorodzinny:
|
||||
24m: 99
|
||||
bezterminowa: 149
|
||||
|
||||
funkcje:
|
||||
pobieranie: "300 Mb/s"
|
||||
wysylanie: "150 Mb/s"
|
||||
router: true
|
||||
kanaly: "184"
|
||||
hd: "139"
|
||||
umowa_info: "24 / Bezterminowa"
|
||||
@@ -1,15 +0,0 @@
|
||||
title:
|
||||
- PAKIET INTERNET + TELEWIZJA
|
||||
|
||||
paragraphs:
|
||||
- title:
|
||||
# content: |
|
||||
# Przygotowaliśmy sześć pakietów dopasowanych do różnych potrzeb.
|
||||
|
||||
# Wybierając którykolwiek z nich, zyskujesz nie tylko korzystną cenę, ale przede wszystkim wygodę i oszczędność czasu.
|
||||
|
||||
# Jedna umowa, jeden rachunek, jedno miejsce kontaktu. Internet, telewizja i telefon – wszystko w jednym miejscu. Jeśli masz pytania lub potrzebujesz pomocy, jesteśmy do Twojej dyspozycji.
|
||||
|
||||
# Oszczędzaj czas i ciesz się prostotą, wszystko czego potrzebujesz, w jednym miejscu.
|
||||
|
||||
# Kolejne sekcje mozna dodawać poja wiać się bedą pod tabela produktów
|
||||
@@ -1,19 +1,4 @@
|
||||
sections:
|
||||
# - title: Dodatkowe możliwości naszej telewizji"
|
||||
# image: "ekosystem-kyanit.webp"
|
||||
# content: |
|
||||
# - **Catchup** — na wybranych kanałach możesz obejrzeć audycję z ostatnich 7 dni. [Więcej →](#catchup "Przeczytaj o usłudze CatchUp")
|
||||
|
||||
# - **Nagrywanie** — nagraj interesującą Cię audycję i obejrzyj ją kiedy chcesz. [Więcej →](#nagrywarka "Przeczytaj o nagrywaniu audycji")
|
||||
|
||||
# - **StartOver** — obejrzyj od początku audycję, która już się rozpoczęła (do 3h wstecz). [Więcej →](#startover "Przeczytaj o usłudze StartOver")
|
||||
|
||||
# - **Nagrywanie serii** — zaplanuj nagrywanie kolejnych odcinków ulubionego serialu. [Więcej →](#nagrywanie_cykliczne "Przeczytaj o nagrywaniu cyklicznym")
|
||||
|
||||
# - **Pauzowanie** — zatrzymuj i cofaj audycje.
|
||||
|
||||
# - **Wyszukiwarka tekstowa** — wyszukaj dowolną frazę audycji i zaplanuj nagranie.
|
||||
|
||||
- title: "Dekoder telewizyjny"
|
||||
image: "VIP4302.png"
|
||||
content: |
|
||||
@@ -32,27 +17,3 @@ sections:
|
||||
- Interfejsy tylnego panelu obejmują m.in. Ethernet, USB, HDMI, CVBS, Optyczny i analogowy audio 3,5mm
|
||||
- Przedni panel zawiera m.in. diodę LED i odbiornik podczerwieni
|
||||
- Wymiary modelu (szer/dł/wys): 130 x 130 x 26 mm
|
||||
|
||||
# - type: "iframe-channels"
|
||||
# title: Sprawdź listę kanałów w interesującym Cię pakiecie
|
||||
# content: ""
|
||||
|
||||
# iframe_sets:
|
||||
# - id: "canal_smart"
|
||||
# name: "SMART"
|
||||
# p: 86
|
||||
# - id: "canal_optimum"
|
||||
# name: "OPTIMUM"
|
||||
# p: 87
|
||||
# - id: "canal_platinum"
|
||||
# name: "PLATINUM"
|
||||
# p: 88
|
||||
# - id: "canal_podstawowy"
|
||||
# name: "PODSTAWOWY"
|
||||
# p: 75
|
||||
# - id: "canal_korzystny"
|
||||
# name: "KORZYSTNY"
|
||||
# p: 76
|
||||
# - id: "canal_bogaty"
|
||||
# name: "BOGATY"
|
||||
# p: 77
|
||||
|
||||
@@ -29,7 +29,7 @@ sections:
|
||||
dimmed: true
|
||||
button:
|
||||
text: "Zobacz ofertę telefonu →"
|
||||
url: "/internet-telewizja/"
|
||||
url: "/telefon/"
|
||||
title: "Przejdź do oferty telefonu"
|
||||
content: |
|
||||
Nasza telefonia wykorzystuje zaawansowaną technologię VoIP, dzięki której dźwięk jest wyraźny, a połączenia stabilne.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
8281
src/data/ServiceRange2.csv
Normal file
8281
src/data/ServiceRange2.csv
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
||||
export function getActiveLabel(switches, selected, switchId) {
|
||||
const sw = switches.find((s) => s.id === switchId);
|
||||
if (!sw) return "";
|
||||
const opt = sw.opcje.find((o) => o.id === selected[switchId]);
|
||||
return opt?.nazwa ?? "";
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
export function getInstallationPrice(plan, switches, selected) {
|
||||
// Dla telefonii – instalacja to po prostu funkcja wpisana w plan
|
||||
if (!plan.koszty) {
|
||||
// instalacja jest zapisana jako tekst "1,23 zł"
|
||||
const raw = plan.funkcje?.instalacja;
|
||||
if (!raw) return 0;
|
||||
|
||||
const num = parseFloat(raw.replace(",", "."));
|
||||
return isNaN(num) ? 0 : num;
|
||||
}
|
||||
|
||||
// Internet / TV
|
||||
const budynek = selected.budynek;
|
||||
const umowa = selected.umowa;
|
||||
|
||||
return (
|
||||
plan.koszty?.instalacja?.[budynek]?.[umowa] ??
|
||||
plan.koszty?.instalacja?.[budynek]?.default ??
|
||||
plan.koszty?.instalacja?.default ??
|
||||
0
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
export function getPrice(plan, switches, selected) {
|
||||
try {
|
||||
if (plan.cena) return plan.cena;
|
||||
if (!switches.length) return plan.ceny || "-";
|
||||
|
||||
let v = plan.ceny;
|
||||
for (const sw of switches) {
|
||||
const key = selected[sw.id];
|
||||
if (!v || !(key in v)) return "-";
|
||||
v = v[key];
|
||||
}
|
||||
return v;
|
||||
} catch {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { useState, useEffect } from "preact/hooks";
|
||||
|
||||
export default function ChannelSwitcher({ sets = [], title = "" }) {
|
||||
const [activeId, setActiveId] = useState(sets[0]?.id);
|
||||
const [channels, setChannels] = useState([]);
|
||||
|
||||
const active = sets.find((x) => x.id === activeId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
|
||||
fetch(`/api/jambox/${active.p}`)
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
setChannels(data);
|
||||
})
|
||||
.catch(() => setChannels([]));
|
||||
}, [active]);
|
||||
|
||||
return (
|
||||
<div class="w-full">
|
||||
|
||||
{/* SWITCHER */}
|
||||
<div class="flex justify-center mb-10">
|
||||
<div class="f-switch-group">
|
||||
{sets.map((s) => (
|
||||
<button
|
||||
type="button"
|
||||
class={`f-switch ${activeId === s.id ? "active" : ""}`}
|
||||
onClick={() => setActiveId(s.id)}
|
||||
title={title}
|
||||
>
|
||||
{s.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* LISTA KANAŁÓW */}
|
||||
<div class="f-section-channel">
|
||||
|
||||
{channels.length === 0 && (
|
||||
<p class="text-center col-span-full py-1">
|
||||
Ładowanie…
|
||||
</p>
|
||||
)}
|
||||
|
||||
{channels.map((ch) => (
|
||||
<div
|
||||
class="f-channel-box"
|
||||
>
|
||||
<img
|
||||
src={ch.logo}
|
||||
alt={ch.title}
|
||||
class="h-14 object-contain "
|
||||
loading="lazy"
|
||||
/>
|
||||
<p class="text-center text-sm text-[var(--fuz-text)] mt-2">
|
||||
{ch.title}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
//InternetCards.jsx
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import InternetAddonsModal from "./InternetAddonsModal.jsx";
|
||||
import "../../styles/offers/offers-table.css";
|
||||
@@ -14,7 +15,7 @@ export default function InternetDbOffersCards({
|
||||
const [activePlan, setActivePlan] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined" && window.fuzSwitchState) {
|
||||
if (typeof window !== "undefined" && window.fuzSwitchState) {
|
||||
const { selected: sel, labels: labs } = window.fuzSwitchState;
|
||||
if (sel) setSelected(sel);
|
||||
if (labs) setLabels(labs);
|
||||
@@ -46,7 +46,6 @@ export default function FuzMarkdown({ text, ctx = {} }) {
|
||||
|
||||
let processed = applyShortcodes(text, ctx);
|
||||
|
||||
// Konwersja kinków na modal linki
|
||||
processed = processed.replace(
|
||||
/\[([^\]]+)\]\(#([^) "]+)(?:\s+"([^"]+)")?\)/g,
|
||||
(match, label, modalId, title) => {
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import { marked } from "marked";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "../styles/modal.css";
|
||||
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
headerIds: false,
|
||||
mangle: false,
|
||||
smartLists: true,
|
||||
});
|
||||
|
||||
export default function Modal({ modalData }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [title, setTitle] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const clickHandler = (e) => {
|
||||
const el = e.target.closest("[data-modal]");
|
||||
if (!el) return;
|
||||
|
||||
// 🚀 BLOKADA WSZYSTKICH DOMYŚLNYCH ZACHOWAŃ
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const id = el.getAttribute("data-modal");
|
||||
const modal = modalData[id];
|
||||
if (!modal) return;
|
||||
|
||||
setTitle(modal.title || "");
|
||||
setContent((modal.content || "").replace(/\n(?!\n)/g, "\n\n"));
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
document.addEventListener("click", clickHandler, { capture: true });
|
||||
return () =>
|
||||
document.removeEventListener("click", clickHandler, { capture: true });
|
||||
}, [modalData]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div class="fuz-modal-overlay" onClick={() => setOpen(false)}>
|
||||
<button
|
||||
class="fuz-modal-close"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
<div class="fuz-modal-panel" onClick={(e) => e.stopPropagation()}>
|
||||
<div class="fuz-modal-inner">
|
||||
<h2 class="fuz-modal-title">{title}</h2>
|
||||
|
||||
<div
|
||||
class="fuz-modal-content"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: marked(content),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { getPrice } from "../../helpers/getPrice";
|
||||
import { getActiveLabel } from "../../helpers/getActiveLabel";
|
||||
import { getInstallationPrice } from "../../helpers/getInstallationPrice";
|
||||
import "../../styles/offers/offers-table.css";
|
||||
|
||||
export default function OffersCards({ data, switches, selected }) {
|
||||
const plans = data.plany;
|
||||
|
||||
return (
|
||||
<section class="f-offers">
|
||||
{data.plany_title && (
|
||||
<h2 class="f-offers-title">{data.plany_title}</h2>
|
||||
)}
|
||||
|
||||
<div class={`f-offers-grid f-count-${plans.length}`}>
|
||||
{plans.map((plan) => (
|
||||
<OfferCard
|
||||
key={plan.id}
|
||||
plan={plan}
|
||||
features={data.funkcje}
|
||||
switches={switches}
|
||||
selected={selected}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function OfferCard({ plan, features, switches, selected }) {
|
||||
const basePrice = getPrice(plan, switches, selected);
|
||||
|
||||
// NOWE: cena instalacji dynamiczna
|
||||
const installPrice = getInstallationPrice(plan, switches, selected);
|
||||
|
||||
return (
|
||||
<div class={`f-card ${plan.popularny ? "f-card-popular" : ""}`}>
|
||||
{plan.popularny && (
|
||||
<div class="f-card-badge">Najczęściej wybierany</div>
|
||||
)}
|
||||
|
||||
<div class="f-card-header">
|
||||
<div class="f-card-name">{plan.nazwa}</div>
|
||||
<div class="f-card-price">{basePrice} zł/mies.</div>
|
||||
</div>
|
||||
|
||||
<ul class="f-card-features">
|
||||
{features.map((f) => {
|
||||
let val =
|
||||
f.id === "umowa_info"
|
||||
? getActiveLabel(switches, selected, "umowa")
|
||||
: plan.funkcje?.[f.id];
|
||||
|
||||
// PODMIANA pola instalacji — nie ze statycznego YAML tylko dynamicznie
|
||||
if (f.id === "instalacja") {
|
||||
val = installPrice + " zł";
|
||||
}
|
||||
|
||||
return (
|
||||
<li class="f-card-row">
|
||||
<span class="f-card-label">{f.etykieta}</span>
|
||||
<span class="f-card-value">
|
||||
{val === true
|
||||
? "✓"
|
||||
: val === false || val == null
|
||||
? "✕"
|
||||
: val}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import FuzMarkdown from "../Markdown.jsx";
|
||||
import "../../styles/offers/offers-extra.css";
|
||||
|
||||
|
||||
export default function OffersExtraServices({
|
||||
extraServices,
|
||||
openId,
|
||||
toggle,
|
||||
services
|
||||
}) {
|
||||
if (!extraServices.length) return null;
|
||||
|
||||
return (
|
||||
<div class="f-extra-services">
|
||||
<h1 class="f-services-title">{services.title}</h1>
|
||||
<p class="f-services-body">{services.description}</p>
|
||||
|
||||
<div class="f-table-wrapper">
|
||||
<table class="f-table">
|
||||
<thead class="f-table-head">
|
||||
<tr>
|
||||
<th class="f-table-heading">{services.items[0]}</th>
|
||||
<th class="f-table-heading center">{services.items[1]}</th>
|
||||
<th class="f-table-heading center w-32">{services.items[2]}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{extraServices.map((srv, i) => (
|
||||
<>
|
||||
<tr class={i % 2 === 0 ? "f-row-even" : "f-row-odd"}>
|
||||
<td class="f-feature-name">{srv.nazwa}</td>
|
||||
<td class="f-feature-cell center">{srv.cena}</td>
|
||||
<td class="f-feature-cell-btn center">
|
||||
<button class="f-feature-link" onClick={() => toggle(srv.id)} title={openId === srv.id ? "Zwiń opis usługi" : "Rozwiń opis usługi"}>
|
||||
{openId === srv.id ? "Zwiń" : "Przeczytaj ..."}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{openId === srv.id && (
|
||||
<tr>
|
||||
<td colSpan={3} class="f-expand-details">
|
||||
<FuzMarkdown text={srv.opis} ctx={{ kanaly: srv.kanaly }} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
import OffersSwitches from "./Offers/OffersSwitches.jsx";
|
||||
import OffersCards from "./Offers/OffersCards.jsx"; // <-- WAŻNE!!
|
||||
// import OffersExtraServices from "./Offers/OffersExtraServices.jsx";
|
||||
|
||||
export default function OffersIsland({ data }) {
|
||||
const switches = data.przelaczniki ?? [];
|
||||
|
||||
// selected state
|
||||
const [selected, setSelected] = useState(() => {
|
||||
const init = {};
|
||||
switches.forEach((sw) => (init[sw.id] = sw.domyslny));
|
||||
return init;
|
||||
});
|
||||
|
||||
// services accordion (jeśli potrzebne)
|
||||
const [openId, setOpenId] = useState(null);
|
||||
|
||||
const handleSwitch = (switchId, value) => {
|
||||
setSelected((prev) => ({
|
||||
...prev,
|
||||
[switchId]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleExtra = (id) => {
|
||||
setOpenId((prev) => (prev === id ? null : id));
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="f-offers-wrapper">
|
||||
|
||||
{/* SWITCHERY */}
|
||||
<OffersSwitches
|
||||
switches={switches}
|
||||
selected={selected}
|
||||
onSwitch={handleSwitch}
|
||||
/>
|
||||
|
||||
{/* KARTY OFERT */}
|
||||
<OffersCards
|
||||
data={data}
|
||||
switches={switches}
|
||||
selected={selected}
|
||||
/>
|
||||
|
||||
{/* USŁUGI DODATKOWE */}
|
||||
{data.uslugi_dodatkowe && (
|
||||
<OffersExtraServices
|
||||
extraServices={data.uslugi_dodatkowe}
|
||||
services={data.uslugi}
|
||||
openId={openId}
|
||||
toggle={toggleExtra}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// src/islands/JamboxBasePackages.jsx
|
||||
// src/islands/JamboxCards.jsx
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "../../styles/offers/offers-table.css";
|
||||
import JamboxChannelsModal from "./JamboxChannelsModal.jsx";
|
||||
137
src/islands/jambox/JamboxChannelsSearch.jsx
Normal file
137
src/islands/jambox/JamboxChannelsSearch.jsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import "../../styles/channels-search.css";
|
||||
|
||||
export default function JamboxChannelsSearch() {
|
||||
const [q, setQ] = useState("");
|
||||
const [items, setItems] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [err, setErr] = useState("");
|
||||
|
||||
const abortRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const qq = q.trim();
|
||||
setErr("");
|
||||
|
||||
if (qq.length < 2) {
|
||||
setItems([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const t = setTimeout(async () => {
|
||||
try {
|
||||
if (abortRef.current) abortRef.current.abort();
|
||||
const ac = new AbortController();
|
||||
abortRef.current = ac;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set("q", qq);
|
||||
params.set("limit", "80");
|
||||
|
||||
const res = await fetch(`/api/jambox/channels-search?${params.toString()}`, {
|
||||
signal: ac.signal,
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
if (!res.ok || !json.ok) throw new Error(json?.error || "API_ERROR");
|
||||
|
||||
setItems(Array.isArray(json.data) ? json.data : []);
|
||||
} catch (e) {
|
||||
if (e?.name !== "AbortError") {
|
||||
console.error("❌ channels search:", e);
|
||||
setErr("Błąd wyszukiwania.");
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, 250);
|
||||
|
||||
return () => clearTimeout(t);
|
||||
}, [q]);
|
||||
|
||||
const meta = useMemo(() => {
|
||||
const qq = q.trim();
|
||||
if (qq.length < 2) return "Wpisz min. 2 znaki";
|
||||
if (loading) return "Szukam…";
|
||||
if (err) return err;
|
||||
return `Znaleziono: ${items.length}`;
|
||||
}, [q, loading, err, items]);
|
||||
|
||||
return (
|
||||
<div class="fuz-chsearch">
|
||||
<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">
|
||||
{meta}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fuz-chsearch__list" role="list">
|
||||
{items.map((c) => (
|
||||
<div class="fuz-chsearch__row" role="listitem" key={`${c.name}-${c.logo_url || ""}`}>
|
||||
{/* kolumna 1 */}
|
||||
<div class="fuz-chsearch__left">
|
||||
{c.logo_url && (
|
||||
<img
|
||||
src={c.logo_url}
|
||||
alt={c.name}
|
||||
class="fuz-chsearch__logo"
|
||||
loading="lazy"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div class="fuz-chsearch__channel-name">
|
||||
{c.name}
|
||||
</div>
|
||||
|
||||
<div class="fuz-chsearch__channel-number">
|
||||
kanał {c.min_number || "—"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* kolumna 2 */}
|
||||
<div class="fuz-chsearch__right">
|
||||
<div
|
||||
class="fuz-chsearch__desc fuz-chsearch__desc--html"
|
||||
dangerouslySetInnerHTML={{ __html: c.description || "<em>—</em>" }}
|
||||
/>
|
||||
|
||||
{Array.isArray(c.packages) && c.packages.length > 0 && (
|
||||
<div class="fuz-chsearch__packages">
|
||||
Dostępny w:
|
||||
{c.packages.map((p, i) => (
|
||||
<span class="fuz-chsearch__pkg" key={p.id}>
|
||||
{p.name}{" "}
|
||||
<span class="fuz-chsearch__pkgnum">({p.number})</span>
|
||||
{i < c.packages.length - 1 ? ", " : ""}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{q.trim().length >= 2 && !loading && items.length === 0 && (
|
||||
<div class="fuz-chsearch__empty">
|
||||
Brak wyników dla: <strong>{q}</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export default function PhoneDbOffersCards({
|
||||
setPlans(Array.isArray(json.data) ? json.data : []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd pobierania planów telefonii:", err);
|
||||
console.error("Błąd pobierania planów telefonii:", err);
|
||||
if (!cancelled) {
|
||||
setError("Nie udało się załadować pakietów telefonicznych.");
|
||||
}
|
||||
|
||||
@@ -2,27 +2,21 @@
|
||||
import yaml from "js-yaml";
|
||||
import fs from "fs";
|
||||
|
||||
// SEO z YAML (np. import seo from "../content/seo/home.yaml")
|
||||
const seo = Astro.props.seo ?? {};
|
||||
|
||||
// Global SEO (site + company)
|
||||
const globalSeo = yaml.load(
|
||||
fs.readFileSync("./src/content/seo/home.yaml", "utf8")
|
||||
fs.readFileSync("./src/content/home/seo.yaml", "utf8")
|
||||
);
|
||||
|
||||
const { site, company } = globalSeo;
|
||||
|
||||
// Page SEO (sekcja "page" w YAML)
|
||||
const page = seo.page ?? {};
|
||||
|
||||
// FINAL VALUES
|
||||
const title = page.title ?? site.name;
|
||||
const description = page.description ?? site.description;
|
||||
const image = page.image ?? site.logo;
|
||||
const canonical = site.url + (page.url ?? "/");
|
||||
const keywords = page.keywords ?? [];
|
||||
|
||||
// Extra structured data (optional)
|
||||
const extraSchema = page.schema ?? null;
|
||||
|
||||
// JSON-LD objects
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
export async function POST({ request }) {
|
||||
try {
|
||||
const form = await request.json();
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: import.meta.env.SMTP_HOST,
|
||||
port: Number(import.meta.env.SMTP_PORT),
|
||||
secure: true,
|
||||
secure: true, // true = 465, false = 587
|
||||
auth: {
|
||||
user: import.meta.env.SMTP_USER,
|
||||
pass: import.meta.env.SMTP_PASS,
|
||||
},
|
||||
tls: { rejectUnauthorized: false }
|
||||
// ⚠️ tylko jeśli masz self-signed / dziwny cert
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
});
|
||||
|
||||
await transporter.sendMail({
|
||||
@@ -32,10 +34,17 @@ ${form.message}
|
||||
`.trim(),
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
||||
return new Response(
|
||||
JSON.stringify({ ok: true }),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error("MAIL ERROR:", error);
|
||||
return new Response(JSON.stringify({ ok: false }), { status: 500 });
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false }),
|
||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
91
src/pages/api/jambox/channels-search.js
Normal file
91
src/pages/api/jambox/channels-search.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const DB_PATH = process.env.FUZ_DB_PATH || "./src/data/ServicesRange.db";
|
||||
|
||||
function getDb() {
|
||||
return new Database(DB_PATH, { readonly: true });
|
||||
}
|
||||
|
||||
function clamp(n, min, max) {
|
||||
return Math.max(min, Math.min(max, n));
|
||||
}
|
||||
|
||||
export function GET({ url }) {
|
||||
const q = (url.searchParams.get("q") || "").trim();
|
||||
const limit = clamp(Number(url.searchParams.get("limit") || 50), 1, 200);
|
||||
|
||||
if (q.length < 2) {
|
||||
return new Response(JSON.stringify({ ok: true, data: [] }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
|
||||
const safe = q.replace(/[%_]/g, (m) => `\\${m}`);
|
||||
const like = `%${safe}%`;
|
||||
|
||||
const db = getDb();
|
||||
|
||||
try {
|
||||
// ✅ 1 rekord na kanał (grupujemy po name+logo_url)
|
||||
// ✅ pakiety zbieramy do jednego pola packages_blob
|
||||
// UWAGA: zakładam, że jambox_base_packages ma kolumnę "name"
|
||||
const rows = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
c.name,
|
||||
c.logo_url,
|
||||
MAX(c.description) AS description,
|
||||
MIN(c.number) AS min_number,
|
||||
GROUP_CONCAT(
|
||||
p.id || '::' || p.name || '::' || c.number || '::' || c.guaranteed,
|
||||
'||'
|
||||
) AS packages_blob
|
||||
FROM jambox_package_channels c
|
||||
JOIN jambox_base_packages p ON p.id = c.package_id
|
||||
WHERE
|
||||
c.name LIKE ? ESCAPE '\\'
|
||||
GROUP BY c.name, c.logo_url
|
||||
ORDER BY min_number ASC, c.name ASC
|
||||
LIMIT ?;
|
||||
`.trim()
|
||||
)
|
||||
.all(like, limit);
|
||||
|
||||
const data = rows.map((r) => {
|
||||
const packages = String(r.packages_blob || "")
|
||||
.split("||")
|
||||
.filter(Boolean)
|
||||
.map((s) => {
|
||||
const [id, name, number, guaranteed] = s.split("::");
|
||||
return {
|
||||
id: Number(id),
|
||||
name,
|
||||
number: Number(number),
|
||||
guaranteed: Number(guaranteed) === 1,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.id - b.id);
|
||||
|
||||
return {
|
||||
name: r.name,
|
||||
logo_url: r.logo_url,
|
||||
description: r.description || "",
|
||||
min_number: Number(r.min_number || 0),
|
||||
packages,
|
||||
};
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ ok: true, data }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd w /api/jambox/channels-search:", err);
|
||||
return new Response(JSON.stringify({ ok: false, error: "DB_ERROR" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
}
|
||||
23
src/pages/dokumenty/index.astro
Normal file
23
src/pages/dokumenty/index.astro
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||
|
||||
const seo = {
|
||||
title: "Oferta – FUZ",
|
||||
description: "Oferta – FUZ",
|
||||
canonical: "/oferta",
|
||||
};
|
||||
---
|
||||
|
||||
<DefaultLayout seo={seo}>
|
||||
<section class="f-section">
|
||||
<div class="f-section-grid-single md:grid-cols-1">
|
||||
<h1 class="f-section-title">Dokumenty</h1>
|
||||
<div class="fuz-markdown max-w-none">
|
||||
<p>
|
||||
Ta podstrona jest na razie szkieletem. Możemy tu później wczytać
|
||||
treść.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</DefaultLayout>
|
||||
@@ -2,17 +2,15 @@
|
||||
import DefaultLayout from "../layouts/DefaultLayout.astro";
|
||||
import Hero from "../components/hero/Hero.astro";
|
||||
import SectionRenderer from "../components/sections/SectionRenderer.astro"
|
||||
import SectionContact from "../components/sections/SectionContact.astro";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import fs from "fs";
|
||||
|
||||
const seo = yaml.load(fs.readFileSync("./src/content/seo/home.yaml", "utf8"));
|
||||
const seo = yaml.load(fs.readFileSync("./src/content/home/seo.yaml", "utf8"));
|
||||
const hero = yaml.load(fs.readFileSync("./src/content/home/hero.yaml", "utf8"));
|
||||
---
|
||||
|
||||
<DefaultLayout seo={seo}>
|
||||
<Hero {...hero} />
|
||||
<SectionRenderer src="./src/content/site/site.section.yaml" />
|
||||
<!-- <SectionContact /> -->
|
||||
</DefaultLayout>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||
import OffersSwitches from "../../islands/Offers/OffersSwitches.jsx";
|
||||
import InternetDbOffersCards from "../../islands/Internet/OffersInternetCards.jsx";
|
||||
import OffersSwitches from "../../islands/OffersSwitches.jsx";
|
||||
import InternetCards from "../../islands/Internet/InternetCards.jsx";
|
||||
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
@@ -20,7 +20,7 @@ const seo = yaml.load(
|
||||
<p>Wybierz rodzaj budynku i czas trwania umowy</p>
|
||||
</div>
|
||||
<OffersSwitches client:load />
|
||||
<InternetDbOffersCards client:load />
|
||||
<InternetCards client:load />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
---
|
||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||
import OffersSwitches from "../../islands/Offers/OffersSwitches.jsx";
|
||||
import OffersJamboxCards from "../../islands/jambox/OffersJamboxCards.jsx";
|
||||
|
||||
import Hero from "../../components/hero/Hero.astro";
|
||||
import OffersSwitches from "../../islands/OffersSwitches.jsx";
|
||||
import JamboxCards from "../../islands/jambox/JamboxCards.jsx";
|
||||
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
||||
import Markdown from "../../islands/Markdown.jsx";
|
||||
import Modal from "../../islands/Modal.jsx";
|
||||
import JamboxMozliwosci from "../../components/sections/SectionJamboxMozliwosci.astro";
|
||||
import SectionChannelsSearch from "../../components/sections/SectionChannelsSearch.astro"
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import fs from "fs";
|
||||
@@ -15,66 +11,20 @@ import fs from "fs";
|
||||
const seo = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-telewizja/seo.yaml", "utf8"),
|
||||
);
|
||||
const hero = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-telewizja/hero.yaml", "utf8"),
|
||||
);
|
||||
const page = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-telewizja/page.yaml", "utf8"),
|
||||
);
|
||||
const modalData = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-telewizja/modal.yaml", "utf8"),
|
||||
);
|
||||
|
||||
type Paragraph = {
|
||||
title?: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
const data = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-telewizja/offers.yaml", "utf8"),
|
||||
);
|
||||
const first = page.paragraphs[0];
|
||||
const rest = page.paragraphs.slice(1);
|
||||
---
|
||||
|
||||
<DefaultLayout seo={seo}>
|
||||
<!-- <Hero {...hero} /> -->
|
||||
|
||||
<!-- <section class="f-section">
|
||||
<div class="f-section-grid-single md:grid-cols-1">
|
||||
{page.title.map((line: any) => <h1 class="f-section-title">{line}</h1>)}
|
||||
{first.title && <h3>{first.title}</h3>}
|
||||
<Markdown text={first.content} />
|
||||
</div>
|
||||
</section> -->
|
||||
|
||||
<section class="f-section">
|
||||
<div class="f-section-grid-single md:grid-cols-1">
|
||||
<div class="f-section-grid-single md:grid-cols-1">
|
||||
<h1 class="f-section-title">Telewizja z interentem</h1>
|
||||
<div class="fuz-markdown max-w-none">
|
||||
<p>Wybierz rodzaj budynku i czas trwania umowy</p>
|
||||
</div>
|
||||
<OffersSwitches client:load />
|
||||
<OffersJamboxCards client:load />
|
||||
|
||||
<!-- <OffersIsland client:load data={data} /> -->
|
||||
<JamboxCards client:load />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- {
|
||||
rest.map((p: Paragraph) => (
|
||||
<section class="f-section">
|
||||
<div class="f-section-grid-single md:grid-cols-1">
|
||||
{p.title && <h3 class="f-section-title">{p.title}</h3>}
|
||||
<Markdown text={p.content.replace(/\n/g, "\n\n")} />
|
||||
</div>
|
||||
</section>
|
||||
))
|
||||
} -->
|
||||
|
||||
<SectionRenderer src="./src/content/internet-telewizja/section.yaml" />
|
||||
|
||||
<!-- <JamboxMozliwosci /> -->
|
||||
|
||||
<!-- <Modal client:load modalData={modalData} /> -->
|
||||
<SectionChannelsSearch/>
|
||||
</DefaultLayout>
|
||||
|
||||
@@ -1,14 +1,179 @@
|
||||
---
|
||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||
import SectionContact from "../../components/sections/SectionContact.astro";
|
||||
import MapGoogle from "../../components/maps/MapGoogle.astro";
|
||||
|
||||
const seo = {
|
||||
title: "Kontakt – FUZ",
|
||||
description: "Kontakt – FUZ",
|
||||
canonical: "/kontakt"
|
||||
};
|
||||
import yaml from "js-yaml";
|
||||
import fs from "fs";
|
||||
|
||||
const data = yaml.load(
|
||||
fs.readFileSync("./src/content/contact/contact.yaml", "utf8"),
|
||||
);
|
||||
|
||||
const seo = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-swiatlowodowy/seo.yaml", "utf8"),
|
||||
);
|
||||
|
||||
const apiKey = import.meta.env.PUBLIC_GOOGLE_MAPS_KEY;
|
||||
const form = data.form;
|
||||
---
|
||||
|
||||
<DefaultLayout seo={seo}>
|
||||
<SectionContact />
|
||||
<section id="kontakt" class="f-section">
|
||||
<div class="f-contact-grid">
|
||||
<!-- Kolumna lewa -->
|
||||
<div class="f-contact-col-1">
|
||||
<h2>{data.title}</h2>
|
||||
<div class="f-contact-item" set:html={data.description} />
|
||||
</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>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
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 title={form.submit.title}>{form.submit.label}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="f-contact-map">
|
||||
<MapGoogle
|
||||
apiKey={apiKey}
|
||||
lat={data.lat}
|
||||
lon={data.lng}
|
||||
zoom={16}
|
||||
title={data.markerTitle}
|
||||
description={data.markerAddress}
|
||||
showMarker={true}
|
||||
mode="contact"
|
||||
mapStyleId={data.maps.mapId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="f-toast"></div>
|
||||
</section>
|
||||
|
||||
<!-- ReCaptcha v3 -->
|
||||
<script
|
||||
is:inline
|
||||
define:vars={{ siteKey: import.meta.env.PUBLIC_RECAPTCHA_SITE_KEY }}
|
||||
>
|
||||
window.FUZ_RECAPTCHA_KEY = siteKey;
|
||||
|
||||
const s = document.createElement("script");
|
||||
s.src = "https://www.google.com/recaptcha/api.js?render=" + siteKey;
|
||||
s.async = true;
|
||||
document.head.appendChild(s);
|
||||
</script>
|
||||
|
||||
<script
|
||||
is:inline
|
||||
define:vars={{
|
||||
successMsg: form.successMessage,
|
||||
errorMsg: form.errorMessage,
|
||||
}}
|
||||
>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const form = document.getElementById("contactForm");
|
||||
const toast = document.getElementById("toast");
|
||||
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
if (!form.reportValidity()) return;
|
||||
e.preventDefault();
|
||||
|
||||
const data = Object.fromEntries(new FormData(form).entries());
|
||||
data.rodo = form.rodo.checked;
|
||||
|
||||
const token = await grecaptcha.execute(window.FUZ_RECAPTCHA_KEY, {
|
||||
action: "submit",
|
||||
});
|
||||
data.recaptcha = token;
|
||||
|
||||
const resp = await fetch("/api/contact/contact", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
const json = await resp.json();
|
||||
|
||||
showToast(
|
||||
json.ok ? successMsg : errorMsg,
|
||||
json.ok ? "success" : "error",
|
||||
);
|
||||
|
||||
if (json.ok) form.reset();
|
||||
});
|
||||
|
||||
function showToast(msg, type) {
|
||||
toast.innerHTML = `<div class="f-toast-msg ${type}">${msg}</div>`;
|
||||
toast.classList.remove("visible");
|
||||
void toast.offsetWidth;
|
||||
toast.classList.add("visible");
|
||||
|
||||
setTimeout(() => toast.classList.remove("visible"), 3000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</DefaultLayout>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||
|
||||
const seo = {
|
||||
title: "Oferta – FUZ",
|
||||
description: "Oferta – FUZ",
|
||||
canonical: "/oferta"
|
||||
};
|
||||
---
|
||||
|
||||
<DefaultLayout seo={seo}>
|
||||
<section class="fuz-section">
|
||||
<div class="fuz-container">
|
||||
<h1 class="fuz-hero-title">Oferta – FUZ</h1>
|
||||
<p class="mt-4 text-gray-600 dark:text-gray-300">
|
||||
Ta podstrona jest na razie szkieletem. Możemy tu później wczytać treść z YAML.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</DefaultLayout>
|
||||
94
src/styles/channels-search.css
Normal file
94
src/styles/channels-search.css
Normal file
@@ -0,0 +1,94 @@
|
||||
.fuz-chsearch {
|
||||
@apply mt-6;
|
||||
}
|
||||
|
||||
.fuz-chsearch__top {
|
||||
@apply flex flex-col gap-1 mb-4;
|
||||
}
|
||||
|
||||
.fuz-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];
|
||||
}
|
||||
|
||||
.fuz-chsearch__meta {
|
||||
@apply text-sm opacity-70 pl-1;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
List + Row layout
|
||||
========================== */
|
||||
.fuz-chsearch__list {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
|
||||
/* ✅ węższa pierwsza kolumna */
|
||||
.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;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Column 1: Channel card
|
||||
========================== */
|
||||
.fuz-chsearch__left {
|
||||
@apply flex flex-col items-center text-center gap-1 px-2;
|
||||
}
|
||||
|
||||
.fuz-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;
|
||||
}
|
||||
|
||||
.fuz-chsearch__channel-name {
|
||||
@apply font-semibold text-[--fuz-header] leading-tight;
|
||||
}
|
||||
|
||||
.fuz-chsearch__channel-number {
|
||||
@apply text-sm opacity-70;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Column 2: Description + Packages
|
||||
========================== */
|
||||
.fuz-chsearch__right {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
|
||||
/* opis NIE ucinany */
|
||||
.fuz-chsearch__desc {
|
||||
@apply text-sm md:text-base opacity-90;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* HTML jak w modalu */
|
||||
.fuz-chsearch__desc--html :global(p) {
|
||||
@apply mb-2;
|
||||
}
|
||||
|
||||
.fuz-chsearch__desc--html :global(ol),
|
||||
.fuz-chsearch__desc--html :global(ul) {
|
||||
@apply pl-6 mb-2;
|
||||
}
|
||||
|
||||
.fuz-chsearch__desc--html :global(li) {
|
||||
@apply mb-1;
|
||||
}
|
||||
|
||||
/* pakiety (bez gwarantowanych) */
|
||||
.fuz-chsearch__packages {
|
||||
@apply text-sm opacity-80;
|
||||
}
|
||||
|
||||
.fuz-chsearch__pkg {
|
||||
@apply inline text-[--btn-background];
|
||||
}
|
||||
|
||||
.fuz-chsearch__pkgnum {
|
||||
@apply opacity-70;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Empty state
|
||||
========================== */
|
||||
.fuz-chsearch__empty {
|
||||
@apply mt-2 p-4 rounded-2xl border border-slate-200 dark:border-slate-700 opacity-80;
|
||||
}
|
||||
Reference in New Issue
Block a user