Porządkowanie kodu, dodanie sekcji wyszukiwania kanałów
This commit is contained in:
@@ -1,19 +1,21 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
export async function POST({ request }) {
|
||||
try {
|
||||
const form = await request.json();
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: import.meta.env.SMTP_HOST,
|
||||
port: Number(import.meta.env.SMTP_PORT),
|
||||
secure: true,
|
||||
secure: true, // true = 465, false = 587
|
||||
auth: {
|
||||
user: import.meta.env.SMTP_USER,
|
||||
pass: import.meta.env.SMTP_PASS,
|
||||
},
|
||||
tls: { rejectUnauthorized: false }
|
||||
// ⚠️ tylko jeśli masz self-signed / dziwny cert
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
});
|
||||
|
||||
await transporter.sendMail({
|
||||
@@ -32,10 +34,17 @@ ${form.message}
|
||||
`.trim(),
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
||||
return new Response(
|
||||
JSON.stringify({ ok: true }),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error("MAIL ERROR:", error);
|
||||
return new Response(JSON.stringify({ ok: false }), { status: 500 });
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false }),
|
||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
91
src/pages/api/jambox/channels-search.js
Normal file
91
src/pages/api/jambox/channels-search.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const DB_PATH = process.env.FUZ_DB_PATH || "./src/data/ServicesRange.db";
|
||||
|
||||
function getDb() {
|
||||
return new Database(DB_PATH, { readonly: true });
|
||||
}
|
||||
|
||||
function clamp(n, min, max) {
|
||||
return Math.max(min, Math.min(max, n));
|
||||
}
|
||||
|
||||
export function GET({ url }) {
|
||||
const q = (url.searchParams.get("q") || "").trim();
|
||||
const limit = clamp(Number(url.searchParams.get("limit") || 50), 1, 200);
|
||||
|
||||
if (q.length < 2) {
|
||||
return new Response(JSON.stringify({ ok: true, data: [] }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
|
||||
const safe = q.replace(/[%_]/g, (m) => `\\${m}`);
|
||||
const like = `%${safe}%`;
|
||||
|
||||
const db = getDb();
|
||||
|
||||
try {
|
||||
// ✅ 1 rekord na kanał (grupujemy po name+logo_url)
|
||||
// ✅ pakiety zbieramy do jednego pola packages_blob
|
||||
// UWAGA: zakładam, że jambox_base_packages ma kolumnę "name"
|
||||
const rows = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
c.name,
|
||||
c.logo_url,
|
||||
MAX(c.description) AS description,
|
||||
MIN(c.number) AS min_number,
|
||||
GROUP_CONCAT(
|
||||
p.id || '::' || p.name || '::' || c.number || '::' || c.guaranteed,
|
||||
'||'
|
||||
) AS packages_blob
|
||||
FROM jambox_package_channels c
|
||||
JOIN jambox_base_packages p ON p.id = c.package_id
|
||||
WHERE
|
||||
c.name LIKE ? ESCAPE '\\'
|
||||
GROUP BY c.name, c.logo_url
|
||||
ORDER BY min_number ASC, c.name ASC
|
||||
LIMIT ?;
|
||||
`.trim()
|
||||
)
|
||||
.all(like, limit);
|
||||
|
||||
const data = rows.map((r) => {
|
||||
const packages = String(r.packages_blob || "")
|
||||
.split("||")
|
||||
.filter(Boolean)
|
||||
.map((s) => {
|
||||
const [id, name, number, guaranteed] = s.split("::");
|
||||
return {
|
||||
id: Number(id),
|
||||
name,
|
||||
number: Number(number),
|
||||
guaranteed: Number(guaranteed) === 1,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.id - b.id);
|
||||
|
||||
return {
|
||||
name: r.name,
|
||||
logo_url: r.logo_url,
|
||||
description: r.description || "",
|
||||
min_number: Number(r.min_number || 0),
|
||||
packages,
|
||||
};
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ ok: true, data }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd w /api/jambox/channels-search:", err);
|
||||
return new Response(JSON.stringify({ ok: false, error: "DB_ERROR" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
}
|
||||
23
src/pages/dokumenty/index.astro
Normal file
23
src/pages/dokumenty/index.astro
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||
|
||||
const seo = {
|
||||
title: "Oferta – FUZ",
|
||||
description: "Oferta – FUZ",
|
||||
canonical: "/oferta",
|
||||
};
|
||||
---
|
||||
|
||||
<DefaultLayout seo={seo}>
|
||||
<section class="f-section">
|
||||
<div class="f-section-grid-single md:grid-cols-1">
|
||||
<h1 class="f-section-title">Dokumenty</h1>
|
||||
<div class="fuz-markdown max-w-none">
|
||||
<p>
|
||||
Ta podstrona jest na razie szkieletem. Możemy tu później wczytać
|
||||
treść.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</DefaultLayout>
|
||||
@@ -2,17 +2,15 @@
|
||||
import DefaultLayout from "../layouts/DefaultLayout.astro";
|
||||
import Hero from "../components/hero/Hero.astro";
|
||||
import SectionRenderer from "../components/sections/SectionRenderer.astro"
|
||||
import SectionContact from "../components/sections/SectionContact.astro";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import fs from "fs";
|
||||
|
||||
const seo = yaml.load(fs.readFileSync("./src/content/seo/home.yaml", "utf8"));
|
||||
const seo = yaml.load(fs.readFileSync("./src/content/home/seo.yaml", "utf8"));
|
||||
const hero = yaml.load(fs.readFileSync("./src/content/home/hero.yaml", "utf8"));
|
||||
---
|
||||
|
||||
<DefaultLayout seo={seo}>
|
||||
<Hero {...hero} />
|
||||
<SectionRenderer src="./src/content/site/site.section.yaml" />
|
||||
<!-- <SectionContact /> -->
|
||||
</DefaultLayout>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||
import OffersSwitches from "../../islands/Offers/OffersSwitches.jsx";
|
||||
import InternetDbOffersCards from "../../islands/Internet/OffersInternetCards.jsx";
|
||||
import OffersSwitches from "../../islands/OffersSwitches.jsx";
|
||||
import InternetCards from "../../islands/Internet/InternetCards.jsx";
|
||||
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
@@ -20,7 +20,7 @@ const seo = yaml.load(
|
||||
<p>Wybierz rodzaj budynku i czas trwania umowy</p>
|
||||
</div>
|
||||
<OffersSwitches client:load />
|
||||
<InternetDbOffersCards client:load />
|
||||
<InternetCards client:load />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
---
|
||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||
import OffersSwitches from "../../islands/Offers/OffersSwitches.jsx";
|
||||
import OffersJamboxCards from "../../islands/jambox/OffersJamboxCards.jsx";
|
||||
|
||||
import Hero from "../../components/hero/Hero.astro";
|
||||
import OffersSwitches from "../../islands/OffersSwitches.jsx";
|
||||
import JamboxCards from "../../islands/jambox/JamboxCards.jsx";
|
||||
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
||||
import Markdown from "../../islands/Markdown.jsx";
|
||||
import Modal from "../../islands/Modal.jsx";
|
||||
import JamboxMozliwosci from "../../components/sections/SectionJamboxMozliwosci.astro";
|
||||
import SectionChannelsSearch from "../../components/sections/SectionChannelsSearch.astro"
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import fs from "fs";
|
||||
@@ -15,66 +11,20 @@ import fs from "fs";
|
||||
const seo = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-telewizja/seo.yaml", "utf8"),
|
||||
);
|
||||
const hero = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-telewizja/hero.yaml", "utf8"),
|
||||
);
|
||||
const page = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-telewizja/page.yaml", "utf8"),
|
||||
);
|
||||
const modalData = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-telewizja/modal.yaml", "utf8"),
|
||||
);
|
||||
|
||||
type Paragraph = {
|
||||
title?: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
const data = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-telewizja/offers.yaml", "utf8"),
|
||||
);
|
||||
const first = page.paragraphs[0];
|
||||
const rest = page.paragraphs.slice(1);
|
||||
---
|
||||
|
||||
<DefaultLayout seo={seo}>
|
||||
<!-- <Hero {...hero} /> -->
|
||||
|
||||
<!-- <section class="f-section">
|
||||
<div class="f-section-grid-single md:grid-cols-1">
|
||||
{page.title.map((line: any) => <h1 class="f-section-title">{line}</h1>)}
|
||||
{first.title && <h3>{first.title}</h3>}
|
||||
<Markdown text={first.content} />
|
||||
</div>
|
||||
</section> -->
|
||||
|
||||
<section class="f-section">
|
||||
<div class="f-section-grid-single md:grid-cols-1">
|
||||
<div class="f-section-grid-single md:grid-cols-1">
|
||||
<h1 class="f-section-title">Telewizja z interentem</h1>
|
||||
<div class="fuz-markdown max-w-none">
|
||||
<p>Wybierz rodzaj budynku i czas trwania umowy</p>
|
||||
</div>
|
||||
<OffersSwitches client:load />
|
||||
<OffersJamboxCards client:load />
|
||||
|
||||
<!-- <OffersIsland client:load data={data} /> -->
|
||||
<JamboxCards client:load />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- {
|
||||
rest.map((p: Paragraph) => (
|
||||
<section class="f-section">
|
||||
<div class="f-section-grid-single md:grid-cols-1">
|
||||
{p.title && <h3 class="f-section-title">{p.title}</h3>}
|
||||
<Markdown text={p.content.replace(/\n/g, "\n\n")} />
|
||||
</div>
|
||||
</section>
|
||||
))
|
||||
} -->
|
||||
|
||||
<SectionRenderer src="./src/content/internet-telewizja/section.yaml" />
|
||||
|
||||
<!-- <JamboxMozliwosci /> -->
|
||||
|
||||
<!-- <Modal client:load modalData={modalData} /> -->
|
||||
<SectionChannelsSearch/>
|
||||
</DefaultLayout>
|
||||
|
||||
@@ -1,14 +1,179 @@
|
||||
---
|
||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||
import SectionContact from "../../components/sections/SectionContact.astro";
|
||||
import MapGoogle from "../../components/maps/MapGoogle.astro";
|
||||
|
||||
const seo = {
|
||||
title: "Kontakt – FUZ",
|
||||
description: "Kontakt – FUZ",
|
||||
canonical: "/kontakt"
|
||||
};
|
||||
import yaml from "js-yaml";
|
||||
import fs from "fs";
|
||||
|
||||
const data = yaml.load(
|
||||
fs.readFileSync("./src/content/contact/contact.yaml", "utf8"),
|
||||
);
|
||||
|
||||
const seo = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-swiatlowodowy/seo.yaml", "utf8"),
|
||||
);
|
||||
|
||||
const apiKey = import.meta.env.PUBLIC_GOOGLE_MAPS_KEY;
|
||||
const form = data.form;
|
||||
---
|
||||
|
||||
<DefaultLayout seo={seo}>
|
||||
<SectionContact />
|
||||
<section id="kontakt" class="f-section">
|
||||
<div class="f-contact-grid">
|
||||
<!-- Kolumna lewa -->
|
||||
<div class="f-contact-col-1">
|
||||
<h2>{data.title}</h2>
|
||||
<div class="f-contact-item" set:html={data.description} />
|
||||
</div>
|
||||
|
||||
<div class="f-contact-col-2">
|
||||
<h2>{data.contactFormTitle}</h2>
|
||||
<form id="contactForm" class="f-contact-form">
|
||||
<div class="f-contact-form-inner">
|
||||
<input
|
||||
type="text"
|
||||
name="firstName"
|
||||
placeholder={form.firstName.placeholder}
|
||||
class="f-input"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="lastName"
|
||||
placeholder={form.lastName.placeholder}
|
||||
class="f-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder={form.email.placeholder}
|
||||
class="f-input"
|
||||
required
|
||||
autocomplete="email"
|
||||
/>
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
placeholder={form.phone.placeholder}
|
||||
class="f-input"
|
||||
required
|
||||
autocomplete="tel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="subject"
|
||||
placeholder={form.subject.placeholder}
|
||||
class="f-input"
|
||||
required
|
||||
/>
|
||||
|
||||
<textarea
|
||||
name="message"
|
||||
rows={form.message.rows}
|
||||
placeholder={form.message.placeholder}
|
||||
class="f-input"
|
||||
required></textarea>
|
||||
|
||||
<label class="f-rodo">
|
||||
<input type="checkbox" name="rodo" required />
|
||||
<span>
|
||||
{form.rodo.label}
|
||||
<a href={form.rodo.policyLink} title={form.rodo.policyTitle}
|
||||
>{form.rodo.policyText}</a
|
||||
>.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<button title={form.submit.title}>{form.submit.label}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="f-contact-map">
|
||||
<MapGoogle
|
||||
apiKey={apiKey}
|
||||
lat={data.lat}
|
||||
lon={data.lng}
|
||||
zoom={16}
|
||||
title={data.markerTitle}
|
||||
description={data.markerAddress}
|
||||
showMarker={true}
|
||||
mode="contact"
|
||||
mapStyleId={data.maps.mapId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="f-toast"></div>
|
||||
</section>
|
||||
|
||||
<!-- ReCaptcha v3 -->
|
||||
<script
|
||||
is:inline
|
||||
define:vars={{ siteKey: import.meta.env.PUBLIC_RECAPTCHA_SITE_KEY }}
|
||||
>
|
||||
window.FUZ_RECAPTCHA_KEY = siteKey;
|
||||
|
||||
const s = document.createElement("script");
|
||||
s.src = "https://www.google.com/recaptcha/api.js?render=" + siteKey;
|
||||
s.async = true;
|
||||
document.head.appendChild(s);
|
||||
</script>
|
||||
|
||||
<script
|
||||
is:inline
|
||||
define:vars={{
|
||||
successMsg: form.successMessage,
|
||||
errorMsg: form.errorMessage,
|
||||
}}
|
||||
>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const form = document.getElementById("contactForm");
|
||||
const toast = document.getElementById("toast");
|
||||
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
if (!form.reportValidity()) return;
|
||||
e.preventDefault();
|
||||
|
||||
const data = Object.fromEntries(new FormData(form).entries());
|
||||
data.rodo = form.rodo.checked;
|
||||
|
||||
const token = await grecaptcha.execute(window.FUZ_RECAPTCHA_KEY, {
|
||||
action: "submit",
|
||||
});
|
||||
data.recaptcha = token;
|
||||
|
||||
const resp = await fetch("/api/contact/contact", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
const json = await resp.json();
|
||||
|
||||
showToast(
|
||||
json.ok ? successMsg : errorMsg,
|
||||
json.ok ? "success" : "error",
|
||||
);
|
||||
|
||||
if (json.ok) form.reset();
|
||||
});
|
||||
|
||||
function showToast(msg, type) {
|
||||
toast.innerHTML = `<div class="f-toast-msg ${type}">${msg}</div>`;
|
||||
toast.classList.remove("visible");
|
||||
void toast.offsetWidth;
|
||||
toast.classList.add("visible");
|
||||
|
||||
setTimeout(() => toast.classList.remove("visible"), 3000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</DefaultLayout>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||
|
||||
const seo = {
|
||||
title: "Oferta – FUZ",
|
||||
description: "Oferta – FUZ",
|
||||
canonical: "/oferta"
|
||||
};
|
||||
---
|
||||
|
||||
<DefaultLayout seo={seo}>
|
||||
<section class="fuz-section">
|
||||
<div class="fuz-container">
|
||||
<h1 class="fuz-hero-title">Oferta – FUZ</h1>
|
||||
<p class="mt-4 text-gray-600 dark:text-gray-300">
|
||||
Ta podstrona jest na razie szkieletem. Możemy tu później wczytać treść z YAML.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</DefaultLayout>
|
||||
Reference in New Issue
Block a user