function getCookie(name) { return document.cookie .split('; ') .find((row) => row.startsWith(`${name}=`)) ?.split('=')[1]; } function jsonHeaders() { return { 'Content-Type': 'application/json', 'X-CSRF-Token': decodeURIComponent(getCookie('csrf_token') || ''), }; } function getCartButtons() { return Array.from(document.querySelectorAll('[data-add-to-cart]')); } function getWishlistButtons() { return Array.from(document.querySelectorAll('[data-wishlist-toggle]')); } function getSelectedCategories() { return Array.from( document.querySelectorAll('[data-filter-category]:checked') ).map((input) => input.value); } function getMinRating() { const values = Array.from( document.querySelectorAll('[data-filter-rating]:checked') ) .map((input) => Number(input.value)) .filter((value) => !Number.isNaN(value)); if (!values.length) return null; return Math.min(...values); } function getPriceRange() { const rangeNode = document.getElementById('priceRange'); if (!rangeNode || !rangeNode.noUiSlider) { return [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY]; } const values = rangeNode.noUiSlider.get().map(Number); return [values[0], values[1]]; } function applyProductFilters() { const cards = Array.from(document.querySelectorAll('.product[data-price]')); if (!cards.length) return; const [minPrice, maxPrice] = getPriceRange(); const categories = getSelectedCategories(); const minRating = getMinRating(); cards.forEach((card) => { const price = Number(card.getAttribute('data-price')); const category = card.getAttribute('data-category'); const rating = Number(card.getAttribute('data-rating')); const matchPrice = price >= minPrice && price <= maxPrice; const matchCategory = !categories.length || categories.includes(category); const matchRating = minRating === null || rating >= minRating; const visible = matchPrice && matchCategory && matchRating; card.style.display = visible ? '' : 'none'; }); } async function updateCartBadge(cartButtons) { const badge = document.getElementById('cartBadge'); if (!badge) return; try { const response = await fetch(`/${window.appLang || 'uk'}/api/cart`); if (!response.ok) return; const data = await response.json(); const items = data.items || []; badge.textContent = items.length; badge.style.display = items.length ? 'grid' : 'none'; const qtyByUuid = new Map( items.map((item) => [item.product_uuid, item.qty]) ); cartButtons.forEach((button) => { const uuid = button.getAttribute('data-product-uuid'); if (!uuid) return; const qty = qtyByUuid.get(uuid) || 0; if (button.dataset.icon === 'cart') { renderCartIconButton(button, qty); return; } const addLabel = button.getAttribute('data-add-label') || 'Add to cart'; const inCartLabel = button.getAttribute('data-in-cart-label') || 'In cart'; if (qty > 0) { button.dataset.inCart = 'true'; button.textContent = `${inCartLabel} (${qty})`; } else { button.dataset.inCart = 'false'; button.textContent = addLabel; } }); } catch (err) { badge.textContent = badge.textContent || '0'; } } const CART_ICON = ''; function renderCartIconButton(button, qty) { const qtyHtml = qty > 0 ? `` : ''; button.innerHTML = `${qtyHtml}`; button.dataset.inCart = qty > 0 ? 'true' : 'false'; } async function updateWishlistState(wishlistButtons) { try { const response = await fetch(`/${window.appLang || 'uk'}/api/wishlist`); if (response.status === 401) { const badge = document.getElementById('wishlistBadge'); if (badge) { badge.textContent = '0'; badge.style.display = 'none'; } return; } if (!response.ok) return; const data = await response.json(); const items = new Set(data.items || []); const badge = document.getElementById('wishlistBadge'); if (badge) { badge.textContent = items.size; badge.style.display = items.size ? 'grid' : 'none'; } if (wishlistButtons.length) { wishlistButtons.forEach((button) => { const uuid = button.getAttribute('data-product-uuid'); if (!uuid) return; const active = items.has(uuid); button.classList.toggle('active', active); if (button.hasAttribute('data-wishlist-icon')) { button.textContent = active ? '♥' : '♡'; } }); } } catch (err) { // ignore } } async function toggleWishlist(button, wishlistButtons) { const productUuid = button.getAttribute('data-product-uuid'); if (!productUuid) return; button.disabled = true; try { const response = await fetch(`/${window.appLang || 'uk'}/api/wishlist/toggle`, { method: 'POST', headers: jsonHeaders(), body: JSON.stringify({ product_uuid: productUuid }), }); if (response.status === 401) { window.location.href = `/${window.appLang || 'uk'}/login`; return; } if (!response.ok) { throw new Error('Wishlist update failed'); } const data = await response.json(); const active = data.active; button.classList.toggle('active', active); if (button.closest('.wishlist-card') && !active) { button.closest('.wishlist-card').remove(); } await updateWishlistState(wishlistButtons); } catch (err) { // ignore } finally { button.disabled = false; } } function initWishlistUI() { const wishlistButtons = getWishlistButtons(); if (wishlistButtons.length) { wishlistButtons.forEach((button) => { button.addEventListener('click', () => toggleWishlist(button, wishlistButtons)); }); } updateWishlistState(wishlistButtons); } async function addToCart(button, cartButtons) { const productCode = button.getAttribute('data-product-code'); const productUuid = button.getAttribute('data-product-uuid'); if (!productCode && !productUuid) return; const isIcon = button.dataset.icon === 'cart'; button.disabled = true; const prevText = button.textContent; if (!isIcon) { button.textContent = 'Adding...'; } try { const response = await fetch(`/${window.appLang || 'uk'}/api/cart/add`, { method: 'POST', headers: jsonHeaders(), body: JSON.stringify({ product_code: productCode, product_uuid: productUuid, qty: 1 }), }); if (!response.ok) { throw new Error('Failed to add'); } await response.json(); if (!isIcon) { button.textContent = 'Added'; } await updateCartBadge(cartButtons); } catch (err) { if (!isIcon) { button.textContent = 'Error'; setTimeout(() => { button.textContent = prevText; }, 1200); } } finally { button.disabled = false; } } function initCartUI() { const cartButtons = getCartButtons(); cartButtons.forEach((button) => { button.addEventListener('click', () => addToCart(button, cartButtons)); }); updateCartBadge(cartButtons); } function initCartRemove() { const removeButtons = Array.from( document.querySelectorAll('[data-remove-from-cart]') ); if (!removeButtons.length) return; removeButtons.forEach((button) => { button.addEventListener('click', async () => { const row = button.closest('[data-cart-row]'); if (!row) return; const productUuid = row.getAttribute('data-product-uuid'); if (!productUuid) return; button.disabled = true; try { const response = await fetch(`/${window.appLang || 'uk'}/api/cart/remove`, { method: 'POST', headers: jsonHeaders(), body: JSON.stringify({ product_uuid: productUuid }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Remove failed'); } row.remove(); const subtotal = document.getElementById('cartSubtotal'); const total = document.getElementById('cartTotal'); if (subtotal) subtotal.textContent = `$${data.total.toFixed(2)}`; if (total) total.textContent = `$${data.total.toFixed(2)}`; await updateCartBadge(getCartButtons()); } catch (err) { button.disabled = false; } }); }); } function initCartQuantity() { const qtyButtons = Array.from( document.querySelectorAll('[data-qty-action]') ); if (!qtyButtons.length) return; qtyButtons.forEach((button) => { button.addEventListener('click', async () => { const row = button.closest('[data-cart-row]'); if (!row) return; const productUuid = row.getAttribute('data-product-uuid'); const action = button.getAttribute('data-qty-action'); if (!productUuid || !action) return; const delta = action === 'inc' ? 1 : -1; button.disabled = true; try { const response = await fetch(`/${window.appLang || 'uk'}/api/cart/update`, { method: 'POST', headers: jsonHeaders(), body: JSON.stringify({ product_uuid: productUuid, delta }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Update failed'); } if (data.removed) { row.remove(); } else { const qtyNode = row.querySelector('[data-qty-value]'); const summNode = row.querySelector('[data-row-summ]'); if (qtyNode) qtyNode.textContent = data.qty; if (summNode) summNode.textContent = `$${data.summ.toFixed(2)}`; } const subtotal = document.getElementById('cartSubtotal'); const total = document.getElementById('cartTotal'); if (subtotal) subtotal.textContent = `$${data.total.toFixed(2)}`; if (total) total.textContent = `$${data.total.toFixed(2)}`; await updateCartBadge(getCartButtons()); } catch (err) { button.disabled = false; } finally { button.disabled = false; } }); }); } function initCheckoutForm() { const checkoutForm = document.getElementById('checkoutForm'); if (!checkoutForm) return; checkoutForm.addEventListener('submit', async (event) => { event.preventDefault(); const formData = new FormData(checkoutForm); const payload = Object.fromEntries(formData.entries()); const resultNode = document.getElementById('checkoutResult'); if (resultNode) { resultNode.textContent = 'Processing...'; } try { const response = await fetch(`/${window.appLang || 'uk'}/api/checkout`, { method: 'POST', headers: jsonHeaders(), body: JSON.stringify(payload), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Checkout failed'); } if (resultNode) { resultNode.textContent = `Order created: ${data.number}`; } window.location.href = `/${window.appLang || 'uk'}/order-success`; } catch (err) { if (resultNode) { resultNode.textContent = err.message; } } }); } async function initCheckoutSummary() { const subtotalNode = document.getElementById('checkoutSubtotal'); const deliveryNode = document.getElementById('checkoutDelivery'); const totalNode = document.getElementById('checkoutTotal'); if (!subtotalNode || !deliveryNode || !totalNode) return; let subtotal = 0; try { const response = await fetch(`/${window.appLang || 'uk'}/api/cart`); if (response.ok) { const data = await response.json(); subtotal = Number(data.total) || 0; } } catch (err) { subtotal = 0; } const updateTotals = () => { const selected = document.querySelector( 'input[name="delivery_method"]:checked' ); const delivery = selected ? Number(selected.dataset.cost || 0) : 0; subtotalNode.textContent = `$${subtotal.toFixed(2)}`; deliveryNode.textContent = `$${delivery.toFixed(2)}`; totalNode.textContent = `$${(subtotal + delivery).toFixed(2)}`; }; const deliveryInputs = Array.from( document.querySelectorAll('input[name="delivery_method"]') ); deliveryInputs.forEach((input) => { input.addEventListener('change', updateTotals); }); updateTotals(); } function initRegisterForm() { const registerForm = document.getElementById('registerForm'); if (!registerForm) return; registerForm.addEventListener('submit', async (event) => { event.preventDefault(); const formData = new FormData(registerForm); const payload = Object.fromEntries(formData.entries()); const resultNode = document.getElementById('registerResult'); if (resultNode) { resultNode.textContent = 'Processing...'; } try { const response = await fetch(`/${window.appLang || 'uk'}/api/register`, { method: 'POST', headers: jsonHeaders(), body: JSON.stringify(payload), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Registration failed'); } if (resultNode) { resultNode.textContent = 'Account created'; } window.location.href = `/${window.appLang || 'uk'}/checkout`; } catch (err) { if (resultNode) { resultNode.textContent = err.message; } } }); } function initLoginForm() { const loginForm = document.getElementById('loginForm'); if (!loginForm) return; loginForm.addEventListener('submit', async (event) => { event.preventDefault(); const formData = new FormData(loginForm); const payload = Object.fromEntries(formData.entries()); const resultNode = document.getElementById('loginResult'); if (resultNode) { resultNode.textContent = 'Processing...'; } try { const response = await fetch(`/${window.appLang || 'uk'}/api/login`, { method: 'POST', headers: jsonHeaders(), body: JSON.stringify(payload), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Login failed'); } if (resultNode) { resultNode.textContent = 'Signed in'; } window.location.href = `/${window.appLang || 'uk'}/account`; } catch (err) { if (resultNode) { resultNode.textContent = err.message; } } }); } function initSearchSuggest() { const input = document.getElementById('searchInput'); const categorySelect = document.querySelector('.search select[name="category"]'); const suggest = document.getElementById('searchSuggest'); if (!input || !suggest) return; let timer = null; const clear = () => { suggest.innerHTML = ''; suggest.classList.remove('open'); }; const render = (items) => { if (!items.length) { clear(); return; } suggest.innerHTML = items .map( (item) => `${item.name}` ) .join(''); suggest.classList.add('open'); }; input.addEventListener('input', () => { const value = input.value.trim(); if (timer) clearTimeout(timer); if (value.length < 2) { clear(); return; } timer = setTimeout(async () => { try { const category = categorySelect ? (categorySelect.value || '').trim() : ''; const categoryParam = category ? `&category=${encodeURIComponent(category)}` : ''; const response = await fetch( `/${window.appLang || 'uk'}/api/search?q=${encodeURIComponent(value)}${categoryParam}` ); if (!response.ok) return; const data = await response.json(); render(data.items || []); } catch (err) { clear(); } }, 200); }); document.addEventListener('click', (event) => { if (!suggest.contains(event.target) && event.target !== input) { clear(); } }); } function initAddressForm() { const form = document.getElementById('addressForm'); if (!form) return; const cancelBtn = document.getElementById('addressCancel'); const title = document.getElementById('addressFormTitle'); const resultNode = document.getElementById('addressResult'); const addressIdField = form.querySelector('[name="address_id"]'); const setMode = (mode) => { form.dataset.mode = mode; if (title) { title.textContent = mode === 'edit' ? 'Edit address' : 'Add new address'; } if (cancelBtn) { cancelBtn.style.display = mode === 'edit' ? 'inline-flex' : 'none'; } }; const resetForm = () => { form.reset(); if (addressIdField) addressIdField.value = ''; setMode('create'); }; setMode('create'); form.addEventListener('submit', async (event) => { event.preventDefault(); const formData = new FormData(form); const addressId = formData.get('address_id') || form.dataset.addressId; if (form.dataset.mode === 'edit' && !addressId) { if (resultNode) resultNode.textContent = 'Missing address id'; return; } const payload = { address_id: addressId || undefined, label: formData.get('label'), phone: formData.get('phone'), street: formData.get('street'), city: formData.get('city'), zip: formData.get('zip'), is_default: formData.get('is_default') === 'on', }; if (resultNode) resultNode.textContent = 'Saving...'; try { const isEdit = Boolean(addressId); const endpoint = isEdit ? `/${window.appLang || 'uk'}/api/addresses/update` : `/${window.appLang || 'uk'}/api/addresses`; const response = await fetch(endpoint, { method: 'POST', headers: jsonHeaders(), body: JSON.stringify(payload), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Save failed'); } window.location.reload(); } catch (err) { if (resultNode) resultNode.textContent = err.message; } }); if (cancelBtn) { cancelBtn.addEventListener('click', resetForm); } } function initAddressActions() { const list = document.querySelector('.address-list'); if (!list) return; list.addEventListener('click', async (event) => { const target = event.target; if (!(target instanceof HTMLElement)) return; const card = target.closest('.address-card'); if (!card) return; const addressId = card.getAttribute('data-address-id'); if (!addressId) return; const form = document.getElementById('addressForm'); if (target.hasAttribute('data-address-edit') && form) { form.dataset.mode = 'edit'; form.dataset.addressId = addressId; const addressIdField = form.querySelector('[name="address_id"]'); if (addressIdField) addressIdField.value = addressId; form.querySelector('[name="label"]').value = card.getAttribute('data-label') || ''; form.querySelector('[name="phone"]').value = card.getAttribute('data-phone') || ''; form.querySelector('[name="street"]').value = card.getAttribute('data-street') || ''; form.querySelector('[name="city"]').value = card.getAttribute('data-city') || ''; form.querySelector('[name="zip"]').value = card.getAttribute('data-zip') || ''; form.querySelector('[name="is_default"]').checked = card.getAttribute('data-default') === '1'; const title = document.getElementById('addressFormTitle'); if (title) title.textContent = 'Edit address'; const cancelBtn = document.getElementById('addressCancel'); if (cancelBtn) cancelBtn.style.display = 'inline-flex'; return; } if (target.hasAttribute('data-address-default')) { target.setAttribute('disabled', 'true'); await fetch(`/${window.appLang || 'uk'}/api/addresses/default`, { method: 'POST', headers: jsonHeaders(), body: JSON.stringify({ address_id: addressId }), }); window.location.reload(); } if (target.hasAttribute('data-address-delete')) { target.setAttribute('disabled', 'true'); await fetch(`/${window.appLang || 'uk'}/api/addresses/delete`, { method: 'POST', headers: jsonHeaders(), body: JSON.stringify({ address_id: addressId }), }); window.location.reload(); } }); } function initReviewForm() { const form = document.getElementById('reviewForm'); if (!form) return; const defaultRating = form.querySelector('input[name="rating"][value="5"]'); if (defaultRating) { defaultRating.checked = true; } form.addEventListener('submit', async (event) => { event.preventDefault(); const productUuid = form.getAttribute('data-product-uuid'); if (!productUuid) return; const formData = new FormData(form); const payload = { product_uuid: productUuid, rating: Number(formData.get('rating')) || 5, body: formData.get('body'), }; const resultNode = document.getElementById('reviewResult'); if (resultNode) resultNode.textContent = 'Saving...'; try { const response = await fetch(`/${window.appLang || 'uk'}/api/reviews`, { method: 'POST', headers: jsonHeaders(), body: JSON.stringify(payload), }); const data = await response.json(); if (!response.ok) { if (response.status === 401) { window.location.href = `/${window.appLang || 'uk'}/login`; return; } throw new Error(data.error || 'Review failed'); } if (resultNode) resultNode.textContent = 'Review added'; const list = document.querySelector('.review-list'); if (list) { const rating = payload.rating || 0; const reviewCard = document.createElement('div'); reviewCard.className = 'review-card'; reviewCard.innerHTML = `
${payload.body || ''}
`; list.prepend(reviewCard); } form.reset(); if (defaultRating) { defaultRating.checked = true; } } catch (err) { if (resultNode) resultNode.textContent = err.message; } }); } function initTabs() { const wrappers = Array.from(document.querySelectorAll('[data-tabs]')); wrappers.forEach((wrapper) => { const buttons = Array.from(wrapper.querySelectorAll('[data-tab]')); const contents = Array.from(wrapper.querySelectorAll('[data-tab-content]')); if (!buttons.length || !contents.length) return; buttons.forEach((button) => { button.addEventListener('click', () => { const target = button.getAttribute('data-tab'); buttons.forEach((btn) => btn.classList.toggle('active', btn === button)); contents.forEach((panel) => { panel.classList.toggle( 'active', panel.getAttribute('data-tab-content') === target ); }); }); }); }); } function initPriceRangeFilter() { const rangeNode = document.getElementById('priceRange'); const minValue = document.getElementById('priceMinValue'); const maxValue = document.getElementById('priceMaxValue'); if (!rangeNode || !minValue || !maxValue || !window.noUiSlider) return; const cards = Array.from(document.querySelectorAll('.product[data-price]')); if (!cards.length) return; const prices = cards .map((card) => Number(card.getAttribute('data-price'))) .filter((value) => !Number.isNaN(value)); if (!prices.length) return; const minPrice = Math.floor(Math.min(...prices)); const maxPrice = Math.ceil(Math.max(...prices)); window.noUiSlider.create(rangeNode, { start: [minPrice, maxPrice], connect: true, range: { min: minPrice, max: maxPrice, }, step: 0.1, }); const render = (values) => { const min = Number(values[0]); const max = Number(values[1]); minValue.textContent = min.toFixed(2); maxValue.textContent = max.toFixed(2); applyProductFilters(); }; rangeNode.noUiSlider.on('update', render); render(rangeNode.noUiSlider.get()); } function initFilterInputs() { const inputs = Array.from( document.querySelectorAll('[data-filter-category], [data-filter-rating]') ); if (!inputs.length) return; inputs.forEach((input) => { input.addEventListener('change', applyProductFilters); }); } function initMobileHeader() { const openBtn = document.querySelector('[data-mobile-menu-toggle]'); const closeBtn = document.querySelector('[data-mobile-menu-close]'); const backdrop = document.querySelector('[data-mobile-backdrop]'); if (!openBtn) return; const close = () => document.body.classList.remove('mobile-nav-open'); openBtn.addEventListener('click', () => document.body.classList.add('mobile-nav-open')); if (closeBtn) closeBtn.addEventListener('click', close); if (backdrop) backdrop.addEventListener('click', close); } function initMobileFilters() { const openButtons = Array.from( document.querySelectorAll('[data-mobile-filters-open]') ); const backdrop = document.querySelector('[data-mobile-filters-backdrop]'); const filterCard = document.querySelector('.filter-card'); if (!openButtons.length || !filterCard) return; let closeBtn = filterCard.querySelector('[data-mobile-filters-close]'); if (!closeBtn) { closeBtn = document.createElement('button'); closeBtn.type = 'button'; closeBtn.className = 'mobile-drawer-close'; closeBtn.setAttribute('data-mobile-filters-close', '1'); closeBtn.setAttribute('aria-label', 'Close filters'); closeBtn.textContent = '✕'; filterCard.prepend(closeBtn); } const close = () => document.body.classList.remove('mobile-filters-open'); openButtons.forEach((button) => { button.addEventListener('click', () => document.body.classList.add('mobile-filters-open')); }); closeBtn.addEventListener('click', close); if (backdrop) backdrop.addEventListener('click', close); } function initMobileCategories() { const openBtn = document.querySelector('[data-mobile-categories-open]'); const closeBtn = document.querySelector('[data-mobile-categories-close]'); const backdrop = document.querySelector('[data-mobile-categories-backdrop]'); const drawer = document.querySelector('[data-mobile-categories-drawer]'); if (!openBtn || !drawer) return; const close = () => document.body.classList.remove('mobile-categories-open'); openBtn.addEventListener('click', () => document.body.classList.add('mobile-categories-open')); if (closeBtn) closeBtn.addEventListener('click', close); if (backdrop) backdrop.addEventListener('click', close); drawer.querySelectorAll('a').forEach((link) => { link.addEventListener('click', close); }); } document.addEventListener('DOMContentLoaded', () => { initCartUI(); initWishlistUI(); initCartRemove(); initCartQuantity(); initCheckoutForm(); initCheckoutSummary(); initRegisterForm(); initLoginForm(); initSearchSuggest(); initAddressForm(); initAddressActions(); initReviewForm(); initTabs(); initPriceRangeFilter(); initFilterInputs(); initMobileHeader(); initMobileFilters(); initMobileCategories(); applyProductFilters(); });