Rezygnacja z bazy, przeniesienie danych do plików yamla
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
interface Channel {
|
||||
title: string;
|
||||
logo: string;
|
||||
}
|
||||
|
||||
const cache = new Map<string, { time: number; data: Channel[] }>();
|
||||
const CACHE_TIME = 1000 * 60 * 60 * 24 * 30; //miesiąc
|
||||
|
||||
export const GET: APIRoute = async ({ params }) => {
|
||||
const id = params.id!;
|
||||
const cached = cache.get(id);
|
||||
|
||||
if (cached && Date.now() - cached.time < CACHE_TIME) {
|
||||
return new Response(JSON.stringify(cached.data), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
|
||||
const url = `https://www.jambox.pl/iframe-pakiet-logo?p=${id}`;
|
||||
const resp = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0" } });
|
||||
|
||||
const html = await resp.text();
|
||||
const dom = new JSDOM(html);
|
||||
|
||||
const images = [
|
||||
...dom.window.document.querySelectorAll("img.imagefield-field_logo")
|
||||
];
|
||||
|
||||
const channels = images.map((img) => ({
|
||||
title: img.getAttribute("alt")?.trim() ?? "",
|
||||
logo: img.getAttribute("src") ?? "",
|
||||
}));
|
||||
|
||||
cache.set(id, { time: Date.now(), data: channels });
|
||||
|
||||
return new Response(JSON.stringify(channels), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const DB_PATH = process.env.FUZ_DB_PATH || "./src/data/ServicesRange.db";
|
||||
|
||||
export async function GET({ url }) {
|
||||
const db = new Database(DB_PATH, { readonly: true });
|
||||
|
||||
try {
|
||||
const packageId = Number(url.searchParams.get("packageId") || 0);
|
||||
if (!packageId) {
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: "MISSING_PACKAGE_ID" }),
|
||||
{ status: 400, headers: { "Content-Type": "application/json; charset=utf-8" } }
|
||||
);
|
||||
}
|
||||
|
||||
const rows = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
a.id AS id,
|
||||
a.name AS name,
|
||||
a.type AS type,
|
||||
a.description AS description,
|
||||
CAST(o.price AS REAL) AS price
|
||||
FROM jambox_package_addon_options o
|
||||
JOIN internet_addons a
|
||||
ON a.id = o.addon_id
|
||||
WHERE o.package_id = ?
|
||||
ORDER BY a.type, a.name
|
||||
`
|
||||
)
|
||||
.all(packageId);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
count: rows.length,
|
||||
data: rows,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd w /api/jambox/addons:", 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();
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
// src/pages/api/jambox/base-packages.js
|
||||
//import { getDb } from "../db.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/jambox/base-packages?source=PLUS|EVIO|ALL&building=1|2&contract=1|2
|
||||
*/
|
||||
export function GET({ url }) {
|
||||
const sourceParam = url.searchParams.get("source") || "PLUS";
|
||||
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 rows = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
p.id AS package_id,
|
||||
p.source AS package_source,
|
||||
p.tid AS package_tid,
|
||||
p.name AS package_name,
|
||||
p.slug AS package_slug,
|
||||
p.sort_order AS package_sort_order,
|
||||
p.updated_at AS package_updated_at,
|
||||
|
||||
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 jambox_base_packages p
|
||||
|
||||
LEFT JOIN jambox_base_package_prices pr
|
||||
ON pr.package_id = p.id
|
||||
AND pr.building_type = ?
|
||||
AND pr.contract_type = ?
|
||||
|
||||
LEFT JOIN jambox_package_feature_values fv
|
||||
ON fv.package_id = p.id
|
||||
|
||||
LEFT JOIN internet_features f
|
||||
ON f.id = fv.feature_id
|
||||
|
||||
WHERE (? = 'ALL' OR p.source = ?)
|
||||
ORDER BY p.sort_order ASC, p.id ASC, f.id ASC;
|
||||
`.trim()
|
||||
)
|
||||
.all(building, contract, sourceParam, sourceParam);
|
||||
|
||||
// grupowanie jak w /api/internet/plans
|
||||
const byPackage = new Map();
|
||||
|
||||
for (const row of rows) {
|
||||
if (!byPackage.has(row.package_id)) {
|
||||
byPackage.set(row.package_id, {
|
||||
id: row.package_id,
|
||||
source: row.package_source,
|
||||
tid: row.package_tid,
|
||||
name: row.package_name,
|
||||
slug: row.package_slug,
|
||||
sort_order: row.package_sort_order,
|
||||
updated_at: row.package_updated_at,
|
||||
price_monthly: row.price_monthly,
|
||||
price_installation: row.price_installation,
|
||||
features: [],
|
||||
});
|
||||
}
|
||||
|
||||
if (row.feature_id) {
|
||||
byPackage.get(row.package_id).features.push({
|
||||
id: row.feature_id,
|
||||
label: row.feature_label,
|
||||
value: row.feature_value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const data = Array.from(byPackage.values());
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
source: sourceParam,
|
||||
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/jambox/base-packages:", err);
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: "DB_ERROR" }),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,17 +10,33 @@ function clamp(n, min, max) {
|
||||
return Math.max(min, Math.min(max, n));
|
||||
}
|
||||
|
||||
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()
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)/g, "");
|
||||
}
|
||||
|
||||
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 < 0) {
|
||||
|
||||
if (q.length < 1) {
|
||||
return new Response(JSON.stringify({ ok: true, data: [] }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
|
||||
// escape LIKE wildcardów
|
||||
const safe = q.replace(/[%_]/g, (m) => `\\${m}`);
|
||||
const like = `%${safe}%`;
|
||||
|
||||
@@ -28,48 +44,43 @@ export function GET({ url }) {
|
||||
|
||||
try {
|
||||
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);
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
nazwa AS name,
|
||||
MAX(image) AS logo_url,
|
||||
MAX(opis) AS description,
|
||||
GROUP_CONCAT(pckg_name, '||') AS packages_blob
|
||||
FROM jambox_channels
|
||||
WHERE nazwa LIKE ? ESCAPE '\\'
|
||||
GROUP BY nazwa
|
||||
ORDER BY name COLLATE NOCASE ASC
|
||||
LIMIT ?;
|
||||
`.trim()
|
||||
)
|
||||
.all(like, limit);
|
||||
|
||||
const data = rows.map((r) => {
|
||||
const packages = String(r.packages_blob || "")
|
||||
const pkgsRaw = 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);
|
||||
.map((x) => x.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
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
|
||||
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,
|
||||
logo_url: r.logo_url || "", // base64 data-url albo ""
|
||||
description: r.description || "",
|
||||
min_number: Number(r.min_number || 0),
|
||||
min_number: 0, // brak numerów
|
||||
packages,
|
||||
};
|
||||
});
|
||||
@@ -84,5 +95,9 @@ export function GET({ url }) {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
} finally {
|
||||
try {
|
||||
db.close();
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
// src/pages/api/jambox/channels.js
|
||||
//import { getDb } from "../db.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({ url }) {
|
||||
const packageIdParam = url.searchParams.get("packageId");
|
||||
const packageId = Number(packageIdParam);
|
||||
|
||||
if (!packageId) {
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: "INVALID_PACKAGE_ID" }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
try {
|
||||
const rows = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
number,
|
||||
name,
|
||||
description,
|
||||
logo_url,
|
||||
guaranteed
|
||||
FROM jambox_package_channels
|
||||
WHERE package_id = ?
|
||||
ORDER BY number ASC;
|
||||
`.trim()
|
||||
)
|
||||
.all(packageId);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ ok: true, data: rows }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd w /api/jambox/channels:", err);
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: "DB_ERROR" }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
231
src/pages/api/jambox/import-channels.js
Normal file
231
src/pages/api/jambox/import-channels.js
Normal file
@@ -0,0 +1,231 @@
|
||||
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" },
|
||||
{ url: "https://www.jambox.pl/xml/listakanalow-platinum.xml", name: "Platinum" },
|
||||
{ url: "https://www.jambox.pl/xml/listakanalow-pluspodstawowy.xml", name: "Podstawowy" },
|
||||
{ url: "https://www.jambox.pl/xml/listakanalow-pluskorzystny.xml", name: "Korzystny" },
|
||||
{ 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,*/*" },
|
||||
});
|
||||
if (!res.ok) throw new Error(`XML HTTP ${res.status} for ${url}`);
|
||||
return await res.text();
|
||||
}
|
||||
|
||||
function parseNodes(xmlText) {
|
||||
const parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
attributeNamePrefix: "@_",
|
||||
});
|
||||
const json = parser.parse(xmlText);
|
||||
const nodes = json?.xml?.node ?? json?.node ?? [];
|
||||
return Array.isArray(nodes) ? nodes : [nodes];
|
||||
}
|
||||
|
||||
function decodeEntities(input) {
|
||||
if (!input) return "";
|
||||
let s = String(input)
|
||||
.replace(/ /g, " ")
|
||||
.replace(/–/g, "–")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/'/g, "'")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
|
||||
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))
|
||||
);
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
function htmlToMarkdown(input) {
|
||||
if (!input) return "";
|
||||
|
||||
let html = "";
|
||||
if (typeof input === "string") html = input;
|
||||
else if (input?.p) {
|
||||
if (typeof input.p === "string") html = `<p>${input.p}</p>`;
|
||||
else if (Array.isArray(input.p))
|
||||
html = input.p.map((p) => `<p>${p}</p>`).join("");
|
||||
} else html = String(input);
|
||||
|
||||
let s = decodeEntities(html);
|
||||
|
||||
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")
|
||||
.replace(/<\s*br\s*\/?\s*>/gi, "\n")
|
||||
.replace(/<\/\s*(p|div)\s*>/gi, "\n")
|
||||
.replace(/<\s*(p|div)[^>]*>/gi, "")
|
||||
.replace(/<[^>]+>/g, "")
|
||||
.replace(/\r/g, "")
|
||||
.replace(/[ \t]+\n/g, "\n")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim();
|
||||
|
||||
const lines = s.split("\n").map((x) => x.trim());
|
||||
const out = [];
|
||||
let inList = false;
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line) {
|
||||
if (!inList) out.push("");
|
||||
continue;
|
||||
}
|
||||
if (line === "__LIST_START__") {
|
||||
inList = true;
|
||||
continue;
|
||||
}
|
||||
if (line === "__LIST_END__") {
|
||||
inList = false;
|
||||
out.push("");
|
||||
continue;
|
||||
}
|
||||
if (inList && line.startsWith("__LI__")) {
|
||||
out.push(`- ${line.replace("__LI__", "").trim()}`);
|
||||
continue;
|
||||
}
|
||||
out.push(line);
|
||||
}
|
||||
|
||||
return out.join("\n").trim();
|
||||
}
|
||||
|
||||
function extractLogoUrl(node) {
|
||||
const logo = node?.field_logo_fid;
|
||||
if (!logo) return null;
|
||||
if (typeof logo === "string") {
|
||||
const m = logo.match(/src="([^"]+)"/);
|
||||
return m?.[1] ?? null;
|
||||
}
|
||||
if (logo?.img?.["@_src"]) return logo.img["@_src"];
|
||||
return null;
|
||||
}
|
||||
|
||||
async function downloadLogoAsBase64(url) {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return null;
|
||||
|
||||
const ct = res.headers.get("content-type") || "image/png";
|
||||
const buf = Buffer.from(await res.arrayBuffer());
|
||||
if (!buf.length) return null;
|
||||
|
||||
return `data:${ct};base64,${buf.toString("base64")}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* =====================
|
||||
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)
|
||||
ON CONFLICT(nazwa, pckg_name) DO UPDATE SET
|
||||
image = COALESCE(excluded.image, jambox_channels.image),
|
||||
opis = COALESCE(excluded.opis, jambox_channels.opis)
|
||||
`);
|
||||
|
||||
const logoCache = new Map(); // nazwa(lower) -> base64 | null
|
||||
const rows = [];
|
||||
|
||||
try {
|
||||
for (const feed of FEEDS) {
|
||||
const xml = await fetchXml(feed.url);
|
||||
const nodes = parseNodes(xml);
|
||||
|
||||
for (const node of nodes) {
|
||||
const name = (node?.nazwa_kanalu ?? node?.nazwa ?? "").trim();
|
||||
if (!name) continue;
|
||||
|
||||
const opis = htmlToMarkdown(node?.opis) || null;
|
||||
|
||||
const key = name.toLowerCase();
|
||||
let img = logoCache.get(key);
|
||||
if (img === undefined) {
|
||||
const logoUrl = extractLogoUrl(node);
|
||||
img = logoUrl ? await downloadLogoAsBase64(logoUrl) : null;
|
||||
logoCache.set(key, img);
|
||||
}
|
||||
|
||||
rows.push({
|
||||
nazwa: name,
|
||||
pckg_name: feed.name,
|
||||
image: img,
|
||||
opis,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const trx = db.transaction((data) => {
|
||||
for (const r of data) upsert.run(r);
|
||||
});
|
||||
|
||||
trx(rows);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
rows: rows.length,
|
||||
db: DB_PATH,
|
||||
}),
|
||||
{ headers: { "content-type": "application/json; charset=utf-8" } }
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("❌ import jambox_channels:", e);
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: String(e.message || e) }),
|
||||
{ status: 500 }
|
||||
);
|
||||
} finally {
|
||||
try { db.close(); } catch {}
|
||||
}
|
||||
}
|
||||
263
src/pages/api/jambox/import-mozliwosci-to-yaml.ts
Normal file
263
src/pages/api/jambox/import-mozliwosci-to-yaml.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
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[] {
|
||||
if (!v) return [];
|
||||
return Array.isArray(v) ? v : [v];
|
||||
}
|
||||
|
||||
/* =======================
|
||||
HTML / XML HELPERS
|
||||
======================= */
|
||||
|
||||
function decodeEntities(input: string): string {
|
||||
if (!input) return "";
|
||||
|
||||
let s = String(input)
|
||||
.replace(/ /g, " ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
|
||||
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))
|
||||
);
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsuje HTML:
|
||||
* - <p>, <div>, <br> → zwykłe nowe linie
|
||||
* - <ul>/<ol><li> → markdown lista
|
||||
*/
|
||||
function parseHtmlContent(input?: string): ContentBlock[] {
|
||||
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
|
||||
.replace(/\r/g, "")
|
||||
.replace(/[ \t]+\n/g, "\n")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim();
|
||||
|
||||
const blocks: ContentBlock[] = [];
|
||||
const lines = s.split("\n");
|
||||
|
||||
let textBuf: string[] = [];
|
||||
let listBuf: string[] | null = null;
|
||||
|
||||
const flushText = () => {
|
||||
const txt = textBuf.join("\n").trim();
|
||||
if (txt) blocks.push({ type: "text", value: txt });
|
||||
textBuf = [];
|
||||
};
|
||||
|
||||
const flushList = () => {
|
||||
if (listBuf && listBuf.length) {
|
||||
blocks.push({
|
||||
type: "list",
|
||||
items: listBuf.map((x) => x.trim()).filter(Boolean),
|
||||
});
|
||||
}
|
||||
listBuf = null;
|
||||
};
|
||||
|
||||
for (const raw of lines) {
|
||||
const line = raw.trim();
|
||||
|
||||
if (line === "__LIST_START__") {
|
||||
flushText();
|
||||
listBuf = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line === "__LIST_END__") {
|
||||
flushList();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (listBuf && line.startsWith("__LI__")) {
|
||||
listBuf.push(line.replace("__LI__", "").trim());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line) {
|
||||
if (!listBuf) textBuf.push("");
|
||||
continue;
|
||||
}
|
||||
|
||||
textBuf.push(line);
|
||||
}
|
||||
|
||||
flushText();
|
||||
flushList();
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function blocksToMarkdown(blocks: ContentBlock[]): string {
|
||||
const out: string[] = [];
|
||||
|
||||
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")
|
||||
.map((x) => x.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
out.push(lines.join("\n"));
|
||||
}
|
||||
|
||||
if (b.type === "list") {
|
||||
for (const item of b.items) {
|
||||
out.push(`- ${item}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out.join("\n\n").trim();
|
||||
}
|
||||
|
||||
|
||||
/* =======================
|
||||
SCREEN / YAML
|
||||
======================= */
|
||||
|
||||
function extractUrlsFromString(s: string): string[] {
|
||||
return s.match(/https?:\/\/[^\s<"]+/g) ?? [];
|
||||
}
|
||||
|
||||
function extractScreens(screen: any): string[] {
|
||||
if (!screen) return [];
|
||||
if (typeof screen === "string") return extractUrlsFromString(screen);
|
||||
|
||||
const divs = (screen as any)?.div;
|
||||
if (divs) {
|
||||
return toArray(divs)
|
||||
.map((d) => (typeof d === "string" ? d : d?.["#text"] ?? ""))
|
||||
.flatMap(extractUrlsFromString);
|
||||
}
|
||||
|
||||
return extractUrlsFromString(JSON.stringify(screen));
|
||||
}
|
||||
|
||||
function yamlQuote(v: string): string {
|
||||
return `"${String(v).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
|
||||
function toYaml(sections: Section[]): string {
|
||||
const out: string[] = ["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")) {
|
||||
out.push(` ${line}`);
|
||||
}
|
||||
|
||||
out.push("");
|
||||
}
|
||||
|
||||
return out.join("\n").trimEnd() + "\n";
|
||||
}
|
||||
|
||||
/* =======================
|
||||
API
|
||||
======================= */
|
||||
|
||||
export const POST: APIRoute = async () => {
|
||||
const res = await fetch(URL, {
|
||||
headers: { accept: "application/xml,text/xml,*/*" },
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return new Response(`JAMBOX XML: HTTP ${res.status}`, { status: 502 });
|
||||
}
|
||||
|
||||
const xml = await res.text();
|
||||
const parser = new XMLParser({ trimValues: true });
|
||||
const parsed = parser.parse(xml);
|
||||
|
||||
const nodes = toArray((parsed as any)?.xml?.node ?? (parsed as any)?.node);
|
||||
|
||||
const sections: Section[] = nodes
|
||||
.map((n: any) => {
|
||||
const title = parseHtmlContent(n?.title)
|
||||
.map((b) => (b.type === "text" ? b.value : ""))
|
||||
.join(" ")
|
||||
.trim();
|
||||
|
||||
if (!title) return null;
|
||||
|
||||
const blocks = [
|
||||
...parseHtmlContent(n?.teaser),
|
||||
...parseHtmlContent(n?.description),
|
||||
];
|
||||
|
||||
const content = blocksToMarkdown(blocks);
|
||||
if (!content) return null;
|
||||
|
||||
const screens = extractScreens(n?.screen);
|
||||
const image = screens?.[0];
|
||||
|
||||
return { title, image, content };
|
||||
})
|
||||
.filter(Boolean) as Section[];
|
||||
|
||||
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" } }
|
||||
);
|
||||
};
|
||||
67
src/pages/api/jambox/package-channels.js
Normal file
67
src/pages/api/jambox/package-channels.js
Normal file
@@ -0,0 +1,67 @@
|
||||
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 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;
|
||||
}
|
||||
|
||||
export function GET({ url }) {
|
||||
const pkg =
|
||||
cleanPkgName(url.searchParams.get("package")) ||
|
||||
cleanPkgName(url.searchParams.get("pckg_name"));
|
||||
|
||||
if (!pkg) {
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: "MISSING_PACKAGE_NAME" }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
try {
|
||||
const rows = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
nazwa AS name,
|
||||
opis AS description,
|
||||
image AS logo_url
|
||||
FROM jambox_channels
|
||||
WHERE pckg_name = ?
|
||||
ORDER BY nazwa COLLATE NOCASE ASC;
|
||||
`.trim()
|
||||
)
|
||||
.all(pkg);
|
||||
|
||||
return new Response(JSON.stringify({ ok: true, data: rows }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd w /api/jambox/channels:", err);
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: "DB_ERROR" }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
db.close();
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const DB_PATH = process.env.FUZ_DB_PATH || "./src/data/ServicesRange.db";
|
||||
|
||||
export async function GET({ url }) {
|
||||
const db = new Database(DB_PATH, { readonly: true });
|
||||
|
||||
try {
|
||||
const packageId = Number(url.searchParams.get("packageId") || 0);
|
||||
if (!packageId) {
|
||||
return new Response(
|
||||
JSON.stringify({ ok: false, error: "MISSING_PACKAGE_ID" }),
|
||||
{ status: 400, headers: { "Content-Type": "application/json; charset=utf-8" } }
|
||||
);
|
||||
}
|
||||
|
||||
const rows = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
a.tid AS tid,
|
||||
a.name AS name,
|
||||
a.kind AS kind,
|
||||
a.is_active AS is_active,
|
||||
CAST(p.price AS REAL) AS price,
|
||||
p.currency AS currency,
|
||||
a.description AS description
|
||||
FROM jambox_tv_addon_prices p
|
||||
JOIN jambox_tv_addons a
|
||||
ON a.tid = p.addon_tid
|
||||
WHERE p.package_id = ?
|
||||
AND a.is_active = 1
|
||||
ORDER BY a.kind, a.name
|
||||
`
|
||||
)
|
||||
.all(packageId);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ ok: true, count: rows.length, data: rows }),
|
||||
{ status: 200, headers: { "Content-Type": "application/json; charset=utf-8" } }
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd w /api/jambox/tv-addons:", 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();
|
||||
}
|
||||
}
|
||||
@@ -1,85 +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 stmt = db.prepare(`
|
||||
SELECT
|
||||
p.id AS plan_id,
|
||||
p.name AS plan_name,
|
||||
IFNULL(p.popular, 0) AS plan_popular,
|
||||
p.price_monthly AS price_monthly,
|
||||
p.currency AS currency,
|
||||
|
||||
f.id AS feature_id,
|
||||
f.label AS feature_label,
|
||||
fv.value AS feature_value
|
||||
|
||||
FROM phone_plans p
|
||||
LEFT JOIN phone_plan_feature_values fv
|
||||
ON fv.plan_id = p.id
|
||||
LEFT JOIN phone_features f
|
||||
ON f.id = fv.feature_id
|
||||
ORDER BY p.id ASC, f.id ASC
|
||||
`);
|
||||
|
||||
const rows = stmt.all();
|
||||
|
||||
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,
|
||||
currency: row.currency || "PLN",
|
||||
features: [],
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
count: data.length,
|
||||
data,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Błąd w /api/phone/plans:", err);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
ok: false,
|
||||
error: err.message || "DB_ERROR",
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,86 @@
|
||||
---
|
||||
import path from "node:path";
|
||||
|
||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||
import OffersSwitches from "../../islands/OffersSwitches.jsx";
|
||||
import InternetCards from "../../islands/Internet/InternetCards.jsx";
|
||||
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
||||
import InternetCards from "../../islands/Internet/InternetCards.jsx";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import fs from "fs";
|
||||
import { loadYamlFile } from "../../lib/loadYaml";
|
||||
|
||||
const seo = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-swiatlowodowy/seo.yaml", "utf8"),
|
||||
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;
|
||||
waluta?: string;
|
||||
cena_opis?: string;
|
||||
cards?: InternetCard[];
|
||||
};
|
||||
|
||||
// TELEFON YAML (twój format)
|
||||
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[] };
|
||||
|
||||
// ADDONS YAML (twój format)
|
||||
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 waluta = data?.waluta ?? "PLN";
|
||||
const cenaOpis = data?.cena_opis ?? "zł/mies.";
|
||||
|
||||
const cards: InternetCard[] = Array.isArray(data?.cards)
|
||||
? data.cards.filter((c) => c?.widoczny === true)
|
||||
: [];
|
||||
|
||||
const phoneCards: PhoneCard[] = Array.isArray(phoneData?.cards)
|
||||
? phoneData.cards.filter((c) => c?.widoczny === true)
|
||||
: [];
|
||||
|
||||
const addons: Addon[] = Array.isArray(addonsData?.dodatki)
|
||||
? addonsData.dodatki
|
||||
: [];
|
||||
|
||||
// jeśli chcesz, możesz nadpisać cenaOpis w modalu z addons.yaml:
|
||||
const addonsCenaOpis = addonsData?.cena_opis ?? cenaOpis;
|
||||
---
|
||||
|
||||
<DefaultLayout seo={seo}>
|
||||
<section class="f-section">
|
||||
<div class="f-section-grid-single md:grid-cols-1">
|
||||
<h1 class="f-section-title">Internet światłowodowy</h1>
|
||||
<div class="fuz-markdown max-w-none">
|
||||
<p>Wybierz rodzaj budynku i czas trwania umowy</p>
|
||||
</div>
|
||||
<OffersSwitches client:load />
|
||||
<InternetCards client:load />
|
||||
<InternetCards
|
||||
client:load
|
||||
title={tytul}
|
||||
description={opis}
|
||||
cards={cards}
|
||||
waluta={waluta}
|
||||
cenaOpis={cenaOpis}
|
||||
phoneCards={phoneCards}
|
||||
addons={addons}
|
||||
addonsCenaOpis={addonsCenaOpis}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,27 +1,165 @@
|
||||
---
|
||||
import path from "node:path";
|
||||
|
||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||
import OffersSwitches from "../../islands/OffersSwitches.jsx";
|
||||
import JamboxCards from "../../islands/jambox/JamboxCards.jsx";
|
||||
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
||||
import JamboxCards from "../../islands/jambox/JamboxCards.jsx";
|
||||
import SectionChannelsSearch from "../../components/sections/SectionChannelsSearch.astro";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import fs from "fs";
|
||||
import { loadYamlFile } from "../../lib/loadYaml";
|
||||
|
||||
const seo = yaml.load(
|
||||
fs.readFileSync("./src/content/internet-telewizja/seo.yaml", "utf8"),
|
||||
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;
|
||||
waluta?: string;
|
||||
cena_opis?: string;
|
||||
internet_parametry_wspolne?: Param[];
|
||||
cards?: Card[];
|
||||
};
|
||||
|
||||
// ✅ telefon z YAML (do modala)
|
||||
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; cena: number };
|
||||
|
||||
// ✅ dodatki z YAML (do modala)
|
||||
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[];
|
||||
};
|
||||
|
||||
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"),
|
||||
);
|
||||
|
||||
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 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)
|
||||
: [];
|
||||
|
||||
// ✅ NOWE: dane do modala dodatków (bez ruszania reszty)
|
||||
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;
|
||||
---
|
||||
|
||||
<DefaultLayout seo={seo}>
|
||||
<section class="f-section">
|
||||
<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 />
|
||||
<JamboxCards client:load />
|
||||
<JamboxCards
|
||||
client:load
|
||||
title={tytul}
|
||||
description={opis}
|
||||
cards={cards}
|
||||
internetWspolne={internetWspolne}
|
||||
waluta={waluta}
|
||||
cenaOpis={cenaOpis}
|
||||
tvAddons={tvAddons}
|
||||
phoneCards={phoneCards}
|
||||
decoders={decoders}
|
||||
addons={addons}
|
||||
addonsCenaOpis={addonsCenaOpis}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<SectionChannelsSearch />
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
export const prerender = false;
|
||||
|
||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||
import Markdown from "../../islands/Markdown.jsx";
|
||||
import { fetchMozliwosci, type Feature } from "../../lib/mozliwosci";
|
||||
|
||||
let items: Feature[] = [];
|
||||
let err = "";
|
||||
|
||||
try {
|
||||
items = await fetchMozliwosci(60_000);
|
||||
} catch (e) {
|
||||
err = e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
|
||||
function buildMarkdown(it: Feature) {
|
||||
const parts: string[] = [];
|
||||
if (it.teaser) parts.push(`> ${it.teaser}\n`);
|
||||
if (it.description) parts.push(it.description);
|
||||
return parts.join("\n\n").trim();
|
||||
}
|
||||
---
|
||||
|
||||
<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>
|
||||
</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ę pobrać danych</p>
|
||||
<p class="opacity-80">{err}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
|
||||
{
|
||||
!err && (
|
||||
<>
|
||||
{items.map((it, index) => {
|
||||
const reverse = index % 2 === 1;
|
||||
const imageUrl = it.screens?.[0] || "";
|
||||
const hasImage = !!imageUrl;
|
||||
|
||||
return (
|
||||
<section class="f-section" id={it.id}>
|
||||
<div
|
||||
class={`f-section-grid ${
|
||||
hasImage
|
||||
? "md:grid-cols-2"
|
||||
: "md:grid-cols-1"
|
||||
}`}
|
||||
>
|
||||
{hasImage && (
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={it.title}
|
||||
class={`f-section-image ${
|
||||
reverse
|
||||
? "md:order-1"
|
||||
: "md:order-2"
|
||||
} rounded-2xl border border-[--f-border-color]`}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
class={`${hasImage ? (reverse ? "md:order-2" : "md:order-1") : ""}`}
|
||||
>
|
||||
<h2 class="f-section-title">{it.title}</h2>
|
||||
|
||||
<Markdown text={buildMarkdown(it)} />
|
||||
|
||||
<div class="f-section-nav">
|
||||
<a href="#top" class="btn btn-outline">
|
||||
Do góry ↑
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</DefaultLayout>
|
||||
94
src/pages/internet-telewizja/telewizja-mozliwosci.astro
Normal file
94
src/pages/internet-telewizja/telewizja-mozliwosci.astro
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
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>
|
||||
@@ -1,21 +1,56 @@
|
||||
---
|
||||
import path from "node:path";
|
||||
|
||||
import DefaultLayout from "../../layouts/DefaultLayout.astro";
|
||||
import SectionRenderer from "../../components/sections/SectionRenderer.astro";
|
||||
import OffersPhoneCards from "../../islands/phone/OffersPhoneCards.jsx";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import fs from "fs";
|
||||
import { loadYamlFile } from "../../lib/loadYaml";
|
||||
|
||||
const seo = yaml.load(
|
||||
fs.readFileSync("./src/content/telefon/seo.yaml", "utf8"),
|
||||
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 />
|
||||
|
||||
<OffersPhoneCards client:load title={tytul} description={opis} cards={cards} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user