Porządkowanie kodu, dodanie sekcji wyszukiwania kanałów
This commit is contained in:
@@ -25,16 +25,13 @@ const {
|
|||||||
ctas = []
|
ctas = []
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
// Wyciągnij nazwę bazową bez rozszerzenia
|
|
||||||
const imageBase = imageUrl.replace(/\.(webp|png|jpg|jpeg)$/i, '');
|
const imageBase = imageUrl.replace(/\.(webp|png|jpg|jpeg)$/i, '');
|
||||||
|
|
||||||
// Importuj wszystkie obrazki
|
|
||||||
const images = import.meta.glob<{ default: ImageMetadata }>(
|
const images = import.meta.glob<{ default: ImageMetadata }>(
|
||||||
'/src/assets/hero/**/*.webp',
|
'/src/assets/hero/**/*.webp',
|
||||||
{ eager: true }
|
{ eager: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Funkcja do znajdowania obrazka dla danego rozmiaru
|
|
||||||
function findImage(folder: string): ImageMetadata | null {
|
function findImage(folder: string): ImageMetadata | null {
|
||||||
const key = `/src/assets/hero/${folder}/${imageBase}-${folder}.webp`;
|
const key = `/src/assets/hero/${folder}/${imageBase}-${folder}.webp`;
|
||||||
return images[key]?.default || null;
|
return images[key]?.default || null;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const links = [
|
|||||||
{ name: "TELEFON", href: "/telefon" },
|
{ name: "TELEFON", href: "/telefon" },
|
||||||
{ name: "ZASIĘG SIECI", href: "/mapa-zasiegu" },
|
{ name: "ZASIĘG SIECI", href: "/mapa-zasiegu" },
|
||||||
{ name: "KONTAKT", href: "/kontakt" },
|
{ name: "KONTAKT", href: "/kontakt" },
|
||||||
|
{ name: "DOKUMENTY", href: "/dokumenty" },
|
||||||
{
|
{
|
||||||
name: "BOK",
|
name: "BOK",
|
||||||
href: "https://panel.fuz.pl/userpanel/auth",
|
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
|
<div
|
||||||
class={`f-section-grid ${hasImage ? (reverse ? "md:order-2" : "md:order-1") : ""}`}
|
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>
|
<h2 class="f-section-title">{section.title}</h2>
|
||||||
|
|
||||||
<Markdown text={section.content} />
|
<Markdown text={section.content} />
|
||||||
|
|
||||||
{
|
{
|
||||||
section.button && (
|
section.button && (
|
||||||
<div class="f-section-nav">
|
<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 { marked } from "marked";
|
||||||
|
|
||||||
import SectionDefault from "./SectionDefault.astro";
|
import SectionDefault from "./SectionDefault.astro";
|
||||||
import SectionIframeChannels from "./SectionIframeChannels.astro";
|
|
||||||
|
|
||||||
const { src } = Astro.props;
|
const { src } = Astro.props;
|
||||||
|
|
||||||
@@ -19,9 +19,5 @@ const sections = (data.sections as any[]).map((s: any) => ({
|
|||||||
{sections.map((section: any, index: number) => {
|
{sections.map((section: any, index: number) => {
|
||||||
const type = section.type || "default";
|
const type = section.type || "default";
|
||||||
|
|
||||||
if (type === "iframe-channels") {
|
|
||||||
return <SectionIframeChannels section={section} index={index} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <SectionDefault 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"
|
href: "/internet-telewizja"
|
||||||
title: "Przejdź do oferty Internet + Telewizja w FUZ"
|
title: "Przejdź do oferty Internet + Telewizja w FUZ"
|
||||||
primary: false
|
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:
|
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
|
- title: Router WiFi HL-4BX3V-F
|
||||||
image: "HL-4BX3V-F.webp"
|
image: "HL-4BX3V-F.webp"
|
||||||
content: |
|
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:
|
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"
|
- title: "Dekoder telewizyjny"
|
||||||
image: "VIP4302.png"
|
image: "VIP4302.png"
|
||||||
content: |
|
content: |
|
||||||
@@ -32,27 +17,3 @@ sections:
|
|||||||
- Interfejsy tylnego panelu obejmują m.in. Ethernet, USB, HDMI, CVBS, Optyczny i analogowy audio 3,5mm
|
- 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
|
- Przedni panel zawiera m.in. diodę LED i odbiornik podczerwieni
|
||||||
- Wymiary modelu (szer/dł/wys): 130 x 130 x 26 mm
|
- 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
|
dimmed: true
|
||||||
button:
|
button:
|
||||||
text: "Zobacz ofertę telefonu →"
|
text: "Zobacz ofertę telefonu →"
|
||||||
url: "/internet-telewizja/"
|
url: "/telefon/"
|
||||||
title: "Przejdź do oferty telefonu"
|
title: "Przejdź do oferty telefonu"
|
||||||
content: |
|
content: |
|
||||||
Nasza telefonia wykorzystuje zaawansowaną technologię VoIP, dzięki której dźwięk jest wyraźny, a połączenia stabilne.
|
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 { useEffect, useState } from "preact/hooks";
|
||||||
import InternetAddonsModal from "./InternetAddonsModal.jsx";
|
import InternetAddonsModal from "./InternetAddonsModal.jsx";
|
||||||
import "../../styles/offers/offers-table.css";
|
import "../../styles/offers/offers-table.css";
|
||||||
@@ -46,7 +46,6 @@ export default function FuzMarkdown({ text, ctx = {} }) {
|
|||||||
|
|
||||||
let processed = applyShortcodes(text, ctx);
|
let processed = applyShortcodes(text, ctx);
|
||||||
|
|
||||||
// Konwersja kinków na modal linki
|
|
||||||
processed = processed.replace(
|
processed = processed.replace(
|
||||||
/\[([^\]]+)\]\(#([^) "]+)(?:\s+"([^"]+)")?\)/g,
|
/\[([^\]]+)\]\(#([^) "]+)(?:\s+"([^"]+)")?\)/g,
|
||||||
(match, label, modalId, title) => {
|
(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 { useEffect, useState } from "preact/hooks";
|
||||||
import "../../styles/offers/offers-table.css";
|
import "../../styles/offers/offers-table.css";
|
||||||
import JamboxChannelsModal from "./JamboxChannelsModal.jsx";
|
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 : []);
|
setPlans(Array.isArray(json.data) ? json.data : []);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ Błąd pobierania planów telefonii:", err);
|
console.error("Błąd pobierania planów telefonii:", err);
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setError("Nie udało się załadować pakietów telefonicznych.");
|
setError("Nie udało się załadować pakietów telefonicznych.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,27 +2,21 @@
|
|||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
|
||||||
// SEO z YAML (np. import seo from "../content/seo/home.yaml")
|
|
||||||
const seo = Astro.props.seo ?? {};
|
const seo = Astro.props.seo ?? {};
|
||||||
|
|
||||||
// Global SEO (site + company)
|
|
||||||
const globalSeo = yaml.load(
|
const globalSeo = yaml.load(
|
||||||
fs.readFileSync("./src/content/seo/home.yaml", "utf8")
|
fs.readFileSync("./src/content/home/seo.yaml", "utf8")
|
||||||
);
|
);
|
||||||
|
|
||||||
const { site, company } = globalSeo;
|
const { site, company } = globalSeo;
|
||||||
|
|
||||||
// Page SEO (sekcja "page" w YAML)
|
|
||||||
const page = seo.page ?? {};
|
const page = seo.page ?? {};
|
||||||
|
|
||||||
// FINAL VALUES
|
|
||||||
const title = page.title ?? site.name;
|
const title = page.title ?? site.name;
|
||||||
const description = page.description ?? site.description;
|
const description = page.description ?? site.description;
|
||||||
const image = page.image ?? site.logo;
|
const image = page.image ?? site.logo;
|
||||||
const canonical = site.url + (page.url ?? "/");
|
const canonical = site.url + (page.url ?? "/");
|
||||||
const keywords = page.keywords ?? [];
|
const keywords = page.keywords ?? [];
|
||||||
|
|
||||||
// Extra structured data (optional)
|
|
||||||
const extraSchema = page.schema ?? null;
|
const extraSchema = page.schema ?? null;
|
||||||
|
|
||||||
// JSON-LD objects
|
// JSON-LD objects
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import type { APIRoute } from "astro";
|
|
||||||
import nodemailer from "nodemailer";
|
import nodemailer from "nodemailer";
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export async function POST({ request }) {
|
||||||
try {
|
try {
|
||||||
const form = await request.json();
|
const form = await request.json();
|
||||||
|
|
||||||
const transporter = nodemailer.createTransport({
|
const transporter = nodemailer.createTransport({
|
||||||
host: import.meta.env.SMTP_HOST,
|
host: import.meta.env.SMTP_HOST,
|
||||||
port: Number(import.meta.env.SMTP_PORT),
|
port: Number(import.meta.env.SMTP_PORT),
|
||||||
secure: true,
|
secure: true, // true = 465, false = 587
|
||||||
auth: {
|
auth: {
|
||||||
user: import.meta.env.SMTP_USER,
|
user: import.meta.env.SMTP_USER,
|
||||||
pass: import.meta.env.SMTP_PASS,
|
pass: import.meta.env.SMTP_PASS,
|
||||||
},
|
},
|
||||||
tls: { rejectUnauthorized: false }
|
// ⚠️ tylko jeśli masz self-signed / dziwny cert
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await transporter.sendMail({
|
await transporter.sendMail({
|
||||||
@@ -32,10 +34,17 @@ ${form.message}
|
|||||||
`.trim(),
|
`.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) {
|
} catch (error) {
|
||||||
console.error("MAIL ERROR:", 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 DefaultLayout from "../layouts/DefaultLayout.astro";
|
||||||
import Hero from "../components/hero/Hero.astro";
|
import Hero from "../components/hero/Hero.astro";
|
||||||
import SectionRenderer from "../components/sections/SectionRenderer.astro"
|
import SectionRenderer from "../components/sections/SectionRenderer.astro"
|
||||||
import SectionContact from "../components/sections/SectionContact.astro";
|
|
||||||
|
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import fs from "fs";
|
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"));
|
const hero = yaml.load(fs.readFileSync("./src/content/home/hero.yaml", "utf8"));
|
||||||
---
|
---
|
||||||
|
|
||||||
<DefaultLayout seo={seo}>
|
<DefaultLayout seo={seo}>
|
||||||
<Hero {...hero} />
|
<Hero {...hero} />
|
||||||
<SectionRenderer src="./src/content/site/site.section.yaml" />
|
<SectionRenderer src="./src/content/site/site.section.yaml" />
|
||||||
<!-- <SectionContact /> -->
|
|
||||||
</DefaultLayout>
|
</DefaultLayout>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||||
import OffersSwitches from "../../islands/Offers/OffersSwitches.jsx";
|
import OffersSwitches from "../../islands/OffersSwitches.jsx";
|
||||||
import InternetDbOffersCards from "../../islands/Internet/OffersInternetCards.jsx";
|
import InternetCards from "../../islands/Internet/InternetCards.jsx";
|
||||||
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
||||||
|
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
@@ -20,7 +20,7 @@ const seo = yaml.load(
|
|||||||
<p>Wybierz rodzaj budynku i czas trwania umowy</p>
|
<p>Wybierz rodzaj budynku i czas trwania umowy</p>
|
||||||
</div>
|
</div>
|
||||||
<OffersSwitches client:load />
|
<OffersSwitches client:load />
|
||||||
<InternetDbOffersCards client:load />
|
<InternetCards client:load />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
---
|
---
|
||||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||||
import OffersSwitches from "../../islands/Offers/OffersSwitches.jsx";
|
import OffersSwitches from "../../islands/OffersSwitches.jsx";
|
||||||
import OffersJamboxCards from "../../islands/jambox/OffersJamboxCards.jsx";
|
import JamboxCards from "../../islands/jambox/JamboxCards.jsx";
|
||||||
|
|
||||||
import Hero from "../../components/hero/Hero.astro";
|
|
||||||
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
||||||
import Markdown from "../../islands/Markdown.jsx";
|
import SectionChannelsSearch from "../../components/sections/SectionChannelsSearch.astro"
|
||||||
import Modal from "../../islands/Modal.jsx";
|
|
||||||
import JamboxMozliwosci from "../../components/sections/SectionJamboxMozliwosci.astro";
|
|
||||||
|
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
@@ -15,39 +11,9 @@ import fs from "fs";
|
|||||||
const seo = yaml.load(
|
const seo = yaml.load(
|
||||||
fs.readFileSync("./src/content/internet-telewizja/seo.yaml", "utf8"),
|
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}>
|
<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">
|
<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>
|
<h1 class="f-section-title">Telewizja z interentem</h1>
|
||||||
@@ -55,26 +21,10 @@ const rest = page.paragraphs.slice(1);
|
|||||||
<p>Wybierz rodzaj budynku i czas trwania umowy</p>
|
<p>Wybierz rodzaj budynku i czas trwania umowy</p>
|
||||||
</div>
|
</div>
|
||||||
<OffersSwitches client:load />
|
<OffersSwitches client:load />
|
||||||
<OffersJamboxCards client:load />
|
<JamboxCards client:load />
|
||||||
|
|
||||||
<!-- <OffersIsland client:load data={data} /> -->
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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" />
|
<SectionRenderer src="./src/content/internet-telewizja/section.yaml" />
|
||||||
|
<SectionChannelsSearch/>
|
||||||
<!-- <JamboxMozliwosci /> -->
|
|
||||||
|
|
||||||
<!-- <Modal client:load modalData={modalData} /> -->
|
|
||||||
</DefaultLayout>
|
</DefaultLayout>
|
||||||
|
|||||||
@@ -1,14 +1,179 @@
|
|||||||
---
|
---
|
||||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||||
import SectionContact from "../../components/sections/SectionContact.astro";
|
import MapGoogle from "../../components/maps/MapGoogle.astro";
|
||||||
|
|
||||||
const seo = {
|
import yaml from "js-yaml";
|
||||||
title: "Kontakt – FUZ",
|
import fs from "fs";
|
||||||
description: "Kontakt – FUZ",
|
|
||||||
canonical: "/kontakt"
|
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}>
|
<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>
|
</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