bep40 commited on
Commit
b4f6832
·
verified ·
1 Parent(s): 6b2cf33

Add /api/promos overlay store + BIG SALE SEO meta

Browse files
Files changed (1) hide show
  1. index.ts +85 -0
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 {