Files
fuz-site/astro-pages-dump.txt
2025-12-20 14:56:16 +01:00

1426 lines
38 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
ASTRO DUMP (root: D:\DARIUSZM\Desktop\fuz-site\src\pages)
Found files: 11
================================================================================
FILE: src/pages/dokumenty/[slug].astro
--------------------------------------------------------------------------------
---
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import Markdown from "../../islands/Markdown.jsx";
import { marked } from "marked";
import { getDocumentBySlug } from "../../lib/documents";
import "../../styles/document.css";
const { slug } = Astro.params;
const doc = slug ? getDocumentBySlug(slug) : null;
if (!doc || doc.visible !== true) {
return Astro.redirect("/dokumenty", 302);
}
const html = marked.parse(doc.content);
---
<DefaultLayout title={doc.title}>
<section class="f-section">
<div class="f-section-grid-single">
<a href="/dokumenty" class="f-document-link">
← Wróć do dokumentów
</a>
<h1 class="f-section-title">
{doc.title}
</h1>
<div class="fuz-markdown max-w-none">
<Markdown text={html} />
</div>
</div>
</section>
</DefaultLayout>
================================================================================
FILE: src/pages/dokumenty/index.astro
--------------------------------------------------------------------------------
---
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import yaml from "js-yaml";
import fs from "node:fs";
import Markdown from "../../islands/Markdown.jsx";
import "../../styles/document.css";
type DocFile = {
nazwa?: string;
file?: string;
slug?: string;
};
type DocGroup = {
tytul?: string;
pliki?: DocFile[];
};
type DocsYaml = {
tytul?: string;
opis?: string;
grupy?: Record<string, DocGroup>;
};
const doc = yaml.load(
fs.readFileSync("./src/content/document/documents.yaml", "utf8"),
) as DocsYaml;
const pageTitle = doc?.tytul;
const pageDesc = doc?.opis;
const groups = doc?.grupy ?? {};
const left = groups["otworz"] ?? {};
const right = groups["pobierz"] ?? {};
function normalizePublicHref(input?: string) {
let s = String(input ?? "").trim();
if (!s) return "";
if (s.startsWith("/public/")) s = s.replace("/public", "");
s = s.replace(/ /g, "%20");
return s;
}
---
<DefaultLayout title={pageTitle} description={pageDesc}>
<section class="f-section">
<div class="f-section-grid-top md:grid-cols-2 gap-10">
<div>
<h3 class="f-section-title mt-0">{left.tytul ?? "Przeczytaj"}</h3>
{!left.pliki?.length ? (
<p class="opacity-70 mt-4">Brak dokumentów.</p>
) : (
<div class="f-documents-grid">
{left.pliki.map((p) => (
p.slug ? (
<a
class="f-document-card"
href={`/dokumenty/${p.slug}`}
title={p.nazwa}
>
<div class="f-document-title">{p.nazwa}</div>
</a>
) : null
))}
</div>
)}
</div>
<div>
<h3 class="f-section-title mt-0">{right.tytul ?? "Pobierz"}</h3>
{!right.pliki?.length ? (
<p class="opacity-70 mt-4">Brak plików.</p>
) : (
<div class="f-documents-grid">
{right.pliki.map((p) => {
const href = normalizePublicHref(p.file);
if (!href) return null;
return (
<a
class="f-document-card"
href={href}
download
title={p.nazwa}
>
<div class="f-document-title">{p.nazwa}</div>
</a>
);
})}
</div>
)}
</div>
</div>
</section>
</DefaultLayout>
================================================================================
FILE: src/pages/index.astro
--------------------------------------------------------------------------------
---
import DefaultLayout from "../layouts/DefaultLayout.astro";
import Hero from "../components/hero/Hero.astro";
import SectionRenderer from "../components/sections/SectionRenderer.astro"
import yaml from "js-yaml";
import fs from "fs";
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" />
</DefaultLayout>
================================================================================
FILE: src/pages/internet-swiatlowodowy/index.astro
--------------------------------------------------------------------------------
---
import path from "node:path";
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
import InternetCards from "../../islands/Internet/InternetCards.jsx";
import { loadYamlFile } from "../../lib/loadYaml";
type SeoYaml = any;
type InternetParam = { klucz: string; label: string; value: string | number };
type InternetCena = {
budynek: number | string;
umowa: number | string;
miesiecznie: number;
aktywacja?: number;
};
type InternetCard = {
nazwa: string;
widoczny?: boolean;
popularny?: boolean;
parametry?: InternetParam[];
ceny?: InternetCena[];
};
type InternetCardsYaml = {
tytul?: string;
opis?: string;
uwaga?: string;
waluta?: string;
cena_opis?: string;
cards?: InternetCard[];
};
type PhoneParam = { klucz: string; label: string; value: string | number };
type PhoneCard = {
nazwa: string;
widoczny?: boolean;
popularny?: boolean;
cena?: { wartosc: number; opis?: string };
parametry?: PhoneParam[];
};
type PhoneCardsYaml = { cards?: PhoneCard[] };
type Addon = {
id: string;
nazwa: string;
typ?: string;
ilosc?: boolean;
min?: number;
max?: number;
krok?: number;
opis?: string;
cena: number;
};
type AddonsYaml = { cena_opis?: string; dodatki?: Addon[] };
const seo = loadYamlFile<SeoYaml>(
path.join(
process.cwd(),
"src",
"content",
"internet-swiatlowodowy",
"seo.yaml",
),
);
const data = loadYamlFile<InternetCardsYaml>(
path.join(
process.cwd(),
"src",
"content",
"internet-swiatlowodowy",
"cards.yaml",
),
);
const phoneData = loadYamlFile<PhoneCardsYaml>(
path.join(process.cwd(), "src", "content", "telefon", "cards.yaml"),
);
const addonsData = loadYamlFile<AddonsYaml>(
path.join(
process.cwd(),
"src",
"content",
"internet-swiatlowodowy",
"addons.yaml",
),
);
const tytul = data?.tytul ?? "";
const opis = data?.opis ?? "Wybierz rodzaj budynku i czas trwania umowy";
const uwaga = data?.uwaga ?? "";
const waluta = data?.waluta ?? "PLN";
const cenaOpis = data?.cena_opis ?? "zł/mies.";
const cards = (
Array.isArray(data?.cards)
? data.cards.filter((c) => c?.widoczny === true)
: []
) as InternetCard[];
const phoneCards = (
Array.isArray(phoneData?.cards)
? phoneData.cards.filter((c) => c?.widoczny === true)
: []
) as PhoneCard[];
const addons = (
Array.isArray(addonsData?.dodatki) ? addonsData.dodatki : []
) as Addon[];
const addonsCenaOpis = addonsData?.cena_opis ?? cenaOpis;
type SwitchOption = { id: string | number; nazwa: string };
type SwitchDef = {
id: string;
etykieta?: string;
title?: string;
domyslny?: string | number;
opcje: SwitchOption[];
};
type SwitchesYaml = { switches?: SwitchDef[] };
const switchesData = loadYamlFile<SwitchesYaml>(
path.join(process.cwd(), "src", "content", "site", "switches.yaml"),
);
const switches = (
Array.isArray(switchesData?.switches) ? switchesData.switches : []
) as SwitchDef[];
---
<DefaultLayout seo={seo}>
<section class="f-section">
<div class="f-section-grid-single md:grid-cols-1">
<InternetCards
client:load
title={tytul}
description={opis}
uwaga={uwaga}
cards={cards}
waluta={waluta}
cenaOpis={cenaOpis}
phoneCards={phoneCards}
addons={addons}
addonsCenaOpis={addonsCenaOpis}
switches={switches}
/>
<p><span class="f-card-price text-sm">* </span>{uwaga}</p>
</div>
</section>
<SectionRenderer src="./src/content/internet-swiatlowodowy/section.yaml" />
</DefaultLayout>
================================================================================
FILE: src/pages/internet-telewizja/index.astro
--------------------------------------------------------------------------------
---
import path from "node:path";
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
import JamboxCards from "../../islands/jambox/JamboxCards.jsx";
import SectionChannelsSearch from "../../components/sections/SectionChannelsSearch.astro";
import { loadYamlFile } from "../../lib/loadYaml";
type SeoYaml = any;
type Param = { klucz: string; label: string; value: string | number };
type Cena = {
budynek: number | string;
umowa: number | string;
miesiecznie: number;
aktywacja?: number;
};
type Card = {
id?: string;
source?: string;
tid?: number;
nazwa: string;
slug?: string;
widoczny?: boolean;
popularny?: boolean;
parametry?: Param[];
ceny?: Cena[];
};
type CardsYaml = {
tytul?: string;
opis?: string;
uwaga?: string;
waluta?: string;
cena_opis?: string;
internet_parametry_wspolne?: Param[];
cards?: Card[];
};
type PhoneParam = { klucz: string; label: string; value: string | number };
type PhoneCard = {
id?: string;
nazwa: string;
widoczny?: boolean;
popularny?: boolean;
cena?: { wartosc: number; opis?: string };
parametry?: PhoneParam[];
};
type PhoneYaml = { cards?: PhoneCard[] };
type Decoder = { id: string; nazwa: string; opis: string; cena: number };
type Addon = {
id: string;
nazwa: string;
typ?: "checkbox" | "quantity";
ilosc?: boolean;
min?: number;
max?: number;
krok?: number;
opis?: string;
cena: number;
};
type AddonsYaml = {
tytul?: string;
opis?: string;
cena_opis?: string;
dekodery?: Decoder[];
dodatki?: Addon[];
};
const seo = loadYamlFile<SeoYaml>(
path.join(process.cwd(), "src", "content", "internet-telewizja", "seo.yaml"),
);
const data = loadYamlFile<CardsYaml>(
path.join(
process.cwd(),
"src",
"content",
"internet-telewizja",
"cards.yaml",
),
);
const tytul = data?.tytul ?? "";
const opis = data?.opis ?? "Wybierz rodzaj budynku i czas trwania umowy";
const uwaga = data?.uwaga ?? "";
const waluta = data?.waluta ?? "PLN";
const cenaOpis = data?.cena_opis ?? "zł/mies.";
const internetWspolne: Param[] = Array.isArray(data?.internet_parametry_wspolne)
? data.internet_parametry_wspolne
: [];
const cards: Card[] = Array.isArray(data?.cards)
? data.cards.filter((c) => c?.widoczny === true)
: [];
const phoneYaml = loadYamlFile<PhoneYaml>(
path.join(process.cwd(), "src", "content", "telefon", "cards.yaml"),
);
const tvAddonsYaml = loadYamlFile<any>(
path.join(process.cwd(), "src", "content", "internet-telewizja", "tv-addons.yaml"),
);
const phoneCards = Array.isArray(phoneYaml?.cards) ? phoneYaml.cards : [];
const tvAddons = Array.isArray(tvAddonsYaml?.dodatki) ? tvAddonsYaml.dodatki : [];
const addonsYaml = loadYamlFile<AddonsYaml>(
path.join(
process.cwd(),
"src",
"content",
"internet-telewizja",
"addons.yaml",
),
);
const addons: Addon[] = Array.isArray(addonsYaml?.dodatki)
? addonsYaml.dodatki
: [];
const decoders: Decoder[] = Array.isArray(addonsYaml?.dekodery)
? addonsYaml.dekodery
: [];
const addonsCenaOpis = addonsYaml?.cena_opis ?? cenaOpis;
type SwitchOption = { id: string | number; nazwa: string };
type SwitchDef = {
id: string;
etykieta?: string;
title?: string;
domyslny?: string | number;
opcje: SwitchOption[];
};
type SwitchesYaml = { switches?: SwitchDef[] };
const switchesYaml = loadYamlFile<SwitchesYaml>(
path.join(process.cwd(), "src", "content", "site", "switches.yaml"),
);
const switches: SwitchDef[] = Array.isArray(switchesYaml?.switches)
? switchesYaml.switches
: [];
---
<DefaultLayout seo={seo}>
<section class="f-section">
<div class="f-section-grid-single md:grid-cols-1">
<JamboxCards
client:load
title={tytul}
description={opis}
uwaga={uwaga}
cards={cards}
internetWspolne={internetWspolne}
waluta={waluta}
cenaOpis={cenaOpis}
tvAddons={tvAddons}
phoneCards={phoneCards}
decoders={decoders}
addons={addons}
addonsCenaOpis={addonsCenaOpis}
switches={switches}
/>
<p><span class="f-card-price text-sm">* </span>{uwaga}</p>
</div>
</section>
<SectionChannelsSearch />
<SectionRenderer src="./src/content/internet-telewizja/section.yaml" />
</DefaultLayout>
================================================================================
FILE: src/pages/internet-telewizja/telewizja-mozliwosci.astro
--------------------------------------------------------------------------------
---
export const prerender = false;
import path from "node:path";
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import { loadYamlFile } from "../../lib/loadYaml";
import MozliwosciSearch from "../../islands/jambox/JamboxMozliwosciSearch.jsx";
type YamlSection = {
title: string;
image?: string;
content?: string;
};
type YamlData = {
sections?: YamlSection[];
};
function slugify(s: string) {
return String(s || "")
.toLowerCase()
.replace(/[\u0105]/g, "a")
.replace(/[\u0107]/g, "c")
.replace(/[\u0119]/g, "e")
.replace(/[\u0142]/g, "l")
.replace(/[\u0144]/g, "n")
.replace(/[\u00f3]/g, "o")
.replace(/[\u015b]/g, "s")
.replace(/[\u017a\u017c]/g, "z")
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
}
let items: Array<{
id: string;
title: string;
image?: string;
content: string;
}> = [];
let err = "";
try {
const data = loadYamlFile<YamlData>(
path.join(
process.cwd(),
"src",
"content",
"internet-telewizja",
"telewizja-mozliwosci.yaml"
)
);
const sections = Array.isArray(data?.sections) ? data.sections : [];
items = sections
.filter((s) => s?.title)
.map((s) => ({
id: slugify(s.title),
title: s.title,
image: s.image,
content: (s.content || "").trim(),
}))
.filter((x) => x.content);
} catch (e) {
err = e instanceof Error ? e.message : String(e);
}
---
<DefaultLayout title="Możliwości JAMBOX">
<!-- NAGŁÓWEK STRONY zgodny z FUZ -->
<section class="f-section" id="top">
<div class="f-section-grid-single">
<h1 class="f-section-title">Możliwości JAMBOX</h1>
<div class="fuz-markdown max-w-none">
<p>Funkcje i udogodnienia dostępne w JAMBOX.</p>
</div>
{!err && <MozliwosciSearch client:load items={items} />}
</div>
{
err && (
<div class="mt-6 max-w-7xl mx-auto text-left rounded-2xl border border-red-300 bg-red-50 p-4">
<p class="font-bold">Nie udało się wczytać danych</p>
<p class="opacity-80">{err}</p>
</div>
)
}
</section>
{/* UWAGA: render sekcji przeniesiony do wyspy, żeby filtr działał */}
</DefaultLayout>
================================================================================
FILE: src/pages/kontakt/index.astro
--------------------------------------------------------------------------------
---
import path from "node:path";
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import MapGoogle from "../../components/maps/MapGoogle.astro";
import Markdown from "../../islands/Markdown.jsx";
import { loadYamlFile } from "../../lib/loadYaml";
import "../../styles/contact.css";
type SeoYaml = any;
const seo = loadYamlFile<SeoYaml>(
path.join(process.cwd(), "src", "content", "contact", "seo.yaml"),
);
type ContactData = any;
const data = loadYamlFile<ContactData>(
path.join(process.cwd(), "src", "content", "contact", "contact.yaml"),
);
const apiKey = import.meta.env.PUBLIC_GOOGLE_MAPS_KEY;
const form = data.form;
---
<DefaultLayout seo={seo}>
<section class="f-section">
<div class="f-section-grid md:grid-cols-2 gap-10 items-start">
<div class="f-contact-item">
<h1 class="f-section-title">{data.title}</h1>
<Markdown text={data.description} />
</div>
<div id="form">
<h1 class="f-section-title">{data.contactFormTitle}</h1>
<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-1 sm: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>
<!-- widoczne tylko gdy jest oferta -->
<div id="offerSummaryWrap" class="hidden">
<textarea
id="offerSummary"
name="offerSummary"
rows="6"
class="f-input"
readonly
placeholder="Wybrana oferta pojawi się tutaj."></textarea>
</div>
<label class="f-rodo">
<input type="checkbox" name="rodo" required />
<span>
{form.rodo.label}
<a href={form.rodo.policyLink} title={form.rodo.policyTitle}>
{form.rodo.policyText}
</a>.
</span>
</label>
<button
type="submit"
class="btn btn-primary w-full py-3"
title={form.submit.title}
>
{form.submit.label}
</button>
</form>
</div>
</div>
<div class="mt-10">
<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>
<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 formEl = document.getElementById("contactForm");
const toast = document.getElementById("toast");
if (!formEl) return;
const LS_KEY = "fuz_offer_config_v1";
const SS_INJECTED_KEY = "fuz_offer_injected_v1";
function showToast(msg, type) {
if (!toast) return;
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);
}
function clearOfferDraft() {
try {
localStorage.removeItem(LS_KEY);
sessionStorage.removeItem(SS_INJECTED_KEY);
const wrap = document.getElementById("offerSummaryWrap");
if (wrap) wrap.classList.add("hidden");
const offerSummary = document.getElementById("offerSummary");
if (offerSummary) offerSummary.value = "";
} catch {}
}
function hydrateOfferIntoForm() {
try {
if (sessionStorage.getItem(SS_INJECTED_KEY) === "1") return;
const raw = localStorage.getItem(LS_KEY);
if (!raw) return;
const payload = JSON.parse(raw);
const subject = formEl.querySelector('input[name="subject"]');
const wrap = document.getElementById("offerSummaryWrap");
const offerSummary = document.getElementById("offerSummary");
const offerText =
typeof payload?.message === "string" && payload.message.trim()
? payload.message
: null;
if (!offerText) return;
if (wrap) wrap.classList.remove("hidden");
if (subject && !subject.value) {
subject.value = `Zapytanie: ${payload?.pkg?.name || "Oferta"}`;
}
if (offerSummary) offerSummary.value = offerText;
sessionStorage.setItem(SS_INJECTED_KEY, "1");
} catch {}
}
hydrateOfferIntoForm();
window.addEventListener("pagehide", clearOfferDraft);
window.addEventListener("beforeunload", clearOfferDraft);
formEl.addEventListener("submit", async (e) => {
if (!formEl.reportValidity()) return;
e.preventDefault();
try {
const data = Object.fromEntries(new FormData(formEl).entries());
data.rodo = formEl.rodo?.checked === true;
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().catch(() => ({}));
showToast(
json?.ok ? successMsg : errorMsg,
json?.ok ? "success" : "error",
);
if (json?.ok) {
formEl.reset();
clearOfferDraft();
}
} catch {
showToast(errorMsg, "error");
}
});
});
</script>
</DefaultLayout>
================================================================================
FILE: src/pages/mapa-zasiegu/index.astro
--------------------------------------------------------------------------------
---
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import MapGoogle from "../../components/maps/MapGoogle.astro";
import RangeForm from "../../islands/RangeForm.jsx";
import "../../styles/map-google.css";
const apiKey = import.meta.env.PUBLIC_GOOGLE_MAPS_KEY;
const lat = 52.597388;
const lon = 21.456797;
const mapStyleId = "8e0a97af9476f2d3";
---
<script>
declare global {
interface Window {
showAddressOnMap?: (result: any) => void;
fuzMaps?: any;
}
}
</script>
<DefaultLayout title="FUZ Mapa zasięgu sieci światłowodowej">
<section class="flex flex-col md:flex-row h-full min-h-[80vh]">
<!-- PANEL LEWY -->
<aside
class="w-full md:w-[340px] bg-[var(--f-background)] text-[var(--f-text)]
pt-6 px-6 flex flex-col gap-6 overflow-y-auto z-40"
>
<h3 class="text-3xl">Sprawdź dostępność usług</h3>
<p class="text-sm">
Wybierz swoją miejscowość i ulicę oraz numer budynku, aby sprawdzić
dostępność usług światłowodowych FUZ.
</p>
<RangeForm client:load />
</aside>
<!-- MAPA -->
<div class="flex-1 relative min-h-[50vh] md:min-h-0">
<MapGoogle
apiKey={apiKey}
lat={lat}
lon={lon}
zoom={17}
showMarker={true}
mode="full"
mapStyleId={mapStyleId}
/>
</div>
</section>
<script is:inline>
let fiberLayer = null;
window.getActiveMap = function () {
if (!window.fuzMaps) return null;
return Object.values(window.fuzMaps)[0] || null;
};
function enableFiberLayer() {
const map = window.getActiveMap();
if (!map) return;
if (fiberLayer) {
fiberLayer.setMap(null);
}
fiberLayer = new google.maps.KmlLayer(
"https://www.google.com/maps/d/kml?mid=1Or8SF_9qx6QMdidS-99V_jqQuhF9de0&forcekml=1",
{ suppressInfoWindows: true, preserveViewport: false },
);
fiberLayer.setMap(map);
}
// Czekamy aż mapa się załaduje
const int = setInterval(() => {
const map = window.getActiveMap();
if (map && window.google?.maps) {
clearInterval(int);
enableFiberLayer();
}
}, 100);
</script>
<script is:inline>
window.showAddressOnMap = async function (result) {
const map = window.getActiveMap();
if (!map) return;
// Czekamy aż API będzie gotowe
if (!window.google?.maps?.importLibrary) {
await new Promise((resolve) => {
const int = setInterval(() => {
if (window.google?.maps?.importLibrary) {
clearInterval(int);
resolve(true);
}
}, 50);
});
}
const { AdvancedMarkerElement } =
await google.maps.importLibrary("marker");
const { InfoWindow } = await google.maps.importLibrary("maps");
if (window._activeMarker) window._activeMarker.map = null;
if (window._activeInfo) window._activeInfo.close();
const pos = { lat: result.lat, lng: result.lon };
const overlay = document.createElement("div");
overlay.className = "pulse-marker";
const projection = map.getProjection();
if (projection) {
const point = projection.fromLatLngToPoint(new google.maps.LatLng(pos));
overlay.style.left = point.x + "px";
overlay.style.top = point.y + "px";
}
setTimeout(() => {
overlay.remove();
}, 1500);
map.getDiv().appendChild(overlay);
map.panTo(pos);
let targetZoom = 16;
setTimeout(() => {
map.setZoom(targetZoom);
}, 250);
const marker = new AdvancedMarkerElement({
map,
position: pos,
title: `${result.city} ${result.street ?? ""} ${result.number}`,
className: "marker-bounce",
});
window._activeMarker = marker;
// InfoWindow
const html = `
<div class="f-info-window">
<div class="f-info-header">
<div class="f-info-heading">
${
result.available
? `<span class="ok">✔</span> Internet światłowodowy dostępny`
: `<span class="no">✖</span> Światłowód niedostępny`
}
</div>
<div class="f-info-city">${result.city}</div>
<div class="f-info-street">${result.street ?? ""} ${result.number}</div>
</div>
</div>
<div class="w-full flex justify-center mb-4">
<a href="/kontakt" class="btn btn-primary">Przejdź do kontaktu →</a>
</div>
</div>
`;
const info = new InfoWindow({ content: html });
window._activeInfo = info;
info.open({ map, anchor: marker });
};
</script>
</DefaultLayout>
================================================================================
FILE: src/pages/premium/[tid].astro
--------------------------------------------------------------------------------
---
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import yaml from "js-yaml";
import fs from "node:fs";
import Markdown from "../../islands/Markdown.jsx";
import AddonChannelsGrid from "../../islands/jambox/AddonChannelsModal.jsx";
import "../../styles/jambox-tematyczne.css";
type AddonPriceRow = {
pakiety?: string[] | any;
"12m"?: number | string;
bezterminowo?: number | string;
};
type TvAddon = {
id?: string;
nazwa?: string;
tid?: number;
typ?: string;
opis?: string;
image?: string;
cena?: AddonPriceRow[];
group?: string;
group_mode?: string;
};
type GroupCta = {
label?: string;
href?: string;
title?: string;
opis?: string;
};
type GroupMeta = {
tytul?: string;
rejestracja?: GroupCta;
};
type TvAddonsDoc = {
tytul?: string;
opis?: string;
dodatki?: TvAddon[];
grupy?: Record<string, GroupMeta>;
};
const doc = yaml.load(
fs.readFileSync("./src/content/internet-telewizja/tv-addons.yaml", "utf8"),
) as TvAddonsDoc;
const addons = Array.isArray(doc?.dodatki) ? doc.dodatki : [];
const groupMeta = doc?.grupy ?? {};
const tid = Number(Astro.params.tid);
const picked = addons.find((a) => Number(a?.tid) === tid);
if (!picked) {
return new Response("Nie znaleziono pakietu.", { status: 404 });
}
const pickedGroup = String(picked?.group ?? "").trim();
const viewAddons = pickedGroup
? addons.filter((a) => String(a?.group ?? "").trim() === pickedGroup)
: [picked];
const footerCta =
pickedGroup ? groupMeta[pickedGroup]?.rejestracja : undefined;
---
<DefaultLayout
title={picked?.nazwa ?? doc?.tytul}
description={doc?.opis}
>
{
viewAddons.map((addon, index) => {
const pkgName = String(addon?.nazwa ?? "").trim();
const hasYamlImage = !!String(addon?.image ?? "").trim();
const assumeHasMedia = pkgName || hasYamlImage;
const isAboveFold = index === 0;
return (
<section class="f-section" id={`tid-${addon.tid}`}>
<div
class={`f-section-grid f-addon-section ${
assumeHasMedia ? "md:grid-cols-2" : "md:grid-cols-1"
}`}
data-addon-section
data-has-media={assumeHasMedia ? "1" : "0"}
>
{/* MEDIA — odpowiednik <Image /> */}
<div class="f-addon-media md:order-2">
{pkgName ? (
<AddonChannelsGrid
client:idle
packageName={pkgName}
fallbackImage={String(addon?.image ?? "")}
aboveFold={isAboveFold}
title={pkgName}
/>
) : null}
</div>
{/* TEKST */}
<div class="md:order-1">
{pkgName && <h2 class="f-section-title">{pkgName}</h2>}
{addon?.opis && <Markdown text={addon.opis} />}
</div>
</div>
</section>
);
})
}
{footerCta?.href && footerCta?.label ? (
<section class="f-section">
<div class="f-section-grid md:grid-cols-1">
<div class="fuz-markdown max-w-none">
{footerCta.opis && <p>{footerCta.opis}</p>}
<div class="f-section-nav">
<a
class="btn btn-primary"
href={footerCta.href}
title={footerCta.title ?? footerCta.label}
>
{footerCta.label}
</a>
</div>
</div>
</div>
</section>
) : null}
</DefaultLayout>
================================================================================
FILE: src/pages/premium/index.astro
--------------------------------------------------------------------------------
---
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import yaml from "js-yaml";
import fs from "node:fs";
import Markdown from "../../islands/Markdown.jsx";
import AddonChannelsGrid from "../../islands/jambox/AddonChannelsModal.jsx";
import "../../styles/jambox-tematyczne.css";
type AddonPriceRow = {
pakiety?: string[] | any;
"12m"?: number | string;
bezterminowo?: number | string;
};
type TvAddon = {
id?: string;
nazwa?: string;
tid?: number;
typ?: string;
opis?: string;
image?: string;
cena?: AddonPriceRow[];
group?: string;
group_mode?: string;
};
// ✅ OPCJA A: metadane grupy + CTA
type GroupCta = {
label?: string;
href?: string;
title?: string;
opis?: string;
};
type GroupMeta = {
tytul?: string;
rejestracja?: GroupCta;
};
type TvAddonsDoc = {
tytul?: string;
opis?: string;
cena_opis?: string;
dodatki?: TvAddon[];
grupy?: Record<string, GroupMeta>; // ✅
};
const doc = yaml.load(
fs.readFileSync("./src/content/internet-telewizja/tv-addons.yaml", "utf8"),
) as TvAddonsDoc;
const pageTitle = doc?.tytul ?? "Dodatkowe pakiety TV";
const pageDesc = doc?.opis ?? "";
const addons: TvAddon[] = Array.isArray(doc?.dodatki) ? doc.dodatki : [];
const detailsBase = "/pakiety-premium";
// ✅ mapa meta grup (np. hbo_max -> { rejestracja: {...} })
const groupMeta: Record<string, GroupMeta> = doc?.grupy ?? {};
type Group = {
key: string;
title?: string;
items: TvAddon[];
groupId?: string;
};
const groupsMap = new Map<string, Group>();
for (const a of addons) {
const g = String(a?.group ?? "").trim();
const key = g ? `g:${g}` : `s:${a?.tid ?? a?.id ?? a?.nazwa ?? ""}`;
if (!groupsMap.has(key)) {
groupsMap.set(key, {
key,
groupId: g || undefined,
title: g || undefined,
items: [],
});
}
groupsMap.get(key)!.items.push(a);
}
const groups: Group[] = Array.from(groupsMap.values());
---
<DefaultLayout title={pageTitle} description={pageDesc}>
<section class="f-section">
<div class="f-section-grid-single">
<h1 class="f-section-title">{pageTitle}</h1>
{pageDesc && <Markdown text={pageDesc} />}
</div>
</section>
{
groups.map((group, groupIndex) => {
const isSingle = group.key.startsWith("s:");
const gId = String(group.groupId ?? "").trim(); // np. "hbo_max"
const meta = gId ? groupMeta[gId] : undefined;
const footerCta = meta?.rejestracja;
const footerTitle =
meta?.tytul || (gId ? gId.replace(/_/g, " ") : undefined);
return (
<div class={`f-addon-group ${isSingle ? "f-addon-group--single" : ""}`}>
{group.items.map((addon: TvAddon, index: number) => {
const isAboveFold = groupIndex === 0 && index === 0;
const pkgName = String(addon?.nazwa ?? "").trim();
const hasYamlImage = !!String(addon?.image ?? "").trim();
// ✅ zachowanie jak wcześniej (1 kolumna + ukrycie media gdy brak)
const assumeHasMedia = pkgName ? true : hasYamlImage;
const href =
addon?.tid != null ? `${detailsBase}/${addon.tid}` : null;
return (
<section class="f-section f-addon-group-item">
<div
class={`f-section-grid f-addon-grid f-addon-section ${
assumeHasMedia ? "md:grid-cols-2" : "md:grid-cols-1"
}`}
data-addon-section
data-has-media={assumeHasMedia ? "1" : "0"}
>
<div class="f-addon-text">
{pkgName && <h3 class="f-section-title">{pkgName}</h3>}
{addon?.opis && <Markdown text={addon.opis} />}
</div>
<div class="f-addon-media">
{pkgName ? (
<AddonChannelsGrid
client:idle
packageName={pkgName}
fallbackImage={String(addon?.image ?? "")}
aboveFold={isAboveFold}
title={pkgName}
/>
) : null}
</div>
</div>
</section>
);
})}
{/* ✅ STOPKA GRUPY: przycisk rejestracji dla całej grupy */}
{!isSingle && footerCta?.href && footerCta?.label ? (
<div class="f-addon-group-footer fuz-markdown max-w-none">
{footerCta.opis ? <p>{footerCta.opis}</p> : null}
<a
class="btn btn-primary"
href={footerCta.href}
title={footerCta.title ?? footerCta.label}
target="_blank"
rel="noopener noreferrer"
>
{footerCta.label}
</a>
</div>
) : null}
</div>
);
})
}
</DefaultLayout>
================================================================================
FILE: src/pages/telefon/index.astro
--------------------------------------------------------------------------------
---
import path from "node:path";
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
import OffersPhoneCards from "../../islands/phone/PhoneCards.jsx";
import { loadYamlFile } from "../../lib/loadYaml";
type SeoYaml = any;
type PhoneParam = {
klucz: string;
label: string;
value: string | number;
};
type PhoneCard = {
nazwa: string;
widoczny?: boolean;
popularny?: boolean;
cena?: { wartosc: number; opis?: string };
parametry?: PhoneParam[];
};
type PhoneCardsYaml = {
tytul?: string;
opis?: string;
cards?: PhoneCard[];
};
const seo = loadYamlFile<SeoYaml>(
path.join(process.cwd(), "src", "content", "telefon", "seo.yaml"),
);
const phoneCards = loadYamlFile<PhoneCardsYaml>(
path.join(process.cwd(), "src", "content", "telefon", "cards.yaml"),
);
const tytul = phoneCards?.tytul ?? "";
const opis = phoneCards?.opis ?? "";
const cards: PhoneCard[] = Array.isArray(phoneCards?.cards)
? phoneCards.cards.filter((c) => c?.widoczny === true)
: [];
---
<DefaultLayout seo={seo}>
<section class="f-section">
<div class="f-section-grid-single md:grid-cols-1">
<h1 class="f-section-title">Usługa telefonu</h1>
<OffersPhoneCards client:load title={tytul} description={opis} cards={cards} />
</div>
</section>
<SectionRenderer src="./src/content/telefon/section.yaml" />
</DefaultLayout>
================================================================================