Porządkowanie kodu, dodanie sekcji wyszukiwania kanałów

This commit is contained in:
dm
2025-12-12 19:48:53 +01:00
parent bf67147cf5
commit 5822237745
47 changed files with 17203 additions and 15686 deletions

View File

@@ -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;

View File

@@ -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",

View 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>

View File

@@ -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>

View File

@@ -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">
@@ -63,4 +61,4 @@ if (section.image) {
}
</div>
</div>
</section>
</section>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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} />;
})}

View File

@@ -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>
)}

View File

@@ -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>