/* ========================================================================= * main.js — общ UI слой (хедър, търсене, тостове, продуктови карти, reveal) * Фаза 2: остава почти непроменен — работи върху server-rendered HTML. * ========================================================================= */ (function () { 'use strict'; function $(sel, root) { return (root || document).querySelector(sel); } function $$(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); } /* XSS защита при рендериране на текст в DOM (правило от salex) */ function esc(s) { return String(s).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); } window.ACM = window.ACM || {}; window.ACM.esc = esc; /* ======================== Тостове (както в оригинала) =================== */ var toastTimer = null; function showToast(message, opts) { opts = opts || {}; var root = $('#toast-root'); if (!root) return; root.innerHTML = ''; if (toastTimer) { clearTimeout(toastTimer); toastTimer = null; } var box = document.createElement('div'); box.className = 'acm-toast pointer-events-auto flex w-full max-w-sm items-center gap-3 rounded-card bg-ink px-4 py-3 text-white shadow-lg'; var p = document.createElement('p'); p.className = 'flex-1 text-[14px]'; p.textContent = message; box.appendChild(p); if (opts.actionLabel) { var act = document.createElement('button'); act.type = 'button'; act.className = 'shrink-0 rounded-control px-2 py-1 text-[14px] font-medium text-brand-green transition-colors hover:bg-white/10'; act.textContent = opts.actionLabel; act.addEventListener('click', function () { if (opts.onAction) opts.onAction(); dismiss(); }); box.appendChild(act); } var close = document.createElement('button'); close.type = 'button'; close.setAttribute('aria-label', 'Затвори'); close.className = 'grid h-8 w-8 shrink-0 place-items-center rounded-control text-white/70 transition-colors hover:bg-white/10 hover:text-white'; close.innerHTML = ''; close.addEventListener('click', dismiss); box.appendChild(close); root.appendChild(box); function dismiss() { if (box.parentNode) box.parentNode.removeChild(box); if (toastTimer) { clearTimeout(toastTimer); toastTimer = null; } } toastTimer = setTimeout(dismiss, opts.actionLabel ? 6000 : 4000); } window.ACM.showToast = showToast; /* ======================== Бадж на количката ============================ */ function updateCartBadge() { if (!window.ACM.cart) return; var n = window.ACM.cart.totalUnits(); $$('[data-cart-badge]').forEach(function (b) { if (n > 0) { b.textContent = n > 99 ? '99+' : String(n); b.hidden = false; } else { b.hidden = true; } }); } document.addEventListener('acm:cart-changed', updateCartBadge); /* ======================== Хедър ======================================== */ function initHeader() { /* Профил dropdown */ var pBtn = $('#profile-menu-btn'), pPanel = $('#profile-menu-panel'), pRoot = $('#profile-menu-root'); if (pBtn && pPanel) { pBtn.addEventListener('click', function () { var open = pPanel.hidden; pPanel.hidden = !open; pBtn.setAttribute('aria-expanded', String(open)); var chev = pBtn.querySelector('.lucide-chevron-down'); if (chev) chev.classList.toggle('rotate-180', open); }); document.addEventListener('pointerdown', function (e) { if (!pPanel.hidden && pRoot && !pRoot.contains(e.target)) closeProfile(); }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeProfile(); }); } function closeProfile() { if (pPanel && !pPanel.hidden) { pPanel.hidden = true; pBtn.setAttribute('aria-expanded', 'false'); var chev = pBtn.querySelector('.lucide-chevron-down'); if (chev) chev.classList.remove('rotate-180'); } } /* Мобилно меню */ var mBtn = $('#mobile-menu-btn'), mPanel = $('#mobile-menu-panel'), mScrim = $('#mobile-menu-scrim'), mClose = $('#mobile-menu-close'); function setMenu(open) { if (!mPanel) return; mPanel.hidden = !open; mScrim.hidden = !open; mBtn.setAttribute('aria-expanded', String(open)); document.body.style.overflow = open ? 'hidden' : ''; } if (mBtn) mBtn.addEventListener('click', function () { setMenu(mPanel.hidden); }); if (mClose) mClose.addEventListener('click', function () { setMenu(false); }); if (mScrim) mScrim.addEventListener('click', function () { setMenu(false); }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && mPanel && !mPanel.hidden) setMenu(false); }); /* Мобилно търсене */ var sBtn = $('#mobile-search-btn'), sRow = $('#mobile-search-row'), sInput = $('#mobile-search-input'); if (sBtn) sBtn.addEventListener('click', function () { var open = sRow.hidden; sRow.hidden = !open; sBtn.setAttribute('aria-expanded', String(open)); if (open && sInput) sInput.focus(); }); if (sInput) sInput.addEventListener('keydown', function (e) { if (e.key === 'Enter' && sInput.value.trim()) { window.location.href = '/catalog/?q=' + encodeURIComponent(sInput.value.trim()); } }); initSearchSuggest(); } /* ======================== Търсене с подсказки ========================== */ function searchProducts(q) { var list = window.ACM_PRODUCTS || []; q = q.toLowerCase(); var byName = [], bySubst = []; list.forEach(function (p) { if (p.name.toLowerCase().indexOf(q) !== -1) byName.push(p); else if ((p.activeSubstances || []).some(function (s) { return s.name.toLowerCase().indexOf(q) !== -1; })) bySubst.push(p); }); return byName.concat(bySubst); } window.ACM.searchProducts = searchProducts; function stockDotsHTML(p) { var st = window.ACM.demo.stockStatus; return ''; } function initSearchSuggest() { var input = $('#search-input'), box = $('#search-suggest'); if (!input || !box) return; function render() { var q = input.value.trim(); if (q.length < 2) { box.hidden = true; box.innerHTML = ''; input.setAttribute('aria-expanded', 'false'); return; } var res = searchProducts(q).slice(0, 5); var html = res.map(function (p) { return '' + '' + '' + esc(p.name) + '' + 'Арт. № ' + esc(p.artNumber) + '' + '' + stockDotsHTML(p) + ''; }).join(''); html += '' + '' + 'Виж всички резултати за „' + esc(q) + '"'; box.innerHTML = html; box.hidden = false; input.setAttribute('aria-expanded', 'true'); } input.addEventListener('input', render); input.addEventListener('focus', render); input.addEventListener('keydown', function (e) { if (e.key === 'Enter' && input.value.trim()) { window.location.href = '/catalog/?q=' + encodeURIComponent(input.value.trim()); } if (e.key === 'Escape') { box.hidden = true; } }); document.addEventListener('pointerdown', function (e) { var root = $('#search-root'); if (root && !root.contains(e.target)) box.hidden = true; }); } /* ======================== Продуктови карти / купуване =================== */ /* Картите нямат data-атрибути (идват от server-rendered HTML) — * productId се извлича от линка на картата или от URL на страницата. */ function productIdFromCard(card) { var a = card.querySelector('a[href^="/product/"]'); if (!a) return null; var m = a.getAttribute('href').match(/^\/product\/([^\/]+)\//); return m ? m[1] : null; } function productIdFromURL() { var m = window.location.pathname.match(/^\/product\/([^\/]+)\//); return m ? m[1] : null; } function initStepper(root, onChange) { var input = root.querySelector('input[aria-label="Количество"]'); var btns = root.querySelectorAll('button'); if (!input || btns.length < 2) return null; var minus = btns[0], plus = btns[btns.length - 1]; function val() { var v = parseInt(input.value, 10); return isNaN(v) ? 1 : Math.min(9999, Math.max(1, v)); } function sync() { input.value = String(val()); minus.disabled = val() <= 1; plus.disabled = val() >= 9999; if (onChange) onChange(val()); } minus.addEventListener('click', function () { input.value = String(val() - 1); sync(); }); plus.addEventListener('click', function () { input.value = String(val() + 1); sync(); }); input.addEventListener('change', sync); sync(); return { value: val }; } function findAddButton(scope) { return $$('button', scope).find(function (b) { var t = b.textContent.trim(); return t === 'Добавяне' || t === 'Добавяне в количката'; }) || null; } function wireBuyControls(scope, productId) { if (!productId || !window.ACM.findProduct(productId)) return; var stepperRoot = $$('div', scope).find(function (d) { return d.className && String(d.className).indexOf('items-stretch') !== -1 && d.querySelector('input[aria-label="Количество"]'); }); var stepper = stepperRoot ? initStepper(stepperRoot) : null; var addBtn = findAddButton(scope); if (addBtn) { addBtn.addEventListener('click', function () { var qty = stepper ? stepper.value() : 1; var p = window.ACM.findProduct(productId); window.ACM.cart.add(productId, qty); showToast(p.name + ' е добавен в количката', { actionLabel: 'Виж количката', onAction: function () { window.location.href = '/cart/'; } }); }); } } function initProductCards() { /* Карти в списъци (начало, каталог): коренът е .group с линк към продукт */ $$('div.group').forEach(function (card) { var id = productIdFromCard(card); if (id && findAddButton(card)) wireBuyControls(card, id); }); /* Продуктова страница: id от URL, контролите са в
*/ var pid = productIdFromURL(); if (pid) { var main = $('main'); if (main && findAddButton(main)) wireBuyControls(main, pid); } } /* ======================== Складови точки — подсказки ==================== */ function initStockTitles() { $$('button[aria-label^="Склад "]').forEach(function (b) { b.title = b.getAttribute('aria-label'); }); } /* ======================== Reveal анимации ============================== */ function initReveal() { var els = $$('[data-reveal]'); if (!els.length) return; if (!('IntersectionObserver' in window)) { els.forEach(function (el) { el.classList.add('revealed'); }); return; } var io = new IntersectionObserver(function (entries) { entries.forEach(function (en) { if (en.isIntersecting) { en.target.classList.add('revealed'); io.unobserve(en.target); } }); }, { rootMargin: '0px 0px -40px 0px', threshold: 0.05 }); els.forEach(function (el) { io.observe(el); }); } /* ======================== Бутoни-навигация (hero CTA) =================== */ /* В оригинала са