Spaces:
Running
Running
bep40 commited on
Commit ·
50d5689
1
Parent(s): 22442da
Danh sách chia sẻ (BIGSALE/COMBO/FLASHSALE) + Xóa admin + /api/delete-promo
Browse files- big_sale.js +102 -5
- index.html +5 -4
- index.ts +40 -0
- src/vaix-add-product.js +85 -1
- src/vaix-combo.js +127 -15
- src/vaix-flashsale.js +259 -0
- src/vaix-rag.js +0 -14
big_sale.js
CHANGED
|
@@ -84,9 +84,7 @@ function buildProductCard(p) {
|
|
| 84 |
<span class="big-sale-price-original">${formatVND(p.price)}</span>
|
| 85 |
<span class="big-sale-price-new">${formatVND(p.bigsale)}</span>
|
| 86 |
</div>
|
| 87 |
-
<
|
| 88 |
-
<i class="fas fa-share-alt"></i> Chia sẻ link
|
| 89 |
-
</button>
|
| 90 |
</div>`;
|
| 91 |
}
|
| 92 |
|
|
@@ -117,7 +115,10 @@ function createBigSaleElements() {
|
|
| 117 |
<span class="big-sale-panel-icon">🔥</span>
|
| 118 |
<span>CHƯƠNG TRÌNH BIG SALE</span>
|
| 119 |
</div>
|
| 120 |
-
<
|
|
|
|
|
|
|
|
|
|
| 121 |
</div>
|
| 122 |
<div class="big-sale-product-list">
|
| 123 |
${BIG_SALE_PRODUCTS.map(p => buildProductCard(p)).join('')}
|
|
@@ -494,6 +495,25 @@ function handleBigSaleUrlParam() {
|
|
| 494 |
var params = new URLSearchParams(location.search);
|
| 495 |
var code = params.get('bigsale') || '';
|
| 496 |
if (!code) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
var tries = 0;
|
| 498 |
var iv = setInterval(function () {
|
| 499 |
tries++;
|
|
@@ -600,6 +620,81 @@ function refreshBigSalePanel() {
|
|
| 600 |
if (countEl) countEl.textContent = BIG_SALE_PRODUCTS.length + ' sản phẩm';
|
| 601 |
if (countBadge) countBadge.textContent = BIG_SALE_PRODUCTS.length + ' SP';
|
| 602 |
if (wasOpen) panel.classList.add('open'); else panel.classList.remove('open');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
} catch (e) {}
|
| 604 |
}
|
| 605 |
|
|
@@ -609,13 +704,15 @@ function initBigSale() {
|
|
| 609 |
// Load admin-managed BIGSALE products from the Promo CMS backend.
|
| 610 |
loadBigSaleProducts().then(function (list) {
|
| 611 |
console.log('[BIG SALE] Dynamic list loaded: ' + list.length + ' products');
|
|
|
|
| 612 |
handleBigSaleUrlParam();
|
| 613 |
}).catch(function () {
|
|
|
|
| 614 |
handleBigSaleUrlParam();
|
| 615 |
});
|
| 616 |
// Listen for CMS saves so the panel stays in sync.
|
| 617 |
try {
|
| 618 |
-
window.addEventListener('vai-promo-changed', function () { loadBigSaleProducts(true); });
|
| 619 |
} catch (e) {}
|
| 620 |
}
|
| 621 |
|
|
|
|
| 84 |
<span class="big-sale-price-original">${formatVND(p.price)}</span>
|
| 85 |
<span class="big-sale-price-new">${formatVND(p.bigsale)}</span>
|
| 86 |
</div>
|
| 87 |
+
<div class="big-sale-card-actions" id="bsa-${p.code}" style="display:flex;gap:6px;margin-top:8px;flex-wrap:wrap"></div>
|
|
|
|
|
|
|
| 88 |
</div>`;
|
| 89 |
}
|
| 90 |
|
|
|
|
| 115 |
<span class="big-sale-panel-icon">🔥</span>
|
| 116 |
<span>CHƯƠNG TRÌNH BIG SALE</span>
|
| 117 |
</div>
|
| 118 |
+
<div style="display:flex;align-items:center;gap:8px">
|
| 119 |
+
<span class="big-sale-product-count">${BIG_SALE_PRODUCTS.length} sản phẩm</span>
|
| 120 |
+
<button type="button" class="big-sale-list-share-btn" onclick="event.stopPropagation();window.shareBigSaleList()" style="padding:5px 10px;border:none;border-radius:8px;background:#f97316;color:#fff;font-size:0.68rem;font-weight:700;cursor:pointer;font-family:inherit;white-space:nowrap"><i class="fas fa-share-alt"></i> Chia sẻ danh sách</button>
|
| 121 |
+
</div>
|
| 122 |
</div>
|
| 123 |
<div class="big-sale-product-list">
|
| 124 |
${BIG_SALE_PRODUCTS.map(p => buildProductCard(p)).join('')}
|
|
|
|
| 495 |
var params = new URLSearchParams(location.search);
|
| 496 |
var code = params.get('bigsale') || '';
|
| 497 |
if (!code) return;
|
| 498 |
+
// ?bigsale=all → open the FULL list (not a single product).
|
| 499 |
+
var codeNorm = String(code).trim().toUpperCase().replace(/\s+/g, ' ');
|
| 500 |
+
if (codeNorm === 'ALL' || codeNorm === 'DANHSACH' || codeNorm === 'LIST') {
|
| 501 |
+
var triesAll = 0;
|
| 502 |
+
var ivAll = setInterval(function () {
|
| 503 |
+
triesAll++;
|
| 504 |
+
try {
|
| 505 |
+
if (document.getElementById('big-sale-floating')) {
|
| 506 |
+
if (!document.getElementById('big-sale-panel')) initBigSale();
|
| 507 |
+
dismissWelcomeForBigSale();
|
| 508 |
+
openBigSalePanel();
|
| 509 |
+
clearInterval(ivAll);
|
| 510 |
+
return;
|
| 511 |
+
}
|
| 512 |
+
} catch (e) {}
|
| 513 |
+
if (triesAll > 40) clearInterval(ivAll);
|
| 514 |
+
}, 500);
|
| 515 |
+
return;
|
| 516 |
+
}
|
| 517 |
var tries = 0;
|
| 518 |
var iv = setInterval(function () {
|
| 519 |
tries++;
|
|
|
|
| 620 |
if (countEl) countEl.textContent = BIG_SALE_PRODUCTS.length + ' sản phẩm';
|
| 621 |
if (countBadge) countBadge.textContent = BIG_SALE_PRODUCTS.length + ' SP';
|
| 622 |
if (wasOpen) panel.classList.add('open'); else panel.classList.remove('open');
|
| 623 |
+
injectBigSaleAdminActions();
|
| 624 |
+
} catch (e) {}
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
// ── List-level share (toàn bộ danh sách BIG SALE) ──
|
| 628 |
+
// Copies a self-hosted deep link (?bigsale=all) that opens the FULL BIG SALE
|
| 629 |
+
// panel — NOT a single product. Replaces the old per-product share.
|
| 630 |
+
window.shareBigSaleList = function () {
|
| 631 |
+
var base = window.location.origin + window.location.pathname;
|
| 632 |
+
var url = base + '?bigsale=all';
|
| 633 |
+
var done = function () {
|
| 634 |
+
if (typeof window.toast === 'function') window.toast('✓ Đã copy link chia sẻ danh sách BIG SALE');
|
| 635 |
+
else alert('Link chia sẻ danh sách BIG SALE:\n' + url);
|
| 636 |
+
};
|
| 637 |
+
if (window.navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
| 638 |
+
navigator.clipboard.writeText(url).then(done).catch(function () { window.prompt('Link chia sẻ danh sách BIG SALE:', url); });
|
| 639 |
+
} else { window.prompt('Link chia sẻ danh sách BIG SALE:', url); }
|
| 640 |
+
};
|
| 641 |
+
|
| 642 |
+
// Open the full BIG SALE panel (used by ?bigsale=all deep link).
|
| 643 |
+
function openBigSalePanel() {
|
| 644 |
+
try {
|
| 645 |
+
var panel = document.getElementById('big-sale-panel');
|
| 646 |
+
var floating = document.getElementById('big-sale-floating');
|
| 647 |
+
if (panel) panel.classList.add('open');
|
| 648 |
+
if (floating) floating.classList.add('open');
|
| 649 |
+
} catch (e) {}
|
| 650 |
+
}
|
| 651 |
+
window.openBigSalePanel = openBigSalePanel;
|
| 652 |
+
|
| 653 |
+
// Admin-only delete for a BIG SALE item (permanent, via /api/delete-promo).
|
| 654 |
+
async function deleteBigSaleProduct(code) {
|
| 655 |
+
try {
|
| 656 |
+
var isAdmin = (typeof window.isVaAdmin === 'function') ? window.isVaAdmin() : false;
|
| 657 |
+
if (!isAdmin) { alert('🔒 Cần nhập mã V.AISTUDIO trên trang chủ.'); return; }
|
| 658 |
+
if (!window.confirm('Xóa sản phẩm ' + code + ' khỏi danh sách BIG SALE?')) return;
|
| 659 |
+
var r = await fetch('/api/delete-promo', {
|
| 660 |
+
method: 'POST',
|
| 661 |
+
headers: { 'Content-Type': 'application/json', 'x-vai-admin': '1' },
|
| 662 |
+
body: JSON.stringify({ type: 'bigsale', key: code })
|
| 663 |
+
});
|
| 664 |
+
var d = await r.json().catch(function () { return {}; });
|
| 665 |
+
if (!r.ok) { alert('❌ ' + (d.error || 'Lỗi xóa')); return; }
|
| 666 |
+
if (window.toast) window.toast('✓ Đã xóa khỏi BIG SALE');
|
| 667 |
+
loadBigSaleProducts(true);
|
| 668 |
+
} catch (e) { alert('❌ Lỗi xóa: ' + (e && e.message || e)); }
|
| 669 |
+
}
|
| 670 |
+
window.deleteBigSaleProduct = deleteBigSaleProduct;
|
| 671 |
+
|
| 672 |
+
// Inject admin delete buttons into every BIG SALE card (after each render).
|
| 673 |
+
function injectBigSaleAdminActions() {
|
| 674 |
+
try {
|
| 675 |
+
var isAdmin = (typeof window.isVaAdmin === 'function') ? window.isVaAdmin() : false;
|
| 676 |
+
if (!isAdmin) return;
|
| 677 |
+
var panel = document.getElementById('big-sale-panel');
|
| 678 |
+
if (!panel) return;
|
| 679 |
+
var norm = function (s) { return String(s || '').trim().toUpperCase().replace(/\s+/g, ' '); };
|
| 680 |
+
var cards = panel.querySelectorAll('.big-sale-product');
|
| 681 |
+
for (var i = 0; i < cards.length; i++) {
|
| 682 |
+
var card = cards[i];
|
| 683 |
+
var code = card.getAttribute('data-code') || '';
|
| 684 |
+
if (!code) continue;
|
| 685 |
+
var actions = card.querySelector('.big-sale-card-actions');
|
| 686 |
+
if (!actions || actions.querySelector('.bs-del-btn')) continue;
|
| 687 |
+
var del = document.createElement('button');
|
| 688 |
+
del.type = 'button';
|
| 689 |
+
del.className = 'bs-del-btn';
|
| 690 |
+
del.innerHTML = '<i class="fas fa-trash"></i> Xóa';
|
| 691 |
+
del.style.cssText = "flex:1;padding:6px 8px;border:none;border-radius:8px;background:#dc2626;color:#fff;font-size:0.7rem;font-weight:700;cursor:pointer;font-family:inherit;display:inline-flex;align-items:center;justify-content:center;gap:5px";
|
| 692 |
+
del.addEventListener('click', function (e) {
|
| 693 |
+
e.stopPropagation();
|
| 694 |
+
window.deleteBigSaleProduct(code);
|
| 695 |
+
});
|
| 696 |
+
actions.appendChild(del);
|
| 697 |
+
}
|
| 698 |
} catch (e) {}
|
| 699 |
}
|
| 700 |
|
|
|
|
| 704 |
// Load admin-managed BIGSALE products from the Promo CMS backend.
|
| 705 |
loadBigSaleProducts().then(function (list) {
|
| 706 |
console.log('[BIG SALE] Dynamic list loaded: ' + list.length + ' products');
|
| 707 |
+
injectBigSaleAdminActions();
|
| 708 |
handleBigSaleUrlParam();
|
| 709 |
}).catch(function () {
|
| 710 |
+
injectBigSaleAdminActions();
|
| 711 |
handleBigSaleUrlParam();
|
| 712 |
});
|
| 713 |
// Listen for CMS saves so the panel stays in sync.
|
| 714 |
try {
|
| 715 |
+
window.addEventListener('vai-promo-changed', function () { loadBigSaleProducts(true).then(function () { injectBigSaleAdminActions(); }); });
|
| 716 |
} catch (e) {}
|
| 717 |
}
|
| 718 |
|
index.html
CHANGED
|
@@ -758,17 +758,18 @@
|
|
| 758 |
first paint — the welcome/avatar-picker modal renders immediately. -->
|
| 759 |
<script defer src="./src/cart-quote.js?gc=33"></script> <!-- Cart + Quote module (Giỏ hàng / Báo giá) -->
|
| 760 |
<script defer src="./src/customers.js?gc=17"></script> <!-- Customer list (Danh sách khách hàng) -->
|
| 761 |
-
<script defer src="./src/vaix-rag.js?gc=
|
| 762 |
<script defer src="./src/greeting-news.js?gc=40"></script> <!-- Greeting HOT news cards + source links -->
|
| 763 |
<script defer src="./src/order-sync.js?gc=17"></script> <!-- Order sync with V.AISTUDIO backend -->
|
| 764 |
<script type="module" src="./src/app.js?gc=59"></script>
|
| 765 |
<script defer src="./src/greeting-source-cards.js?gc=15"></script> <!-- FIX: Nguồn tin cards under greeting -->
|
| 766 |
<script defer src="./src/avatar-picker.js?gc=13"></script> <!-- ✨ Circular avatar picker (Mr V / Lisamy) + loading progress bar -->
|
| 767 |
<!-- BIG SALE Floating Promotion Module -->
|
| 768 |
-
<script defer src="./big_sale.js?v=
|
| 769 |
<!-- V.AI STUDIO Promo CMS (admin editor + BIGSALE/COMBO manager) -->
|
| 770 |
<script defer src="./src/vaix-promo-cms.js?v=3"></script>
|
| 771 |
-
<script defer src="./src/vaix-combo.js?v=
|
|
|
|
| 772 |
|
| 773 |
<script>
|
| 774 |
// Wire the "Đơn hàng" buttons to the order modal
|
|
@@ -789,6 +790,6 @@
|
|
| 789 |
|
| 790 |
<!-- ── Thêm SP (Add Product) module: 100% port from V.AISTUDIO, gated by the
|
| 791 |
homepage V.AISTUDIO access code (window.isVaAdmin()). ── -->
|
| 792 |
-
<script defer src="./src/vaix-add-product.js?v=
|
| 793 |
</body>
|
| 794 |
</html>
|
|
|
|
| 758 |
first paint — the welcome/avatar-picker modal renders immediately. -->
|
| 759 |
<script defer src="./src/cart-quote.js?gc=33"></script> <!-- Cart + Quote module (Giỏ hàng / Báo giá) -->
|
| 760 |
<script defer src="./src/customers.js?gc=17"></script> <!-- Customer list (Danh sách khách hàng) -->
|
| 761 |
+
<script defer src="./src/vaix-rag.js?gc=57"></script> <!-- V.AI STUDIO RAG Module - loads FASTER via light index; lazy galleries; spec-aware search -->
|
| 762 |
<script defer src="./src/greeting-news.js?gc=40"></script> <!-- Greeting HOT news cards + source links -->
|
| 763 |
<script defer src="./src/order-sync.js?gc=17"></script> <!-- Order sync with V.AISTUDIO backend -->
|
| 764 |
<script type="module" src="./src/app.js?gc=59"></script>
|
| 765 |
<script defer src="./src/greeting-source-cards.js?gc=15"></script> <!-- FIX: Nguồn tin cards under greeting -->
|
| 766 |
<script defer src="./src/avatar-picker.js?gc=13"></script> <!-- ✨ Circular avatar picker (Mr V / Lisamy) + loading progress bar -->
|
| 767 |
<!-- BIG SALE Floating Promotion Module -->
|
| 768 |
+
<script defer src="./big_sale.js?v=14"></script>
|
| 769 |
<!-- V.AI STUDIO Promo CMS (admin editor + BIGSALE/COMBO manager) -->
|
| 770 |
<script defer src="./src/vaix-promo-cms.js?v=3"></script>
|
| 771 |
+
<script defer src="./src/vaix-combo.js?v=5"></script>
|
| 772 |
+
<script defer src="./src/vaix-flashsale.js?v=1"></script>
|
| 773 |
|
| 774 |
<script>
|
| 775 |
// Wire the "Đơn hàng" buttons to the order modal
|
|
|
|
| 790 |
|
| 791 |
<!-- ── Thêm SP (Add Product) module: 100% port from V.AISTUDIO, gated by the
|
| 792 |
homepage V.AISTUDIO access code (window.isVaAdmin()). ── -->
|
| 793 |
+
<script defer src="./src/vaix-add-product.js?v=5"></script>
|
| 794 |
</body>
|
| 795 |
</html>
|
index.ts
CHANGED
|
@@ -1675,6 +1675,46 @@ const server = Bun.serve({
|
|
| 1675 |
}
|
| 1676 |
},
|
| 1677 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1678 |
"/api/promos": {
|
| 1679 |
GET: async () => {
|
| 1680 |
try {
|
|
|
|
| 1675 |
}
|
| 1676 |
},
|
| 1677 |
},
|
| 1678 |
+
"/api/delete-promo": {
|
| 1679 |
+
POST: async (req: Request) => {
|
| 1680 |
+
// Admin-only: permanently remove a BIGSALE item (by code) or a COMBO
|
| 1681 |
+
// (by code/name/slug) from promos.json. Same x-vai-admin gate as
|
| 1682 |
+
// /api/delete-product so discount-link visitors stay locked out.
|
| 1683 |
+
try {
|
| 1684 |
+
const token = (process.env.HF_INFERENCE_TOKEN || process.env.HF_TOKEN || "").trim();
|
| 1685 |
+
if (!token) return Response.json({ error: "No write token on server" }, { status: 500 });
|
| 1686 |
+
const isAdmin = (req.headers.get("x-vai-admin") || "") === "1";
|
| 1687 |
+
if (!isAdmin) return Response.json({ error: "Admin unlock required" }, { status: 403 });
|
| 1688 |
+
const body: any = await req.json().catch(() => ({}));
|
| 1689 |
+
const type = String(body?.type || "").trim();
|
| 1690 |
+
const key = String(body?.key || "").trim();
|
| 1691 |
+
if (!["bigsale", "combo"].includes(type)) return Response.json({ error: "type must be bigsale|combo" }, { status: 400 });
|
| 1692 |
+
if (!key) return Response.json({ error: "Missing key" }, { status: 400 });
|
| 1693 |
+
const cur = await readPromos(token);
|
| 1694 |
+
const normK = (s: any) =>
|
| 1695 |
+
String(s || "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[đĐ]/g, "d").replace(/[.\-\s]/g, "");
|
| 1696 |
+
const normKey = normK(key);
|
| 1697 |
+
let removed = 0;
|
| 1698 |
+
if (type === "bigsale") {
|
| 1699 |
+
const arr = Array.isArray(cur.bigsale) ? cur.bigsale : [];
|
| 1700 |
+
const next = arr.filter((it: any) => normK(it?.code) !== normKey);
|
| 1701 |
+
removed = arr.length - next.length;
|
| 1702 |
+
cur.bigsale = next;
|
| 1703 |
+
} else {
|
| 1704 |
+
const arr = Array.isArray(cur.combo) ? cur.combo : [];
|
| 1705 |
+
const next = arr.filter((it: any) => normK(it?.code || it?.name || it?.slug) !== normKey);
|
| 1706 |
+
removed = arr.length - next.length;
|
| 1707 |
+
cur.combo = next;
|
| 1708 |
+
}
|
| 1709 |
+
if (!removed) return Response.json({ ok: true, removed: 0 });
|
| 1710 |
+
await writePromos(token, cur);
|
| 1711 |
+
return Response.json({ ok: true, removed, type });
|
| 1712 |
+
} catch (e: any) {
|
| 1713 |
+
console.error("[delete-promo]", e?.message);
|
| 1714 |
+
return Response.json({ error: e.message || String(e) }, { status: 502 });
|
| 1715 |
+
}
|
| 1716 |
+
},
|
| 1717 |
+
},
|
| 1718 |
"/api/promos": {
|
| 1719 |
GET: async () => {
|
| 1720 |
try {
|
src/vaix-add-product.js
CHANGED
|
@@ -756,7 +756,91 @@
|
|
| 756 |
var brand = getMeta(doc, 'og:site_name') || (ld && ld.brand) || '';
|
| 757 |
if (!brand) { try { var host = new URL(pageUrl).hostname; brand = host.replace(/^www\./, '').split('.')[0]; } catch (e) {} }
|
| 758 |
brand = String(brand || '').replace(/^www\./i, '');
|
| 759 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 760 |
name: name, sku: sku, img: primaryImg, gallery: gallery, desc: desc,
|
| 761 |
price: priceInfo.salePrice || priceInfo.price, pn: priceInfo.salePn || priceInfo.pn,
|
| 762 |
listPrice: priceInfo.listPrice, listPn: priceInfo.listPn,
|
|
|
|
| 756 |
var brand = getMeta(doc, 'og:site_name') || (ld && ld.brand) || '';
|
| 757 |
if (!brand) { try { var host = new URL(pageUrl).hostname; brand = host.replace(/^www\./, '').split('.')[0]; } catch (e) {} }
|
| 758 |
brand = String(brand || '').replace(/^www\./i, '');
|
| 759 |
+
// ── dienmayxanh.com override (v2024 layout) ──
|
| 760 |
+
if (/dienmayxanh\.com/i.test(pageUrl || '')) {
|
| 761 |
+
// NAME: h1 direct text only
|
| 762 |
+
var h1d = doc.querySelector('h1');
|
| 763 |
+
if (h1d) { var nm = ''; for (var ci = 0; ci < h1d.childNodes.length; ci++) { if (h1d.childNodes[ci].nodeType === 3) nm += h1d.childNodes[ci].nodeValue; } nm = String(nm || '').trim(); if (nm) name = nm; }
|
| 764 |
+
// PRICES: .bs_price strong (sale) + em (list) + data attrs
|
| 765 |
+
var bs = doc.querySelector('.bs_price');
|
| 766 |
+
if (bs) {
|
| 767 |
+
var sEl = bs.querySelector('strong'), eEl = bs.querySelector('em');
|
| 768 |
+
var sT = sEl ? sEl.textContent.trim() : '';
|
| 769 |
+
var lT = eEl ? eEl.textContent.trim() : '';
|
| 770 |
+
if (sT) priceInfo.salePn = parseInt(String(sT).replace(/[^\d]/g, ''), 10);
|
| 771 |
+
if (lT) priceInfo.listPn = parseInt(String(lT).replace(/[^\d]/g, ''), 10);
|
| 772 |
+
if (!priceInfo.salePn && bs.getAttribute('data-discountorigin')) priceInfo.salePn = parseInt(String(bs.getAttribute('data-discountorigin')).replace(/[^\d]/g, ''), 10);
|
| 773 |
+
if (!priceInfo.listPn && bs.getAttribute('data-priceorg')) priceInfo.listPn = parseInt(String(bs.getAttribute('data-priceorg')).replace(/[^\d]/g, ''), 10);
|
| 774 |
+
if (priceInfo.listPn && priceInfo.salePn && priceInfo.listPn < priceInfo.salePn) { var tt = priceInfo.listPn; priceInfo.listPn = priceInfo.salePn; priceInfo.salePn = tt; }
|
| 775 |
+
priceInfo.pn = priceInfo.salePn || priceInfo.listPn;
|
| 776 |
+
priceInfo.salePrice = priceInfo.salePn ? money(priceInfo.salePn) : '';
|
| 777 |
+
priceInfo.price = priceInfo.salePrice || (priceInfo.pn ? money(priceInfo.pn) : '');
|
| 778 |
+
priceInfo.listPrice = priceInfo.listPn ? money(priceInfo.listPn) : '';
|
| 779 |
+
}
|
| 780 |
+
// GALLERY: Product/Images URLs (may have NO extension)
|
| 781 |
+
var gl = [];
|
| 782 |
+
var dimgs = doc.querySelectorAll('.thubmail-slide .owl-carousel .owl-dot img, .thubmail-slide img, #slider-detail img');
|
| 783 |
+
for (var dg1 = 0; dg1 < dimgs.length; dg1++) {
|
| 784 |
+
var dsr = dimgs[dg1].getAttribute('data-original') || dimgs[dg1].getAttribute('data-src') || dimgs[dg1].getAttribute('src') || '';
|
| 785 |
+
if (!dsr || dsr.indexOf('Products/Images') < 0) continue;
|
| 786 |
+
if (dsr.indexOf('comment') >= 0 || dsr.indexOf('/Common/') >= 0 || dsr.indexOf('/Banner/') >= 0) continue;
|
| 787 |
+
var du = cleanUrl(dsr, pageUrl);
|
| 788 |
+
if (du && gallery.indexOf(du) < 0) gallery.push(du);
|
| 789 |
+
}
|
| 790 |
+
if (gallery.length) {
|
| 791 |
+
var firstFolder = gallery[0].substring(0, gallery[0].lastIndexOf('/') + 1);
|
| 792 |
+
var allImgs2 = doc.querySelectorAll('img');
|
| 793 |
+
for (var dg2 = 0; dg2 < allImgs2.length; dg2++) {
|
| 794 |
+
var ds2 = allImgs2[dg2].getAttribute('data-original') || allImgs2[dg2].getAttribute('data-src') || allImgs2[dg2].getAttribute('src') || '';
|
| 795 |
+
if (!ds2 || ds2.indexOf('Products/Images') < 0 || ds2.indexOf('comment') >= 0) continue;
|
| 796 |
+
var da = cleanUrl(ds2, pageUrl);
|
| 797 |
+
if (da && da.indexOf(firstFolder) === 0 && gallery.indexOf(da) < 0) gallery.push(da);
|
| 798 |
+
}
|
| 799 |
+
gallery = gallery.filter(function (u) { return u.indexOf('-thumb') < 0; });
|
| 800 |
+
}
|
| 801 |
+
if (gallery.length) { primaryImg = gallery[0]; if (gallery.indexOf(primaryImg) < 0) gallery.unshift(primaryImg); }
|
| 802 |
+
// SPECS: #tab-2 .text-specifi li (aside[0]=key, aside[1]=value)
|
| 803 |
+
var t2b = doc.getElementById('tab-2') || doc.getElementById('tab-02');
|
| 804 |
+
if (t2b) {
|
| 805 |
+
var s2b = {};
|
| 806 |
+
var tlis = t2b.querySelectorAll('.text-specifi li, .parameter__list li');
|
| 807 |
+
for (var tl2 = 0; tl2 < tlis.length; tl2++) {
|
| 808 |
+
var as2 = tlis[tl2].querySelectorAll('aside');
|
| 809 |
+
if (as2.length >= 2) {
|
| 810 |
+
var k2b = as2[0].textContent.replace(/\s+/g, ' ').trim().replace(/:$/, '');
|
| 811 |
+
var v2b = as2[1].textContent.replace(/\s+/g, ' ').trim();
|
| 812 |
+
if (k2b && v2b && ['Tên sản phẩm', 'Mã số', 'Giá', 'Thương hiệu', 'Khuyến mãi'].indexOf(k2b) < 0) s2b[k2b] = v2b;
|
| 813 |
+
}
|
| 814 |
+
}
|
| 815 |
+
if (Object.keys(s2b).length) specs = s2b;
|
| 816 |
+
}
|
| 817 |
+
// FEATURES: paragraphs under h3 in #tab-3 (Thông tin sản phẩm)
|
| 818 |
+
var t3b = doc.getElementById('tab-3') || doc.getElementById('tab-03');
|
| 819 |
+
var f2 = [];
|
| 820 |
+
if (t3b) {
|
| 821 |
+
var p3 = t3b.querySelectorAll('p');
|
| 822 |
+
for (var pi2 = 0; pi2 < p3.length && f2.length < 12; pi2++) {
|
| 823 |
+
var txb = p3[pi2].textContent.replace(/\s+/g, ' ').trim();
|
| 824 |
+
if (txb.length >= 30 && txb.length <= 200 && txb.indexOf('*Hình ảnh') < 0 && txb.indexOf('minh họa') < 0) f2.push(txb);
|
| 825 |
+
}
|
| 826 |
+
}
|
| 827 |
+
if (f2.length) feats = f2.slice(0, 12);
|
| 828 |
+
// BRAND: breadcrumb last link → "Máy giặt Hitachi" → "Hitachi"
|
| 829 |
+
var cr2 = doc.querySelectorAll('.box_breadcrumb li a, .breadcrumb li a, .breadcrumb a');
|
| 830 |
+
if (cr2.length) {
|
| 831 |
+
var lst = cr2[cr2.length - 1].textContent.replace(/\s+/g, ' ').trim();
|
| 832 |
+
['Máy giặt ', 'Máy lạnh ', 'Tủ lạnh ', 'Tivi ', 'Điện thoại ', 'Máy hút mùi ', 'Bếp ', 'Lò '].forEach(function (pr) { if (lst.indexOf(pr) === 0) lst = lst.slice(pr.length); });
|
| 833 |
+
lst = lst.replace(/(Inverter|Digital|Smart|Auto).*$/i, '').trim();
|
| 834 |
+
if (lst && lst.length <= 40 && !/ số | kg| inch/i.test(lst)) brand = lst;
|
| 835 |
+
}
|
| 836 |
+
// DESC: full #tab-3 text
|
| 837 |
+
if (t3b) {
|
| 838 |
+
var dtx = t3b.textContent.replace(/\s+/g, ' ').trim();
|
| 839 |
+
if (dtx.length > 60) desc = dtx.slice(0, 8000);
|
| 840 |
+
}
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
return {
|
| 844 |
name: name, sku: sku, img: primaryImg, gallery: gallery, desc: desc,
|
| 845 |
price: priceInfo.salePrice || priceInfo.price, pn: priceInfo.salePn || priceInfo.pn,
|
| 846 |
listPrice: priceInfo.listPrice, listPn: priceInfo.listPn,
|
src/vaix-combo.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
| 27 |
var panel = document.createElement("div");
|
| 28 |
panel.id = "combo-panel";
|
| 29 |
panel.style.cssText = "position:fixed;right:18px;bottom:170px;z-index:9998;width:320px;max-height:460px;background:#fff;border-radius:16px;box-shadow:0 16px 48px rgba(0,0,0,0.2);overflow:hidden;display:none;flex-direction:column;font-family:inherit;border:1px solid #bfdbfe";
|
| 30 |
-
panel.innerHTML = '<div style="padding:12px 14px;background:linear-gradient(135deg,#eff6ff,#dbeafe);display:flex;align-items:center;justify-content:space-between"><span style="font-weight:800;color:#1d4ed8;font-size:0.85rem">🧩 COMBO KHUYẾN MÃI</span><button class="combo-close" style="background:none;border:none;font-size:1.2rem;cursor:pointer;color:#1e3a8a">×</button></div><div class="combo-list" style="flex:1;overflow:auto;padding:8px 10px;display:flex;flex-direction:column;gap:6px"></div>';
|
| 31 |
|
| 32 |
document.body.appendChild(btn);
|
| 33 |
document.body.appendChild(panel);
|
|
@@ -175,38 +175,149 @@
|
|
| 175 |
'<div style="font-size:0.7rem;color:#64748b;margin-bottom:4px">' + itemsHtml + '</div>' +
|
| 176 |
'<div style="display:flex;align-items:center;justify-content:space-between;gap:6px">' +
|
| 177 |
'<span style="font-weight:700;color:#1d4ed8;font-size:0.8rem">' + fmt(priceNum) + '</span>' +
|
| 178 |
-
'<
|
| 179 |
'</div>' +
|
| 180 |
'</div>';
|
| 181 |
}).join("");
|
|
|
|
| 182 |
} catch(e) { console.warn("[COMBO] refresh:", e); }
|
| 183 |
}
|
| 184 |
// Per-combo "Chia sẻ link" (SEO). Copy a self-hosted deep link that opens the
|
| 185 |
// COMBO KHUYẾN MÃI panel. Exposed globally because combo cards are HTML strings.
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
| 187 |
try {
|
| 188 |
if (ev) { ev.stopPropagation(); }
|
| 189 |
-
var combos = loadCombos() || [];
|
| 190 |
-
var c = combos[idx];
|
| 191 |
-
if (!c) return;
|
| 192 |
-
var slug = String(c.slug || c.name || "combo-" + (idx + 1))
|
| 193 |
-
.toLowerCase()
|
| 194 |
-
.replace(/[^a-z0-9]+/g, "-")
|
| 195 |
-
.replace(/^-+|-+$/g, "");
|
| 196 |
var base = window.location.origin + window.location.pathname;
|
| 197 |
-
var url = base + "?combo="
|
| 198 |
var done = function () {
|
| 199 |
-
if (typeof window.toast === "function") window.toast("\u2713 \u0110\u00e3 copy link chia s\u1ebb
|
| 200 |
-
else alert("Link chia s\u1ebb COMBO:\n" + url);
|
| 201 |
};
|
| 202 |
if (window.navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
| 203 |
-
navigator.clipboard.writeText(url).then(done).catch(function () { window.prompt("Link chia s\u1ebb COMBO:", url); });
|
| 204 |
-
} else { window.prompt("Link chia s\u1ebb COMBO:", url); }
|
| 205 |
} catch (err) {
|
| 206 |
alert("L\u1ed7i chia s\u1ebb: " + (err && err.message || err));
|
| 207 |
}
|
| 208 |
};
|
| 209 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
function boot() {
|
| 211 |
// Retry createComboElements until vaixPromo is ready (can load after CMS module)
|
| 212 |
var _retry = 0;
|
|
@@ -214,6 +325,7 @@
|
|
| 214 |
_retry++;
|
| 215 |
try {
|
| 216 |
createComboElements();
|
|
|
|
| 217 |
} catch(e) {
|
| 218 |
if (_retry < 30) { setTimeout(tryCreate, 500); return; }
|
| 219 |
}
|
|
|
|
| 27 |
var panel = document.createElement("div");
|
| 28 |
panel.id = "combo-panel";
|
| 29 |
panel.style.cssText = "position:fixed;right:18px;bottom:170px;z-index:9998;width:320px;max-height:460px;background:#fff;border-radius:16px;box-shadow:0 16px 48px rgba(0,0,0,0.2);overflow:hidden;display:none;flex-direction:column;font-family:inherit;border:1px solid #bfdbfe";
|
| 30 |
+
panel.innerHTML = '<div style="padding:12px 14px;background:linear-gradient(135deg,#eff6ff,#dbeafe);display:flex;align-items:center;justify-content:space-between;gap:8px"><span style="font-weight:800;color:#1d4ed8;font-size:0.85rem">🧩 COMBO KHUYẾN MÃI</span><div style="display:flex;align-items:center;gap:6px"><button type="button" class="combo-list-share-btn" onclick="window.shareComboList(event)" style="padding:5px 9px;border:none;border-radius:8px;background:#1d4ed8;color:#fff;font-size:0.64rem;font-weight:700;cursor:pointer;font-family:inherit;white-space:nowrap"><i class="fas fa-share-alt"></i> Chia sẻ danh sách</button><button class="combo-close" style="background:none;border:none;font-size:1.2rem;cursor:pointer;color:#1e3a8a">×</button></div></div><div class="combo-list" style="flex:1;overflow:auto;padding:8px 10px;display:flex;flex-direction:column;gap:6px"></div>';
|
| 31 |
|
| 32 |
document.body.appendChild(btn);
|
| 33 |
document.body.appendChild(panel);
|
|
|
|
| 175 |
'<div style="font-size:0.7rem;color:#64748b;margin-bottom:4px">' + itemsHtml + '</div>' +
|
| 176 |
'<div style="display:flex;align-items:center;justify-content:space-between;gap:6px">' +
|
| 177 |
'<span style="font-weight:700;color:#1d4ed8;font-size:0.8rem">' + fmt(priceNum) + '</span>' +
|
| 178 |
+
'<span class="combo-card-actions" style="display:flex;gap:4px"></span>' +
|
| 179 |
'</div>' +
|
| 180 |
'</div>';
|
| 181 |
}).join("");
|
| 182 |
+
injectComboAdminActions();
|
| 183 |
} catch(e) { console.warn("[COMBO] refresh:", e); }
|
| 184 |
}
|
| 185 |
// Per-combo "Chia sẻ link" (SEO). Copy a self-hosted deep link that opens the
|
| 186 |
// COMBO KHUYẾN MÃI panel. Exposed globally because combo cards are HTML strings.
|
| 187 |
+
|
| 188 |
+
// List-level share (toàn bộ danh sách COMBO). Copy a self-hosted deep link
|
| 189 |
+
// (?combo=all) that opens the FULL COMBO panel — NOT a single combo.
|
| 190 |
+
window.shareComboList = function (ev) {
|
| 191 |
try {
|
| 192 |
if (ev) { ev.stopPropagation(); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
var base = window.location.origin + window.location.pathname;
|
| 194 |
+
var url = base + "?combo=all";
|
| 195 |
var done = function () {
|
| 196 |
+
if (typeof window.toast === "function") window.toast("\u2713 \u0110\u00e3 copy link chia s\u1ebb danh s\u00e1ch COMBO");
|
| 197 |
+
else alert("Link chia s\u1ebb danh s\u00e1ch COMBO:\n" + url);
|
| 198 |
};
|
| 199 |
if (window.navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
| 200 |
+
navigator.clipboard.writeText(url).then(done).catch(function () { window.prompt("Link chia s\u1ebb danh s\u00e1ch COMBO:", url); });
|
| 201 |
+
} else { window.prompt("Link chia s\u1ebb danh s\u00e1ch COMBO:", url); }
|
| 202 |
} catch (err) {
|
| 203 |
alert("L\u1ed7i chia s\u1ebb: " + (err && err.message || err));
|
| 204 |
}
|
| 205 |
};
|
| 206 |
|
| 207 |
+
// Admin-only: permanently delete a COMBO from promos.json (/api/delete-promo).
|
| 208 |
+
window.deleteCombo = async function (idx, ev) {
|
| 209 |
+
try {
|
| 210 |
+
if (ev) { ev.stopPropagation(); }
|
| 211 |
+
var isAdmin = (typeof window.isVaAdmin === "function") ? window.isVaAdmin() : false;
|
| 212 |
+
if (!isAdmin) { alert("\uD83D\uDD12 C\u1ea7n nh\u1eadp m\u00e3 V.AISTUDIO tr\u00ean trang ch\u1ee7."); return; }
|
| 213 |
+
var combos = loadCombos() || [];
|
| 214 |
+
var c = combos[idx];
|
| 215 |
+
if (!c) return;
|
| 216 |
+
if (!window.confirm("X\u00f3a COMBO \"" + (c.name || "COMBO") + "\" kh\u1ecfi danh s\u00e1ch?")) return;
|
| 217 |
+
var key = String(c.code || c.slug || c.name || "combo-" + (idx + 1));
|
| 218 |
+
var r = await fetch("/api/delete-promo", {
|
| 219 |
+
method: "POST",
|
| 220 |
+
headers: { "Content-Type": "application/json", "x-vai-admin": "1" },
|
| 221 |
+
body: JSON.stringify({ type: "combo", key: key })
|
| 222 |
+
});
|
| 223 |
+
var d = await r.json().catch(function () { return {}; });
|
| 224 |
+
if (!r.ok) { alert("\u274c " + (d.error || "L\u1ed7i x\u00f3a")); return; }
|
| 225 |
+
if (window.toast) window.toast("\u2713 \u0110\u00e3 x\u00f3a COMBO");
|
| 226 |
+
// refresh from server
|
| 227 |
+
if (window.vaixPromo && typeof window.vaixPromo.loadPromos === "function") {
|
| 228 |
+
window.vaixPromo.loadPromos().then(function () { refreshPanel(); }).catch(function () { refreshPanel(); });
|
| 229 |
+
} else { refreshPanel(); }
|
| 230 |
+
} catch (err) {
|
| 231 |
+
alert("\u274c L\u1ed7i x\u00f3a: " + (err && err.message || err));
|
| 232 |
+
}
|
| 233 |
+
};
|
| 234 |
+
// Inject admin delete buttons into combo cards (after each render).
|
| 235 |
+
function injectComboAdminActions() {
|
| 236 |
+
try {
|
| 237 |
+
var isAdmin = (typeof window.isVaAdmin === "function") ? window.isVaAdmin() : false;
|
| 238 |
+
if (!isAdmin) return;
|
| 239 |
+
var panel = document.getElementById("combo-panel");
|
| 240 |
+
if (!panel) return;
|
| 241 |
+
var cards = panel.querySelectorAll(".c-card");
|
| 242 |
+
for (var i = 0; i < cards.length; i++) {
|
| 243 |
+
var card = cards[i];
|
| 244 |
+
var idx = parseInt(card.getAttribute("data-ci"), 10);
|
| 245 |
+
if (isNaN(idx) || idx < 0) continue;
|
| 246 |
+
var actions = card.querySelector(".combo-card-actions");
|
| 247 |
+
if (!actions || actions.querySelector(".combo-del-btn")) continue;
|
| 248 |
+
var del = document.createElement("button");
|
| 249 |
+
del.type = "button";
|
| 250 |
+
del.className = "combo-del-btn";
|
| 251 |
+
del.innerHTML = '<i class="fas fa-trash"></i> X\u00f3a';
|
| 252 |
+
del.style.cssText = "padding:4px 8px;border:none;border-radius:7px;background:#dc2626;color:#fff;font-size:0.62rem;font-weight:700;cursor:pointer;font-family:inherit;display:inline-flex;align-items:center;gap:4px;white-space:nowrap";
|
| 253 |
+
del.addEventListener("click", function (e) { window.deleteCombo(idx, e); });
|
| 254 |
+
actions.appendChild(del);
|
| 255 |
+
}
|
| 256 |
+
} catch (e) { console.warn("[COMBO] inject:", e); }
|
| 257 |
+
}
|
| 258 |
+
// Open the FULL combo panel (used by ?combo=all deep link).
|
| 259 |
+
window.openComboPanel = function () {
|
| 260 |
+
try {
|
| 261 |
+
var panel = document.getElementById("combo-panel");
|
| 262 |
+
var btn = document.getElementById("combo-floating");
|
| 263 |
+
if (panel) panel.style.display = "flex";
|
| 264 |
+
if (btn) btn.style.display = "";
|
| 265 |
+
if (panel) {
|
| 266 |
+
var list = panel.querySelector(".combo-list");
|
| 267 |
+
if (list) list.innerHTML = "";
|
| 268 |
+
refreshPanel();
|
| 269 |
+
}
|
| 270 |
+
} catch (e) {}
|
| 271 |
+
};
|
| 272 |
+
|
| 273 |
+
// ── ?combo= deep link: "all" opens the full list; a slug opens that COMBO ──
|
| 274 |
+
function handleComboUrlParam() {
|
| 275 |
+
try {
|
| 276 |
+
var params = new URLSearchParams(location.search);
|
| 277 |
+
var val = params.get("combo") || "";
|
| 278 |
+
if (!val) return;
|
| 279 |
+
var valNorm = String(val).trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
| 280 |
+
var tries = 0;
|
| 281 |
+
var iv = setInterval(function () {
|
| 282 |
+
tries++;
|
| 283 |
+
try {
|
| 284 |
+
if (!document.getElementById("combo-floating")) {
|
| 285 |
+
if (tries > 30) { clearInterval(iv); return; }
|
| 286 |
+
return;
|
| 287 |
+
}
|
| 288 |
+
var panel = document.getElementById("combo-panel");
|
| 289 |
+
if (!panel) { clearInterval(iv); return; }
|
| 290 |
+
panel.style.display = "flex";
|
| 291 |
+
var combos = loadCombos() || [];
|
| 292 |
+
if (valNorm && valNorm !== "all") {
|
| 293 |
+
// Try to highlight/scroll to the requested combo card and open it.
|
| 294 |
+
var idx = -1;
|
| 295 |
+
for (var i = 0; i < combos.length; i++) {
|
| 296 |
+
var c = combos[i];
|
| 297 |
+
var slug = String(c.slug || c.code || c.name || "combo-" + (i + 1))
|
| 298 |
+
.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
| 299 |
+
if (slug === valNorm || String(c.code || "").toLowerCase() === val) { idx = i; break; }
|
| 300 |
+
}
|
| 301 |
+
refreshPanel();
|
| 302 |
+
if (idx >= 0) {
|
| 303 |
+
var cards = panel.querySelectorAll(".c-card");
|
| 304 |
+
if (cards[idx]) {
|
| 305 |
+
cards[idx].scrollIntoView({ behavior: "smooth", block: "center" });
|
| 306 |
+
cards[idx].style.border = "2px solid #f59e0b";
|
| 307 |
+
setTimeout(function () { if (cards[idx]) cards[idx].style.border = ""; }, 2500);
|
| 308 |
+
}
|
| 309 |
+
}
|
| 310 |
+
} else {
|
| 311 |
+
refreshPanel();
|
| 312 |
+
}
|
| 313 |
+
clearInterval(iv);
|
| 314 |
+
} catch (e) {
|
| 315 |
+
if (tries > 30) clearInterval(iv);
|
| 316 |
+
}
|
| 317 |
+
}, 400);
|
| 318 |
+
} catch (e) { /* never block */ }
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
function boot() {
|
| 322 |
// Retry createComboElements until vaixPromo is ready (can load after CMS module)
|
| 323 |
var _retry = 0;
|
|
|
|
| 325 |
_retry++;
|
| 326 |
try {
|
| 327 |
createComboElements();
|
| 328 |
+
handleComboUrlParam();
|
| 329 |
} catch(e) {
|
| 330 |
if (_retry < 30) { setTimeout(tryCreate, 500); return; }
|
| 331 |
}
|
src/vaix-flashsale.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// V.AI STUDIO FLASHSALE floating panel v1.0
|
| 2 |
+
// Nút FLASHSALE đặt NGAY CẠNH nút COMBO (bottom:240px, combo ở 170px).
|
| 3 |
+
// Danh sách FLASHSALE = mọi sản phẩm khuyến mãi CÓ ĐỦ 2 GIÁ:
|
| 4 |
+
// - Từ catalog (vaix.allProducts): listPn (niêm yết) + salePn (khuyến mãi), listPn > salePn
|
| 5 |
+
// - Từ BIG SALE (window.BIG_SALE_PRODUCTS): price (niêm yết) + bigsale (giá KM)
|
| 6 |
+
// Chia sẻ cả danh sách qua ?flashsale=all (self-hosted, mở đúng panel này).
|
| 7 |
+
(function () {
|
| 8 |
+
if (window.__vaixFlashsaleLoaded) return;
|
| 9 |
+
window.__vaixFlashsaleLoaded = true;
|
| 10 |
+
console.log("[FLASHSALE] Module loaded");
|
| 11 |
+
|
| 12 |
+
function fmt(n) {
|
| 13 |
+
n = Number(n) || 0;
|
| 14 |
+
return n.toLocaleString("vi-VN") + " ₫";
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
function catalogProductsRaw() {
|
| 18 |
+
try {
|
| 19 |
+
if (window.vaix && typeof window.vaix.allProducts === "function") {
|
| 20 |
+
var arr = window.vaix.allProducts();
|
| 21 |
+
return Array.isArray(arr) ? arr : [];
|
| 22 |
+
}
|
| 23 |
+
} catch (e) {}
|
| 24 |
+
return [];
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// Catalog products that have BOTH listPn and salePn, list > sale.
|
| 28 |
+
function catalogBuild() {
|
| 29 |
+
var out = [];
|
| 30 |
+
var arr = catalogProductsRaw();
|
| 31 |
+
for (var i = 0; i < arr.length; i++) {
|
| 32 |
+
var p = arr[i];
|
| 33 |
+
var listN = Number(p.listPn) || (p.listPrice ? parseInt(String(p.listPrice).replace(/[^\d]/g, ""), 10) : 0) || 0;
|
| 34 |
+
var saleN = Number(p.salePn) || (p.salePrice ? parseInt(String(p.salePrice).replace(/[^\d]/g, ""), 10) : 0) || Number(p.priceNum) || 0;
|
| 35 |
+
if (!listN || !saleN || listN <= saleN) continue;
|
| 36 |
+
if (p._deleted || p.deleted) continue;
|
| 37 |
+
out.push({
|
| 38 |
+
name: p.title_clean || p.name || p.title || "",
|
| 39 |
+
code: p.sku || p.model || p.code || "",
|
| 40 |
+
image: p.image || (p.images && p.images[0]) || "",
|
| 41 |
+
listPn: listN, salePn: saleN,
|
| 42 |
+
discount: saleN ? Math.round((listN - saleN) / listN * 100) + "%" : "",
|
| 43 |
+
slug: p.slug || "", catalog: true
|
| 44 |
+
});
|
| 45 |
+
}
|
| 46 |
+
return out;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// FLASHSALE = products having BOTH list price and sale price (list > sale).
|
| 50 |
+
function buildFlashProducts() {
|
| 51 |
+
var out = [];
|
| 52 |
+
var seen = {};
|
| 53 |
+
function push(p) {
|
| 54 |
+
if (!p) return;
|
| 55 |
+
var key = String(p.code || p.sku || p.model || p.name || "");
|
| 56 |
+
if (!key) return;
|
| 57 |
+
key = key.toUpperCase().replace(/\s+/g, " ");
|
| 58 |
+
if (seen[key]) return;
|
| 59 |
+
seen[key] = 1;
|
| 60 |
+
out.push(p);
|
| 61 |
+
}
|
| 62 |
+
// 1) Catalog products with both prices.
|
| 63 |
+
var cat = catalogBuild();
|
| 64 |
+
for (var i = 0; i < cat.length; i++) push(cat[i]);
|
| 65 |
+
// 2) BIG SALE products (they always have list price + bigsale discount price).
|
| 66 |
+
try {
|
| 67 |
+
var bs = (window.BIG_SALE_PRODUCTS || []);
|
| 68 |
+
for (var j = 0; j < bs.length; j++) {
|
| 69 |
+
var b = bs[j];
|
| 70 |
+
var listN = parseInt(String(b.price || "0").replace(/\./g, ""), 10) || 0;
|
| 71 |
+
var saleN = parseInt(String(b.bigsale || "0").replace(/\./g, ""), 10) || 0;
|
| 72 |
+
if (listN <= 0 || saleN <= 0 || listN <= saleN) continue;
|
| 73 |
+
push({
|
| 74 |
+
name: b.name, code: b.code, image: b.image || "", listPn: listN, salePn: saleN,
|
| 75 |
+
discount: b.discount || "", _fromBigSale: true
|
| 76 |
+
});
|
| 77 |
+
}
|
| 78 |
+
} catch (e) {}
|
| 79 |
+
return out;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
function createElements() {
|
| 83 |
+
if (document.getElementById("flashsale-floating")) return;
|
| 84 |
+
var btn = document.createElement("button");
|
| 85 |
+
btn.id = "flashsale-floating";
|
| 86 |
+
btn.style.cssText = "position:fixed;right:18px;bottom:240px;z-index:9999;border:none;cursor:pointer;display:flex;align-items:center;gap:6px;background:#fff;border-radius:30px;padding:8px 14px;box-shadow:0 4px 14px rgba(0,0,0,0.15);font-family:inherit;font-size:0.75rem;font-weight:700;color:#c2410c;border:1px solid #fed7aa;transition:all 0.25s";
|
| 87 |
+
btn.innerHTML = '<span style="background:#ea580c;color:#fff;font-weight:800;font-size:0.65rem;padding:2px 8px;border-radius:20px">FLASHSALE</span><span class="flashsale-count" style="font-size:0.72rem;color:#9a3412"> 0</span><span style="margin-left:2px;font-size:0.65rem;color:#9a3412">▼</span>';
|
| 88 |
+
|
| 89 |
+
var panel = document.createElement("div");
|
| 90 |
+
panel.id = "flashsale-panel";
|
| 91 |
+
panel.style.cssText = "position:fixed;right:18px;bottom:240px;z-index:9998;width:320px;max-height:460px;background:#fff;border-radius:16px;box-shadow:0 16px 48px rgba(0,0,0,0.2);overflow:hidden;display:none;flex-direction:column;font-family:inherit;border:1px solid #fed7aa";
|
| 92 |
+
panel.innerHTML = '<div style="padding:12px 14px;background:linear-gradient(135deg,#fff7ed,#ffedd5);display:flex;align-items:center;justify-content:space-between;gap:8px"><span style="font-weight:800;color:#c2410c;font-size:0.85rem">⚡ FLASHSALE 2 GIÁ</span><div style="display:flex;align-items:center;gap:6px"><button type="button" class="flashsale-list-share-btn" onclick="window.shareFlashsaleList(event)" style="padding:5px 9px;border:none;border-radius:8px;background:#ea580c;color:#fff;font-size:0.64rem;font-weight:700;cursor:pointer;font-family:inherit;white-space:nowrap"><i class="fas fa-share-alt"></i> Chia sẻ danh sách</button><button class="flashsale-close" style="background:none;border:none;font-size:1.2rem;cursor:pointer;color:#9a3412">×</button></div></div><div class="flashsale-list" style="flex:1;overflow:auto;padding:8px 10px;display:flex;flex-direction:column;gap:6px"></div>';
|
| 93 |
+
|
| 94 |
+
document.body.appendChild(btn);
|
| 95 |
+
document.body.appendChild(panel);
|
| 96 |
+
|
| 97 |
+
var isOpen = false;
|
| 98 |
+
btn.addEventListener("click", function (e) {
|
| 99 |
+
e.stopPropagation();
|
| 100 |
+
isOpen = !isOpen;
|
| 101 |
+
panel.style.display = isOpen ? "flex" : "none";
|
| 102 |
+
if (isOpen) refreshPanel();
|
| 103 |
+
});
|
| 104 |
+
var closeBtn = panel.querySelector(".flashsale-close");
|
| 105 |
+
if (closeBtn) closeBtn.onclick = function () { panel.style.display = "none"; isOpen = false; };
|
| 106 |
+
document.addEventListener("click", function (e) {
|
| 107 |
+
if (isOpen && !panel.contains(e.target) && !btn.contains(e.target)) {
|
| 108 |
+
panel.style.display = "none"; isOpen = false;
|
| 109 |
+
}
|
| 110 |
+
});
|
| 111 |
+
|
| 112 |
+
panel.addEventListener("click", function (e) {
|
| 113 |
+
var card = e.target && e.target.closest ? e.target.closest(".fs-card") : null;
|
| 114 |
+
if (!card) return;
|
| 115 |
+
e.stopPropagation();
|
| 116 |
+
var code = card.getAttribute("data-code") || "";
|
| 117 |
+
if (!code) return;
|
| 118 |
+
var p = buildFlashProducts().find(function (x) {
|
| 119 |
+
return String(x.code || "").trim().toUpperCase() === code.toUpperCase();
|
| 120 |
+
});
|
| 121 |
+
if (p) openDetail(p);
|
| 122 |
+
});
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
function openDetail(p) {
|
| 126 |
+
var overlay = document.getElementById("vaistudio-detail-overlay");
|
| 127 |
+
var modal = document.getElementById("vaistudio-detail-modal");
|
| 128 |
+
if (!overlay || !modal) return;
|
| 129 |
+
var titleEl = document.getElementById("detail-title");
|
| 130 |
+
var modelEl = document.getElementById("detail-model");
|
| 131 |
+
var priceEl = document.getElementById("detail-price");
|
| 132 |
+
var imagesEl = document.getElementById("detail-images");
|
| 133 |
+
try {
|
| 134 |
+
if (titleEl) titleEl.textContent = p.name || "";
|
| 135 |
+
if (modelEl) modelEl.textContent = "Mã: " + (p.code || "");
|
| 136 |
+
if (priceEl) {
|
| 137 |
+
priceEl.innerHTML = '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">' +
|
| 138 |
+
'<span style="color:#c2410c;font-size:1.3rem;font-weight:800">' + fmt(p.salePn) + "</span>" +
|
| 139 |
+
'<span style="font-size:0.8rem;color:#64748b;text-decoration:line-through">' + fmt(p.listPn) + "</span>" +
|
| 140 |
+
(p.discount ? '<span style="font-size:0.85rem;background:#fef3c7;color:#92400e;padding:4px 10px;border-radius:8px;font-weight:700">-' + p.discount + "</span>" : "") +
|
| 141 |
+
"</div>";
|
| 142 |
+
}
|
| 143 |
+
if (imagesEl) {
|
| 144 |
+
var gal = p.image ? [p.image] : [];
|
| 145 |
+
imagesEl.innerHTML = gal.length
|
| 146 |
+
? gal.map(function (u) { return '<img src="' + u + '" alt="' + (p.code || "") + '" style="flex:1;min-width:120px;height:120px;border-radius:10px;object-fit:cover;border:1px solid #e2e8f0;background:#f1f5f9" onerror="this.style.display=\'none\'">'; }).join("")
|
| 147 |
+
: '<div style="flex:1;min-width:120px;height:120px;background:linear-gradient(135deg,#fff7ed,#ffedd5);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:2.5rem">⚡</div>';
|
| 148 |
+
}
|
| 149 |
+
if (overlay) overlay.classList.add("show");
|
| 150 |
+
if (modal) modal.classList.add("show");
|
| 151 |
+
} catch (e) {}
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
function refreshPanel() {
|
| 155 |
+
try {
|
| 156 |
+
var panel = document.getElementById("flashsale-panel");
|
| 157 |
+
var btn = document.getElementById("flashsale-floating");
|
| 158 |
+
if (!panel || !btn) return;
|
| 159 |
+
var list = panel.querySelector(".flashsale-list");
|
| 160 |
+
if (!list) return;
|
| 161 |
+
var prods = buildFlashProducts();
|
| 162 |
+
var cEl = btn.querySelector(".flashsale-count");
|
| 163 |
+
if (cEl) cEl.textContent = " " + prods.length;
|
| 164 |
+
if (!prods.length) {
|
| 165 |
+
list.innerHTML = '<div style="font-size:0.75rem;color:#94a3b8;text-align:center;padding:20px 0">Chưa có sản phẩm 2 giá nào.</div>';
|
| 166 |
+
return;
|
| 167 |
+
}
|
| 168 |
+
list.innerHTML = prods.map(function (p, i) {
|
| 169 |
+
var img = p.image ? '<img src="' + p.image + '" alt="" onerror="this.style.display='none'" style="width:40px;height:40px;border-radius:8px;object-fit:cover;background:#f1f5f9;margin-right:6px;flex-shrink:0">' : "";
|
| 170 |
+
return '<div class="fs-card" data-code="' + (p.code || "") + '" style="cursor:pointer;padding:8px 10px;background:#fffaf5;border:1px solid #fed7aa;border-radius:10px;display:flex;align-items:center;transition:border-color 0.2s">' +
|
| 171 |
+
img +
|
| 172 |
+
'<div style="flex:1;min-width:0">' +
|
| 173 |
+
'<div style="font-weight:700;font-size:0.74rem;color:#431407;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + (p.name || "") + '</div>' +
|
| 174 |
+
'<div style="font-size:0.64rem;color:#9a3412;margin-top:2px">' +
|
| 175 |
+
'<span style="text-decoration:line-through;color:#94a3b8">' + (p.listPn ? fmt(p.listPn) : "") + '</span> → <b style="color:#c2410c">' + (p.salePn ? fmt(p.salePn) : "") + '</b>' +
|
| 176 |
+
'</div>' +
|
| 177 |
+
'</div>' +
|
| 178 |
+
'</div>';
|
| 179 |
+
}).join("");
|
| 180 |
+
} catch (e) {
|
| 181 |
+
console.warn("[FLASHSALE] refresh:", e);
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
// Chia sẻ TOÀN BỘ danh sách (?flashsale=all).
|
| 186 |
+
window.shareFlashsaleList = function (ev) {
|
| 187 |
+
try {
|
| 188 |
+
if (ev) { ev.stopPropagation(); }
|
| 189 |
+
var base = window.location.origin + window.location.pathname;
|
| 190 |
+
var url = base + "?flashsale=all";
|
| 191 |
+
var done = function () {
|
| 192 |
+
if (typeof window.toast === "function") window.toast("✓ Đã copy link chia sẻ danh sách FLASHSALE");
|
| 193 |
+
else alert("Link chia sẻ danh sách FLASHSALE:\n" + url);
|
| 194 |
+
};
|
| 195 |
+
if (window.navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
| 196 |
+
navigator.clipboard.writeText(url).then(done).catch(function () { window.prompt("Link chia sẻ danh sách FLASHSALE:", url); });
|
| 197 |
+
} else { window.prompt("Link chia sẻ danh sách FLASHSALE:", url); }
|
| 198 |
+
} catch (err) {
|
| 199 |
+
alert("Lỗi chia sẻ: " + (err && err.message || err));
|
| 200 |
+
}
|
| 201 |
+
};
|
| 202 |
+
// Mở full panel khi vào link ?flashsale=all.
|
| 203 |
+
window.openFlashsalePanel = function () {
|
| 204 |
+
try {
|
| 205 |
+
var panel = document.getElementById("flashsale-panel");
|
| 206 |
+
var btn = document.getElementById("flashsale-floating");
|
| 207 |
+
if (panel) panel.style.display = "flex";
|
| 208 |
+
if (btn) btn.style.display = "";
|
| 209 |
+
if (panel) refreshPanel();
|
| 210 |
+
} catch (e) {}
|
| 211 |
+
};
|
| 212 |
+
|
| 213 |
+
function handleUrlParam() {
|
| 214 |
+
try {
|
| 215 |
+
var params = new URLSearchParams(location.search);
|
| 216 |
+
var v = params.get("flashsale") || "";
|
| 217 |
+
if (!v) return;
|
| 218 |
+
var tries = 0;
|
| 219 |
+
var iv = setInterval(function () {
|
| 220 |
+
tries++;
|
| 221 |
+
try {
|
| 222 |
+
if (document.getElementById("flashsale-floating")) {
|
| 223 |
+
window.openFlashsalePanel();
|
| 224 |
+
clearInterval(iv);
|
| 225 |
+
} else if (tries > 40) clearInterval(iv);
|
| 226 |
+
} catch (e) { if (tries > 40) clearInterval(iv); }
|
| 227 |
+
}, 400);
|
| 228 |
+
} catch (e) {}
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
function boot() {
|
| 232 |
+
var _retry = 0;
|
| 233 |
+
(function tryCreate() {
|
| 234 |
+
_retry++;
|
| 235 |
+
try {
|
| 236 |
+
createElements();
|
| 237 |
+
refreshPanel();
|
| 238 |
+
handleUrlParam();
|
| 239 |
+
// Recompute when the catalog finishes loading or promo data changes.
|
| 240 |
+
if (window.vaix && typeof window.vaix.load === "function") {
|
| 241 |
+
window.vaix.load().then(function () { refreshPanel(); }).catch(function () {});
|
| 242 |
+
}
|
| 243 |
+
window.addEventListener("vai-promo-changed", function () { setTimeout(refreshPanel, 200); });
|
| 244 |
+
window.addEventListener("vai-catalog-loaded", function () { refreshPanel(); });
|
| 245 |
+
} catch (e) {
|
| 246 |
+
if (_retry < 30) setTimeout(tryCreate, 500);
|
| 247 |
+
}
|
| 248 |
+
})();
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
// Quick alias so the module still works even before vaix finishes loading.
|
| 252 |
+
window.addEventListener("DOMContentLoaded", function () {
|
| 253 |
+
try {
|
| 254 |
+
var d = document.getElementById("flashsale-floating");
|
| 255 |
+
if (!d) boot();
|
| 256 |
+
} catch (e) {}
|
| 257 |
+
});
|
| 258 |
+
if (document.readyState !== "loading") boot();
|
| 259 |
+
})();
|
src/vaix-rag.js
CHANGED
|
@@ -1772,20 +1772,6 @@ function renderPanelResults(products) {
|
|
| 1772 |
const adminBtns = document.createElement("div");
|
| 1773 |
adminBtns.className = "product-card-admin";
|
| 1774 |
adminBtns.style.cssText = "display:flex;gap:6px;margin-top:8px;flex-wrap:wrap";
|
| 1775 |
-
// Share link button (SEO-friendly ?product=<slug> URL, copied to clipboard).
|
| 1776 |
-
const shareBtn = document.createElement("button");
|
| 1777 |
-
shareBtn.type = "button";
|
| 1778 |
-
shareBtn.className = "product-card-btn share-btn";
|
| 1779 |
-
shareBtn.style.cssText = "flex:1;padding:7px 8px;border:none;border-radius:8px;background:#1e40af;color:#fff;font-size:.72rem;font-weight:700;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:5px;font-family:inherit";
|
| 1780 |
-
shareBtn.innerHTML = '<i class="fas fa-bullhorn"></i> Chia sẻ';
|
| 1781 |
-
shareBtn.addEventListener("click", function(e) {
|
| 1782 |
-
e.stopPropagation();
|
| 1783 |
-
const url = shareableProductUrl(p);
|
| 1784 |
-
if (window.navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
| 1785 |
-
navigator.clipboard.writeText(url).then(function(){ toast('✓ Đã copy link chia sẻ (SEO)'); }).catch(function(){ window.prompt('Link chia sẻ:', url); });
|
| 1786 |
-
} else { window.prompt('Link chia sẻ:', url); }
|
| 1787 |
-
});
|
| 1788 |
-
adminBtns.appendChild(shareBtn);
|
| 1789 |
// Delete button (admin only) — appears on admin-added products and
|
| 1790 |
// permanently removes them from the shared dataset.
|
| 1791 |
if (isAddedProduct) {
|
|
|
|
| 1772 |
const adminBtns = document.createElement("div");
|
| 1773 |
adminBtns.className = "product-card-admin";
|
| 1774 |
adminBtns.style.cssText = "display:flex;gap:6px;margin-top:8px;flex-wrap:wrap";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1775 |
// Delete button (admin only) — appears on admin-added products and
|
| 1776 |
// permanently removes them from the shared dataset.
|
| 1777 |
if (isAddedProduct) {
|