Spaces:
Running
Running
Add /api/promos overlay store + BIG SALE SEO meta
Browse files
index.ts
CHANGED
|
@@ -69,6 +69,36 @@ async function loadVaixProducts(): Promise<void> {
|
|
| 69 |
loadVaixProducts().catch(() => {});
|
| 70 |
setTimeout(() => { if (!VAIX_LOADED) loadVaixProducts().catch(() => {}); }, 3000);
|
| 71 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
function norm(s: string): string {
|
| 73 |
return String(s || "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[đĐ]/g, "d").replace(/[.\-\s]/g, "");
|
| 74 |
}
|
|
@@ -218,6 +248,31 @@ async function serveProductOG(req: Request): Promise<Response> {
|
|
| 218 |
}
|
| 219 |
}
|
| 220 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
// Fallback OG tags — always set for any request (no product match or no ?product param)
|
| 222 |
const fallbackTitle = "Gemma Avatar + V.AI STUDIO - Chat & Voice AI";
|
| 223 |
const fallbackDesc = "Trò chuyện voice/text với Gemma 4. Khám phá 8000+ sản phẩm gia dụng.";
|
|
@@ -1423,6 +1478,36 @@ const server = Bun.serve({
|
|
| 1423 |
}
|
| 1424 |
},
|
| 1425 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1426 |
"/api/customers": {
|
| 1427 |
GET: async () => {
|
| 1428 |
try {
|
|
|
|
| 69 |
loadVaixProducts().catch(() => {});
|
| 70 |
setTimeout(() => { if (!VAIX_LOADED) loadVaixProducts().catch(() => {}); }, 3000);
|
| 71 |
|
| 72 |
+
// ── Promo overlay store (product edits + BIGSALE + COMBO) ──
|
| 73 |
+
// Durable truth lives in the dataset bep40/vaistudio-data/promos.json (never the
|
| 74 |
+
// Space repo, so saving never triggers a rebuild). Shape:
|
| 75 |
+
// { productEdits: {<sku>:{name,description,priceNum,image,images[],specs{},features[],deleted}},
|
| 76 |
+
// bigsale: [ {code,name,category,price,bigsale,discount,status,qty,image} ],
|
| 77 |
+
// combo: [ {code,name,price,items:[{name,sku,price,image}]} ] }
|
| 78 |
+
const PROMOS_DATASET = "bep40/vaistudio-data";
|
| 79 |
+
const PROMOS_FILE = "promos.json";
|
| 80 |
+
async function readPromos(token?: string): Promise<any> {
|
| 81 |
+
try {
|
| 82 |
+
const h = token ? { Authorization: "Bearer " + token } : {};
|
| 83 |
+
const r = await fetch("https://huggingface.co/datasets/" + PROMOS_DATASET + "/resolve/main/" + PROMOS_FILE, { headers: h, signal: AbortSignal.timeout(6000) });
|
| 84 |
+
if (r.ok) { const d = await r.json(); if (d && typeof d === "object") return d; }
|
| 85 |
+
} catch (e: any) { /* ignore */ }
|
| 86 |
+
return { productEdits: {}, bigsale: [], combo: [] };
|
| 87 |
+
}
|
| 88 |
+
async function writePromos(token: string, data: any): Promise<void> {
|
| 89 |
+
const content = JSON.stringify(data);
|
| 90 |
+
const payload = [
|
| 91 |
+
{ key: "header", value: { summary: "Update promos via api", repo: { type: "dataset", id: PROMOS_DATASET } } },
|
| 92 |
+
{ key: "file", value: { path: PROMOS_FILE, content } },
|
| 93 |
+
].map((x) => JSON.stringify(x)).join("\n");
|
| 94 |
+
const commit = await fetch("https://huggingface.co/api/datasets/" + PROMOS_DATASET + "/commit/main", {
|
| 95 |
+
method: "POST",
|
| 96 |
+
headers: { Authorization: "Bearer " + token, "Content-Type": "application/x-ndjson" },
|
| 97 |
+
body: payload,
|
| 98 |
+
});
|
| 99 |
+
if (!commit.ok) throw new Error("Commit failed: HTTP " + commit.status + " " + (await commit.text().catch(() => "")).slice(0, 300));
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
function norm(s: string): string {
|
| 103 |
return String(s || "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[đĐ]/g, "d").replace(/[.\-\s]/g, "");
|
| 104 |
}
|
|
|
|
| 248 |
}
|
| 249 |
}
|
| 250 |
|
| 251 |
+
// ── BIG SALE deep-link SEO (?bigsale=<code>) ── shows the discounted price.
|
| 252 |
+
const bigsaleCodeRaw = url.searchParams.get("bigsale");
|
| 253 |
+
if (bigsaleCodeRaw && !productSlug) {
|
| 254 |
+
try {
|
| 255 |
+
const token = (process.env.HF_INFERENCE_TOKEN || process.env.HF_TOKEN || "").trim();
|
| 256 |
+
const promos = await readPromos(token ? token : undefined);
|
| 257 |
+
const normBs = (s: string) => String(s || "").trim().toUpperCase().replace(/\s+/g, " ");
|
| 258 |
+
const target = normBs(decodeURIComponent(bigsaleCodeRaw));
|
| 259 |
+
const bsProd = (Array.isArray(promos.bigsale) ? promos.bigsale : []).find((b: any) => normBs(b.code) === target);
|
| 260 |
+
if (bsProd) {
|
| 261 |
+
const bsName = esc(bsProd.name || bsProd.code || "Sản phẩm BIG SALE");
|
| 262 |
+
const bsPrice = Number(String(bsProd.bigsale || "").replace(/\./g, "")) || 0;
|
| 263 |
+
const bsImgRaw = bsProd.image || "";
|
| 264 |
+
const bsImg = bsImgRaw ? escURL(bsImgRaw) : fallbackImg;
|
| 265 |
+
const ogTitle = "🔥 BIG SALE: " + bsName + (bsPrice > 0 ? " - " + Number(bsPrice).toLocaleString("vi-VN") + "₫" : "") + " | V.AI STUDIO";
|
| 266 |
+
const ogDesc = (bsPrice > 0 ? "Giá BIG SALE " + bsName + " chỉ còn " + Number(bsPrice).toLocaleString("vi-VN") + "₫" : "Chương trình BIG SALE V.AI STUDIO") + " | Giảm " + esc("-" + (bsProd.discount || "0%"));
|
| 267 |
+
const ogTags = buildOGTags(ogTitle, ogDesc, bsImg, url.href, true, bsPrice);
|
| 268 |
+
return new Response(index.replace(/<\/head>/, ogTags + "\n </head>"), {
|
| 269 |
+
status: 200,
|
| 270 |
+
headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0" },
|
| 271 |
+
});
|
| 272 |
+
}
|
| 273 |
+
} catch (e: any) { /* fall through to default og */ }
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
// Fallback OG tags — always set for any request (no product match or no ?product param)
|
| 277 |
const fallbackTitle = "Gemma Avatar + V.AI STUDIO - Chat & Voice AI";
|
| 278 |
const fallbackDesc = "Trò chuyện voice/text với Gemma 4. Khám phá 8000+ sản phẩm gia dụng.";
|
|
|
|
| 1478 |
}
|
| 1479 |
},
|
| 1480 |
},
|
| 1481 |
+
"/api/promos": {
|
| 1482 |
+
GET: async () => {
|
| 1483 |
+
try {
|
| 1484 |
+
const token = (process.env.HF_INFERENCE_TOKEN || process.env.HF_TOKEN || "").trim();
|
| 1485 |
+
return Response.json(await readPromos(token || undefined));
|
| 1486 |
+
} catch (e: any) { return Response.json({ error: e.message }, { status: 500 }); }
|
| 1487 |
+
},
|
| 1488 |
+
POST: async (req: Request) => {
|
| 1489 |
+
try {
|
| 1490 |
+
const token = (process.env.HF_INFERENCE_TOKEN || process.env.HF_TOKEN || "").trim();
|
| 1491 |
+
if (!token) return Response.json({ error: "No write token on server" }, { status: 500 });
|
| 1492 |
+
let body: any = {};
|
| 1493 |
+
try { body = await req.json(); } catch (_) {}
|
| 1494 |
+
if (!body || typeof body !== "object") return Response.json({ error: "Bad body" }, { status: 400 });
|
| 1495 |
+
const cur = await readPromos(token);
|
| 1496 |
+
const merge = cur;
|
| 1497 |
+
if (!merge.productEdits || typeof merge.productEdits !== "object") merge.productEdits = {};
|
| 1498 |
+
if (body.resetProductEdits) { merge.productEdits = {}; }
|
| 1499 |
+
if (body.productEdits && typeof body.productEdits === "object" && !body.resetProductEdits) {
|
| 1500 |
+
for (const k in body.productEdits) merge.productEdits[k] = body.productEdits[k];
|
| 1501 |
+
}
|
| 1502 |
+
if (body.resetBigsale === true) { merge.bigsale = Array.isArray(body.bigsale) ? body.bigsale : []; }
|
| 1503 |
+
else if (Array.isArray(body.bigsale)) { merge.bigsale = body.bigsale; }
|
| 1504 |
+
if (body.resetCombo === true) { merge.combo = Array.isArray(body.combo) ? body.combo : []; }
|
| 1505 |
+
else if (Array.isArray(body.combo)) { merge.combo = body.combo; }
|
| 1506 |
+
await writePromos(token, merge);
|
| 1507 |
+
return Response.json({ ok: true, promos: merge });
|
| 1508 |
+
} catch (e: any) { return Response.json({ error: e.message }, { status: 502 }); }
|
| 1509 |
+
},
|
| 1510 |
+
},
|
| 1511 |
"/api/customers": {
|
| 1512 |
GET: async () => {
|
| 1513 |
try {
|