/** * Revelor Search Bundle * Combined and bundled JavaScript for Shoptet * * Contains: * - Console Wrapper (admin-only logging) * - DataLayer Helper (GTM analytics) * - Search Tracker (backend tracking) * - Search Overlay (main search functionality) * - Search Results Page (results page logic) * * Build date: 2025-11-01T00:12:21.753Z * Project: Revelor Textilomanie */ // ============================================================================= // console-wrapper.js // ============================================================================= /** * @fileoverview Console Wrapper for Admin-Only Logging * Přepisuje console metody - výstupy se zobrazí pouze pokud je admin přihlášený * @version 1.0.0 * @author Revelor by Webotvůrci */ (function() { 'use strict'; /** * Zkontroluje, jestli je admin přihlášený * Kontroluje dynamicky při každém volání (BODY může být ještě ne načtený) * @returns {boolean} */ function isAdminLoggedIn() { // Pokud BODY ještě není načtené, vrať false (bezpečnější) if (!document.body) { return false; } // Zkontroluj, jestli má BODY class 'admin-logged' return document.body.classList.contains('admin-logged'); } /** * Uložit původní console metody */ const originalConsole = { log: console.log.bind(console), error: console.error.bind(console), warn: console.warn.bind(console), info: console.info.bind(console), debug: console.debug.bind(console), trace: console.trace.bind(console), table: console.table.bind(console), group: console.group.bind(console), groupEnd: console.groupEnd.bind(console), groupCollapsed: console.groupCollapsed.bind(console), time: console.time.bind(console), timeEnd: console.timeEnd.bind(console), assert: console.assert.bind(console) }; /** * No-op funkce (nic nedělá) */ function noop() {} /** * Wrapper funkce, která zkontroluje admin status */ function createConsoleWrapper(originalMethod) { return function(...args) { if (isAdminLoggedIn()) { originalMethod.apply(console, args); } // Pokud není admin, nic se nestane (no-op) }; } // Přepiš console metody pouze pokud ještě nejsou přepsané if (!window._revelorConsoleWrapped) { console.log = createConsoleWrapper(originalConsole.log); console.error = createConsoleWrapper(originalConsole.error); console.warn = createConsoleWrapper(originalConsole.warn); console.info = createConsoleWrapper(originalConsole.info); console.debug = createConsoleWrapper(originalConsole.debug); console.trace = createConsoleWrapper(originalConsole.trace); console.table = createConsoleWrapper(originalConsole.table); console.group = createConsoleWrapper(originalConsole.group); console.groupEnd = createConsoleWrapper(originalConsole.groupEnd); console.groupCollapsed = createConsoleWrapper(originalConsole.groupCollapsed); console.time = createConsoleWrapper(originalConsole.time); console.timeEnd = createConsoleWrapper(originalConsole.timeEnd); console.assert = createConsoleWrapper(originalConsole.assert); // Označit, že jsme už wrapper aplikovali window._revelorConsoleWrapped = true; } })(); // ============================================================================= // datalayer-helper.js // ============================================================================= /** * @fileoverview DataLayer Helper for Revelor Search Analytics * Manages all dataLayer pushes for Google Tag Manager * @version 2.0.0 * @author Revelor by Webotvůrci * @contact jsme@webotvurci.cz | +420 728 089 029 */ class RevelorDataLayer { constructor() { // Initialize dataLayer if not exists window.dataLayer = window.dataLayer || []; // Session storage for tracking search journey this.sessionKey = 'revelor_search_session'; this.initSession(); } /** * Initialize search session */ initSession() { if (!sessionStorage.getItem(this.sessionKey)) { sessionStorage.setItem(this.sessionKey, JSON.stringify({ searchCount: 0, lastSearchTerm: null, lastSearchTime: null, searchStartTime: null })); } } /** * Get search session data */ getSession() { return JSON.parse(sessionStorage.getItem(this.sessionKey) || '{}'); } /** * Update search session */ updateSession(updates) { const session = this.getSession(); const newSession = { ...session, ...updates }; sessionStorage.setItem(this.sessionKey, JSON.stringify(newSession)); } /** * Get current timestamp in ISO format */ getTimestamp() { return new Date().toISOString(); } /** * Get user type (new vs returning) */ getUserType() { // Check if user has visited before (using cookie or localStorage) return localStorage.getItem('user_visited') ? 'returning' : 'new'; } /** * Get page type from URL or body class */ getPageType() { const path = window.location.pathname; if (path === '/' || path === '') return 'homepage'; if (path.includes('/produkt/') || path.includes('/product/')) return 'product'; if (path.includes('/kategorie/') || path.includes('/category/')) return 'category'; if (path.includes('/vysledky-hledani') || path.includes('/search')) return 'search_results'; return 'other'; } /** * Calculate time difference in seconds */ getTimeDiff(startTime) { if (!startTime) return 0; return ((Date.now() - new Date(startTime).getTime()) / 1000).toFixed(1); } /** * Push event to dataLayer */ push(eventData) { console.log('📊 Revelor DataLayer Push:', eventData); window.dataLayer.push(eventData); } /** * 1. Search Started */ searchStarted(context = 'overlay') { const session = this.getSession(); this.push({ event: 'revelor_search_started', search_context: context, timestamp: this.getTimestamp(), user_type: this.getUserType(), page_type: this.getPageType(), session_search_count: session.searchCount || 0 }); // Update session this.updateSession({ searchStartTime: this.getTimestamp(), searchCount: (session.searchCount || 0) + 1 }); } /** * 2. Search Query Submitted */ searchQuerySubmitted(searchTerm, context = 'overlay') { const session = this.getSession(); const isRefinement = session.lastSearchTerm && session.lastSearchTerm !== searchTerm; this.push({ event: 'revelor_search_query_submitted', search_term: searchTerm, search_term_length: searchTerm.length, search_context: context, is_refinement: isRefinement, previous_search_term: isRefinement ? session.lastSearchTerm : null, timestamp: this.getTimestamp() }); // Update session this.updateSession({ lastSearchTerm: searchTerm, lastSearchTime: this.getTimestamp() }); } /** * 3. Search Results Displayed */ searchResultsDisplayed(searchTerm, resultsData, responseTime, page = 1) { const totalResults = resultsData.total_products || 0; const productsCount = resultsData.products?.length || 0; const categoriesCount = resultsData.categories?.length || 0; const articlesCount = resultsData.articles?.length || 0; const brandsCount = resultsData.brands?.length || 0; this.push({ event: 'revelor_search_results_displayed', search_term: searchTerm, results_count: { total: totalResults, products: productsCount, categories: categoriesCount, articles: articlesCount, brands: brandsCount }, has_results: totalResults > 0, page: page, response_time_ms: responseTime, timestamp: this.getTimestamp() }); } /** * 4. Search No Results */ searchNoResults(searchTerm) { // Simple typo detection (contains numbers mixed with letters, etc.) const potentialTypo = /\d/.test(searchTerm) || searchTerm.length < 3; this.push({ event: 'revelor_search_no_results', search_term: searchTerm, search_term_length: searchTerm.length, potential_typo: potentialTypo, timestamp: this.getTimestamp() }); } /** * 5. Search Result Click */ searchResultClick(searchTerm, resultType, resultData, position, totalResults) { const session = this.getSession(); const clickTimeFromSearch = this.getTimeDiff(session.lastSearchTime); const eventData = { event: 'revelor_search_result_click', search_term: searchTerm, result_type: resultType, result_position: position, total_results: totalResults, click_time_from_search: parseFloat(clickTimeFromSearch), timestamp: this.getTimestamp() }; // Add result-specific data if (resultType === 'product') { eventData.result_data = { item_id: resultData._id || resultData.id, item_name: resultData.title, item_category: resultData.category || 'Neznámá kategorie', price: resultData.price_s_dph || 0, currency: 'CZK', availability: resultData.skladem ? 'in_stock' : 'out_of_stock', item_url: resultData.url }; // GA4 Enhanced Ecommerce format eventData.ecommerce = { currency: 'CZK', value: resultData.price_s_dph || 0, items: [{ item_id: resultData._id || resultData.id, item_name: resultData.title, item_list_name: 'Search Results', item_list_id: 'search_results', price: resultData.price_s_dph || 0, index: position, quantity: 1 }] }; } else if (resultType === 'category') { eventData.result_data = { category_name: resultData.title, category_url: resultData.url }; } else if (resultType === 'article') { eventData.result_data = { article_title: resultData.title, article_url: resultData.url }; } this.push(eventData); } /** * 6. Search Load More */ searchLoadMore(searchTerm, currentPage, totalPages, productsLoaded, totalProductsShown) { this.push({ event: 'revelor_search_load_more', search_term: searchTerm, current_page: currentPage, next_page: currentPage + 1, total_pages: totalPages, products_loaded: productsLoaded, total_products_shown: totalProductsShown, timestamp: this.getTimestamp() }); } /** * 7. Search Refinement */ searchRefinement(originalTerm, refinedTerm, originalResults, refinedResults) { // Determine refinement type let refinementType = 'modification'; if (refinedTerm.length > originalTerm.length) { refinementType = 'expansion'; } else if (refinedTerm.length < originalTerm.length) { refinementType = 'narrowing'; } else if (Math.abs(refinedTerm.length - originalTerm.length) <= 2) { refinementType = 'correction'; } this.push({ event: 'revelor_search_refinement', original_search_term: originalTerm, refined_search_term: refinedTerm, refinement_type: refinementType, original_results_count: originalResults, refined_results_count: refinedResults, timestamp: this.getTimestamp() }); } /** * 8. Search Suggestion Click */ searchSuggestionClick(typedQuery, suggestedTerm, position, suggestionType) { this.push({ event: 'revelor_search_suggestion_click', typed_query: typedQuery, suggested_term: suggestedTerm, suggestion_position: position, suggestion_type: suggestionType, timestamp: this.getTimestamp() }); } /** * 9. Search Recommendations Shown */ searchRecommendationsShown(recommendationsData) { const products = recommendationsData.products || []; const categories = recommendationsData.categories || []; this.push({ event: 'revelor_search_recommendations_shown', recommendations_count: products.length + categories.length, products_count: products.length, categories_count: categories.length, recommendation_type: 'default', // trending/popular/personalized timestamp: this.getTimestamp() }); } /** * 10. Search Category Expand */ searchCategoryExpand(searchTerm, expandType, action, visibleBefore, visibleAfter) { this.push({ event: 'revelor_search_category_expand', search_term: searchTerm, expand_type: expandType, // 'categories' | 'articles' action: action, // 'expand' | 'collapse' visible_items_before: visibleBefore, visible_items_after: visibleAfter, timestamp: this.getTimestamp() }); } /** * 11. Search Abandoned */ searchAbandoned(searchTerm, resultsCount, timeSpent, reason = 'closed_overlay') { this.push({ event: 'revelor_search_abandoned', search_term: searchTerm, results_count: resultsCount, time_spent: parseFloat(timeSpent), reason: reason, // 'closed_overlay' | 'navigated_away' | 'no_interaction' timestamp: this.getTimestamp() }); } } // Initialize global instance window.revelorDataLayer = new RevelorDataLayer(); // ============================================================================= // search-tracker.js // ============================================================================= /** * @fileoverview Search Tracker for Backend Analytics * Tracks search events and clicks to backend API according to metrics-tracking.md * @version 1.0.0 * @author Revelor by Webotvůrci * @contact jsme@webotvurci.cz | +420 728 089 029 */ class SearchTracker { constructor(config = {}) { this.apiEndpoint = config.apiEndpoint || 'https://textilomanie.revelor.cz/api/search/track'; this.projectId = config.projectId || '312030'; this.sessionTimeout = config.sessionTimeout || 1800000; // 30 minut v ms this.sessionKey = 'search_session_id'; this.lastSearchTimeKey = 'search_last_time'; // Generuj session ID pokud neexistuje nebo vypršela this.getOrCreateSession(); } /** * Get or create session ID */ getOrCreateSession() { const sessionId = sessionStorage.getItem(this.sessionKey); const lastSearchTime = sessionStorage.getItem(this.lastSearchTimeKey); const now = Date.now(); // Pokud session neexistuje nebo vypršela (30 min), vytvoř novou if (!sessionId || !lastSearchTime || (now - parseInt(lastSearchTime)) > this.sessionTimeout) { const newSessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); sessionStorage.setItem(this.sessionKey, newSessionId); sessionStorage.setItem(this.lastSearchTimeKey, now.toString()); return newSessionId; } return sessionId; } /** * Get current session ID */ getSessionId() { return this.getOrCreateSession(); } /** * Update last search time */ updateLastSearchTime() { sessionStorage.setItem(this.lastSearchTimeKey, Date.now().toString()); } /** * Track search event * @param {Object} data - Search tracking data * @param {string} data.query - Search query (min 3 chars) * @param {number} [data.results_count] - Number of results * @param {string} [data.search_type] - 'manual' | 'suggestion' | 'live_search' */ async trackSearch(data) { const { query, results_count, search_type = 'live_search' } = data; // Backend požaduje minimálně 3 znaky if (!query || query.length < 3) { console.log('🔇 Search tracking skipped - query too short:', query); return; } const sessionId = this.getSessionId(); this.updateLastSearchTime(); const eventData = { event_type: 'search', query: query, session_id: sessionId, url: window.location.href, referrer: document.referrer || '', timestamp: new Date().toISOString(), user_agent: navigator.userAgent, search_type: search_type }; if (results_count !== undefined) { eventData.results_count = results_count; } await this.sendEvent(eventData); } /** * Track click event * @param {Object} data - Click tracking data * @param {string} data.clicked_result - Product/article/category ID (required) * @param {string} data.result_type - 'product' | 'article' | 'category' * @param {string} data.result_title - Title of the result * @param {string} data.result_url - URL of the result * @param {number} data.result_position - Position in results (1-based) * @param {string} data.query - Search query */ async trackClick(data) { const { clicked_result, product_id, result_type, result_title, result_url, result_position, query } = data; // Backend vyžaduje clicked_result nebo product_id const resultId = clicked_result || product_id; if (!resultId) { console.error('❌ Click tracking error - missing clicked_result or product_id'); return; } const sessionId = this.getSessionId(); const eventData = { event_type: 'click', clicked_result: resultId, result_type: result_type || 'product', query: query || '', session_id: sessionId, url: window.location.href, referrer: document.referrer || '', timestamp: new Date().toISOString(), user_agent: navigator.userAgent }; if (result_title) { eventData.result_title = result_title; } if (result_url) { eventData.result_url = result_url; } if (result_position !== undefined) { eventData.result_position = result_position; } // Alternativní název pro product_id (pro kompatibilitu) if (product_id) { eventData.product_id = product_id; } await this.sendEvent(eventData); } /** * Send event to backend API * @param {Object} eventData - Event data to send */ async sendEvent(eventData) { try { const response = await fetch(this.apiEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Tenant': this.projectId }, body: JSON.stringify(eventData) }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); console.error('❌ Search tracking error:', response.status, errorData); return; } const responseData = await response.json().catch(() => ({})); if (responseData.status === 'skipped') { console.log('⏭️ Search tracking skipped:', responseData.message); } else { console.log('✅ Search tracking success:', responseData); } } catch (error) { // Fail silently - tracking should not break the app console.warn('⚠️ Search tracking failed:', error); } } } // Create global instance window.searchTracker = new SearchTracker({ projectId: (typeof dataLayer !== 'undefined' && dataLayer[0] && dataLayer[0].shoptet && dataLayer[0].shoptet.projectId) || '312030' }); // ============================================================================= // searching.js // ============================================================================= /** * @fileoverview Search Overlay Component * Implements search functionality using Shoptet API * @version 2.0.0 * @author Revelor by Webotvůrci * @contact jsme@webotvurci.cz | +420 728 089 029 */ class SearchOverlay { constructor() { this.isOpen = false; this.apiBaseUrl = 'https://textilomanie.revelor.cz/api/public'; this.projectId = this.getProjectId(); this.minSearchLength = 1; this.previousActiveElement = null; // Store previous focus this.focusableElements = []; // Focusable elements in overlay this.currentFocusIndex = -1; // Current focus index for keyboard navigation // Performance optimizations this.abortController = null; // Request cancellation this.cache = new Map(); // Simple cache this.cacheTTL = 300000; // 5 minutes // Search history this.historyKey = 'revelor_search_history'; this.maxHistoryItems = 10; this.init(); } /** * Initialize search overlay */ init() { this.createOverlay(); this.bindEvents(); this.loadPlaceholders(); } /** * Get Project ID from dataLayer or use default */ getProjectId() { if ( typeof dataLayer !== 'undefined' && dataLayer[0] && dataLayer[0].shoptet ) { return dataLayer[0].shoptet.projectId; } return '312030'; // Default fallback } /** * Create and inject search overlay HTML */ createOverlay() { const overlayHTML = ` `; // Insert before #content-wrapper or at the beginning of body const contentWrapper = document.getElementById('content-wrapper'); if (contentWrapper) { contentWrapper.insertAdjacentHTML('beforebegin', overlayHTML); } else { document.body.insertAdjacentHTML('afterbegin', overlayHTML); } } /** * Bind event listeners */ bindEvents() { const formSearchForm = document.getElementById('formSearchForm'); const overlay = document.getElementById('search-overlay'); const input = document.getElementById('search-input'); const close = document.getElementById('search-close'); const submit = document.getElementById('search-submit'); // Open search from header form if (formSearchForm) { console.log( '✅ SEARCH EVENTS - formSearchForm found, adding click listener' ); formSearchForm.addEventListener('click', async e => { console.log('🖱️ SEARCH EVENTS - formSearchForm clicked!'); e.preventDefault(); await this.open(); }); } else { console.log('❌ SEARCH EVENTS - formSearchForm NOT FOUND!'); } // Close search close?.addEventListener('click', () => this.close()); overlay?.addEventListener('click', e => { if (e.target === overlay) this.close(); }); // Submit search submit?.addEventListener('click', () => { const query = this.getCurrentQuery().trim(); if (query.length >= this.minSearchLength) { this.redirectToSearchResults(query); } }); // Search input input?.addEventListener('input', e => this.handleSearch(e.target.value)); input?.addEventListener('keydown', e => { if (e.key === 'Escape') { this.close(); } else if (e.key === 'Enter') { e.preventDefault(); const query = this.getCurrentQuery().trim(); if (query.length >= this.minSearchLength) { this.redirectToSearchResults(query); } } else if (e.key === 'ArrowDown') { e.preventDefault(); this.focusNextResult(); } else if (e.key === 'ArrowUp') { e.preventDefault(); this.focusPreviousResult(); } }); // Keyboard navigation - global document.addEventListener('keydown', e => { if (e.key === '/' && !this.isOpen && document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') { e.preventDefault(); this.open(); } // Handle Tab key for focus trap when overlay is open if (this.isOpen && e.key === 'Tab') { this.handleTabKey(e); } }); // Setup swipe to close (mobile) this.setupSwipeToClose(); } /** * Setup swipe to close for mobile */ setupSwipeToClose() { const overlay = document.getElementById('search-overlay'); if (!overlay) return; let startY = 0; let currentY = 0; let isDragging = false; overlay.addEventListener('touchstart', (e) => { // Only if touch starts at the top of content if (e.target.closest('.search-overlay__header')) { startY = e.touches[0].clientY; isDragging = true; } }, { passive: true }); overlay.addEventListener('touchmove', (e) => { if (!isDragging) return; currentY = e.touches[0].clientY; const diff = currentY - startY; // Only allow swipe down if (diff > 0) { const content = overlay.querySelector('.search-overlay__content'); if (content) { content.style.transform = `translateX(-50%) translateY(${diff}px)`; content.style.opacity = Math.max(0.5, 1 - diff / 400); } } }, { passive: true }); overlay.addEventListener('touchend', () => { if (!isDragging) return; const diff = currentY - startY; const content = overlay.querySelector('.search-overlay__content'); if (diff > 100) { // Swipe down threshold - close overlay this.close(); } else { // Return to position if (content) { content.style.transform = 'translateX(-50%)'; content.style.opacity = '1'; } } isDragging = false; startY = 0; currentY = 0; }); } /** * Handle Tab key for focus trap */ handleTabKey(e) { const overlay = document.getElementById('search-overlay'); if (!overlay) return; const focusableElements = overlay.querySelectorAll( 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' ); const firstFocusable = focusableElements[0]; const lastFocusable = focusableElements[focusableElements.length - 1]; if (e.shiftKey) { // Shift + Tab if (document.activeElement === firstFocusable) { e.preventDefault(); lastFocusable.focus(); } } else { // Tab if (document.activeElement === lastFocusable) { e.preventDefault(); firstFocusable.focus(); } } } /** * Focus next result in list */ focusNextResult() { const results = this.getFocusableResults(); if (results.length === 0) return; this.currentFocusIndex = (this.currentFocusIndex + 1) % results.length; results[this.currentFocusIndex].focus(); } /** * Focus previous result in list */ focusPreviousResult() { const results = this.getFocusableResults(); if (results.length === 0) return; this.currentFocusIndex = this.currentFocusIndex <= 0 ? results.length - 1 : this.currentFocusIndex - 1; results[this.currentFocusIndex].focus(); } /** * Get all focusable results */ getFocusableResults() { const resultsContainer = document.getElementById('search-content-results'); if (!resultsContainer || resultsContainer.style.display === 'none') { return []; } return Array.from(resultsContainer.querySelectorAll('a[href], button:not([disabled])')); } /** * Load dynamic placeholders */ async loadPlaceholders() { try { const response = await fetch( `${this.apiBaseUrl}/placeholders?projectId=${this.projectId}`, { headers: { 'X-Tenant': this.projectId, }, } ); const data = await response.json(); if (data.placeholders && data.placeholders.length > 0) { const input = document.getElementById('search-input'); if (input) { input.placeholder = data.placeholders[0]; } } } catch (error) { console.warn('Failed to load placeholders:', error); } } /** * Load recommendations for empty state */ async loadRecommendations() { console.log('🚀 Starting to load recommendations...'); console.log('API Base URL:', this.apiBaseUrl); console.log('Project ID:', this.projectId); try { const url = `${this.apiBaseUrl}/recommendations?projectId=${this.projectId}`; console.log('🔗 Fetching from URL:', url); const response = await fetch(url, { headers: { 'X-Tenant': this.projectId, }, }); console.log('📡 Response status:', response.status); console.log('📡 Response ok:', response.ok); const data = await response.json(); console.log('🔍 SEARCH RECOMMENDATIONS API - Data loaded:', data); return data; } catch (error) { console.error('❌ Failed to load recommendations:', error); return null; } } /** * Open search overlay */ async open() { console.log('🚀 SEARCH OVERLAY - Opening search overlay...'); // Store current focus to return later this.previousActiveElement = document.activeElement; // DataLayer: Search Started if (window.revelorDataLayer) { window.revelorDataLayer.searchStarted('overlay'); } // Store start time for abandoned tracking this.searchStartTime = Date.now(); this.lastSearchTerm = null; this.lastResultsCount = 0; this.isOpen = true; const overlay = document.getElementById('search-overlay'); const input = document.getElementById('search-input'); if (overlay) { overlay.classList.add('search-overlay--active'); document.body.classList.add('search-overlay-open'); console.log('🚀 SEARCH OVERLAY - Overlay activated'); } else { console.log('❌ SEARCH OVERLAY - Overlay element not found!'); } // Update aria-expanded on input if (input) { input.setAttribute('aria-expanded', 'true'); } // Focus input after animation setTimeout(async () => { input?.focus(); console.log('🚀 SEARCH OVERLAY - Input focused'); // Show empty state with recommendations await this.showEmptyState(); }, 100); } /** * Close search overlay */ close() { // DataLayer: Search Abandoned (if there was a search term but no click) if (window.revelorDataLayer && this.lastSearchTerm && this.searchStartTime) { const timeSpent = ((Date.now() - this.searchStartTime) / 1000).toFixed(1); window.revelorDataLayer.searchAbandoned( this.lastSearchTerm, this.lastResultsCount, timeSpent, 'closed_overlay' ); } this.isOpen = false; const overlay = document.getElementById('search-overlay'); const input = document.getElementById('search-input'); if (overlay) { overlay.classList.remove('search-overlay--active'); document.body.classList.remove('search-overlay-open'); } // Update aria-expanded on input if (input) { input.setAttribute('aria-expanded', 'false'); } // Return focus to previous element setTimeout(() => { if (this.previousActiveElement && this.previousActiveElement.focus) { this.previousActiveElement.focus(); } }, 100); this.clearSearch(); } /** * Clear search input and results */ async clearSearch() { const input = document.getElementById('search-input'); if (input) { input.value = ''; input.focus(); } await this.showEmptyState(); } /** * Get current search query */ getCurrentQuery() { const input = document.getElementById('search-input'); return input ? input.value : ''; } /** * Format price with dot as thousands separator and remove unnecessary decimals */ formatPrice(price) { if (!price) return ''; // Convert to number const numPrice = parseFloat(price); // Format as integer (bez desetinných míst) s tečkou jako oddělovač tisíců return Math.floor(numPrice).toLocaleString('cs-CZ', { minimumFractionDigits: 0, maximumFractionDigits: 0, }); } /** * Redirect to search results page */ redirectToSearchResults(query) { const url = `/vyhledavani/?string=${encodeURIComponent(query)}`; console.log('Redirecting to search results:', url); // Create temporary link and click it const tempLink = document.createElement('a'); tempLink.href = url; tempLink.style.display = 'none'; document.body.appendChild(tempLink); tempLink.click(); document.body.removeChild(tempLink); } /** * Add product to cart (Shoptet API) - for Search Overlay * @param {Object} product - Product data * @param {HTMLElement} button - Button element */ async addToCart(product, button) { const originalHTML = button.innerHTML; // Show loading state button.disabled = true; button.classList.add('loading'); button.style.opacity = '0.5'; try { // Check if Shoptet API is available if (typeof shoptet === 'undefined' || !shoptet.cartShared) { throw new Error('Shoptet API není k dispozici'); } // Determine which method to use (podle Shoptet dokumentace) let addParams = {}; if (product.priceId) { // Nejlepší varianta: priceId addParams = { priceId: product.priceId }; } else if (product.kod || product.code) { // Fallback: product code addParams = { productCode: product.kod || product.code }; } else if (product._id || product.id) { // Poslední varianta: product ID addParams = { productId: product._id || product.id }; } else { throw new Error('Nelze identifikovat produkt'); } // Add to cart (silent mode = true to prevent Shoptet's modal) await shoptet.cartShared.addToCart(addParams, true); // Success feedback button.classList.remove('loading'); button.classList.add('success'); button.style.opacity = '1'; // TODO: Add toast notification if available // Tracking if (window.revelorDataLayer) { window.dataLayer.push({ event: 'revelor_quick_add_to_cart', product_id: product._id || product.id, product_name: product.title, price: product.price_s_dph, source: 'search_overlay' }); } // Reset button after 2s setTimeout(() => { button.disabled = false; button.classList.remove('success'); button.style.opacity = ''; }, 2000); } catch (error) { console.error('Add to Cart Error:', error); button.innerHTML = originalHTML; button.disabled = false; button.classList.remove('loading'); button.style.opacity = ''; // TODO: Add error notification } } /** * Handle search input */ async handleSearch(query) { if (query.length < this.minSearchLength) { await this.showEmptyState(); return; } // Only show loading if no results are currently displayed const resultsContainer = document.getElementById('search-content-results'); const isShowingResults = resultsContainer && resultsContainer.style.display !== 'none'; if (!isShowingResults) { this.showLoadingState(); } // Perform search immediately this.performSearch(query); } /** * Perform search using API */ async performSearch(query) { const searchUrl = `${this.apiBaseUrl}/suggestions?q=${encodeURIComponent(query)}&projectId=${this.projectId}&products_limit=6&categories_limit=6&articles_limit=6&brands_limit=0`; // DataLayer: Search Query Submitted if (window.revelorDataLayer) { window.revelorDataLayer.searchQuerySubmitted(query, 'overlay'); } // Store for refinement tracking this.lastSearchTerm = query; // Check cache first const cacheKey = `${query}_${this.projectId}_suggestions`; const cached = this.getFromCache(cacheKey); if (cached) { console.log('📦 Using cached results for:', query); this.displayResults(cached, 0); return; } // Cancel previous request if (this.abortController) { this.abortController.abort(); } this.abortController = new AbortController(); const startTime = Date.now(); try { const response = await this.fetchWithRetry(searchUrl, { signal: this.abortController.signal, headers: { 'X-Tenant': this.projectId, }, }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); const responseTime = Date.now() - startTime; // Save to cache this.saveToCache(cacheKey, data); // Add to search history if has results const hasResults = data.products?.length > 0 || data.categories?.length > 0 || data.articles?.length > 0; if (hasResults) { this.addToSearchHistory(query); } this.displayResults(data, responseTime); } catch (error) { if (error.name === 'AbortError') { console.log('🚫 Request cancelled for:', query); return; } console.error('Search API Error:', error); // Try to use cached results as fallback const fallbackCached = this.getFromCache(cacheKey, true); if (fallbackCached) { console.log('📦 Using stale cache as fallback'); this.displayResults(fallbackCached, 0); this.announceToScreenReader('Zobrazuji uložené výsledky (offline)'); return; } this.showErrorState(); } } /** * Save to cache */ saveToCache(key, data) { this.cache.set(key, { data, timestamp: Date.now() }); } /** * Get from cache */ getFromCache(key, ignoreExpiry = false) { const cached = this.cache.get(key); if (!cached) return null; // Check TTL (unless ignoreExpiry for fallback) if (!ignoreExpiry && Date.now() - cached.timestamp > this.cacheTTL) { this.cache.delete(key); return null; } return cached.data; } /** * Announce message to screen readers */ announceToScreenReader(message) { const liveRegion = document.getElementById('search-live-region'); if (liveRegion) { liveRegion.textContent = message; // Clear after announcement setTimeout(() => { liveRegion.textContent = ''; }, 1000); } } /** * Display search results */ displayResults(data, responseTime = 0) { this.hideAllStates(); const hasResults = data.products?.length > 0 || data.categories?.length > 0 || data.articles?.length > 0; if (!hasResults) { this.showNoResultsState(); this.announceToScreenReader('Žádné výsledky nebyly nalezeny'); // DataLayer: Search No Results if (window.revelorDataLayer && this.lastSearchTerm) { window.revelorDataLayer.searchNoResults(this.lastSearchTerm); } this.lastResultsCount = 0; return; } this.showResultsState(); // Count results const totalResults = (data.products?.length || 0) + (data.categories?.length || 0) + (data.articles?.length || 0); // Store for abandoned tracking this.lastResultsCount = totalResults; // Announce results const resultTypes = []; if (data.products?.length) resultTypes.push(`${data.products.length} ${data.products.length === 1 ? 'produkt' : 'produkty'}`); if (data.categories?.length) resultTypes.push(`${data.categories.length} ${data.categories.length === 1 ? 'kategorie' : 'kategorie'}`); if (data.articles?.length) resultTypes.push(`${data.articles.length} ${data.articles.length === 1 ? 'článek' : 'články'}`); this.announceToScreenReader(`Nalezeno ${totalResults} výsledků: ${resultTypes.join(', ')}`); // DataLayer: Search Results Displayed if (window.revelorDataLayer && this.lastSearchTerm) { const resultsData = { products: data.products || [], categories: data.categories || [], articles: data.articles || [], total_products: totalResults }; window.revelorDataLayer.searchResultsDisplayed( this.lastSearchTerm, resultsData, responseTime, 1 ); } // Backend API: Track search event if (window.searchTracker && this.lastSearchTerm && this.lastSearchTerm.length >= 3) { window.searchTracker.trackSearch({ query: this.lastSearchTerm, results_count: totalResults, search_type: 'live_search' }); } // Display products if (data.products?.length > 0) { this.displayProducts(data.products); } // Display categories if (data.categories?.length > 0) { this.displayCategories(data.categories); } // Display articles if (data.articles?.length > 0) { this.displayArticles(data.articles); } // Reset focus index this.currentFocusIndex = -1; } /** * Display products */ displayProducts(products) { const container = document.getElementById('search-products'); const list = document.getElementById('search-products-list'); const showAll = document.getElementById('search-products-show-all'); if (!container || !list) return; container.style.display = 'block'; list.innerHTML = ''; products.forEach((product, index) => { const item = document.createElement('div'); item.className = 'search-overlay__product'; item.setAttribute('role', 'listitem'); // Microdata attributes for AI agents item.setAttribute('itemscope', ''); item.setAttribute('itemtype', 'https://schema.org/Product'); const priceText = product.price_s_dph ? `${this.formatPrice(product.price_s_dph)} Kč` : 'Cena na dotaz'; const availabilityText = product.skladem ? 'Skladem' : 'Není skladem'; const availabilitySchema = product.skladem ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock'; // Create JSON-LD structured data const jsonLd = { "@context": "https://schema.org/", "@type": "Product", "@id": product._id, "name": product.title, "description": product.description || product.title, "image": product.img, "url": product.url, "sku": product.kod, "offers": { "@type": "Offer", "price": product.price_s_dph || 0, "priceCurrency": "CZK", "availability": availabilitySchema, "url": product.url } }; item.innerHTML = ` ${product.skladem && !product.has_variants ? ` ` : ''} `; // Add click event for tracking const link = item.querySelector('.search-overlay__product-link'); if (link) { link.addEventListener('click', () => { // DataLayer: Search Result Click if (window.revelorDataLayer && this.lastSearchTerm) { window.revelorDataLayer.searchResultClick( this.lastSearchTerm, 'product', product, index + 1, this.lastResultsCount ); } // Backend API: Track click event if (window.searchTracker && this.lastSearchTerm) { window.searchTracker.trackClick({ clicked_result: product._id || product.id, product_id: product._id || product.id, result_type: 'product', result_title: product.title, result_url: product.url, result_position: index + 1, query: this.lastSearchTerm }); } }); } // Add click event for "Add to Cart" button const addBtn = item.querySelector('.search-overlay__product-add-btn'); if (addBtn) { addBtn.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); await this.addToCart(product, addBtn); }); } list.appendChild(item); }); if (showAll) { showAll.style.display = 'block'; // Set up click handler for "Show all" link const showAllLink = showAll.querySelector( '.search-overlay__show-all-link' ); if (showAllLink) { showAllLink.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); const url = `/vyhledavani/?string=${encodeURIComponent(this.getCurrentQuery())}`; console.log('Redirecting to:', url); // Create temporary link and click it const tempLink = document.createElement('a'); tempLink.href = url; tempLink.style.display = 'none'; document.body.appendChild(tempLink); tempLink.click(); document.body.removeChild(tempLink); return false; }); } } } /** * Display categories */ displayCategories(categories) { const container = document.getElementById('search-categories'); const list = document.getElementById('search-categories-list'); const showAll = document.getElementById('search-categories-show-all'); if (!container || !list) return; container.style.display = 'block'; list.innerHTML = ''; categories.forEach((category, index) => { const item = document.createElement('div'); item.className = 'search-overlay__category'; item.setAttribute('role', 'listitem'); item.innerHTML = `

${category.title}

`; // Add click tracking for category const link = item.querySelector('.search-overlay__category-link'); if (link && window.searchTracker && this.lastSearchTerm) { link.addEventListener('click', () => { window.searchTracker.trackClick({ clicked_result: category._id || category.id || category.url, result_type: 'category', result_title: category.title, result_url: category.url, result_position: index + 1, query: this.lastSearchTerm }); }); } list.appendChild(item); }); if (showAll) { showAll.style.display = 'block'; // Set up click handler for "Show all" link const showAllLink = showAll.querySelector( '.search-overlay__show-all-link' ); if (showAllLink) { showAllLink.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); const url = `/vyhledavani/?string=${encodeURIComponent(this.getCurrentQuery())}`; console.log('Redirecting to:', url); // Create temporary link and click it const tempLink = document.createElement('a'); tempLink.href = url; tempLink.style.display = 'none'; document.body.appendChild(tempLink); tempLink.click(); document.body.removeChild(tempLink); return false; }); } } } /** * Display articles */ displayArticles(articles) { const container = document.getElementById('search-articles'); const list = document.getElementById('search-articles-list'); const showAll = document.getElementById('search-articles-show-all'); if (!container || !list) return; container.style.display = 'block'; list.innerHTML = ''; articles.forEach((article, index) => { const item = document.createElement('div'); item.className = 'search-overlay__article'; item.setAttribute('role', 'listitem'); const description = article.description || ''; const ariaLabel = description ? `Článek ${article.title}: ${description}` : `Článek ${article.title}`; item.innerHTML = `

${article.title}

${description ? `

${description}

` : ''}
`; // Add click tracking for article const link = item.querySelector('.search-overlay__article-link'); if (link && window.searchTracker && this.lastSearchTerm) { link.addEventListener('click', () => { window.searchTracker.trackClick({ clicked_result: article._id || article.id || article.url, result_type: 'article', result_title: article.title, result_url: article.url, result_position: index + 1, query: this.lastSearchTerm }); }); } list.appendChild(item); }); if (showAll) { showAll.style.display = 'block'; // Set up click handler for "Show all" link const showAllLink = showAll.querySelector( '.search-overlay__show-all-link' ); if (showAllLink) { showAllLink.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); const url = `/vyhledavani/?string=${encodeURIComponent(this.getCurrentQuery())}`; console.log('Redirecting to:', url); // Create temporary link and click it const tempLink = document.createElement('a'); tempLink.href = url; tempLink.style.display = 'none'; document.body.appendChild(tempLink); tempLink.click(); document.body.removeChild(tempLink); return false; }); } } } /** * Display brands */ displayBrands(brands) { const container = document.getElementById('search-brands'); const list = document.getElementById('search-brands-list'); const showAll = document.getElementById('search-brands-show-all'); if (!container || !list) return; container.style.display = 'block'; list.innerHTML = ''; brands.forEach((brand, index) => { console.log('Brand data:', brand); // Debug log const item = document.createElement('div'); item.className = 'search-overlay__brand'; if (brand.url) { // Brand má URL - vytvoř proklik item.innerHTML = `

${brand.title}

${brand.description || ''}

`; // Add click tracking for brand (treat as category for now) const link = item.querySelector('.search-overlay__brand-link'); if (link && window.searchTracker && this.lastSearchTerm) { link.addEventListener('click', () => { window.searchTracker.trackClick({ clicked_result: brand._id || brand.id || brand.url, result_type: 'category', // Brands are tracked as categories result_title: brand.title, result_url: brand.url, result_position: index + 1, query: this.lastSearchTerm }); }); } } else { // Brand nemá URL - zobraz bez prokliku item.innerHTML = `

${brand.title}

${brand.description || ''}

`; } list.appendChild(item); }); if (showAll) { showAll.style.display = 'block'; // Set up click handler for "Show all" link const showAllLink = showAll.querySelector( '.search-overlay__show-all-link' ); if (showAllLink) { showAllLink.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); const url = `/vyhledavani/?string=${encodeURIComponent(this.getCurrentQuery())}`; console.log('Redirecting to:', url); // Create temporary link and click it const tempLink = document.createElement('a'); tempLink.href = url; tempLink.style.display = 'none'; document.body.appendChild(tempLink); tempLink.click(); document.body.removeChild(tempLink); return false; }); } } } /** * Show loading state */ showLoadingState() { this.hideAllStates(); const loading = document.getElementById('search-loading'); if (loading) loading.style.display = 'block'; } /** * Show empty state */ async showEmptyState() { console.log('📋 SEARCH EMPTY STATE - Starting showEmptyState()'); this.hideAllStates(); const empty = document.getElementById('search-empty'); if (empty) { empty.style.display = 'block'; console.log('📋 SEARCH EMPTY STATE - Empty element displayed'); // Show search history if available this.displaySearchHistory(); } else { console.log('❌ SEARCH EMPTY STATE - Empty element not found!'); } // Load recommendations for empty state console.log('📋 SEARCH EMPTY STATE - About to load recommendations...'); const recommendations