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

@@ -1,19 +1,21 @@
import type { APIRoute } from "astro";
import nodemailer from "nodemailer";
export const POST: APIRoute = async ({ request }) => {
export async function POST({ request }) {
try {
const form = await request.json();
const transporter = nodemailer.createTransport({
host: import.meta.env.SMTP_HOST,
port: Number(import.meta.env.SMTP_PORT),
secure: true,
secure: true, // true = 465, false = 587
auth: {
user: import.meta.env.SMTP_USER,
pass: import.meta.env.SMTP_PASS,
},
tls: { rejectUnauthorized: false }
// ⚠️ tylko jeśli masz self-signed / dziwny cert
tls: {
rejectUnauthorized: false,
},
});
await transporter.sendMail({
@@ -32,10 +34,17 @@ ${form.message}
`.trim(),
});
return new Response(JSON.stringify({ ok: true }), { status: 200 });
return new Response(
JSON.stringify({ ok: true }),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
} catch (error) {
console.error("MAIL ERROR:", error);
return new Response(JSON.stringify({ ok: false }), { status: 500 });
return new Response(
JSON.stringify({ ok: false }),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
};
}

View 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" },
});
}
}

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

View File

@@ -2,17 +2,15 @@
import DefaultLayout from "../layouts/DefaultLayout.astro";
import Hero from "../components/hero/Hero.astro";
import SectionRenderer from "../components/sections/SectionRenderer.astro"
import SectionContact from "../components/sections/SectionContact.astro";
import yaml from "js-yaml";
import fs from "fs";
const seo = yaml.load(fs.readFileSync("./src/content/seo/home.yaml", "utf8"));
const seo = yaml.load(fs.readFileSync("./src/content/home/seo.yaml", "utf8"));
const hero = yaml.load(fs.readFileSync("./src/content/home/hero.yaml", "utf8"));
---
<DefaultLayout seo={seo}>
<Hero {...hero} />
<SectionRenderer src="./src/content/site/site.section.yaml" />
<!-- <SectionContact /> -->
</DefaultLayout>

View File

@@ -1,7 +1,7 @@
---
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import OffersSwitches from "../../islands/Offers/OffersSwitches.jsx";
import InternetDbOffersCards from "../../islands/Internet/OffersInternetCards.jsx";
import OffersSwitches from "../../islands/OffersSwitches.jsx";
import InternetCards from "../../islands/Internet/InternetCards.jsx";
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
import yaml from "js-yaml";
@@ -20,7 +20,7 @@ const seo = yaml.load(
<p>Wybierz rodzaj budynku i czas trwania umowy</p>
</div>
<OffersSwitches client:load />
<InternetDbOffersCards client:load />
<InternetCards client:load />
</div>
</section>

View File

@@ -1,13 +1,9 @@
---
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import OffersSwitches from "../../islands/Offers/OffersSwitches.jsx";
import OffersJamboxCards from "../../islands/jambox/OffersJamboxCards.jsx";
import Hero from "../../components/hero/Hero.astro";
import OffersSwitches from "../../islands/OffersSwitches.jsx";
import JamboxCards from "../../islands/jambox/JamboxCards.jsx";
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
import Markdown from "../../islands/Markdown.jsx";
import Modal from "../../islands/Modal.jsx";
import JamboxMozliwosci from "../../components/sections/SectionJamboxMozliwosci.astro";
import SectionChannelsSearch from "../../components/sections/SectionChannelsSearch.astro"
import yaml from "js-yaml";
import fs from "fs";
@@ -15,66 +11,20 @@ import fs from "fs";
const seo = yaml.load(
fs.readFileSync("./src/content/internet-telewizja/seo.yaml", "utf8"),
);
const hero = yaml.load(
fs.readFileSync("./src/content/internet-telewizja/hero.yaml", "utf8"),
);
const page = yaml.load(
fs.readFileSync("./src/content/internet-telewizja/page.yaml", "utf8"),
);
const modalData = yaml.load(
fs.readFileSync("./src/content/internet-telewizja/modal.yaml", "utf8"),
);
type Paragraph = {
title?: string;
content: string;
};
const data = yaml.load(
fs.readFileSync("./src/content/internet-telewizja/offers.yaml", "utf8"),
);
const first = page.paragraphs[0];
const rest = page.paragraphs.slice(1);
---
<DefaultLayout seo={seo}>
<!-- <Hero {...hero} /> -->
<!-- <section class="f-section">
<div class="f-section-grid-single md:grid-cols-1">
{page.title.map((line: any) => <h1 class="f-section-title">{line}</h1>)}
{first.title && <h3>{first.title}</h3>}
<Markdown text={first.content} />
</div>
</section> -->
<section class="f-section">
<div class="f-section-grid-single md:grid-cols-1">
<div class="f-section-grid-single md:grid-cols-1">
<h1 class="f-section-title">Telewizja z interentem</h1>
<div class="fuz-markdown max-w-none">
<p>Wybierz rodzaj budynku i czas trwania umowy</p>
</div>
<OffersSwitches client:load />
<OffersJamboxCards client:load />
<!-- <OffersIsland client:load data={data} /> -->
<JamboxCards client:load />
</div>
</section>
<!-- {
rest.map((p: Paragraph) => (
<section class="f-section">
<div class="f-section-grid-single md:grid-cols-1">
{p.title && <h3 class="f-section-title">{p.title}</h3>}
<Markdown text={p.content.replace(/\n/g, "\n\n")} />
</div>
</section>
))
} -->
<SectionRenderer src="./src/content/internet-telewizja/section.yaml" />
<!-- <JamboxMozliwosci /> -->
<!-- <Modal client:load modalData={modalData} /> -->
<SectionChannelsSearch/>
</DefaultLayout>

View File

@@ -1,14 +1,179 @@
---
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import SectionContact from "../../components/sections/SectionContact.astro";
import MapGoogle from "../../components/maps/MapGoogle.astro";
const seo = {
title: "Kontakt FUZ",
description: "Kontakt FUZ",
canonical: "/kontakt"
};
import yaml from "js-yaml";
import fs from "fs";
const data = yaml.load(
fs.readFileSync("./src/content/contact/contact.yaml", "utf8"),
);
const seo = yaml.load(
fs.readFileSync("./src/content/internet-swiatlowodowy/seo.yaml", "utf8"),
);
const apiKey = import.meta.env.PUBLIC_GOOGLE_MAPS_KEY;
const form = data.form;
---
<DefaultLayout seo={seo}>
<SectionContact />
<section id="kontakt" class="f-section">
<div class="f-contact-grid">
<!-- Kolumna lewa -->
<div class="f-contact-col-1">
<h2>{data.title}</h2>
<div class="f-contact-item" set:html={data.description} />
</div>
<div class="f-contact-col-2">
<h2>{data.contactFormTitle}</h2>
<form id="contactForm" class="f-contact-form">
<div class="f-contact-form-inner">
<input
type="text"
name="firstName"
placeholder={form.firstName.placeholder}
class="f-input"
required
/>
<input
type="text"
name="lastName"
placeholder={form.lastName.placeholder}
class="f-input"
required
/>
</div>
<div class="grid grid-cols-2 gap-4">
<input
type="email"
name="email"
placeholder={form.email.placeholder}
class="f-input"
required
autocomplete="email"
/>
<input
type="tel"
name="phone"
placeholder={form.phone.placeholder}
class="f-input"
required
autocomplete="tel"
/>
</div>
<input
type="text"
name="subject"
placeholder={form.subject.placeholder}
class="f-input"
required
/>
<textarea
name="message"
rows={form.message.rows}
placeholder={form.message.placeholder}
class="f-input"
required></textarea>
<label class="f-rodo">
<input type="checkbox" name="rodo" required />
<span>
{form.rodo.label}
<a href={form.rodo.policyLink} title={form.rodo.policyTitle}
>{form.rodo.policyText}</a
>.
</span>
</label>
<button title={form.submit.title}>{form.submit.label}</button>
</form>
</div>
</div>
<div class="f-contact-map">
<MapGoogle
apiKey={apiKey}
lat={data.lat}
lon={data.lng}
zoom={16}
title={data.markerTitle}
description={data.markerAddress}
showMarker={true}
mode="contact"
mapStyleId={data.maps.mapId}
/>
</div>
<div id="toast" class="f-toast"></div>
</section>
<!-- ReCaptcha v3 -->
<script
is:inline
define:vars={{ siteKey: import.meta.env.PUBLIC_RECAPTCHA_SITE_KEY }}
>
window.FUZ_RECAPTCHA_KEY = siteKey;
const s = document.createElement("script");
s.src = "https://www.google.com/recaptcha/api.js?render=" + siteKey;
s.async = true;
document.head.appendChild(s);
</script>
<script
is:inline
define:vars={{
successMsg: form.successMessage,
errorMsg: form.errorMessage,
}}
>
document.addEventListener("DOMContentLoaded", () => {
const form = document.getElementById("contactForm");
const toast = document.getElementById("toast");
if (!form) return;
form.addEventListener("submit", async (e) => {
if (!form.reportValidity()) return;
e.preventDefault();
const data = Object.fromEntries(new FormData(form).entries());
data.rodo = form.rodo.checked;
const token = await grecaptcha.execute(window.FUZ_RECAPTCHA_KEY, {
action: "submit",
});
data.recaptcha = token;
const resp = await fetch("/api/contact/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
const json = await resp.json();
showToast(
json.ok ? successMsg : errorMsg,
json.ok ? "success" : "error",
);
if (json.ok) form.reset();
});
function showToast(msg, type) {
toast.innerHTML = `<div class="f-toast-msg ${type}">${msg}</div>`;
toast.classList.remove("visible");
void toast.offsetWidth;
toast.classList.add("visible");
setTimeout(() => toast.classList.remove("visible"), 3000);
}
});
</script>
</DefaultLayout>

View File

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