import './bootstrap'; const SCROLL_KEY = 'pc-preserve-scroll-y'; const ALERT_TIMEOUT = 5000; const NAV_HIDE_OFFSET = 96; const NAV_SCROLL_DELTA = 8; const CHAT_OPEN_KEY = 'pc-chat-widget-open'; const header = document.querySelector('.pc-header'); const mobileMenuToggle = document.getElementById('pc-mobile-menu-toggle'); let lastScrollY = window.scrollY; let isHeaderTicking = false; const updateHeaderNavVisibility = () => { if (!header) { return; } const currentScrollY = window.scrollY; const scrollDelta = currentScrollY - lastScrollY; const isMobileMenuOpen = mobileMenuToggle instanceof HTMLInputElement && mobileMenuToggle.checked; if (currentScrollY <= NAV_HIDE_OFFSET || scrollDelta < -NAV_SCROLL_DELTA || isMobileMenuOpen) { header.classList.remove('is-nav-hidden'); } else if (scrollDelta > NAV_SCROLL_DELTA) { header.classList.add('is-nav-hidden'); } lastScrollY = currentScrollY; }; const initChatWidget = () => { const widget = document.querySelector('[data-chat-widget="true"]'); if (!(widget instanceof HTMLElement)) { return; } const toggle = widget.querySelector('[data-chat-toggle]'); const close = widget.querySelector('[data-chat-close]'); const panel = widget.querySelector('[data-chat-panel]'); const messages = widget.querySelector('[data-chat-messages]'); const form = widget.querySelector('[data-chat-form]'); const note = widget.querySelector('[data-chat-note]'); if ( !(toggle instanceof HTMLButtonElement) || !(panel instanceof HTMLElement) || !(messages instanceof HTMLElement) || !(form instanceof HTMLFormElement) ) { return; } const fetchUrl = widget.dataset.fetchUrl; const sendUrl = widget.dataset.sendUrl; const csrf = widget.dataset.csrf; const textarea = form.querySelector('textarea[name="message"]'); const submitButton = form.querySelector('button[type="submit"]'); if ( typeof fetchUrl !== 'string' || fetchUrl === '' || typeof sendUrl !== 'string' || sendUrl === '' || typeof csrf !== 'string' || csrf === '' || !(textarea instanceof HTMLTextAreaElement) || !(submitButton instanceof HTMLButtonElement) ) { return; } let pollTimer = null; let isFetching = false; let conversationClosed = false; const defaultNoteText = note instanceof HTMLElement ? note.textContent ?? '' : ''; const applyConversationState = (conversation) => { conversationClosed = Boolean(conversation && conversation.is_closed); if (note instanceof HTMLElement) { const closedNotice = typeof conversation?.notice === 'string' ? conversation.notice : ''; note.textContent = conversationClosed && closedNotice !== '' ? closedNotice : defaultNoteText; } submitButton.textContent = conversationClosed ? 'Начать новый чат' : 'Отправить'; if (conversationClosed) { stopPolling(); } }; const renderMessages = (items) => { if (!Array.isArray(items)) { return; } messages.innerHTML = ''; items.forEach((message) => { const row = document.createElement('div'); row.className = `pc-chat-message ${message.sender === 'admin' ? 'is-admin' : 'is-customer'}`; const bubble = document.createElement('div'); bubble.className = 'pc-chat-bubble'; bubble.textContent = typeof message.body === 'string' ? message.body : ''; const meta = document.createElement('span'); meta.className = 'pc-chat-meta'; meta.textContent = typeof message.time === 'string' ? message.time : ''; row.appendChild(bubble); row.appendChild(meta); messages.appendChild(row); }); messages.scrollTop = messages.scrollHeight; }; const fetchMessages = async () => { if (isFetching) { return; } isFetching = true; try { const response = await fetch(fetchUrl, { method: 'GET', headers: { Accept: 'application/json', }, }); if (!response.ok) { return; } const payload = await response.json(); renderMessages(payload.messages ?? []); applyConversationState(payload.conversation ?? null); } catch (error) { // Network errors are expected during reconnects; do nothing. } finally { isFetching = false; } }; const startPolling = () => { if (pollTimer !== null || conversationClosed) { return; } pollTimer = window.setInterval(fetchMessages, 4000); }; const stopPolling = () => { if (pollTimer === null) { return; } window.clearInterval(pollTimer); pollTimer = null; }; const openPanel = async () => { panel.hidden = false; widget.classList.add('is-open'); window.localStorage.setItem(CHAT_OPEN_KEY, '1'); await fetchMessages(); startPolling(); window.setTimeout(() => textarea.focus(), 80); }; const closePanel = () => { panel.hidden = true; widget.classList.remove('is-open'); window.localStorage.removeItem(CHAT_OPEN_KEY); stopPolling(); }; toggle.addEventListener('click', () => { if (widget.classList.contains('is-open')) { closePanel(); } else { void openPanel(); } }); if (close instanceof HTMLButtonElement) { close.addEventListener('click', closePanel); } form.addEventListener('submit', async (event) => { event.preventDefault(); const body = textarea.value.trim(); if (body === '') { return; } submitButton.disabled = true; textarea.disabled = true; try { const response = await fetch(sendUrl, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf, }, body: JSON.stringify({ message: body }), }); if (!response.ok) { return; } textarea.value = ''; await fetchMessages(); startPolling(); } catch (error) { // Skip noisy errors and keep the form usable. } finally { submitButton.disabled = false; textarea.disabled = false; textarea.focus(); } }); if (window.localStorage.getItem(CHAT_OPEN_KEY) === '1') { void openPanel(); } }; const initHomeSliders = () => { const sliders = document.querySelectorAll('[data-home-slider]'); if (sliders.length === 0) { return; } sliders.forEach((slider) => { const slides = Array.from(slider.querySelectorAll('[data-home-slide]')); const prevButton = slider.querySelector('[data-home-slider-prev]'); const nextButton = slider.querySelector('[data-home-slider-next]'); if ( slides.length === 0 || !(prevButton instanceof HTMLButtonElement) || !(nextButton instanceof HTMLButtonElement) ) { return; } let index = Math.max( 0, slides.findIndex((slide) => slide.classList.contains('is-active')), ); let timerId = null; const setSlide = (newIndex) => { index = (newIndex + slides.length) % slides.length; slides.forEach((slide, slideIndex) => { slide.classList.toggle('is-active', slideIndex === index); }); }; const runNext = () => setSlide(index + 1); const runPrev = () => setSlide(index - 1); const startAuto = () => { if (slides.length <= 1 || timerId !== null) { return; } timerId = window.setInterval(runNext, 5000); }; const stopAuto = () => { if (timerId === null) { return; } window.clearInterval(timerId); timerId = null; }; prevButton.addEventListener('click', () => { runPrev(); stopAuto(); startAuto(); }); nextButton.addEventListener('click', () => { runNext(); stopAuto(); startAuto(); }); slider.addEventListener('mouseenter', stopAuto); slider.addEventListener('mouseleave', startAuto); setSlide(index); startAuto(); }); }; const initProductCarousels = () => { const carousels = document.querySelectorAll('[data-product-carousel]'); if (carousels.length === 0) { return; } carousels.forEach((carousel) => { const track = carousel.querySelector('[data-product-carousel-track]'); const prevButton = carousel.querySelector('[data-product-carousel-prev]'); const nextButton = carousel.querySelector('[data-product-carousel-next]'); if ( !(track instanceof HTMLElement) || !(prevButton instanceof HTMLButtonElement) || !(nextButton instanceof HTMLButtonElement) ) { return; } const updateButtons = () => { const maxScroll = Math.max(0, track.scrollWidth - track.clientWidth); if (maxScroll <= 1) { prevButton.disabled = true; nextButton.disabled = true; return; } prevButton.disabled = track.scrollLeft <= 2; nextButton.disabled = track.scrollLeft >= maxScroll - 2; }; const scrollByItem = (direction) => { const firstItem = track.querySelector('.pc-product-carousel-item'); const styles = window.getComputedStyle(track); const gapValue = styles.columnGap || styles.gap || '0px'; const gap = Number.parseFloat(gapValue) || 0; const itemWidth = firstItem instanceof HTMLElement ? firstItem.getBoundingClientRect().width : Math.max(track.clientWidth * 0.9, 220); const scrollAmount = itemWidth + gap; track.scrollBy({ left: scrollAmount * direction, behavior: 'smooth', }); }; prevButton.addEventListener('click', () => { scrollByItem(-1); }); nextButton.addEventListener('click', () => { scrollByItem(1); }); track.addEventListener( 'scroll', () => { window.requestAnimationFrame(updateButtons); }, { passive: true }, ); window.addEventListener('resize', updateButtons); updateButtons(); }); }; const initProductGallery = () => { const galleries = document.querySelectorAll('[data-product-gallery]'); if (galleries.length === 0) { return; } galleries.forEach((gallery) => { const mainImage = gallery.querySelector('[data-product-gallery-main]'); if (!(mainImage instanceof HTMLImageElement)) { return; } const thumbs = Array.from(gallery.querySelectorAll('[data-product-gallery-thumb]')).filter( (thumb) => thumb instanceof HTMLButtonElement, ); if (thumbs.length === 0) { return; } let activeIndex = 0; let touchStartX = null; let touchStartY = null; const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const setActiveThumb = (activeThumb) => { thumbs.forEach((thumb) => { const isActive = thumb === activeThumb; thumb.classList.toggle('is-active', isActive); thumb.setAttribute('aria-pressed', isActive ? 'true' : 'false'); }); }; const animateMainImage = (direction) => { if (prefersReducedMotion || typeof mainImage.animate !== 'function') { return; } const offset = direction === 0 ? 0 : 28 * direction; mainImage.getAnimations().forEach((animation) => animation.cancel()); mainImage.animate([ { opacity: 0.38, transform: `translate3d(${offset}px, 0, 0) scale(1.02)`, filter: 'blur(10px)', }, { opacity: 1, transform: 'translate3d(0, 0, 0) scale(1)', filter: 'blur(0)', }, ], { duration: 320, easing: 'cubic-bezier(0.22, 1, 0.36, 1)', }); }; const applyImage = (index) => { const normalizedIndex = ((index % thumbs.length) + thumbs.length) % thumbs.length; const thumb = thumbs[normalizedIndex]; if (!(thumb instanceof HTMLButtonElement)) { return; } const src = (thumb.dataset.imageSrc || '').trim(); if (src === '') { return; } const direction = normalizedIndex === activeIndex ? 0 : normalizedIndex > activeIndex ? 1 : -1; activeIndex = normalizedIndex; if (mainImage.getAttribute('src') !== src) { mainImage.setAttribute('src', src); } const alt = (thumb.dataset.imageAlt || '').trim(); if (alt !== '') { mainImage.setAttribute('alt', alt); } animateMainImage(direction); setActiveThumb(thumb); thumb.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest', }); }; thumbs.forEach((thumb, index) => { thumb.addEventListener('click', () => { applyImage(index); }); }); mainImage.addEventListener('touchstart', (event) => { const touch = event.changedTouches[0]; if (!touch) { return; } touchStartX = touch.clientX; touchStartY = touch.clientY; }, { passive: true }); mainImage.addEventListener('touchend', (event) => { const touch = event.changedTouches[0]; if (!touch || touchStartX === null || touchStartY === null) { return; } const deltaX = touch.clientX - touchStartX; const deltaY = touch.clientY - touchStartY; touchStartX = null; touchStartY = null; if (Math.abs(deltaX) < 36 || Math.abs(deltaX) <= Math.abs(deltaY)) { return; } if (deltaX < 0) { applyImage(activeIndex + 1); } else { applyImage(activeIndex - 1); } }, { passive: true }); const initialThumb = thumbs.find((thumb) => thumb.classList.contains('is-active')) ?? thumbs[0]; if (initialThumb) { applyImage(thumbs.indexOf(initialThumb)); } }); }; const initCategoryFilterToggle = () => { const toggles = document.querySelectorAll('[data-filter-toggle]'); if (toggles.length === 0) { return; } toggles.forEach((toggle) => { if (!(toggle instanceof HTMLButtonElement)) { return; } const targetId = toggle.getAttribute('aria-controls'); if (!targetId) { return; } const container = document.getElementById(targetId); if (!(container instanceof HTMLElement)) { return; } const syncState = () => { const isOpen = container instanceof HTMLDetailsElement ? container.open : container.classList.contains('is-open'); toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); }; toggle.addEventListener('click', () => { if (container instanceof HTMLDetailsElement) { container.open = !container.open; } else { container.classList.toggle('is-open'); } syncState(); }); if (container instanceof HTMLDetailsElement) { container.addEventListener('toggle', syncState); } syncState(); }); }; document.addEventListener('submit', (event) => { const form = event.target; if (!(form instanceof HTMLFormElement)) { return; } if (form.dataset.preserveScroll === 'true') { sessionStorage.setItem(SCROLL_KEY, String(window.scrollY)); } }); window.addEventListener( 'scroll', () => { if (!header || isHeaderTicking) { return; } isHeaderTicking = true; window.requestAnimationFrame(() => { updateHeaderNavVisibility(); isHeaderTicking = false; }); }, { passive: true }, ); if (mobileMenuToggle instanceof HTMLInputElement) { mobileMenuToggle.addEventListener('change', () => { if (mobileMenuToggle.checked) { header?.classList.remove('is-nav-hidden'); } }); } window.addEventListener('pageshow', () => { const savedScrollY = sessionStorage.getItem(SCROLL_KEY); if (savedScrollY !== null) { sessionStorage.removeItem(SCROLL_KEY); window.requestAnimationFrame(() => { window.scrollTo(0, Number(savedScrollY) || 0); updateHeaderNavVisibility(); }); } else { updateHeaderNavVisibility(); } document.querySelectorAll('.pc-alert').forEach((alert) => { window.setTimeout(() => { alert.classList.add('is-hiding'); window.setTimeout(() => alert.remove(), 260); }, ALERT_TIMEOUT); }); }); initChatWidget(); initHomeSliders(); initProductCarousels(); initProductGallery(); initCategoryFilterToggle();