Kolejne zmiany,
This commit is contained in:
@@ -1,50 +1,163 @@
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
function esc(str = "") {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function buildHtmlMail(form) {
|
||||
const when = new Date().toLocaleString("pl-PL");
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Nowa wiadomość – FUZ</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f5f7fa;font-family:Arial,Helvetica,sans-serif;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="background:#f5f7fa;">
|
||||
<tr>
|
||||
<td align="center" style="padding:24px;">
|
||||
<table width="600" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#ffffff;border-radius:14px;overflow:hidden;box-shadow:0 8px 24px rgba(0,0,0,0.08);">
|
||||
|
||||
<!-- Header -->
|
||||
<tr>
|
||||
<td style="background:#0066ff;color:#ffffff;padding:20px 24px;">
|
||||
<div style="font-size:12px;opacity:0.9;margin-bottom:6px;">FUZ • Formularz kontaktowy</div>
|
||||
<div style="font-size:20px;font-weight:700;line-height:1.2;">📩 Nowa wiadomość</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Content -->
|
||||
<tr>
|
||||
<td style="padding:24px;">
|
||||
<div style="font-size:14px;color:#111827;margin-bottom:14px;">
|
||||
Poniżej szczegóły zgłoszenia:
|
||||
</div>
|
||||
|
||||
<table width="100%" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="font-size:14px;color:#111827;border-collapse:collapse;">
|
||||
<tr>
|
||||
<td style="padding:8px 0;width:160px;color:#374151;"><strong>Imię</strong></td>
|
||||
<td style="padding:8px 0;">${esc(form.firstName)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#374151;"><strong>Nazwisko</strong></td>
|
||||
<td style="padding:8px 0;">${esc(form.lastName)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#374151;"><strong>Email</strong></td>
|
||||
<td style="padding:8px 0;">
|
||||
<a href="mailto:${esc(form.email)}" style="color:#0066ff;text-decoration:none;">
|
||||
${esc(form.email)}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#374151;"><strong>Telefon</strong></td>
|
||||
<td style="padding:8px 0;">${esc(form.phone)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#374151;"><strong>Temat</strong></td>
|
||||
<td style="padding:8px 0;">${esc(form.subject)}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div style="height:1px;background:#e5e7eb;margin:18px 0;"></div>
|
||||
|
||||
<div style="font-size:14px;color:#111827;margin:0 0 8px;font-weight:700;">
|
||||
Wiadomość:
|
||||
</div>
|
||||
|
||||
<div style="font-size:14px;color:#111827;white-space:pre-line;line-height:1.6;
|
||||
background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:14px;">
|
||||
${esc(form.message)}
|
||||
</div>
|
||||
|
||||
<div style="margin-top:16px;font-size:12px;color:#6b7280;">
|
||||
Wysłano: ${when}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="background:#f1f5f9;padding:14px 24px;font-size:12px;color:#6b7280;">
|
||||
To jest automatyczna wiadomość wygenerowana przez formularz na stronie FUZ.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
try {
|
||||
const form = await request.json();
|
||||
|
||||
// (opcjonalnie) prosta walidacja minimum:
|
||||
if (!form?.email || !form?.message) {
|
||||
return new Response(JSON.stringify({ ok: false, error: "Brak danych" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: import.meta.env.SMTP_HOST,
|
||||
port: Number(import.meta.env.SMTP_PORT),
|
||||
secure: true, // true = 465, false = 587
|
||||
secure: true,
|
||||
auth: {
|
||||
user: import.meta.env.SMTP_USER,
|
||||
pass: import.meta.env.SMTP_PASS,
|
||||
},
|
||||
// ⚠️ tylko jeśli masz self-signed / dziwny cert
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
// Uwaga: lepiej NIE wyłączać TLS w prod, ale zostawiam zgodnie z Twoją wersją
|
||||
tls: { rejectUnauthorized: false },
|
||||
});
|
||||
|
||||
const subject = `FUZ: wiadomość od ${form.firstName || ""} ${form.lastName || ""}`.trim();
|
||||
|
||||
const text = `
|
||||
Imię: ${form.firstName || ""}
|
||||
Nazwisko: ${form.lastName || ""}
|
||||
Email: ${form.email || ""}
|
||||
Telefon: ${form.phone || ""}
|
||||
Temat: ${form.subject || ""}
|
||||
|
||||
Wiadomość:
|
||||
${form.message || ""}
|
||||
`.trim();
|
||||
|
||||
const html = buildHtmlMail(form);
|
||||
|
||||
await transporter.sendMail({
|
||||
from: `"${import.meta.env.SMTP_FROM_NAME}" <${import.meta.env.SMTP_USER}>`,
|
||||
to: import.meta.env.SMTP_TO,
|
||||
subject: `FUZ: wiadomość od ${form.firstName} ${form.lastName}`,
|
||||
text: `
|
||||
Imię: ${form.firstName}
|
||||
Nazwisko: ${form.lastName}
|
||||
Email: ${form.email}
|
||||
Telefon: ${form.phone}
|
||||
Temat: ${form.subject}
|
||||
|
||||
Wiadomość:
|
||||
${form.message}
|
||||
`.trim(),
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
replyTo: form.email ? String(form.email) : undefined, // wygodne do "Odpowiedz"
|
||||
});
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ ok: true }),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
|
||||
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, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
return new Response(JSON.stringify({ ok: false }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const DB_PATH =
|
||||
process.env.FUZ_DB_PATH || "./src/data/ServicesRange.db";
|
||||
|
||||
export async function GET() {
|
||||
const db = new Database(DB_PATH, { readonly: true });
|
||||
|
||||
try {
|
||||
const addonsRows = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT id, name, type, description
|
||||
FROM internet_addons
|
||||
ORDER BY id
|
||||
`
|
||||
)
|
||||
.all();
|
||||
|
||||
const optionsRows = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT id, addon_id, code, name, price
|
||||
FROM internet_addon_options
|
||||
ORDER BY addon_id, id
|
||||
`
|
||||
)
|
||||
.all();
|
||||
|
||||
const byAddon = new Map();
|
||||
|
||||
for (const addon of addonsRows) {
|
||||
byAddon.set(addon.id, {
|
||||
id: addon.id,
|
||||
name: addon.name,
|
||||
type: addon.type, // 'checkbox' / 'select'
|
||||
description: addon.description || "",
|
||||
options: [],
|
||||
});
|
||||
}
|
||||
|
||||
for (const opt of optionsRows) {
|
||||
const parent = byAddon.get(opt.addon_id);
|
||||
if (!parent) continue;
|
||||
parent.options.push({
|
||||
id: opt.id,
|
||||
code: opt.code,
|
||||
name: opt.name,
|
||||
price: opt.price,
|
||||
});
|
||||
}
|
||||
|
||||
const data = Array.from(byAddon.values());
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
count: data.length,
|
||||
data,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd w /api/internet/addons:", err);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
ok: false,
|
||||
error: err.message || "DB_ERROR",
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
// src/pages/api/internet/plans.js
|
||||
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 });
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/internet/plans?building=1|2&contract=1|2
|
||||
*/
|
||||
export function GET({ url }) {
|
||||
const buildingParam = url.searchParams.get("building");
|
||||
const contractParam = url.searchParams.get("contract");
|
||||
|
||||
const building = buildingParam ? Number(buildingParam) : 1;
|
||||
const contract = contractParam ? Number(contractParam) : 1;
|
||||
|
||||
const db = getDb();
|
||||
|
||||
try {
|
||||
const stmt = db.prepare(
|
||||
`
|
||||
SELECT
|
||||
p.id AS plan_id,
|
||||
p.name AS plan_name,
|
||||
p.popular AS plan_popular,
|
||||
|
||||
pr.price_monthly AS price_monthly,
|
||||
pr.price_installation AS price_installation,
|
||||
|
||||
f.id AS feature_id,
|
||||
f.label AS feature_label,
|
||||
fv.value AS feature_value
|
||||
|
||||
FROM internet_plans p
|
||||
LEFT JOIN internet_plan_prices pr
|
||||
ON pr.plan_id = p.id
|
||||
AND pr.building_type = ?
|
||||
AND pr.contract_type = ?
|
||||
|
||||
LEFT JOIN internet_plan_feature_values fv
|
||||
ON fv.plan_id = p.id
|
||||
|
||||
LEFT JOIN internet_features f
|
||||
ON f.id = fv.feature_id
|
||||
|
||||
ORDER BY p.id ASC, f.id ASC;
|
||||
`.trim()
|
||||
);
|
||||
|
||||
const rows = stmt.all(building, contract);
|
||||
|
||||
// grupowanie do struktury: jeden plan = jedna karta
|
||||
const byPlan = new Map();
|
||||
|
||||
for (const row of rows) {
|
||||
if (!byPlan.has(row.plan_id)) {
|
||||
byPlan.set(row.plan_id, {
|
||||
id: row.plan_id,
|
||||
code: row.plan_code,
|
||||
name: row.plan_name,
|
||||
popular: !!row.plan_popular,
|
||||
price_monthly: row.price_monthly,
|
||||
price_installation: row.price_installation,
|
||||
features: [], // później wypełniamy
|
||||
});
|
||||
}
|
||||
|
||||
if (row.feature_id) {
|
||||
byPlan.get(row.plan_id).features.push({
|
||||
id: row.feature_id,
|
||||
label: row.feature_label,
|
||||
value: row.feature_value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const data = Array.from(byPlan.values());
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
building,
|
||||
contract,
|
||||
count: data.length,
|
||||
data,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Cache-Control": "public, max-age=30",
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd w /api/internet/plans:", err);
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: "DB_ERROR" }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,6 @@ import path from "node:path";
|
||||
import { XMLParser } from "fast-xml-parser";
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
/* =====================
|
||||
KONFIG
|
||||
===================== */
|
||||
|
||||
const FEEDS = [
|
||||
{ url: "https://www.jambox.pl/xml/listakanalow-smart.xml", name: "Smart" },
|
||||
{ url: "https://www.jambox.pl/xml/listakanalow-optimum.xml", name: "Optimum" },
|
||||
@@ -15,25 +11,16 @@ const FEEDS = [
|
||||
{ url: "https://www.jambox.pl/xml/listakanalow-plusbogaty.xml", name: "Bogaty" },
|
||||
];
|
||||
|
||||
// 👉 ustaw jeśli chcesz inną bazę
|
||||
const DB_PATH =
|
||||
process.env.FUZ_DB_PATH ||
|
||||
path.join(process.cwd(), "src", "data", "ServicesRange.db");
|
||||
|
||||
/* =====================
|
||||
DB
|
||||
===================== */
|
||||
|
||||
function getDb() {
|
||||
const db = new Database(DB_PATH);
|
||||
db.pragma("journal_mode = WAL");
|
||||
return db;
|
||||
}
|
||||
|
||||
/* =====================
|
||||
XML / HTML HELPERS
|
||||
===================== */
|
||||
|
||||
async function fetchXml(url) {
|
||||
const res = await fetch(url, {
|
||||
headers: { accept: "application/xml,text/xml,*/*" },
|
||||
@@ -155,17 +142,9 @@ async function downloadLogoAsBase64(url) {
|
||||
}
|
||||
}
|
||||
|
||||
/* =====================
|
||||
API ROUTE
|
||||
===================== */
|
||||
|
||||
export async function POST() {
|
||||
const db = getDb();
|
||||
|
||||
// ⚠️ WYMAGANE:
|
||||
// CREATE UNIQUE INDEX ux_jambox_channels_nazwa_pckg
|
||||
// ON jambox_channels(nazwa, pckg_name);
|
||||
|
||||
const upsert = db.prepare(`
|
||||
INSERT INTO jambox_channels (nazwa, pckg_name, image, opis)
|
||||
VALUES (@nazwa, @pckg_name, @image, @opis)
|
||||
@@ -174,7 +153,7 @@ export async function POST() {
|
||||
opis = COALESCE(excluded.opis, jambox_channels.opis)
|
||||
`);
|
||||
|
||||
const logoCache = new Map(); // nazwa(lower) -> base64 | null
|
||||
const logoCache = new Map();
|
||||
const rows = [];
|
||||
|
||||
try {
|
||||
@@ -220,7 +199,7 @@ export async function POST() {
|
||||
{ headers: { "content-type": "application/json; charset=utf-8" } }
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("❌ import jambox_channels:", e);
|
||||
console.error("import jambox_channels:", e);
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: String(e.message || e) }),
|
||||
{ status: 500 }
|
||||
@@ -1,30 +1,15 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { XMLParser } from "fast-xml-parser";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs/promises";
|
||||
|
||||
const URL = "https://www.jambox.pl/xml/mozliwosci.xml";
|
||||
|
||||
type Section = {
|
||||
title: string;
|
||||
image?: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
type ContentBlock =
|
||||
| { type: "text"; value: string }
|
||||
| { type: "list"; items: string[] };
|
||||
|
||||
function toArray<T>(v: T | T[] | undefined | null): T[] {
|
||||
function toArray(v) {
|
||||
if (!v) return [];
|
||||
return Array.isArray(v) ? v : [v];
|
||||
}
|
||||
|
||||
/* =======================
|
||||
HTML / XML HELPERS
|
||||
======================= */
|
||||
|
||||
function decodeEntities(input: string): string {
|
||||
function decodeEntities(input) {
|
||||
if (!input) return "";
|
||||
|
||||
let s = String(input)
|
||||
@@ -35,9 +20,7 @@ function decodeEntities(input: string): string {
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
|
||||
s = s.replace(/&#(\d+);/g, (_, d) =>
|
||||
String.fromCodePoint(Number(d))
|
||||
);
|
||||
s = s.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)));
|
||||
s = s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) =>
|
||||
String.fromCodePoint(parseInt(h, 16))
|
||||
);
|
||||
@@ -45,30 +28,22 @@ function decodeEntities(input: string): string {
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsuje HTML:
|
||||
* - <p>, <div>, <br> → zwykłe nowe linie
|
||||
* - <ul>/<ol><li> → markdown lista
|
||||
*/
|
||||
function parseHtmlContent(input?: string): ContentBlock[] {
|
||||
function parseHtmlContent(input) {
|
||||
if (!input) return [];
|
||||
|
||||
let s = decodeEntities(String(input));
|
||||
|
||||
// znaczniki list
|
||||
s = s
|
||||
.replace(/<\s*(ul|ol)[^>]*>/gi, "\n__LIST_START__\n")
|
||||
.replace(/<\/\s*(ul|ol)\s*>/gi, "\n__LIST_END__\n")
|
||||
.replace(/<\s*li[^>]*>/gi, "__LI__")
|
||||
.replace(/<\/\s*li\s*>/gi, "\n");
|
||||
|
||||
// normalne bloki
|
||||
s = s
|
||||
.replace(/<\s*br\s*\/?\s*>/gi, "\n")
|
||||
.replace(/<\/\s*(p|div)\s*>/gi, "\n")
|
||||
.replace(/<\s*(p|div)[^>]*>/gi, "");
|
||||
|
||||
// usuń resztę tagów
|
||||
s = s.replace(/<[^>]+>/g, "");
|
||||
|
||||
s = s
|
||||
@@ -77,11 +52,11 @@ function parseHtmlContent(input?: string): ContentBlock[] {
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim();
|
||||
|
||||
const blocks: ContentBlock[] = [];
|
||||
const blocks = [];
|
||||
const lines = s.split("\n");
|
||||
|
||||
let textBuf: string[] = [];
|
||||
let listBuf: string[] | null = null;
|
||||
let textBuf = [];
|
||||
let listBuf = null;
|
||||
|
||||
const flushText = () => {
|
||||
const txt = textBuf.join("\n").trim();
|
||||
@@ -132,12 +107,11 @@ function parseHtmlContent(input?: string): ContentBlock[] {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function blocksToMarkdown(blocks: ContentBlock[]): string {
|
||||
const out: string[] = [];
|
||||
function blocksToMarkdown(blocks) {
|
||||
const out = [];
|
||||
|
||||
for (const b of blocks) {
|
||||
if (b.type === "text") {
|
||||
// 👉 każde zdanie zakończone kropką = nowa linia
|
||||
const lines = b.value
|
||||
.replace(/\.\s+/g, ".\n")
|
||||
.split("\n")
|
||||
@@ -157,20 +131,15 @@ function blocksToMarkdown(blocks: ContentBlock[]): string {
|
||||
return out.join("\n\n").trim();
|
||||
}
|
||||
|
||||
|
||||
/* =======================
|
||||
SCREEN / YAML
|
||||
======================= */
|
||||
|
||||
function extractUrlsFromString(s: string): string[] {
|
||||
return s.match(/https?:\/\/[^\s<"]+/g) ?? [];
|
||||
function extractUrlsFromString(s) {
|
||||
return String(s).match(/https?:\/\/[^\s<"]+/g) ?? [];
|
||||
}
|
||||
|
||||
function extractScreens(screen: any): string[] {
|
||||
function extractScreens(screen) {
|
||||
if (!screen) return [];
|
||||
if (typeof screen === "string") return extractUrlsFromString(screen);
|
||||
|
||||
const divs = (screen as any)?.div;
|
||||
const divs = screen?.div;
|
||||
if (divs) {
|
||||
return toArray(divs)
|
||||
.map((d) => (typeof d === "string" ? d : d?.["#text"] ?? ""))
|
||||
@@ -180,19 +149,19 @@ function extractScreens(screen: any): string[] {
|
||||
return extractUrlsFromString(JSON.stringify(screen));
|
||||
}
|
||||
|
||||
function yamlQuote(v: string): string {
|
||||
function yamlQuote(v) {
|
||||
return `"${String(v).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
|
||||
function toYaml(sections: Section[]): string {
|
||||
const out: string[] = ["sections:"];
|
||||
function toYaml(sections) {
|
||||
const out = ["sections:"];
|
||||
|
||||
for (const s of sections) {
|
||||
out.push(` - title: ${yamlQuote(s.title)}`);
|
||||
if (s.image) out.push(` image: ${yamlQuote(s.image)}`);
|
||||
out.push(" content: |");
|
||||
|
||||
for (const line of s.content.split("\n")) {
|
||||
for (const line of String(s.content).split("\n")) {
|
||||
out.push(` ${line}`);
|
||||
}
|
||||
|
||||
@@ -202,11 +171,7 @@ function toYaml(sections: Section[]): string {
|
||||
return out.join("\n").trimEnd() + "\n";
|
||||
}
|
||||
|
||||
/* =======================
|
||||
API
|
||||
======================= */
|
||||
|
||||
export const POST: APIRoute = async () => {
|
||||
export async function POST() {
|
||||
const res = await fetch(URL, {
|
||||
headers: { accept: "application/xml,text/xml,*/*" },
|
||||
});
|
||||
@@ -219,10 +184,10 @@ export const POST: APIRoute = async () => {
|
||||
const parser = new XMLParser({ trimValues: true });
|
||||
const parsed = parser.parse(xml);
|
||||
|
||||
const nodes = toArray((parsed as any)?.xml?.node ?? (parsed as any)?.node);
|
||||
const nodes = toArray(parsed?.xml?.node ?? parsed?.node);
|
||||
|
||||
const sections: Section[] = nodes
|
||||
.map((n: any) => {
|
||||
const sections = nodes
|
||||
.map((n) => {
|
||||
const title = parseHtmlContent(n?.title)
|
||||
.map((b) => (b.type === "text" ? b.value : ""))
|
||||
.join(" ")
|
||||
@@ -243,21 +208,15 @@ export const POST: APIRoute = async () => {
|
||||
|
||||
return { title, image, content };
|
||||
})
|
||||
.filter(Boolean) as Section[];
|
||||
.filter(Boolean);
|
||||
|
||||
const outDir = path.join(
|
||||
process.cwd(),
|
||||
"src",
|
||||
"content",
|
||||
"internet-telewizja"
|
||||
);
|
||||
const outDir = path.join(process.cwd(), "src", "content", "internet-telewizja");
|
||||
const outFile = path.join(outDir, "telewizja-mozliwosci.yaml");
|
||||
|
||||
await fs.mkdir(outDir, { recursive: true });
|
||||
await fs.writeFile(outFile, toYaml(sections), "utf8");
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ ok: true, count: sections.length }),
|
||||
{ headers: { "content-type": "application/json" } }
|
||||
);
|
||||
};
|
||||
return new Response(JSON.stringify({ ok: true, count: sections.length }), {
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
@@ -8,7 +8,6 @@ function getDb() {
|
||||
|
||||
function cleanPkgName(v) {
|
||||
const s = String(v || "").trim();
|
||||
// prosta sanity: niepuste, nieprzesadnie długie
|
||||
if (!s) return null;
|
||||
if (s.length > 64) return null;
|
||||
return s;
|
||||
@@ -51,7 +50,7 @@ export function GET({ url }) {
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd w /api/jambox/channels:", err);
|
||||
console.error("Błąd w /api/jambox/jambox-channels-package:", err);
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: "DB_ERROR" }),
|
||||
{
|
||||
@@ -14,7 +14,6 @@ function uniq(arr) {
|
||||
return Array.from(new Set(arr));
|
||||
}
|
||||
|
||||
// jeśli chcesz id do scrollowania (pkg-smart), to możesz dać slug
|
||||
function slugifyPkg(name) {
|
||||
return String(name || "")
|
||||
.toLowerCase()
|
||||
@@ -28,7 +27,6 @@ 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 < 1) {
|
||||
return new Response(JSON.stringify({ ok: true, data: [] }), {
|
||||
status: 200,
|
||||
@@ -36,7 +34,6 @@ export function GET({ url }) {
|
||||
});
|
||||
}
|
||||
|
||||
// escape LIKE wildcardów
|
||||
const safe = q.replace(/[%_]/g, (m) => `\\${m}`);
|
||||
const like = `%${safe}%`;
|
||||
|
||||
@@ -68,19 +65,15 @@ export function GET({ url }) {
|
||||
|
||||
const packages = uniq(pkgsRaw)
|
||||
.map((p) => ({
|
||||
// jeśli UI wymaga ID do scrolla, to to jest najbezpieczniejsze:
|
||||
id: slugifyPkg(p), // np. "smart" -> użyjesz jako pkg-smart
|
||||
id: slugifyPkg(p),
|
||||
name: p,
|
||||
number: "—", // brak w nowej tabeli
|
||||
guaranteed: false, // brak w nowej tabeli
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name, "pl"));
|
||||
|
||||
return {
|
||||
name: r.name,
|
||||
logo_url: r.logo_url || "", // base64 data-url albo ""
|
||||
logo_url: r.logo_url || "",
|
||||
description: r.description || "",
|
||||
min_number: 0, // brak numerów
|
||||
packages,
|
||||
};
|
||||
});
|
||||
@@ -90,7 +83,7 @@ export function GET({ url }) {
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd w /api/jambox/channels-search:", err);
|
||||
console.error("Błąd w /api/jambox/jambox-channels-search:", err);
|
||||
return new Response(JSON.stringify({ ok: false, error: "DB_ERROR" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
@@ -1,70 +0,0 @@
|
||||
// src/pages/api/switches.js
|
||||
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 });
|
||||
}
|
||||
|
||||
export function GET() {
|
||||
const db = getDb();
|
||||
|
||||
try {
|
||||
const buildingTypes = db
|
||||
.prepare("SELECT code, label FROM jambox_building_types ORDER BY is_default DESC, code")
|
||||
.all();
|
||||
|
||||
const contractTypes = db
|
||||
.prepare("SELECT code, label FROM jambox_contract_types ORDER BY is_default DESC, code")
|
||||
.all();
|
||||
|
||||
const switches = [
|
||||
{
|
||||
id: "budynek",
|
||||
etykieta: "Rodzaj budynku",
|
||||
domyslny: buildingTypes[0]?.code ?? 1,
|
||||
title: "Zmień rodzaj budynku by zobaczyć odpowiednie ceny",
|
||||
opcje: buildingTypes.map((b) => ({
|
||||
id: b.code,
|
||||
nazwa: b.label,
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "umowa",
|
||||
etykieta: "Okres umowy",
|
||||
domyslny: contractTypes[0]?.code ?? 1,
|
||||
title: "Wybierz okres umowy by zobaczyć odpowiednie ceny",
|
||||
opcje: contractTypes.map((c) => ({
|
||||
id: c.code,
|
||||
nazwa: c.label,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ ok: true, data: switches }),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Cache-Control": "public, max-age=60",
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Błąd w /api/switches:", err);
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: err.message || "DB_ERROR" }),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
@@ -15,18 +15,19 @@ const html = marked.parse(doc.content);
|
||||
---
|
||||
|
||||
<DefaultLayout title={doc.title}>
|
||||
<section class="max-w-4xl mx-auto px-4 py-10">
|
||||
<a href="/dokumenty" class="text-sm opacity-70 hover:opacity-100">
|
||||
← Wróć do dokumentów
|
||||
</a>
|
||||
<section class="f-section">
|
||||
<div class="f-section-grid-single">
|
||||
<a href="/dokumenty" class="text-sm opacity-70 hover:opacity-100">
|
||||
← Wróć do dokumentów
|
||||
</a>
|
||||
|
||||
<h1 class="mt-4 text-4xl md:text-5xl font-bold text-[--f-header]">
|
||||
{doc.title}
|
||||
</h1>
|
||||
<h1 class="f-section-title">
|
||||
{doc.title}
|
||||
</h1>
|
||||
|
||||
<article class="mt-8 prose max-w-none">
|
||||
<div class="fuz-markdown max-w-none">
|
||||
<Markdown text={html} />
|
||||
<!-- <div set:html={html} /> -->
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</DefaultLayout>
|
||||
|
||||
@@ -65,6 +65,24 @@ const addons: Addon[] = Array.isArray(addonsData?.dodatki)
|
||||
|
||||
// jeśli chcesz, możesz nadpisać cenaOpis w modalu z addons.yaml:
|
||||
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: SwitchDef[] = Array.isArray(switchesData?.switches)
|
||||
? switchesData.switches
|
||||
: [];
|
||||
---
|
||||
|
||||
<DefaultLayout seo={seo}>
|
||||
@@ -80,6 +98,7 @@ const addonsCenaOpis = addonsData?.cena_opis ?? cenaOpis;
|
||||
phoneCards={phoneCards}
|
||||
addons={addons}
|
||||
addonsCenaOpis={addonsCenaOpis}
|
||||
switches={switches}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -50,7 +50,7 @@ type PhoneCard = {
|
||||
};
|
||||
type PhoneYaml = { cards?: PhoneCard[] };
|
||||
|
||||
type Decoder = { id: string; nazwa: string; cena: number };
|
||||
type Decoder = { id: string; nazwa: string; opis: string; cena: number };
|
||||
|
||||
// ✅ dodatki z YAML (do modala)
|
||||
type Addon = {
|
||||
@@ -72,16 +72,16 @@ type AddonsYaml = {
|
||||
dodatki?: Addon[];
|
||||
};
|
||||
|
||||
type ChannelsYaml = {
|
||||
title?: string;
|
||||
updated_at?: string;
|
||||
channels?: Array<{
|
||||
nazwa: string;
|
||||
opis?: string;
|
||||
image?: string;
|
||||
pakiety?: string[];
|
||||
}>;
|
||||
};
|
||||
// type ChannelsYaml = {
|
||||
// title?: string;
|
||||
// updated_at?: string;
|
||||
// channels?: Array<{
|
||||
// nazwa: string;
|
||||
// opis?: string;
|
||||
// image?: string;
|
||||
// pakiety?: string[];
|
||||
// }>;
|
||||
// };
|
||||
|
||||
const seo = loadYamlFile<SeoYaml>(
|
||||
path.join(process.cwd(), "src", "content", "internet-telewizja", "seo.yaml"),
|
||||
@@ -141,6 +141,25 @@ const decoders: Decoder[] = Array.isArray(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}>
|
||||
@@ -159,6 +178,7 @@ const addonsCenaOpis = addonsYaml?.cena_opis ?? cenaOpis;
|
||||
decoders={decoders}
|
||||
addons={addons}
|
||||
addonsCenaOpis={addonsCenaOpis}
|
||||
switches={switches}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
import yaml from "js-yaml";
|
||||
import fs from "fs";
|
||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||
import Markdown from "../../islands/Markdown.jsx";
|
||||
|
||||
const privacy = yaml.load(
|
||||
fs.readFileSync("./src/content/polityka-prywatnosci/privacy.yaml", "utf8"),
|
||||
);
|
||||
---
|
||||
|
||||
<DefaultLayout title={privacy.title}>
|
||||
<section class="f-section">
|
||||
<div class="f-section-grid-single">
|
||||
<h1 class="f-section-title">
|
||||
{privacy.title}
|
||||
</h1>
|
||||
<Markdown text={privacy.content} />
|
||||
</div>
|
||||
</section>
|
||||
</DefaultLayout>
|
||||
Reference in New Issue
Block a user