Webotvurci

Untitled

Oct 31st, 2025
231
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /**
  2.  * Revelor Search Bundle
  3.  * Combined and bundled JavaScript for Shoptet
  4.  *
  5.  * Contains:
  6.  * - Console Wrapper (admin-only logging)
  7.  * - DataLayer Helper (GTM analytics)
  8.  * - Search Tracker (backend tracking)
  9.  * - Search Overlay (main search functionality)
  10.  * - Search Results Page (results page logic)
  11.  *
  12.  * Build date: 2025-11-01T00:12:21.753Z
  13.  * Project: Revelor Textilomanie
  14.  */
  15.  
  16. // =============================================================================
  17. // console-wrapper.js
  18. // =============================================================================
  19.  
  20. /**
  21.  * @fileoverview Console Wrapper for Admin-Only Logging
  22.  * Přepisuje console metody - výstupy se zobrazí pouze pokud je admin přihlášený
  23.  * @version 1.0.0
  24.  * @author Revelor by Webotvůrci
  25.  */
  26.  
  27. (function() {
  28.   'use strict';
  29.  
  30.   /**
  31.    * Zkontroluje, jestli je admin přihlášený
  32.    * Kontroluje dynamicky při každém volání (BODY může být ještě ne načtený)
  33.    * @returns {boolean}
  34.    */
  35.   function isAdminLoggedIn() {
  36.     // Pokud BODY ještě není načtené, vrať false (bezpečnější)
  37.     if (!document.body) {
  38.       return false;
  39.     }
  40.     // Zkontroluj, jestli má BODY class 'admin-logged'
  41.     return document.body.classList.contains('admin-logged');
  42.   }
  43.  
  44.   /**
  45.    * Uložit původní console metody
  46.    */
  47.   const originalConsole = {
  48.     log: console.log.bind(console),
  49.     error: console.error.bind(console),
  50.     warn: console.warn.bind(console),
  51.     info: console.info.bind(console),
  52.     debug: console.debug.bind(console),
  53.     trace: console.trace.bind(console),
  54.     table: console.table.bind(console),
  55.     group: console.group.bind(console),
  56.     groupEnd: console.groupEnd.bind(console),
  57.     groupCollapsed: console.groupCollapsed.bind(console),
  58.     time: console.time.bind(console),
  59.     timeEnd: console.timeEnd.bind(console),
  60.     assert: console.assert.bind(console)
  61.   };
  62.  
  63.   /**
  64.    * No-op funkce (nic nedělá)
  65.    */
  66.   function noop() {}
  67.  
  68.   /**
  69.    * Wrapper funkce, která zkontroluje admin status
  70.    */
  71.   function createConsoleWrapper(originalMethod) {
  72.     return function(...args) {
  73.       if (isAdminLoggedIn()) {
  74.         originalMethod.apply(console, args);
  75.       }
  76.       // Pokud není admin, nic se nestane (no-op)
  77.     };
  78.   }
  79.  
  80.   // Přepiš console metody pouze pokud ještě nejsou přepsané
  81.   if (!window._revelorConsoleWrapped) {
  82.     console.log = createConsoleWrapper(originalConsole.log);
  83.     console.error = createConsoleWrapper(originalConsole.error);
  84.     console.warn = createConsoleWrapper(originalConsole.warn);
  85.     console.info = createConsoleWrapper(originalConsole.info);
  86.     console.debug = createConsoleWrapper(originalConsole.debug);
  87.     console.trace = createConsoleWrapper(originalConsole.trace);
  88.     console.table = createConsoleWrapper(originalConsole.table);
  89.     console.group = createConsoleWrapper(originalConsole.group);
  90.     console.groupEnd = createConsoleWrapper(originalConsole.groupEnd);
  91.     console.groupCollapsed = createConsoleWrapper(originalConsole.groupCollapsed);
  92.     console.time = createConsoleWrapper(originalConsole.time);
  93.     console.timeEnd = createConsoleWrapper(originalConsole.timeEnd);
  94.     console.assert = createConsoleWrapper(originalConsole.assert);
  95.  
  96.     // Označit, že jsme už wrapper aplikovali
  97.     window._revelorConsoleWrapped = true;
  98.   }
  99. })();
  100.  
  101.  
  102.  
  103.  
  104. // =============================================================================
  105. // datalayer-helper.js
  106. // =============================================================================
  107.  
  108. /**
  109.  * @fileoverview DataLayer Helper for Revelor Search Analytics
  110.  * Manages all dataLayer pushes for Google Tag Manager
  111.  * @version 2.0.0
  112.  * @author Revelor by Webotvůrci
  113.  * @contact [email protected] | +420 728 089 029
  114.  */
  115.  
  116. class RevelorDataLayer {
  117.   constructor() {
  118.     // Initialize dataLayer if not exists
  119.     window.dataLayer = window.dataLayer || [];
  120.    
  121.     // Session storage for tracking search journey
  122.     this.sessionKey = 'revelor_search_session';
  123.     this.initSession();
  124.   }
  125.  
  126.   /**
  127.    * Initialize search session
  128.    */
  129.   initSession() {
  130.     if (!sessionStorage.getItem(this.sessionKey)) {
  131.       sessionStorage.setItem(this.sessionKey, JSON.stringify({
  132.         searchCount: 0,
  133.         lastSearchTerm: null,
  134.         lastSearchTime: null,
  135.         searchStartTime: null
  136.       }));
  137.     }
  138.   }
  139.  
  140.   /**
  141.    * Get search session data
  142.    */
  143.   getSession() {
  144.     return JSON.parse(sessionStorage.getItem(this.sessionKey) || '{}');
  145.   }
  146.  
  147.   /**
  148.    * Update search session
  149.    */
  150.   updateSession(updates) {
  151.     const session = this.getSession();
  152.     const newSession = { ...session, ...updates };
  153.     sessionStorage.setItem(this.sessionKey, JSON.stringify(newSession));
  154.   }
  155.  
  156.   /**
  157.    * Get current timestamp in ISO format
  158.    */
  159.   getTimestamp() {
  160.     return new Date().toISOString();
  161.   }
  162.  
  163.   /**
  164.    * Get user type (new vs returning)
  165.    */
  166.   getUserType() {
  167.     // Check if user has visited before (using cookie or localStorage)
  168.     return localStorage.getItem('user_visited') ? 'returning' : 'new';
  169.   }
  170.  
  171.   /**
  172.    * Get page type from URL or body class
  173.    */
  174.   getPageType() {
  175.     const path = window.location.pathname;
  176.     if (path === '/' || path === '') return 'homepage';
  177.     if (path.includes('/produkt/') || path.includes('/product/')) return 'product';
  178.     if (path.includes('/kategorie/') || path.includes('/category/')) return 'category';
  179.     if (path.includes('/vysledky-hledani') || path.includes('/search')) return 'search_results';
  180.     return 'other';
  181.   }
  182.  
  183.   /**
  184.    * Calculate time difference in seconds
  185.    */
  186.   getTimeDiff(startTime) {
  187.     if (!startTime) return 0;
  188.     return ((Date.now() - new Date(startTime).getTime()) / 1000).toFixed(1);
  189.   }
  190.  
  191.   /**
  192.    * Push event to dataLayer
  193.    */
  194.   push(eventData) {
  195.     console.log('📊 Revelor DataLayer Push:', eventData);
  196.     window.dataLayer.push(eventData);
  197.   }
  198.  
  199.   /**
  200.    * 1. Search Started
  201.    */
  202.   searchStarted(context = 'overlay') {
  203.     const session = this.getSession();
  204.    
  205.     this.push({
  206.       event: 'revelor_search_started',
  207.       search_context: context,
  208.       timestamp: this.getTimestamp(),
  209.       user_type: this.getUserType(),
  210.       page_type: this.getPageType(),
  211.       session_search_count: session.searchCount || 0
  212.     });
  213.  
  214.     // Update session
  215.     this.updateSession({
  216.       searchStartTime: this.getTimestamp(),
  217.       searchCount: (session.searchCount || 0) + 1
  218.     });
  219.   }
  220.  
  221.   /**
  222.    * 2. Search Query Submitted
  223.    */
  224.   searchQuerySubmitted(searchTerm, context = 'overlay') {
  225.     const session = this.getSession();
  226.     const isRefinement = session.lastSearchTerm && session.lastSearchTerm !== searchTerm;
  227.  
  228.     this.push({
  229.       event: 'revelor_search_query_submitted',
  230.       search_term: searchTerm,
  231.       search_term_length: searchTerm.length,
  232.       search_context: context,
  233.       is_refinement: isRefinement,
  234.       previous_search_term: isRefinement ? session.lastSearchTerm : null,
  235.       timestamp: this.getTimestamp()
  236.     });
  237.  
  238.     // Update session
  239.     this.updateSession({
  240.       lastSearchTerm: searchTerm,
  241.       lastSearchTime: this.getTimestamp()
  242.     });
  243.   }
  244.  
  245.   /**
  246.    * 3. Search Results Displayed
  247.    */
  248.   searchResultsDisplayed(searchTerm, resultsData, responseTime, page = 1) {
  249.     const totalResults = resultsData.total_products || 0;
  250.     const productsCount = resultsData.products?.length || 0;
  251.     const categoriesCount = resultsData.categories?.length || 0;
  252.     const articlesCount = resultsData.articles?.length || 0;
  253.     const brandsCount = resultsData.brands?.length || 0;
  254.  
  255.     this.push({
  256.       event: 'revelor_search_results_displayed',
  257.       search_term: searchTerm,
  258.       results_count: {
  259.         total: totalResults,
  260.         products: productsCount,
  261.         categories: categoriesCount,
  262.         articles: articlesCount,
  263.         brands: brandsCount
  264.       },
  265.       has_results: totalResults > 0,
  266.       page: page,
  267.       response_time_ms: responseTime,
  268.       timestamp: this.getTimestamp()
  269.     });
  270.   }
  271.  
  272.   /**
  273.    * 4. Search No Results
  274.    */
  275.   searchNoResults(searchTerm) {
  276.     // Simple typo detection (contains numbers mixed with letters, etc.)
  277.     const potentialTypo = /\d/.test(searchTerm) || searchTerm.length < 3;
  278.  
  279.     this.push({
  280.       event: 'revelor_search_no_results',
  281.       search_term: searchTerm,
  282.       search_term_length: searchTerm.length,
  283.       potential_typo: potentialTypo,
  284.       timestamp: this.getTimestamp()
  285.     });
  286.   }
  287.  
  288.   /**
  289.    * 5. Search Result Click
  290.    */
  291.   searchResultClick(searchTerm, resultType, resultData, position, totalResults) {
  292.     const session = this.getSession();
  293.     const clickTimeFromSearch = this.getTimeDiff(session.lastSearchTime);
  294.  
  295.     const eventData = {
  296.       event: 'revelor_search_result_click',
  297.       search_term: searchTerm,
  298.       result_type: resultType,
  299.       result_position: position,
  300.       total_results: totalResults,
  301.       click_time_from_search: parseFloat(clickTimeFromSearch),
  302.       timestamp: this.getTimestamp()
  303.     };
  304.  
  305.     // Add result-specific data
  306.     if (resultType === 'product') {
  307.       eventData.result_data = {
  308.         item_id: resultData._id || resultData.id,
  309.         item_name: resultData.title,
  310.         item_category: resultData.category || 'Neznámá kategorie',
  311.         price: resultData.price_s_dph || 0,
  312.         currency: 'CZK',
  313.         availability: resultData.skladem ? 'in_stock' : 'out_of_stock',
  314.         item_url: resultData.url
  315.       };
  316.  
  317.       // GA4 Enhanced Ecommerce format
  318.       eventData.ecommerce = {
  319.         currency: 'CZK',
  320.         value: resultData.price_s_dph || 0,
  321.         items: [{
  322.           item_id: resultData._id || resultData.id,
  323.           item_name: resultData.title,
  324.           item_list_name: 'Search Results',
  325.           item_list_id: 'search_results',
  326.           price: resultData.price_s_dph || 0,
  327.           index: position,
  328.           quantity: 1
  329.         }]
  330.       };
  331.     } else if (resultType === 'category') {
  332.       eventData.result_data = {
  333.         category_name: resultData.title,
  334.         category_url: resultData.url
  335.       };
  336.     } else if (resultType === 'article') {
  337.       eventData.result_data = {
  338.         article_title: resultData.title,
  339.         article_url: resultData.url
  340.       };
  341.     }
  342.  
  343.     this.push(eventData);
  344.   }
  345.  
  346.   /**
  347.    * 6. Search Load More
  348.    */
  349.   searchLoadMore(searchTerm, currentPage, totalPages, productsLoaded, totalProductsShown) {
  350.     this.push({
  351.       event: 'revelor_search_load_more',
  352.       search_term: searchTerm,
  353.       current_page: currentPage,
  354.       next_page: currentPage + 1,
  355.       total_pages: totalPages,
  356.       products_loaded: productsLoaded,
  357.       total_products_shown: totalProductsShown,
  358.       timestamp: this.getTimestamp()
  359.     });
  360.   }
  361.  
  362.   /**
  363.    * 7. Search Refinement
  364.    */
  365.   searchRefinement(originalTerm, refinedTerm, originalResults, refinedResults) {
  366.     // Determine refinement type
  367.     let refinementType = 'modification';
  368.     if (refinedTerm.length > originalTerm.length) {
  369.       refinementType = 'expansion';
  370.     } else if (refinedTerm.length < originalTerm.length) {
  371.       refinementType = 'narrowing';
  372.     } else if (Math.abs(refinedTerm.length - originalTerm.length) <= 2) {
  373.       refinementType = 'correction';
  374.     }
  375.  
  376.     this.push({
  377.       event: 'revelor_search_refinement',
  378.       original_search_term: originalTerm,
  379.       refined_search_term: refinedTerm,
  380.       refinement_type: refinementType,
  381.       original_results_count: originalResults,
  382.       refined_results_count: refinedResults,
  383.       timestamp: this.getTimestamp()
  384.     });
  385.   }
  386.  
  387.   /**
  388.    * 8. Search Suggestion Click
  389.    */
  390.   searchSuggestionClick(typedQuery, suggestedTerm, position, suggestionType) {
  391.     this.push({
  392.       event: 'revelor_search_suggestion_click',
  393.       typed_query: typedQuery,
  394.       suggested_term: suggestedTerm,
  395.       suggestion_position: position,
  396.       suggestion_type: suggestionType,
  397.       timestamp: this.getTimestamp()
  398.     });
  399.   }
  400.  
  401.   /**
  402.    * 9. Search Recommendations Shown
  403.    */
  404.   searchRecommendationsShown(recommendationsData) {
  405.     const products = recommendationsData.products || [];
  406.     const categories = recommendationsData.categories || [];
  407.    
  408.     this.push({
  409.       event: 'revelor_search_recommendations_shown',
  410.       recommendations_count: products.length + categories.length,
  411.       products_count: products.length,
  412.       categories_count: categories.length,
  413.       recommendation_type: 'default', // trending/popular/personalized
  414.       timestamp: this.getTimestamp()
  415.     });
  416.   }
  417.  
  418.   /**
  419.    * 10. Search Category Expand
  420.    */
  421.   searchCategoryExpand(searchTerm, expandType, action, visibleBefore, visibleAfter) {
  422.     this.push({
  423.       event: 'revelor_search_category_expand',
  424.       search_term: searchTerm,
  425.       expand_type: expandType, // 'categories' | 'articles'
  426.       action: action, // 'expand' | 'collapse'
  427.       visible_items_before: visibleBefore,
  428.       visible_items_after: visibleAfter,
  429.       timestamp: this.getTimestamp()
  430.     });
  431.   }
  432.  
  433.   /**
  434.    * 11. Search Abandoned
  435.    */
  436.   searchAbandoned(searchTerm, resultsCount, timeSpent, reason = 'closed_overlay') {
  437.     this.push({
  438.       event: 'revelor_search_abandoned',
  439.       search_term: searchTerm,
  440.       results_count: resultsCount,
  441.       time_spent: parseFloat(timeSpent),
  442.       reason: reason, // 'closed_overlay' | 'navigated_away' | 'no_interaction'
  443.       timestamp: this.getTimestamp()
  444.     });
  445.   }
  446. }
  447.  
  448. // Initialize global instance
  449. window.revelorDataLayer = new RevelorDataLayer();
  450.  
  451.  
  452.  
  453.  
  454. // =============================================================================
  455. // search-tracker.js
  456. // =============================================================================
  457.  
  458. /**
  459.  * @fileoverview Search Tracker for Backend Analytics
  460.  * Tracks search events and clicks to backend API according to metrics-tracking.md
  461.  * @version 1.0.0
  462.  * @author Revelor by Webotvůrci
  463.  * @contact [email protected] | +420 728 089 029
  464.  */
  465.  
  466. class SearchTracker {
  467.   constructor(config = {}) {
  468.     this.apiEndpoint = config.apiEndpoint || 'https://textilomanie.revelor.cz/api/search/track';
  469.     this.projectId = config.projectId || '312030';
  470.     this.sessionTimeout = config.sessionTimeout || 1800000; // 30 minut v ms
  471.     this.sessionKey = 'search_session_id';
  472.     this.lastSearchTimeKey = 'search_last_time';
  473.    
  474.     // Generuj session ID pokud neexistuje nebo vypršela
  475.     this.getOrCreateSession();
  476.   }
  477.  
  478.   /**
  479.    * Get or create session ID
  480.    */
  481.   getOrCreateSession() {
  482.     const sessionId = sessionStorage.getItem(this.sessionKey);
  483.     const lastSearchTime = sessionStorage.getItem(this.lastSearchTimeKey);
  484.     const now = Date.now();
  485.    
  486.     // Pokud session neexistuje nebo vypršela (30 min), vytvoř novou
  487.     if (!sessionId || !lastSearchTime || (now - parseInt(lastSearchTime)) > this.sessionTimeout) {
  488.       const newSessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
  489.       sessionStorage.setItem(this.sessionKey, newSessionId);
  490.       sessionStorage.setItem(this.lastSearchTimeKey, now.toString());
  491.       return newSessionId;
  492.     }
  493.    
  494.     return sessionId;
  495.   }
  496.  
  497.   /**
  498.    * Get current session ID
  499.    */
  500.   getSessionId() {
  501.     return this.getOrCreateSession();
  502.   }
  503.  
  504.   /**
  505.    * Update last search time
  506.    */
  507.   updateLastSearchTime() {
  508.     sessionStorage.setItem(this.lastSearchTimeKey, Date.now().toString());
  509.   }
  510.  
  511.   /**
  512.    * Track search event
  513.    * @param {Object} data - Search tracking data
  514.    * @param {string} data.query - Search query (min 3 chars)
  515.    * @param {number} [data.results_count] - Number of results
  516.    * @param {string} [data.search_type] - 'manual' | 'suggestion' | 'live_search'
  517.    */
  518.   async trackSearch(data) {
  519.     const { query, results_count, search_type = 'live_search' } = data;
  520.    
  521.     // Backend požaduje minimálně 3 znaky
  522.     if (!query || query.length < 3) {
  523.       console.log('🔇 Search tracking skipped - query too short:', query);
  524.       return;
  525.     }
  526.  
  527.     const sessionId = this.getSessionId();
  528.     this.updateLastSearchTime();
  529.  
  530.     const eventData = {
  531.       event_type: 'search',
  532.       query: query,
  533.       session_id: sessionId,
  534.       url: window.location.href,
  535.       referrer: document.referrer || '',
  536.       timestamp: new Date().toISOString(),
  537.       user_agent: navigator.userAgent,
  538.       search_type: search_type
  539.     };
  540.  
  541.     if (results_count !== undefined) {
  542.       eventData.results_count = results_count;
  543.     }
  544.  
  545.     await this.sendEvent(eventData);
  546.   }
  547.  
  548.   /**
  549.    * Track click event
  550.    * @param {Object} data - Click tracking data
  551.    * @param {string} data.clicked_result - Product/article/category ID (required)
  552.    * @param {string} data.result_type - 'product' | 'article' | 'category'
  553.    * @param {string} data.result_title - Title of the result
  554.    * @param {string} data.result_url - URL of the result
  555.    * @param {number} data.result_position - Position in results (1-based)
  556.    * @param {string} data.query - Search query
  557.    */
  558.   async trackClick(data) {
  559.     const { clicked_result, product_id, result_type, result_title, result_url, result_position, query } = data;
  560.    
  561.     // Backend vyžaduje clicked_result nebo product_id
  562.     const resultId = clicked_result || product_id;
  563.     if (!resultId) {
  564.       console.error('❌ Click tracking error - missing clicked_result or product_id');
  565.       return;
  566.     }
  567.  
  568.     const sessionId = this.getSessionId();
  569.  
  570.     const eventData = {
  571.       event_type: 'click',
  572.       clicked_result: resultId,
  573.       result_type: result_type || 'product',
  574.       query: query || '',
  575.       session_id: sessionId,
  576.       url: window.location.href,
  577.       referrer: document.referrer || '',
  578.       timestamp: new Date().toISOString(),
  579.       user_agent: navigator.userAgent
  580.     };
  581.  
  582.     if (result_title) {
  583.       eventData.result_title = result_title;
  584.     }
  585.  
  586.     if (result_url) {
  587.       eventData.result_url = result_url;
  588.     }
  589.  
  590.     if (result_position !== undefined) {
  591.       eventData.result_position = result_position;
  592.     }
  593.  
  594.     // Alternativní název pro product_id (pro kompatibilitu)
  595.     if (product_id) {
  596.       eventData.product_id = product_id;
  597.     }
  598.  
  599.     await this.sendEvent(eventData);
  600.   }
  601.  
  602.   /**
  603.    * Send event to backend API
  604.    * @param {Object} eventData - Event data to send
  605.    */
  606.   async sendEvent(eventData) {
  607.     try {
  608.       const response = await fetch(this.apiEndpoint, {
  609.         method: 'POST',
  610.         headers: {
  611.           'Content-Type': 'application/json',
  612.           'X-Tenant': this.projectId
  613.         },
  614.         body: JSON.stringify(eventData)
  615.       });
  616.  
  617.       if (!response.ok) {
  618.         const errorData = await response.json().catch(() => ({}));
  619.         console.error('❌ Search tracking error:', response.status, errorData);
  620.         return;
  621.       }
  622.  
  623.       const responseData = await response.json().catch(() => ({}));
  624.      
  625.       if (responseData.status === 'skipped') {
  626.         console.log('⏭️ Search tracking skipped:', responseData.message);
  627.       } else {
  628.         console.log('✅ Search tracking success:', responseData);
  629.       }
  630.     } catch (error) {
  631.       // Fail silently - tracking should not break the app
  632.       console.warn('⚠️ Search tracking failed:', error);
  633.     }
  634.   }
  635. }
  636.  
  637. // Create global instance
  638. window.searchTracker = new SearchTracker({
  639.   projectId: (typeof dataLayer !== 'undefined' &&
  640.              dataLayer[0] &&
  641.              dataLayer[0].shoptet &&
  642.              dataLayer[0].shoptet.projectId) || '312030'
  643. });
  644.  
  645.  
  646.  
  647.  
  648. // =============================================================================
  649. // searching.js
  650. // =============================================================================
  651.  
  652. /**
  653.  * @fileoverview Search Overlay Component
  654.  * Implements search functionality using Shoptet API
  655.  * @version 2.0.0
  656.  * @author Revelor by Webotvůrci
  657.  * @contact [email protected] | +420 728 089 029
  658.  */
  659.  
  660. class SearchOverlay {
  661.   constructor() {
  662.     this.isOpen = false;
  663.     this.apiBaseUrl = 'https://textilomanie.revelor.cz/api/public';
  664.     this.projectId = this.getProjectId();
  665.     this.minSearchLength = 1;
  666.     this.previousActiveElement = null; // Store previous focus
  667.     this.focusableElements = []; // Focusable elements in overlay
  668.     this.currentFocusIndex = -1; // Current focus index for keyboard navigation
  669.    
  670.     // Performance optimizations
  671.     this.abortController = null; // Request cancellation
  672.     this.cache = new Map(); // Simple cache
  673.     this.cacheTTL = 300000; // 5 minutes
  674.    
  675.     // Search history
  676.     this.historyKey = 'revelor_search_history';
  677.     this.maxHistoryItems = 10;
  678.  
  679.     this.init();
  680.   }
  681.  
  682.   /**
  683.    * Initialize search overlay
  684.    */
  685.   init() {
  686.     this.createOverlay();
  687.     this.bindEvents();
  688.     this.loadPlaceholders();
  689.   }
  690.  
  691.   /**
  692.    * Get Project ID from dataLayer or use default
  693.    */
  694.   getProjectId() {
  695.     if (
  696.       typeof dataLayer !== 'undefined' &&
  697.       dataLayer[0] &&
  698.       dataLayer[0].shoptet
  699.     ) {
  700.       return dataLayer[0].shoptet.projectId;
  701.     }
  702.     return '312030'; // Default fallback
  703.   }
  704.  
  705.   /**
  706.    * Create and inject search overlay HTML
  707.    */
  708.   createOverlay() {
  709.     const overlayHTML = `
  710.             <div id="search-overlay" class="search-overlay" role="dialog" aria-modal="true" aria-labelledby="search-dialog-title" aria-describedby="search-dialog-description">
  711.                 <div class="search-overlay__content">
  712.                     <span id="search-dialog-title" class="sr-only">Vyhledávací dialog</span>
  713.                     <span id="search-dialog-description" class="sr-only">Použijte toto pole k vyhledání produktů, kategorií, článků nebo značek. Stiskněte Escape pro zavření dialogu.</span>
  714.                    
  715.                     <div class="search-overlay__header">
  716.                         <div class="search-overlay__input-wrapper">
  717.                             <label for="search-input" class="sr-only">Vyhledávací dotaz</label>
  718.                             <input
  719.                                 type="text"
  720.                                 id="search-input"
  721.                                 class="search-overlay__input"
  722.                                 placeholder="Hledejte produkty, články, kategorie..."
  723.                                 autocomplete="off"
  724.                                 aria-label="Vyhledávací pole"
  725.                                 aria-describedby="search-instructions"
  726.                                 aria-autocomplete="list"
  727.                                 aria-controls="search-content-results"
  728.                                 role="combobox"
  729.                                 aria-expanded="false"
  730.                             >
  731.                             <span id="search-instructions" class="sr-only">Začněte psát pro vyhledávání. Použijte šipky nahoru a dolů pro navigaci výsledky. Stiskněte Enter pro přechod na stránku výsledků.</span>
  732.                             <button type="button" id="search-submit" class="search-overlay__submit" aria-label="Vyhledat">
  733.                                 <span class="search-overlay__submit-icon" aria-hidden="true"></span>
  734.                             </button>
  735.                         </div>
  736.                         <button type="button" id="search-close" class="search-overlay__close" aria-label="Zavřít vyhledávání">
  737.                             <span class="search-overlay__close-icon" aria-hidden="true">&times;</span>
  738.                         </button>
  739.                     </div>
  740.  
  741.                     <div class="search-overlay__results" id="search-results" role="region" aria-live="polite">
  742.                         <!-- Live region for screen reader announcements -->
  743.                         <div id="search-live-region" class="sr-only" role="status" aria-live="polite" aria-atomic="true"></div>
  744.                        
  745.                         <div class="search-overlay__loading" id="search-loading" style="display: none;" role="status" aria-live="polite">
  746.                             <div class="search-overlay__spinner" aria-hidden="true"></div>
  747.                             <p>Vyhledávám...</p>
  748.                         </div>
  749.  
  750.                         <div class="search-overlay__empty" id="search-empty" role="status">
  751.                             <div class="search-overlay__empty-icon" aria-hidden="true">🔍</div>
  752.                             <h3>Zadejte hledaný výraz</h3>
  753.                             <p>Začněte psát a uvidíte návrhy</p>
  754.                         </div>
  755.  
  756.                         <div class="search-overlay__no-results" id="search-no-results" style="display: none;" role="status" aria-live="polite">
  757.                             <div class="search-overlay__no-results-icon" aria-hidden="true">😔</div>
  758.                             <h3>Žádné výsledky</h3>
  759.                             <p>Zkuste jiný hledaný výraz</p>
  760.                         </div>
  761.  
  762.                         <div class="search-overlay__content-results" id="search-content-results" style="display: none;">
  763.                             <div class="search-overlay__section" id="search-categories" style="display: none;">
  764.                                 <h3 class="search-overlay__section-title" id="search-categories-title">Kategorie</h3>
  765.                                 <div class="search-overlay__categories" id="search-categories-list" role="list" aria-labelledby="search-categories-title"></div>
  766.                                 <div class="search-overlay__show-all" id="search-categories-show-all" style="display: none;">
  767.                                     <button type="button" class="search-overlay__show-all-link" aria-label="Zobrazit všechny kategorie na stránce výsledků">Zobrazit všechny kategorie</button>
  768.                                 </div>
  769.                             </div>
  770.  
  771.                             <div class="search-overlay__section" id="search-products" style="display: none;">
  772.                                 <h3 class="search-overlay__section-title" id="search-products-title">Produkty</h3>
  773.                                 <div class="search-overlay__products" id="search-products-list" role="list" aria-labelledby="search-products-title"></div>
  774.                                 <div class="search-overlay__show-all" id="search-products-show-all" style="display: none;">
  775.                                     <button type="button" class="search-overlay__show-all-link" aria-label="Zobrazit všechny produkty na stránce výsledků">Zobrazit všechny produkty</button>
  776.                                 </div>
  777.                             </div>
  778.  
  779.                             <div class="search-overlay__section" id="search-articles" style="display: none;">
  780.                                 <h3 class="search-overlay__section-title" id="search-articles-title">Články</h3>
  781.                                 <div class="search-overlay__articles" id="search-articles-list" role="list" aria-labelledby="search-articles-title"></div>
  782.                                 <div class="search-overlay__show-all" id="search-articles-show-all" style="display: none;">
  783.                                     <button type="button" class="search-overlay__show-all-link" aria-label="Zobrazit všechny články na stránce výsledků">Zobrazit všechny články</button>
  784.                                 </div>
  785.                             </div>
  786.  
  787.                             <div class="search-overlay__section" id="search-brands" style="display: none;">
  788.                                 <h3 class="search-overlay__section-title" id="search-brands-title">Značky</h3>
  789.                                 <div class="search-overlay__brands" id="search-brands-list" role="list" aria-labelledby="search-brands-title"></div>
  790.                                 <div class="search-overlay__show-all" id="search-brands-show-all" style="display: none;">
  791.                                     <button type="button" class="search-overlay__show-all-link" aria-label="Zobrazit všechny značky na stránce výsledků">Zobrazit všechny značky</button>
  792.                                 </div>
  793.                             </div>
  794.                         </div>
  795.                     </div>
  796.                 </div>
  797.             </div>
  798.  
  799.         `;
  800.  
  801.     // Insert before #content-wrapper or at the beginning of body
  802.     const contentWrapper = document.getElementById('content-wrapper');
  803.     if (contentWrapper) {
  804.       contentWrapper.insertAdjacentHTML('beforebegin', overlayHTML);
  805.     } else {
  806.       document.body.insertAdjacentHTML('afterbegin', overlayHTML);
  807.     }
  808.   }
  809.  
  810.   /**
  811.    * Bind event listeners
  812.    */
  813.   bindEvents() {
  814.     const formSearchForm = document.getElementById('formSearchForm');
  815.     const overlay = document.getElementById('search-overlay');
  816.     const input = document.getElementById('search-input');
  817.     const close = document.getElementById('search-close');
  818.     const submit = document.getElementById('search-submit');
  819.  
  820.     // Open search from header form
  821.     if (formSearchForm) {
  822.       console.log(
  823.         '✅ SEARCH EVENTS - formSearchForm found, adding click listener'
  824.       );
  825.       formSearchForm.addEventListener('click', async e => {
  826.         console.log('🖱️ SEARCH EVENTS - formSearchForm clicked!');
  827.         e.preventDefault();
  828.         await this.open();
  829.       });
  830.     } else {
  831.       console.log('❌ SEARCH EVENTS - formSearchForm NOT FOUND!');
  832.     }
  833.  
  834.     // Close search
  835.     close?.addEventListener('click', () => this.close());
  836.     overlay?.addEventListener('click', e => {
  837.       if (e.target === overlay) this.close();
  838.     });
  839.  
  840.     // Submit search
  841.     submit?.addEventListener('click', () => {
  842.       const query = this.getCurrentQuery().trim();
  843.       if (query.length >= this.minSearchLength) {
  844.         this.redirectToSearchResults(query);
  845.       }
  846.     });
  847.  
  848.     // Search input
  849.     input?.addEventListener('input', e => this.handleSearch(e.target.value));
  850.     input?.addEventListener('keydown', e => {
  851.       if (e.key === 'Escape') {
  852.         this.close();
  853.       } else if (e.key === 'Enter') {
  854.         e.preventDefault();
  855.         const query = this.getCurrentQuery().trim();
  856.         if (query.length >= this.minSearchLength) {
  857.           this.redirectToSearchResults(query);
  858.         }
  859.       } else if (e.key === 'ArrowDown') {
  860.         e.preventDefault();
  861.         this.focusNextResult();
  862.       } else if (e.key === 'ArrowUp') {
  863.         e.preventDefault();
  864.         this.focusPreviousResult();
  865.       }
  866.     });
  867.  
  868.     // Keyboard navigation - global
  869.     document.addEventListener('keydown', e => {
  870.       if (e.key === '/' && !this.isOpen && document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
  871.         e.preventDefault();
  872.         this.open();
  873.       }
  874.      
  875.       // Handle Tab key for focus trap when overlay is open
  876.       if (this.isOpen && e.key === 'Tab') {
  877.         this.handleTabKey(e);
  878.       }
  879.     });
  880.    
  881.     // Setup swipe to close (mobile)
  882.     this.setupSwipeToClose();
  883.   }
  884.  
  885.   /**
  886.    * Setup swipe to close for mobile
  887.    */
  888.   setupSwipeToClose() {
  889.     const overlay = document.getElementById('search-overlay');
  890.     if (!overlay) return;
  891.    
  892.     let startY = 0;
  893.     let currentY = 0;
  894.     let isDragging = false;
  895.    
  896.     overlay.addEventListener('touchstart', (e) => {
  897.       // Only if touch starts at the top of content
  898.       if (e.target.closest('.search-overlay__header')) {
  899.         startY = e.touches[0].clientY;
  900.         isDragging = true;
  901.       }
  902.     }, { passive: true });
  903.    
  904.     overlay.addEventListener('touchmove', (e) => {
  905.       if (!isDragging) return;
  906.      
  907.       currentY = e.touches[0].clientY;
  908.       const diff = currentY - startY;
  909.      
  910.       // Only allow swipe down
  911.       if (diff > 0) {
  912.         const content = overlay.querySelector('.search-overlay__content');
  913.         if (content) {
  914.           content.style.transform = `translateX(-50%) translateY(${diff}px)`;
  915.           content.style.opacity = Math.max(0.5, 1 - diff / 400);
  916.         }
  917.       }
  918.     }, { passive: true });
  919.    
  920.     overlay.addEventListener('touchend', () => {
  921.       if (!isDragging) return;
  922.      
  923.       const diff = currentY - startY;
  924.       const content = overlay.querySelector('.search-overlay__content');
  925.      
  926.       if (diff > 100) {
  927.         // Swipe down threshold - close overlay
  928.         this.close();
  929.       } else {
  930.         // Return to position
  931.         if (content) {
  932.           content.style.transform = 'translateX(-50%)';
  933.           content.style.opacity = '1';
  934.         }
  935.       }
  936.      
  937.       isDragging = false;
  938.       startY = 0;
  939.       currentY = 0;
  940.     });
  941.   }
  942.  
  943.   /**
  944.    * Handle Tab key for focus trap
  945.    */
  946.   handleTabKey(e) {
  947.     const overlay = document.getElementById('search-overlay');
  948.     if (!overlay) return;
  949.  
  950.     const focusableElements = overlay.querySelectorAll(
  951.       'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
  952.     );
  953.    
  954.     const firstFocusable = focusableElements[0];
  955.     const lastFocusable = focusableElements[focusableElements.length - 1];
  956.  
  957.     if (e.shiftKey) {
  958.       // Shift + Tab
  959.       if (document.activeElement === firstFocusable) {
  960.         e.preventDefault();
  961.         lastFocusable.focus();
  962.       }
  963.     } else {
  964.       // Tab
  965.       if (document.activeElement === lastFocusable) {
  966.         e.preventDefault();
  967.         firstFocusable.focus();
  968.       }
  969.     }
  970.   }
  971.  
  972.   /**
  973.    * Focus next result in list
  974.    */
  975.   focusNextResult() {
  976.     const results = this.getFocusableResults();
  977.     if (results.length === 0) return;
  978.  
  979.     this.currentFocusIndex = (this.currentFocusIndex + 1) % results.length;
  980.     results[this.currentFocusIndex].focus();
  981.   }
  982.  
  983.   /**
  984.    * Focus previous result in list
  985.    */
  986.   focusPreviousResult() {
  987.     const results = this.getFocusableResults();
  988.     if (results.length === 0) return;
  989.  
  990.     this.currentFocusIndex = this.currentFocusIndex <= 0 ? results.length - 1 : this.currentFocusIndex - 1;
  991.     results[this.currentFocusIndex].focus();
  992.   }
  993.  
  994.   /**
  995.    * Get all focusable results
  996.    */
  997.   getFocusableResults() {
  998.     const resultsContainer = document.getElementById('search-content-results');
  999.     if (!resultsContainer || resultsContainer.style.display === 'none') {
  1000.       return [];
  1001.     }
  1002.    
  1003.     return Array.from(resultsContainer.querySelectorAll('a[href], button:not([disabled])'));
  1004.   }
  1005.  
  1006.   /**
  1007.    * Load dynamic placeholders
  1008.    */
  1009.   async loadPlaceholders() {
  1010.     try {
  1011.       const response = await fetch(
  1012.         `${this.apiBaseUrl}/placeholders?projectId=${this.projectId}`,
  1013.         {
  1014.           headers: {
  1015.             'X-Tenant': this.projectId,
  1016.           },
  1017.         }
  1018.       );
  1019.  
  1020.       const data = await response.json();
  1021.  
  1022.       if (data.placeholders && data.placeholders.length > 0) {
  1023.         const input = document.getElementById('search-input');
  1024.         if (input) {
  1025.           input.placeholder = data.placeholders[0];
  1026.         }
  1027.       }
  1028.     } catch (error) {
  1029.       console.warn('Failed to load placeholders:', error);
  1030.     }
  1031.   }
  1032.  
  1033.   /**
  1034.    * Load recommendations for empty state
  1035.    */
  1036.   async loadRecommendations() {
  1037.     console.log('🚀 Starting to load recommendations...');
  1038.     console.log('API Base URL:', this.apiBaseUrl);
  1039.     console.log('Project ID:', this.projectId);
  1040.  
  1041.     try {
  1042.       const url = `${this.apiBaseUrl}/recommendations?projectId=${this.projectId}`;
  1043.       console.log('🔗 Fetching from URL:', url);
  1044.  
  1045.       const response = await fetch(url, {
  1046.         headers: {
  1047.           'X-Tenant': this.projectId,
  1048.         },
  1049.       });
  1050.       console.log('📡 Response status:', response.status);
  1051.       console.log('📡 Response ok:', response.ok);
  1052.  
  1053.       const data = await response.json();
  1054.       console.log('🔍 SEARCH RECOMMENDATIONS API - Data loaded:', data);
  1055.  
  1056.       return data;
  1057.     } catch (error) {
  1058.       console.error('❌ Failed to load recommendations:', error);
  1059.       return null;
  1060.     }
  1061.   }
  1062.  
  1063.   /**
  1064.    * Open search overlay
  1065.    */
  1066.   async open() {
  1067.     console.log('🚀 SEARCH OVERLAY - Opening search overlay...');
  1068.    
  1069.     // Store current focus to return later
  1070.     this.previousActiveElement = document.activeElement;
  1071.    
  1072.     // DataLayer: Search Started
  1073.     if (window.revelorDataLayer) {
  1074.       window.revelorDataLayer.searchStarted('overlay');
  1075.     }
  1076.    
  1077.     // Store start time for abandoned tracking
  1078.     this.searchStartTime = Date.now();
  1079.     this.lastSearchTerm = null;
  1080.     this.lastResultsCount = 0;
  1081.    
  1082.     this.isOpen = true;
  1083.     const overlay = document.getElementById('search-overlay');
  1084.     const input = document.getElementById('search-input');
  1085.  
  1086.     if (overlay) {
  1087.       overlay.classList.add('search-overlay--active');
  1088.       document.body.classList.add('search-overlay-open');
  1089.       console.log('🚀 SEARCH OVERLAY - Overlay activated');
  1090.     } else {
  1091.       console.log('❌ SEARCH OVERLAY - Overlay element not found!');
  1092.     }
  1093.  
  1094.     // Update aria-expanded on input
  1095.     if (input) {
  1096.       input.setAttribute('aria-expanded', 'true');
  1097.     }
  1098.  
  1099.     // Focus input after animation
  1100.     setTimeout(async () => {
  1101.       input?.focus();
  1102.       console.log('🚀 SEARCH OVERLAY - Input focused');
  1103.  
  1104.       // Show empty state with recommendations
  1105.       await this.showEmptyState();
  1106.     }, 100);
  1107.   }
  1108.  
  1109.   /**
  1110.    * Close search overlay
  1111.    */
  1112.   close() {
  1113.     // DataLayer: Search Abandoned (if there was a search term but no click)
  1114.     if (window.revelorDataLayer && this.lastSearchTerm && this.searchStartTime) {
  1115.       const timeSpent = ((Date.now() - this.searchStartTime) / 1000).toFixed(1);
  1116.       window.revelorDataLayer.searchAbandoned(
  1117.         this.lastSearchTerm,
  1118.         this.lastResultsCount,
  1119.         timeSpent,
  1120.         'closed_overlay'
  1121.       );
  1122.     }
  1123.    
  1124.     this.isOpen = false;
  1125.     const overlay = document.getElementById('search-overlay');
  1126.     const input = document.getElementById('search-input');
  1127.  
  1128.     if (overlay) {
  1129.       overlay.classList.remove('search-overlay--active');
  1130.       document.body.classList.remove('search-overlay-open');
  1131.     }
  1132.  
  1133.     // Update aria-expanded on input
  1134.     if (input) {
  1135.       input.setAttribute('aria-expanded', 'false');
  1136.     }
  1137.  
  1138.     // Return focus to previous element
  1139.     setTimeout(() => {
  1140.       if (this.previousActiveElement && this.previousActiveElement.focus) {
  1141.         this.previousActiveElement.focus();
  1142.       }
  1143.     }, 100);
  1144.  
  1145.     this.clearSearch();
  1146.   }
  1147.  
  1148.   /**
  1149.    * Clear search input and results
  1150.    */
  1151.   async clearSearch() {
  1152.     const input = document.getElementById('search-input');
  1153.  
  1154.     if (input) {
  1155.       input.value = '';
  1156.       input.focus();
  1157.     }
  1158.  
  1159.     await this.showEmptyState();
  1160.   }
  1161.  
  1162.   /**
  1163.    * Get current search query
  1164.    */
  1165.   getCurrentQuery() {
  1166.     const input = document.getElementById('search-input');
  1167.     return input ? input.value : '';
  1168.   }
  1169.  
  1170.   /**
  1171.    * Format price with dot as thousands separator and remove unnecessary decimals
  1172.    */
  1173.   formatPrice(price) {
  1174.     if (!price) return '';
  1175.  
  1176.     // Convert to number
  1177.     const numPrice = parseFloat(price);
  1178.  
  1179.     // Format as integer (bez desetinných míst) s tečkou jako oddělovač tisíců
  1180.     return Math.floor(numPrice).toLocaleString('cs-CZ', {
  1181.       minimumFractionDigits: 0,
  1182.       maximumFractionDigits: 0,
  1183.     });
  1184.   }
  1185.  
  1186.   /**
  1187.    * Redirect to search results page
  1188.    */
  1189.   redirectToSearchResults(query) {
  1190.     const url = `/vyhledavani/?string=${encodeURIComponent(query)}`;
  1191.     console.log('Redirecting to search results:', url);
  1192.  
  1193.     // Create temporary link and click it
  1194.     const tempLink = document.createElement('a');
  1195.     tempLink.href = url;
  1196.     tempLink.style.display = 'none';
  1197.     document.body.appendChild(tempLink);
  1198.     tempLink.click();
  1199.     document.body.removeChild(tempLink);
  1200.   }
  1201.  
  1202.   /**
  1203.    * Add product to cart (Shoptet API) - for Search Overlay
  1204.    * @param {Object} product - Product data
  1205.    * @param {HTMLElement} button - Button element
  1206.    */
  1207.   async addToCart(product, button) {
  1208.     const originalHTML = button.innerHTML;
  1209.    
  1210.     // Show loading state
  1211.     button.disabled = true;
  1212.     button.classList.add('loading');
  1213.     button.style.opacity = '0.5';
  1214.    
  1215.     try {
  1216.       // Check if Shoptet API is available
  1217.       if (typeof shoptet === 'undefined' || !shoptet.cartShared) {
  1218.         throw new Error('Shoptet API není k dispozici');
  1219.       }
  1220.      
  1221.       // Determine which method to use (podle Shoptet dokumentace)
  1222.       let addParams = {};
  1223.      
  1224.       if (product.priceId) {
  1225.         // Nejlepší varianta: priceId
  1226.         addParams = { priceId: product.priceId };
  1227.       } else if (product.kod || product.code) {
  1228.         // Fallback: product code
  1229.         addParams = { productCode: product.kod || product.code };
  1230.       } else if (product._id || product.id) {
  1231.         // Poslední varianta: product ID
  1232.         addParams = { productId: product._id || product.id };
  1233.       } else {
  1234.         throw new Error('Nelze identifikovat produkt');
  1235.       }
  1236.      
  1237.       // Add to cart (silent mode = true to prevent Shoptet's modal)
  1238.       await shoptet.cartShared.addToCart(addParams, true);
  1239.      
  1240.       // Success feedback
  1241.       button.classList.remove('loading');
  1242.       button.classList.add('success');
  1243.       button.style.opacity = '1';
  1244.      
  1245.       // TODO: Add toast notification if available
  1246.      
  1247.       // Tracking
  1248.       if (window.revelorDataLayer) {
  1249.         window.dataLayer.push({
  1250.           event: 'revelor_quick_add_to_cart',
  1251.           product_id: product._id || product.id,
  1252.           product_name: product.title,
  1253.           price: product.price_s_dph,
  1254.           source: 'search_overlay'
  1255.         });
  1256.       }
  1257.      
  1258.       // Reset button after 2s
  1259.       setTimeout(() => {
  1260.         button.disabled = false;
  1261.         button.classList.remove('success');
  1262.         button.style.opacity = '';
  1263.       }, 2000);
  1264.      
  1265.     } catch (error) {
  1266.       console.error('Add to Cart Error:', error);
  1267.       button.innerHTML = originalHTML;
  1268.       button.disabled = false;
  1269.       button.classList.remove('loading');
  1270.       button.style.opacity = '';
  1271.       // TODO: Add error notification
  1272.     }
  1273.   }
  1274.  
  1275.   /**
  1276.    * Handle search input
  1277.    */
  1278.   async handleSearch(query) {
  1279.     if (query.length < this.minSearchLength) {
  1280.       await this.showEmptyState();
  1281.       return;
  1282.     }
  1283.  
  1284.     // Only show loading if no results are currently displayed
  1285.     const resultsContainer = document.getElementById('search-content-results');
  1286.     const isShowingResults =
  1287.       resultsContainer && resultsContainer.style.display !== 'none';
  1288.  
  1289.     if (!isShowingResults) {
  1290.       this.showLoadingState();
  1291.     }
  1292.  
  1293.     // Perform search immediately
  1294.     this.performSearch(query);
  1295.   }
  1296.  
  1297.   /**
  1298.    * Perform search using API
  1299.    */
  1300.   async performSearch(query) {
  1301.     const searchUrl = `${this.apiBaseUrl}/suggestions?q=${encodeURIComponent(query)}&projectId=${this.projectId}&products_limit=6&categories_limit=6&articles_limit=6&brands_limit=0`;
  1302.  
  1303.     // DataLayer: Search Query Submitted
  1304.     if (window.revelorDataLayer) {
  1305.       window.revelorDataLayer.searchQuerySubmitted(query, 'overlay');
  1306.     }
  1307.    
  1308.     // Store for refinement tracking
  1309.     this.lastSearchTerm = query;
  1310.    
  1311.     // Check cache first
  1312.     const cacheKey = `${query}_${this.projectId}_suggestions`;
  1313.     const cached = this.getFromCache(cacheKey);
  1314.     if (cached) {
  1315.       console.log('📦 Using cached results for:', query);
  1316.       this.displayResults(cached, 0);
  1317.       return;
  1318.     }
  1319.    
  1320.     // Cancel previous request
  1321.     if (this.abortController) {
  1322.       this.abortController.abort();
  1323.     }
  1324.     this.abortController = new AbortController();
  1325.    
  1326.     const startTime = Date.now();
  1327.  
  1328.     try {
  1329.       const response = await this.fetchWithRetry(searchUrl, {
  1330.         signal: this.abortController.signal,
  1331.         headers: {
  1332.           'X-Tenant': this.projectId,
  1333.         },
  1334.       });
  1335.  
  1336.       if (!response.ok) {
  1337.         throw new Error(`HTTP error! status: ${response.status}`);
  1338.       }
  1339.  
  1340.       const data = await response.json();
  1341.       const responseTime = Date.now() - startTime;
  1342.      
  1343.       // Save to cache
  1344.       this.saveToCache(cacheKey, data);
  1345.      
  1346.       // Add to search history if has results
  1347.       const hasResults = data.products?.length > 0 || data.categories?.length > 0 || data.articles?.length > 0;
  1348.       if (hasResults) {
  1349.         this.addToSearchHistory(query);
  1350.       }
  1351.      
  1352.       this.displayResults(data, responseTime);
  1353.     } catch (error) {
  1354.       if (error.name === 'AbortError') {
  1355.         console.log('🚫 Request cancelled for:', query);
  1356.         return;
  1357.       }
  1358.       console.error('Search API Error:', error);
  1359.      
  1360.       // Try to use cached results as fallback
  1361.       const fallbackCached = this.getFromCache(cacheKey, true);
  1362.       if (fallbackCached) {
  1363.         console.log('📦 Using stale cache as fallback');
  1364.         this.displayResults(fallbackCached, 0);
  1365.         this.announceToScreenReader('Zobrazuji uložené výsledky (offline)');
  1366.         return;
  1367.       }
  1368.      
  1369.       this.showErrorState();
  1370.     }
  1371.   }
  1372.  
  1373.   /**
  1374.    * Save to cache
  1375.    */
  1376.   saveToCache(key, data) {
  1377.     this.cache.set(key, {
  1378.       data,
  1379.       timestamp: Date.now()
  1380.     });
  1381.   }
  1382.  
  1383.   /**
  1384.    * Get from cache
  1385.    */
  1386.   getFromCache(key, ignoreExpiry = false) {
  1387.     const cached = this.cache.get(key);
  1388.     if (!cached) return null;
  1389.    
  1390.     // Check TTL (unless ignoreExpiry for fallback)
  1391.     if (!ignoreExpiry && Date.now() - cached.timestamp > this.cacheTTL) {
  1392.       this.cache.delete(key);
  1393.       return null;
  1394.     }
  1395.    
  1396.     return cached.data;
  1397.   }
  1398.  
  1399.   /**
  1400.    * Announce message to screen readers
  1401.    */
  1402.   announceToScreenReader(message) {
  1403.     const liveRegion = document.getElementById('search-live-region');
  1404.     if (liveRegion) {
  1405.       liveRegion.textContent = message;
  1406.       // Clear after announcement
  1407.       setTimeout(() => {
  1408.         liveRegion.textContent = '';
  1409.       }, 1000);
  1410.     }
  1411.   }
  1412.  
  1413.   /**
  1414.    * Display search results
  1415.    */
  1416.   displayResults(data, responseTime = 0) {
  1417.     this.hideAllStates();
  1418.  
  1419.     const hasResults =
  1420.       data.products?.length > 0 ||
  1421.       data.categories?.length > 0 ||
  1422.       data.articles?.length > 0;
  1423.  
  1424.     if (!hasResults) {
  1425.       this.showNoResultsState();
  1426.       this.announceToScreenReader('Žádné výsledky nebyly nalezeny');
  1427.      
  1428.       // DataLayer: Search No Results
  1429.       if (window.revelorDataLayer && this.lastSearchTerm) {
  1430.         window.revelorDataLayer.searchNoResults(this.lastSearchTerm);
  1431.       }
  1432.      
  1433.       this.lastResultsCount = 0;
  1434.       return;
  1435.     }
  1436.  
  1437.     this.showResultsState();
  1438.  
  1439.     // Count results
  1440.     const totalResults =
  1441.       (data.products?.length || 0) +
  1442.       (data.categories?.length || 0) +
  1443.       (data.articles?.length || 0);
  1444.    
  1445.     // Store for abandoned tracking
  1446.     this.lastResultsCount = totalResults;
  1447.    
  1448.     // Announce results
  1449.     const resultTypes = [];
  1450.     if (data.products?.length) resultTypes.push(`${data.products.length} ${data.products.length === 1 ? 'produkt' : 'produkty'}`);
  1451.     if (data.categories?.length) resultTypes.push(`${data.categories.length} ${data.categories.length === 1 ? 'kategorie' : 'kategorie'}`);
  1452.     if (data.articles?.length) resultTypes.push(`${data.articles.length} ${data.articles.length === 1 ? 'článek' : 'články'}`);
  1453.    
  1454.     this.announceToScreenReader(`Nalezeno ${totalResults} výsledků: ${resultTypes.join(', ')}`);
  1455.  
  1456.     // DataLayer: Search Results Displayed
  1457.     if (window.revelorDataLayer && this.lastSearchTerm) {
  1458.       const resultsData = {
  1459.         products: data.products || [],
  1460.         categories: data.categories || [],
  1461.         articles: data.articles || [],
  1462.         total_products: totalResults
  1463.       };
  1464.       window.revelorDataLayer.searchResultsDisplayed(
  1465.         this.lastSearchTerm,
  1466.         resultsData,
  1467.         responseTime,
  1468.         1
  1469.       );
  1470.     }
  1471.  
  1472.     // Backend API: Track search event
  1473.     if (window.searchTracker && this.lastSearchTerm && this.lastSearchTerm.length >= 3) {
  1474.       window.searchTracker.trackSearch({
  1475.         query: this.lastSearchTerm,
  1476.         results_count: totalResults,
  1477.         search_type: 'live_search'
  1478.       });
  1479.     }
  1480.  
  1481.     // Display products
  1482.     if (data.products?.length > 0) {
  1483.       this.displayProducts(data.products);
  1484.     }
  1485.  
  1486.     // Display categories
  1487.     if (data.categories?.length > 0) {
  1488.       this.displayCategories(data.categories);
  1489.     }
  1490.  
  1491.     // Display articles
  1492.     if (data.articles?.length > 0) {
  1493.       this.displayArticles(data.articles);
  1494.     }
  1495.  
  1496.     // Reset focus index
  1497.     this.currentFocusIndex = -1;
  1498.   }
  1499.  
  1500.   /**
  1501.    * Display products
  1502.    */
  1503.   displayProducts(products) {
  1504.     const container = document.getElementById('search-products');
  1505.     const list = document.getElementById('search-products-list');
  1506.     const showAll = document.getElementById('search-products-show-all');
  1507.  
  1508.     if (!container || !list) return;
  1509.  
  1510.     container.style.display = 'block';
  1511.     list.innerHTML = '';
  1512.  
  1513.     products.forEach((product, index) => {
  1514.       const item = document.createElement('div');
  1515.       item.className = 'search-overlay__product';
  1516.       item.setAttribute('role', 'listitem');
  1517.      
  1518.       // Microdata attributes for AI agents
  1519.       item.setAttribute('itemscope', '');
  1520.       item.setAttribute('itemtype', 'https://schema.org/Product');
  1521.      
  1522.       const priceText = product.price_s_dph ? `${this.formatPrice(product.price_s_dph)} Kč` : 'Cena na dotaz';
  1523.       const availabilityText = product.skladem ? 'Skladem' : 'Není skladem';
  1524.       const availabilitySchema = product.skladem ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock';
  1525.      
  1526.       // Create JSON-LD structured data
  1527.       const jsonLd = {
  1528.         "@context": "https://schema.org/",
  1529.         "@type": "Product",
  1530.         "@id": product._id,
  1531.         "name": product.title,
  1532.         "description": product.description || product.title,
  1533.         "image": product.img,
  1534.         "url": product.url,
  1535.         "sku": product.kod,
  1536.         "offers": {
  1537.           "@type": "Offer",
  1538.           "price": product.price_s_dph || 0,
  1539.           "priceCurrency": "CZK",
  1540.           "availability": availabilitySchema,
  1541.           "url": product.url
  1542.         }
  1543.       };
  1544.      
  1545.       item.innerHTML = `
  1546.                 <!-- JSON-LD for AI agents -->
  1547.                 <script type="application/ld+json">
  1548.                   ${JSON.stringify(jsonLd)}
  1549.                 </script>
  1550.                
  1551.                 <a href="${product.url}" class="search-overlay__product-link" itemprop="url" aria-label="Produkt ${product.title}, ${priceText}, ${availabilityText}" data-product-index="${index}">
  1552.                     <meta itemprop="sku" content="${product.kod || 'N/A'}">
  1553.                     <link itemprop="availability" href="${availabilitySchema}">
  1554.                    
  1555.                     <div class="search-overlay__product-image" aria-hidden="true">
  1556.                         ${product.img ? `<img src="${product.img}" alt="" itemprop="image" loading="lazy">` : ''}
  1557.                     </div>
  1558.                     <div class="search-overlay__product-content">
  1559.                         <h4 class="search-overlay__product-title" itemprop="name">${this.highlightSearchTerm(product.title, this.lastSearchTerm)}</h4>
  1560.                         <div class="search-overlay__product-price" itemprop="offers" itemscope itemtype="https://schema.org/Offer" aria-label="Cena ${priceText}">
  1561.                           <meta itemprop="priceCurrency" content="CZK">
  1562.                           <meta itemprop="price" content="${product.price_s_dph || 0}">
  1563.                           ${product.price_s_dph ? this.formatPrice(product.price_s_dph) + ' Kč' : 'Cena na dotaz'}
  1564.                         </div>
  1565.                     </div>
  1566.                 </a>
  1567.                 ${product.skladem && !product.has_variants ? `
  1568.                   <button type="button" class="search-overlay__product-add-btn" aria-label="Přidat ${product.title} do košíku" data-product-id="${product._id}">
  1569.                     <svg width="24" height="24" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><g><path d="m23 24c0 1.1046-.8954 2-2 2s-2-.8954-2-2v-4h-2c-2.0146 0-3.7078 1.4684-3.9942 3.4258l-3.96123 29.9876-.00311.0226c-.34428 2.3956 1.52644 4.564 3.99564 4.564h27.2443c1.1045 0 2 .8954 2 2s-.8955 2-2 2h-27.2443c-4.87313 0-8.64334-4.2923-7.95669-9.121l3.96151-29.9894.00312-.0226c.56757-3.9493 3.96546-6.867 7.95496-6.867h2v-2c0-6.62742 5.3726-12 12-12s12 5.37258 12 12v2h2c3.9867 0 7.3826 2.9135 7.9537 6.8586l2.0237 13.3415c.1656 1.0921-.5854 2.1116-1.6775 2.2773-1.092.1656-2.1116-.5854-2.2773-1.6775l-2.0247-13.3485-.0023-.0154c-.282-1.9625-1.9775-3.436-3.9956-3.436h-2v4c0 1.1046-.8954 2-2 2s-2-.8954-2-2v-4h-16zm0-8h16v-2c0-4.41828-3.5817-8-8-8s-8 3.58172-8 8z" clip-rule="evenodd" fill-rule="evenodd"></path><path d="m51 42c1.1046 0 2 .8954 2 2v6h6c1.1046 0 2 .8954 2 2s-.8954 2-2 2h-6v6c0 1.1046-.8954 2-2 2s-2-.8954-2-2v-6h-6c-1.1046 0-2-.8954-2-2s.8954-2 2-2h6v-6c0-1.1046.8954-2 2-2z"></path></g></svg>
  1570.                   </button>
  1571.                 ` : ''}
  1572.             `;
  1573.      
  1574.       // Add click event for tracking
  1575.       const link = item.querySelector('.search-overlay__product-link');
  1576.       if (link) {
  1577.         link.addEventListener('click', () => {
  1578.           // DataLayer: Search Result Click
  1579.           if (window.revelorDataLayer && this.lastSearchTerm) {
  1580.             window.revelorDataLayer.searchResultClick(
  1581.               this.lastSearchTerm,
  1582.               'product',
  1583.               product,
  1584.               index + 1,
  1585.               this.lastResultsCount
  1586.             );
  1587.           }
  1588.  
  1589.           // Backend API: Track click event
  1590.           if (window.searchTracker && this.lastSearchTerm) {
  1591.             window.searchTracker.trackClick({
  1592.               clicked_result: product._id || product.id,
  1593.               product_id: product._id || product.id,
  1594.               result_type: 'product',
  1595.               result_title: product.title,
  1596.               result_url: product.url,
  1597.               result_position: index + 1,
  1598.               query: this.lastSearchTerm
  1599.             });
  1600.           }
  1601.         });
  1602.       }
  1603.  
  1604.       // Add click event for "Add to Cart" button
  1605.       const addBtn = item.querySelector('.search-overlay__product-add-btn');
  1606.       if (addBtn) {
  1607.         addBtn.addEventListener('click', async (e) => {
  1608.           e.preventDefault();
  1609.           e.stopPropagation();
  1610.           await this.addToCart(product, addBtn);
  1611.         });
  1612.       }
  1613.  
  1614.       list.appendChild(item);
  1615.     });
  1616.  
  1617.     if (showAll) {
  1618.       showAll.style.display = 'block';
  1619.       // Set up click handler for "Show all" link
  1620.       const showAllLink = showAll.querySelector(
  1621.         '.search-overlay__show-all-link'
  1622.       );
  1623.       if (showAllLink) {
  1624.         showAllLink.addEventListener('click', e => {
  1625.           e.preventDefault();
  1626.           e.stopPropagation();
  1627.           const url = `/vyhledavani/?string=${encodeURIComponent(this.getCurrentQuery())}`;
  1628.           console.log('Redirecting to:', url);
  1629.  
  1630.           // Create temporary link and click it
  1631.           const tempLink = document.createElement('a');
  1632.           tempLink.href = url;
  1633.           tempLink.style.display = 'none';
  1634.           document.body.appendChild(tempLink);
  1635.           tempLink.click();
  1636.           document.body.removeChild(tempLink);
  1637.  
  1638.           return false;
  1639.         });
  1640.       }
  1641.     }
  1642.   }
  1643.  
  1644.   /**
  1645.    * Display categories
  1646.    */
  1647.   displayCategories(categories) {
  1648.     const container = document.getElementById('search-categories');
  1649.     const list = document.getElementById('search-categories-list');
  1650.     const showAll = document.getElementById('search-categories-show-all');
  1651.  
  1652.     if (!container || !list) return;
  1653.  
  1654.     container.style.display = 'block';
  1655.     list.innerHTML = '';
  1656.  
  1657.     categories.forEach((category, index) => {
  1658.       const item = document.createElement('div');
  1659.       item.className = 'search-overlay__category';
  1660.       item.setAttribute('role', 'listitem');
  1661.       item.innerHTML = `
  1662.                 <a href="${category.url}" class="search-overlay__category-link" aria-label="Kategorie ${category.title}">
  1663.                     <div class="search-overlay__category-content">
  1664.                         <h4 class="search-overlay__category-title">${category.title}</h4>
  1665.                     </div>
  1666.                 </a>
  1667.             `;
  1668.  
  1669.       // Add click tracking for category
  1670.       const link = item.querySelector('.search-overlay__category-link');
  1671.       if (link && window.searchTracker && this.lastSearchTerm) {
  1672.         link.addEventListener('click', () => {
  1673.           window.searchTracker.trackClick({
  1674.             clicked_result: category._id || category.id || category.url,
  1675.             result_type: 'category',
  1676.             result_title: category.title,
  1677.             result_url: category.url,
  1678.             result_position: index + 1,
  1679.             query: this.lastSearchTerm
  1680.           });
  1681.         });
  1682.       }
  1683.  
  1684.       list.appendChild(item);
  1685.     });
  1686.  
  1687.     if (showAll) {
  1688.       showAll.style.display = 'block';
  1689.       // Set up click handler for "Show all" link
  1690.       const showAllLink = showAll.querySelector(
  1691.         '.search-overlay__show-all-link'
  1692.       );
  1693.       if (showAllLink) {
  1694.         showAllLink.addEventListener('click', e => {
  1695.           e.preventDefault();
  1696.           e.stopPropagation();
  1697.           const url = `/vyhledavani/?string=${encodeURIComponent(this.getCurrentQuery())}`;
  1698.           console.log('Redirecting to:', url);
  1699.  
  1700.           // Create temporary link and click it
  1701.           const tempLink = document.createElement('a');
  1702.           tempLink.href = url;
  1703.           tempLink.style.display = 'none';
  1704.           document.body.appendChild(tempLink);
  1705.           tempLink.click();
  1706.           document.body.removeChild(tempLink);
  1707.  
  1708.           return false;
  1709.         });
  1710.       }
  1711.     }
  1712.   }
  1713.  
  1714.   /**
  1715.    * Display articles
  1716.    */
  1717.   displayArticles(articles) {
  1718.     const container = document.getElementById('search-articles');
  1719.     const list = document.getElementById('search-articles-list');
  1720.     const showAll = document.getElementById('search-articles-show-all');
  1721.  
  1722.     if (!container || !list) return;
  1723.  
  1724.     container.style.display = 'block';
  1725.     list.innerHTML = '';
  1726.  
  1727.     articles.forEach((article, index) => {
  1728.       const item = document.createElement('div');
  1729.       item.className = 'search-overlay__article';
  1730.       item.setAttribute('role', 'listitem');
  1731.      
  1732.       const description = article.description || '';
  1733.       const ariaLabel = description ? `Článek ${article.title}: ${description}` : `Článek ${article.title}`;
  1734.      
  1735.       item.innerHTML = `
  1736.                 <a href="${article.url}" class="search-overlay__article-link" aria-label="${ariaLabel}">
  1737.                     <div class="search-overlay__article-image" aria-hidden="true">
  1738.                         ${article.img ? `<img src="${article.img}" alt="" loading="lazy">` : ''}
  1739.                     </div>
  1740.                     <div class="search-overlay__article-content">
  1741.                         <h4 class="search-overlay__article-title">${article.title}</h4>
  1742.                         ${description ? `<p class="search-overlay__article-description">${description}</p>` : ''}
  1743.                     </div>
  1744.                 </a>
  1745.             `;
  1746.  
  1747.       // Add click tracking for article
  1748.       const link = item.querySelector('.search-overlay__article-link');
  1749.       if (link && window.searchTracker && this.lastSearchTerm) {
  1750.         link.addEventListener('click', () => {
  1751.           window.searchTracker.trackClick({
  1752.             clicked_result: article._id || article.id || article.url,
  1753.             result_type: 'article',
  1754.             result_title: article.title,
  1755.             result_url: article.url,
  1756.             result_position: index + 1,
  1757.             query: this.lastSearchTerm
  1758.           });
  1759.         });
  1760.       }
  1761.  
  1762.       list.appendChild(item);
  1763.     });
  1764.  
  1765.     if (showAll) {
  1766.       showAll.style.display = 'block';
  1767.       // Set up click handler for "Show all" link
  1768.       const showAllLink = showAll.querySelector(
  1769.         '.search-overlay__show-all-link'
  1770.       );
  1771.       if (showAllLink) {
  1772.         showAllLink.addEventListener('click', e => {
  1773.           e.preventDefault();
  1774.           e.stopPropagation();
  1775.           const url = `/vyhledavani/?string=${encodeURIComponent(this.getCurrentQuery())}`;
  1776.           console.log('Redirecting to:', url);
  1777.  
  1778.           // Create temporary link and click it
  1779.           const tempLink = document.createElement('a');
  1780.           tempLink.href = url;
  1781.           tempLink.style.display = 'none';
  1782.           document.body.appendChild(tempLink);
  1783.           tempLink.click();
  1784.           document.body.removeChild(tempLink);
  1785.  
  1786.           return false;
  1787.         });
  1788.       }
  1789.     }
  1790.   }
  1791.  
  1792.   /**
  1793.    * Display brands
  1794.    */
  1795.   displayBrands(brands) {
  1796.     const container = document.getElementById('search-brands');
  1797.     const list = document.getElementById('search-brands-list');
  1798.     const showAll = document.getElementById('search-brands-show-all');
  1799.  
  1800.     if (!container || !list) return;
  1801.  
  1802.     container.style.display = 'block';
  1803.     list.innerHTML = '';
  1804.  
  1805.     brands.forEach((brand, index) => {
  1806.       console.log('Brand data:', brand); // Debug log
  1807.       const item = document.createElement('div');
  1808.       item.className = 'search-overlay__brand';
  1809.  
  1810.       if (brand.url) {
  1811.         // Brand má URL - vytvoř proklik
  1812.         item.innerHTML = `
  1813.                   <a href="${brand.url}" class="search-overlay__brand-link">
  1814.                       <div class="search-overlay__brand-content">
  1815.                           <h4 class="search-overlay__brand-title">${brand.title}</h4>
  1816.                           <p class="search-overlay__brand-description">${brand.description || ''}</p>
  1817.                       </div>
  1818.                   </a>
  1819.               `;
  1820.        
  1821.         // Add click tracking for brand (treat as category for now)
  1822.         const link = item.querySelector('.search-overlay__brand-link');
  1823.         if (link && window.searchTracker && this.lastSearchTerm) {
  1824.           link.addEventListener('click', () => {
  1825.             window.searchTracker.trackClick({
  1826.               clicked_result: brand._id || brand.id || brand.url,
  1827.               result_type: 'category', // Brands are tracked as categories
  1828.               result_title: brand.title,
  1829.               result_url: brand.url,
  1830.               result_position: index + 1,
  1831.               query: this.lastSearchTerm
  1832.             });
  1833.           });
  1834.         }
  1835.       } else {
  1836.         // Brand nemá URL - zobraz bez prokliku
  1837.         item.innerHTML = `
  1838.                   <div class="search-overlay__brand-content">
  1839.                       <h4 class="search-overlay__brand-title">${brand.title}</h4>
  1840.                       <p class="search-overlay__brand-description">${brand.description || ''}</p>
  1841.                   </div>
  1842.               `;
  1843.       }
  1844.  
  1845.       list.appendChild(item);
  1846.     });
  1847.  
  1848.     if (showAll) {
  1849.       showAll.style.display = 'block';
  1850.       // Set up click handler for "Show all" link
  1851.       const showAllLink = showAll.querySelector(
  1852.         '.search-overlay__show-all-link'
  1853.       );
  1854.       if (showAllLink) {
  1855.         showAllLink.addEventListener('click', e => {
  1856.           e.preventDefault();
  1857.           e.stopPropagation();
  1858.           const url = `/vyhledavani/?string=${encodeURIComponent(this.getCurrentQuery())}`;
  1859.           console.log('Redirecting to:', url);
  1860.  
  1861.           // Create temporary link and click it
  1862.           const tempLink = document.createElement('a');
  1863.           tempLink.href = url;
  1864.           tempLink.style.display = 'none';
  1865.           document.body.appendChild(tempLink);
  1866.           tempLink.click();
  1867.           document.body.removeChild(tempLink);
  1868.  
  1869.           return false;
  1870.         });
  1871.       }
  1872.     }
  1873.   }
  1874.  
  1875.   /**
  1876.    * Show loading state
  1877.    */
  1878.   showLoadingState() {
  1879.     this.hideAllStates();
  1880.     const loading = document.getElementById('search-loading');
  1881.     if (loading) loading.style.display = 'block';
  1882.   }
  1883.  
  1884.   /**
  1885.    * Show empty state
  1886.    */
  1887.   async showEmptyState() {
  1888.     console.log('📋 SEARCH EMPTY STATE - Starting showEmptyState()');
  1889.     this.hideAllStates();
  1890.     const empty = document.getElementById('search-empty');
  1891.     if (empty) {
  1892.       empty.style.display = 'block';
  1893.       console.log('📋 SEARCH EMPTY STATE - Empty element displayed');
  1894.      
  1895.       // Show search history if available
  1896.       this.displaySearchHistory();
  1897.     } else {
  1898.       console.log('❌ SEARCH EMPTY STATE - Empty element not found!');
  1899.     }
  1900.  
  1901.     // Load recommendations for empty state
  1902.     console.log('📋 SEARCH EMPTY STATE - About to load recommendations...');
  1903.     const recommendations = await this.loadRecommendations();
  1904.     if (recommendations) {
  1905.       console.log(
  1906.         '📋 SEARCH EMPTY STATE - Recommendations loaded:',
  1907.         recommendations
  1908.       );
  1909.       this.displayRecommendations(recommendations);
  1910.     } else {
  1911.       console.log('❌ SEARCH EMPTY STATE - No recommendations received');
  1912.     }
  1913.   }
  1914.  
  1915.   /**
  1916.    * Add search term to history
  1917.    */
  1918.   addToSearchHistory(query) {
  1919.     if (!query || query.length < 2) return;
  1920.    
  1921.     let history = this.getSearchHistory();
  1922.    
  1923.     // Remove duplicates and partial matches
  1924.     // If new query is part of existing query or vice versa, keep the longer one
  1925.     const mergedQuery = query;
  1926.     history = history.filter(item => {
  1927.       const existing = item.query.toLowerCase();
  1928.       const newQuery = query.toLowerCase();
  1929.      
  1930.       // If new query is longer and contains existing, remove existing
  1931.       if (newQuery.length > existing.length && newQuery.includes(existing)) {
  1932.         return false;
  1933.       }
  1934.       // If existing is longer and contains new query, keep existing (don't add new)
  1935.       if (existing.length > newQuery.length && existing.includes(newQuery)) {
  1936.         return false;
  1937.       }
  1938.       // If they're identical, remove existing (will add new)
  1939.       if (existing === newQuery) {
  1940.         return false;
  1941.       }
  1942.       return true;
  1943.     });
  1944.    
  1945.     // Add to beginning
  1946.     history.unshift({
  1947.       query,
  1948.       timestamp: Date.now(),
  1949.       count: 1
  1950.     });
  1951.    
  1952.     // Limit to maxHistoryItems
  1953.     history = history.slice(0, this.maxHistoryItems);
  1954.    
  1955.     localStorage.setItem(this.historyKey, JSON.stringify(history));
  1956.   }
  1957.  
  1958.   /**
  1959.    * Get search history from localStorage
  1960.    */
  1961.   getSearchHistory() {
  1962.     try {
  1963.       return JSON.parse(localStorage.getItem(this.historyKey) || '[]');
  1964.     } catch {
  1965.       return [];
  1966.     }
  1967.   }
  1968.  
  1969.   /**
  1970.    * Display search history in empty state
  1971.    */
  1972.   displaySearchHistory() {
  1973.     const history = this.getSearchHistory();
  1974.     if (history.length === 0) return;
  1975.    
  1976.     const empty = document.getElementById('search-empty');
  1977.     if (!empty) return;
  1978.    
  1979.     // Check if history is already displayed
  1980.     if (empty.querySelector('.search-history')) return;
  1981.    
  1982.     const historyHTML = `
  1983.       <div class="search-history">
  1984.         <h4 class="search-history__title">Nedávné hledání</h4>
  1985.         <div class="search-history__items">
  1986.           ${history.map(item => `
  1987.             <button type="button"
  1988.                     class="search-history__item"
  1989.                     data-query="${this.escapeHtml(item.query)}"
  1990.                     aria-label="Hledat ${item.query}">
  1991.               <span class="search-history__text">${this.escapeHtml(item.query)}</span>
  1992.             </button>
  1993.           `).join('')}
  1994.         </div>
  1995.       </div>
  1996.     `;
  1997.    
  1998.     empty.insertAdjacentHTML('beforeend', historyHTML);
  1999.    
  2000.     // Bind click events
  2001.     empty.querySelectorAll('.search-history__item').forEach(button => {
  2002.       button.addEventListener('click', () => {
  2003.         const query = button.dataset.query;
  2004.         const input = document.getElementById('search-input');
  2005.         if (input) {
  2006.           input.value = query;
  2007.           this.handleSearch(query);
  2008.         }
  2009.       });
  2010.     });
  2011.   }
  2012.  
  2013.   /**
  2014.    * Escape HTML to prevent XSS
  2015.    */
  2016.   escapeHtml(text) {
  2017.     const div = document.createElement('div');
  2018.     div.textContent = text;
  2019.     return div.innerHTML;
  2020.   }
  2021.  
  2022.   /**
  2023.    * Highlight search term in text
  2024.    */
  2025.   highlightSearchTerm(text, searchTerm) {
  2026.     if (!searchTerm || !text) return this.escapeHtml(text);
  2027.    
  2028.     const escaped = this.escapeHtml(text);
  2029.     const escapedTerm = this.escapeHtml(searchTerm);
  2030.     const regex = new RegExp(`(${escapedTerm})`, 'gi');
  2031.    
  2032.     return escaped.replace(regex, '<mark class="search-highlight">$1</mark>');
  2033.   }
  2034.  
  2035.   /**
  2036.    * Fetch with retry logic (3 attempts with exponential backoff)
  2037.    */
  2038.   async fetchWithRetry(url, options = {}, retries = 3) {
  2039.     for (let i = 0; i < retries; i++) {
  2040.       try {
  2041.         const response = await fetch(url, options);
  2042.        
  2043.         if (!response.ok && i < retries - 1) {
  2044.           // Wait before retry (exponential backoff: 1s, 2s, 4s)
  2045.           await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
  2046.           continue;
  2047.         }
  2048.        
  2049.         return response;
  2050.       } catch (error) {
  2051.         // If it's AbortError, don't retry
  2052.         if (error.name === 'AbortError') {
  2053.           throw error;
  2054.         }
  2055.        
  2056.         // Last attempt - throw error
  2057.         if (i === retries - 1) {
  2058.           throw error;
  2059.         }
  2060.        
  2061.         // Wait before retry
  2062.         await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
  2063.       }
  2064.     }
  2065.   }
  2066.  
  2067.   /**
  2068.    * Display recommendations in empty state
  2069.    */
  2070.   displayRecommendations(data) {
  2071.     console.log(
  2072.       '📋 RECOMMENDATIONS - Displaying recommendations using existing format'
  2073.     );
  2074.  
  2075.     // Check if we have any recommendations
  2076.     const hasProducts = data.products && data.products.length > 0;
  2077.     const hasCategories = data.categories && data.categories.length > 0;
  2078.     const hasArticles = data.articles && data.articles.length > 0;
  2079.     const hasBrands = data.brands && data.brands.length > 0;
  2080.  
  2081.     if (!hasProducts && !hasCategories && !hasArticles && !hasBrands) {
  2082.       console.log('📋 RECOMMENDATIONS - No recommendations to display');
  2083.       return;
  2084.     }
  2085.  
  2086.     // DataLayer: Search Recommendations Shown
  2087.     if (window.revelorDataLayer) {
  2088.       window.revelorDataLayer.searchRecommendationsShown(data);
  2089.     }
  2090.  
  2091.     // Hide empty state and show results state
  2092.     this.hideAllStates();
  2093.     this.showResultsState();
  2094.  
  2095.     // Display products using existing method
  2096.     if (hasProducts) {
  2097.       this.displayProducts(data.products);
  2098.     }
  2099.  
  2100.     // Display categories using existing method
  2101.     if (hasCategories) {
  2102.       this.displayCategories(data.categories);
  2103.     }
  2104.  
  2105.     // Display articles using existing method
  2106.     if (hasArticles) {
  2107.       this.displayArticles(data.articles);
  2108.     }
  2109.  
  2110.     // Display brands using existing method
  2111.     if (hasBrands) {
  2112.       this.displayBrands(data.brands);
  2113.     }
  2114.  
  2115.     console.log(
  2116.       '✅ RECOMMENDATIONS - Displayed successfully using existing format'
  2117.     );
  2118.   }
  2119.  
  2120.   /**
  2121.    * Show no results state
  2122.    */
  2123.   showNoResultsState() {
  2124.     this.hideAllStates();
  2125.     const noResults = document.getElementById('search-no-results');
  2126.     if (noResults) noResults.style.display = 'block';
  2127.   }
  2128.  
  2129.   /**
  2130.    * Show results state
  2131.    */
  2132.   showResultsState() {
  2133.     this.hideAllStates();
  2134.     const results = document.getElementById('search-content-results');
  2135.     if (results) results.style.display = 'block';
  2136.   }
  2137.  
  2138.   /**
  2139.    * Show error state
  2140.    */
  2141.   showErrorState() {
  2142.     this.hideAllStates();
  2143.     const noResults = document.getElementById('search-no-results');
  2144.     if (noResults) {
  2145.       noResults.style.display = 'block';
  2146.       const title = noResults.querySelector('h3');
  2147.       const text = noResults.querySelector('p');
  2148.       if (title) title.textContent = 'Chyba při vyhledávání';
  2149.       if (text) text.textContent = 'Zkuste to prosím později';
  2150.     }
  2151.   }
  2152.  
  2153.   /**
  2154.    * Hide all states
  2155.    */
  2156.   hideAllStates() {
  2157.     const states = [
  2158.       'search-loading',
  2159.       'search-empty',
  2160.       'search-no-results',
  2161.       'search-content-results',
  2162.     ];
  2163.     states.forEach(stateId => {
  2164.       const element = document.getElementById(stateId);
  2165.       if (element) element.style.display = 'none';
  2166.     });
  2167.  
  2168.     // Hide all sections
  2169.     const sections = [
  2170.       'search-products',
  2171.       'search-categories',
  2172.       'search-articles',
  2173.     ];
  2174.     sections.forEach(sectionId => {
  2175.       const element = document.getElementById(sectionId);
  2176.       if (element) element.style.display = 'none';
  2177.     });
  2178.  
  2179.     // Hide show all links
  2180.     const showAlls = [
  2181.       'search-products-show-all',
  2182.       'search-categories-show-all',
  2183.       'search-articles-show-all',
  2184.     ];
  2185.     showAlls.forEach(showAllId => {
  2186.       const element = document.getElementById(showAllId);
  2187.       if (element) element.style.display = 'none';
  2188.     });
  2189.   }
  2190. }
  2191.  
  2192. // Initialize when DOM is ready
  2193. document.addEventListener('DOMContentLoaded', () => {
  2194.   new SearchOverlay();
  2195. });
  2196.  
  2197.  
  2198.  
  2199. // =============================================================================
  2200. // search-results.js
  2201. // =============================================================================
  2202.  
  2203. /**
  2204.  * @fileoverview Search Results Page
  2205.  * Handles search results page functionality with pagination
  2206.  * @version 2.0.0
  2207.  * @author Revelor by Webotvůrci
  2208.  * @contact [email protected] | +420 728 089 029
  2209.  */
  2210.  
  2211. class SearchResultsPage {
  2212.   constructor() {
  2213.     this.apiBaseUrl = 'https://textilomanie.revelor.cz/api/public';
  2214.     this.projectId = this.getProjectId();
  2215.     this.currentPage = 1;
  2216.     this.pageSize = 20;
  2217.     this.hasMoreResults = false;
  2218.     this.isLoading = false;
  2219.     this.currentQuery = this.getQueryFromURL();
  2220.    
  2221.     // Performance optimizations
  2222.     this.abortController = null;
  2223.     this.cache = new Map();
  2224.     this.cacheTTL = 300000; // 5 minutes
  2225.    
  2226.     // Filters
  2227.     this.allProducts = []; // Store all products for client-side filtering
  2228.     this.allCategories = []; // Store all categories for expand functionality
  2229.     this.activeFilters = {
  2230.       inStock: false,
  2231.       sortBy: 'relevance'
  2232.     };
  2233.  
  2234.     this.init();
  2235.   }
  2236.  
  2237.   /**
  2238.    * Get Project ID from dataLayer or use default
  2239.    */
  2240.   getProjectId() {
  2241.     if (
  2242.       typeof dataLayer !== 'undefined' &&
  2243.       dataLayer[0] &&
  2244.       dataLayer[0].shoptet
  2245.     ) {
  2246.       return dataLayer[0].shoptet.projectId;
  2247.     }
  2248.     return '312030'; // Default fallback
  2249.   }
  2250.  
  2251.   /**
  2252.    * Get search query from URL parameters
  2253.    * Supports both 'q' and 'string' parameters (Shoptet uses 'string')
  2254.    */
  2255.   getQueryFromURL() {
  2256.     const urlParams = new URLSearchParams(window.location.search);
  2257.     return urlParams.get('string') || urlParams.get('q') || '';
  2258.   }
  2259.  
  2260.   /**
  2261.    * Initialize search results page
  2262.    */
  2263.   init() {
  2264.     // Create containers if they don't exist
  2265.     this.createContainersIfNeeded();
  2266.    
  2267.     // Hide Shoptet default search results
  2268.     this.hideShoptetResults();
  2269.    
  2270.     if (!this.currentQuery) {
  2271.       this.showNoResults('Zadejte hledaný výraz');
  2272.       return;
  2273.     }
  2274.  
  2275.     // DataLayer: Search Started (on results page)
  2276.     if (window.revelorDataLayer) {
  2277.       window.revelorDataLayer.searchStarted('results_page');
  2278.     }
  2279.  
  2280.     this.displayQuery();
  2281.     this.loadSearchResults();
  2282.     this.bindEvents();
  2283.   }
  2284.  
  2285.   /**
  2286.    * Create Revelor containers if they don't exist on the page
  2287.    */
  2288.   createContainersIfNeeded() {
  2289.     // Check if our container exists
  2290.     let container = document.getElementById('search-products-container');
  2291.     let section = document.getElementById('search-results-products');
  2292.    
  2293.     // If containers don't exist, create them
  2294.     if (!container || !section) {
  2295.       // Find where to insert - look for Shoptet products block or main content area
  2296.       const shoptetProductsBlock = document.querySelector('.products-block.products-searching, .products-block');
  2297.       const mainContent = document.querySelector('#content-wrapper, main, .content, #content');
  2298.       const insertTarget = shoptetProductsBlock?.parentElement || mainContent || document.body;
  2299.      
  2300.       // Create section wrapper
  2301.       if (!section) {
  2302.         section = document.createElement('section');
  2303.         section.id = 'search-results-products';
  2304.         section.className = 'search-results-section';
  2305.         section.setAttribute('aria-labelledby', 'products-heading');
  2306.         section.style.display = 'none';
  2307.        
  2308.         const heading = document.createElement('h2');
  2309.         heading.id = 'products-heading';
  2310.         heading.className = 'search-results-section-title';
  2311.         heading.textContent = 'Produkty';
  2312.         section.appendChild(heading);
  2313.        
  2314.         // Insert: replace Shoptet products block or insert at appropriate location
  2315.         if (shoptetProductsBlock) {
  2316.           // Replace Shoptet block with our section
  2317.           shoptetProductsBlock.parentElement.insertBefore(section, shoptetProductsBlock);
  2318.         } else if (insertTarget) {
  2319.           // Insert at beginning of content
  2320.           insertTarget.insertBefore(section, insertTarget.firstChild);
  2321.         } else {
  2322.           // Last resort: append to body
  2323.           document.body.appendChild(section);
  2324.         }
  2325.       }
  2326.      
  2327.       // Create products container - matching Shoptet structure
  2328.       if (!container) {
  2329.         container = document.createElement('div');
  2330.         container.id = 'search-products-container';
  2331.         container.className = 'products products-page products-block';
  2332.         container.setAttribute('data-testid', 'productCards');
  2333.         section.appendChild(container);
  2334.       }
  2335.      
  2336.       // Also create other necessary elements if missing
  2337.       if (!document.getElementById('search-results-header')) {
  2338.         const header = document.createElement('div');
  2339.         header.id = 'search-results-header';
  2340.         header.className = 'search-results-header';
  2341.         header.innerHTML = `
  2342.           <div class="search-results-query">
  2343.             <span class="search-results-query-label">Hledaný výraz:</span>
  2344.             <span class="search-results-query-value" id="search-query-display"></span>
  2345.           </div>
  2346.           <div class="search-results-count" id="search-results-count" role="status" aria-live="polite">&nbsp;</div>
  2347.         `;
  2348.         // Insert header before section
  2349.         if (section.parentElement) {
  2350.           section.parentElement.insertBefore(header, section);
  2351.         } else if (insertTarget) {
  2352.           insertTarget.insertBefore(header, insertTarget.firstChild);
  2353.         } else {
  2354.           document.body.appendChild(header);
  2355.         }
  2356.       }
  2357.      
  2358.       if (!document.getElementById('search-filters')) {
  2359.         const filters = document.createElement('div');
  2360.         filters.id = 'search-filters';
  2361.         filters.className = 'search-filters';
  2362.         filters.style.display = 'none';
  2363.         filters.innerHTML = `
  2364.           <div class="search-filters__item">
  2365.             <label class="search-filter-checkbox">
  2366.               <input type="checkbox" id="filter-in-stock" aria-label="Filtrovat pouze produkty skladem">
  2367.               <span>Pouze skladem</span>
  2368.             </label>
  2369.           </div>
  2370.           <div class="search-filters__item">
  2371.             <label for="filter-sort" class="sr-only">Řadit podle</label>
  2372.             <select id="filter-sort" class="search-filter-select" aria-label="Řadit výsledky podle">
  2373.               <option value="relevance">Řadit: Relevance</option>
  2374.               <option value="price-asc">Řadit: Cena ()</option>
  2375.               <option value="price-desc">Řadit: Cena ()</option>
  2376.               <option value="name">Řadit: Název (A-Z)</option>
  2377.             </select>
  2378.           </div>
  2379.         `;
  2380.         section.insertBefore(filters, container);
  2381.       }
  2382.      
  2383.       if (!document.getElementById('search-results-load-more')) {
  2384.         const loadMore = document.createElement('div');
  2385.         loadMore.id = 'search-results-load-more';
  2386.         loadMore.className = 'search-results-load-more';
  2387.         loadMore.style.display = 'none';
  2388.         loadMore.innerHTML = `
  2389.           <button type="button" class="btn btn--primary" id="load-more-button" aria-label="Načíst více výsledků" aria-busy="false">
  2390.             <span class="load-more-text">Načíst více výsledků</span>
  2391.             <span class="load-more-spinner" style="display: none;" aria-hidden="true"></span>
  2392.           </button>
  2393.         `;
  2394.         section.appendChild(loadMore);
  2395.       }
  2396.      
  2397.       // Create loading state if missing
  2398.       if (!document.getElementById('search-results-loading')) {
  2399.         const loading = document.createElement('div');
  2400.         loading.id = 'search-results-loading';
  2401.         loading.className = 'search-results-loading';
  2402.         loading.style.display = 'none';
  2403.         loading.setAttribute('role', 'status');
  2404.         loading.setAttribute('aria-live', 'polite');
  2405.         loading.setAttribute('aria-label', 'Načítám výsledky');
  2406.         loading.innerHTML = `
  2407.           <div class="search-results-spinner" aria-hidden="true">&nbsp;</div>
  2408.           <p>Načítám výsledky...</p>
  2409.         `;
  2410.         if (section.parentElement) {
  2411.           section.parentElement.insertBefore(loading, section);
  2412.         }
  2413.       }
  2414.      
  2415.       // Create no results state if missing
  2416.       if (!document.getElementById('search-results-no-results')) {
  2417.         const noResults = document.createElement('div');
  2418.         noResults.id = 'search-results-no-results';
  2419.         noResults.className = 'search-results-no-results';
  2420.         noResults.style.display = 'none';
  2421.         noResults.setAttribute('role', 'status');
  2422.         noResults.setAttribute('aria-live', 'polite');
  2423.         noResults.innerHTML = `
  2424.           <div class="search-results-no-results-icon" aria-hidden="true">🔍</div>
  2425.           <h2>Žádné výsledky</h2>
  2426.           <p>Pro váš dotaz "<span id="no-results-query"></span>" nebyly nalezeny žádné výsledky.</p>
  2427.           <p>Zkuste jiný hledaný výraz nebo prohlédněte naše kategorie.</p>
  2428.         `;
  2429.         if (section.parentElement) {
  2430.           section.parentElement.insertBefore(noResults, section);
  2431.         }
  2432.       }
  2433.  
  2434.       // Create categories section if missing
  2435.       if (!document.getElementById('search-results-categories')) {
  2436.         const categoriesSection = document.createElement('section');
  2437.         categoriesSection.id = 'search-results-categories';
  2438.         categoriesSection.className = 'search-results-section';
  2439.         categoriesSection.style.display = 'none';
  2440.         categoriesSection.setAttribute('aria-labelledby', 'categories-heading');
  2441.        
  2442.         const categoriesHeading = document.createElement('h2');
  2443.         categoriesHeading.id = 'categories-heading';
  2444.         categoriesHeading.className = 'search-results-section-title';
  2445.         categoriesHeading.textContent = 'Kategorie';
  2446.         categoriesSection.appendChild(categoriesHeading);
  2447.        
  2448.         const categoriesList = document.createElement('ul');
  2449.         categoriesList.id = 'search-categories-container';
  2450.         categoriesList.className = 'subcategories with-image';
  2451.         categoriesList.setAttribute('role', 'list');
  2452.         categoriesSection.appendChild(categoriesList);
  2453.        
  2454.         // Insert after products section
  2455.         if (section.parentElement) {
  2456.           section.parentElement.insertBefore(categoriesSection, section.nextSibling);
  2457.         } else if (insertTarget) {
  2458.           insertTarget.appendChild(categoriesSection);
  2459.         }
  2460.       }
  2461.  
  2462.       // Create articles section if missing
  2463.       if (!document.getElementById('search-results-articles')) {
  2464.         const articlesSection = document.createElement('section');
  2465.         articlesSection.id = 'search-results-articles';
  2466.         articlesSection.className = 'search-results-section';
  2467.         articlesSection.style.display = 'none';
  2468.         articlesSection.setAttribute('aria-labelledby', 'articles-heading');
  2469.        
  2470.         const articlesHeading = document.createElement('h2');
  2471.         articlesHeading.id = 'articles-heading';
  2472.         articlesHeading.className = 'search-results-section-title';
  2473.         articlesHeading.textContent = 'Články';
  2474.         articlesSection.appendChild(articlesHeading);
  2475.        
  2476.         const articlesWrapper = document.createElement('div');
  2477.         articlesWrapper.id = 'newsWrapper';
  2478.         articlesWrapper.className = 'news-wrapper';
  2479.         articlesSection.appendChild(articlesWrapper);
  2480.        
  2481.         // Insert after categories section (before products section)
  2482.         const categoriesSection = document.getElementById('search-results-categories');
  2483.         const insertAfter = categoriesSection || section;
  2484.         if (insertAfter.parentElement) {
  2485.           insertAfter.parentElement.insertBefore(articlesSection, insertAfter.nextSibling);
  2486.         } else if (insertTarget) {
  2487.           insertTarget.appendChild(articlesSection);
  2488.         }
  2489.       }
  2490.  
  2491.       // Ensure correct order: categories -> articles -> products
  2492.       this.reorderSections();
  2493.      
  2494.       console.log('✅ Created Revelor search containers');
  2495.     }
  2496.   }
  2497.  
  2498.   /**
  2499.    * Reorder sections to ensure correct display order: categories -> articles -> products
  2500.    */
  2501.   reorderSections() {
  2502.     const categoriesSection = document.getElementById('search-results-categories');
  2503.     const articlesSection = document.getElementById('search-results-articles');
  2504.     const productsSection = document.getElementById('search-results-products');
  2505.    
  2506.     if (!productsSection || !productsSection.parentElement) return;
  2507.    
  2508.     const parent = productsSection.parentElement;
  2509.    
  2510.     // Ensure order: categories -> articles -> products
  2511.     // 1. Move categories first (if exists)
  2512.     if (categoriesSection && categoriesSection.parentElement === parent) {
  2513.       // Get the first relevant element (categories should be first)
  2514.       const firstRelevant = articlesSection || productsSection;
  2515.       if (categoriesSection.nextSibling !== firstRelevant) {
  2516.         parent.insertBefore(categoriesSection, firstRelevant);
  2517.       }
  2518.     }
  2519.    
  2520.     // 2. Move articles after categories (if exists)
  2521.     if (articlesSection && articlesSection.parentElement === parent) {
  2522.       const targetPosition = categoriesSection ? categoriesSection.nextSibling : productsSection;
  2523.       if (targetPosition !== articlesSection && targetPosition !== productsSection) {
  2524.         parent.insertBefore(articlesSection, targetPosition);
  2525.       } else if (!categoriesSection && articlesSection.nextSibling !== productsSection) {
  2526.         parent.insertBefore(articlesSection, productsSection);
  2527.       } else if (categoriesSection && articlesSection.nextSibling !== productsSection) {
  2528.         // Ensure articles are between categories and products
  2529.         parent.insertBefore(articlesSection, productsSection);
  2530.       }
  2531.     }
  2532.   }
  2533.  
  2534.   /**
  2535.    * Hide Shoptet default search results containers
  2536.    */
  2537.   hideShoptetResults() {
  2538.     // Hide Shoptet products container (but keep our container visible)
  2539.     const shoptetProducts = document.querySelectorAll('.products:not(#search-products-container), .products-block:not(#search-products-container), .products-searching:not(#search-products-container)');
  2540.     shoptetProducts.forEach(block => {
  2541.       block.style.display = 'none';
  2542.     });
  2543.  
  2544.     // Hide Shoptet listing controls (pagination, load more, etc.)
  2545.     const listingControls = document.querySelectorAll('.listingControls');
  2546.     listingControls.forEach(controls => {
  2547.       controls.style.display = 'none';
  2548.     });
  2549.  
  2550.     // Hide Shoptet pagination if exists
  2551.     const shoptetPagination = document.querySelectorAll('.pagination, .products-pagination');
  2552.     shoptetPagination.forEach(pagination => {
  2553.       pagination.style.display = 'none';
  2554.     });
  2555.  
  2556.     // Hide Shoptet "no results" messages
  2557.     const shoptetNoResults = document.querySelectorAll('.no-results:not(#search-results-no-results), .products-no-results');
  2558.     shoptetNoResults.forEach(noResults => {
  2559.       noResults.style.display = 'none';
  2560.     });
  2561.  
  2562.     // Hide category header sorting if exists (on search page)
  2563.     const categoryHeader = document.querySelector('#category-header');
  2564.     if (categoryHeader) {
  2565.       categoryHeader.style.display = 'none';
  2566.     }
  2567.  
  2568.     // Hide #content h2 element
  2569.     const contentH2 = document.querySelector('#content h2');
  2570.     if (contentH2) {
  2571.       contentH2.style.display = 'none';
  2572.     }
  2573.  
  2574.     // Hide #content.search-results-groups
  2575.     const contentSearchGroups = document.querySelector('#content.search-results-groups');
  2576.     if (contentSearchGroups) {
  2577.       contentSearchGroups.style.display = 'none';
  2578.     }
  2579.  
  2580.     // Hide sidebar with categories (left sidebar)
  2581.     const sidebar = document.querySelector('.sidebar.sidebar-left, aside.sidebar');
  2582.     if (sidebar) {
  2583.       sidebar.style.display = 'none';
  2584.     }
  2585.   }
  2586.  
  2587.   /**
  2588.    * Display current search query
  2589.    */
  2590.   displayQuery() {
  2591.     const queryDisplay = document.getElementById('search-query-display');
  2592.     const noResultsQuery = document.getElementById('no-results-query');
  2593.  
  2594.     if (queryDisplay) {
  2595.       queryDisplay.textContent = `"${this.currentQuery}"`;
  2596.     }
  2597.     if (noResultsQuery) {
  2598.       noResultsQuery.textContent = this.currentQuery;
  2599.     }
  2600.   }
  2601.  
  2602.   /**
  2603.    * Bind event listeners
  2604.    */
  2605.   bindEvents() {
  2606.     const loadMoreButton = document.getElementById('load-more-button');
  2607.     if (loadMoreButton) {
  2608.       loadMoreButton.addEventListener('click', () => this.loadMoreResults());
  2609.     }
  2610.    
  2611.     // Initialize infinite scroll
  2612.     this.setupInfiniteScroll();
  2613.    
  2614.     // Filter events
  2615.     const inStockFilter = document.getElementById('filter-in-stock');
  2616.     if (inStockFilter) {
  2617.       inStockFilter.addEventListener('change', (e) => {
  2618.         this.activeFilters.inStock = e.target.checked;
  2619.         this.applyFilters();
  2620.       });
  2621.     }
  2622.    
  2623.     const sortFilter = document.getElementById('filter-sort');
  2624.     if (sortFilter) {
  2625.       sortFilter.addEventListener('change', (e) => {
  2626.         this.activeFilters.sortBy = e.target.value;
  2627.         this.applyFilters();
  2628.       });
  2629.     }
  2630.   }
  2631.  
  2632.   /**
  2633.    * Apply filters to products
  2634.    */
  2635.   applyFilters() {
  2636.     let filtered = [...this.allProducts];
  2637.    
  2638.     // Filter: Only in stock
  2639.     if (this.activeFilters.inStock) {
  2640.       filtered = filtered.filter(p => p.skladem);
  2641.     }
  2642.    
  2643.     // Sort
  2644.     switch (this.activeFilters.sortBy) {
  2645.       case 'price-asc':
  2646.         filtered.sort((a, b) => (a.price_s_dph || 0) - (b.price_s_dph || 0));
  2647.         break;
  2648.       case 'price-desc':
  2649.         filtered.sort((a, b) => (b.price_s_dph || 0) - (a.price_s_dph || 0));
  2650.         break;
  2651.       case 'name':
  2652.         filtered.sort((a, b) => a.title.localeCompare(b.title, 'cs'));
  2653.         break;
  2654.       // 'relevance' - keep original order
  2655.     }
  2656.    
  2657.     // Clear and re-display products
  2658.     const container = document.getElementById('search-products-container');
  2659.     if (container) {
  2660.       container.innerHTML = '';
  2661.       filtered.forEach(product => {
  2662.         container.appendChild(this.createProductElement(product));
  2663.       });
  2664.     }
  2665.    
  2666.     // Show filters applied message
  2667.     const activeCount = (this.activeFilters.inStock ? 1 : 0);
  2668.     if (activeCount > 0 || this.activeFilters.sortBy !== 'relevance') {
  2669.       console.log(`🔍 Filters applied: ${filtered.length}/${this.allProducts.length} products`);
  2670.     }
  2671.   }
  2672.  
  2673.   /**
  2674.    * Setup infinite scroll with IntersectionObserver
  2675.    */
  2676.   setupInfiniteScroll() {
  2677.     const loadMoreContainer = document.getElementById('search-results-load-more');
  2678.     if (!loadMoreContainer) return;
  2679.    
  2680.     const observer = new IntersectionObserver((entries) => {
  2681.       entries.forEach(entry => {
  2682.         if (entry.isIntersecting && this.hasMoreResults && !this.isLoading) {
  2683.           console.log('📜 Infinite scroll triggered - loading more...');
  2684.           this.loadMoreResults();
  2685.         }
  2686.       });
  2687.     }, {
  2688.       rootMargin: '200px' // Load 200px before reaching bottom
  2689.     });
  2690.    
  2691.     observer.observe(loadMoreContainer);
  2692.   }
  2693.  
  2694.   /**
  2695.    * Load search results
  2696.    */
  2697.   async loadSearchResults() {
  2698.     this.showLoading();
  2699.     this.isLoading = true;
  2700.  
  2701.     // DataLayer: Search Query Submitted
  2702.     if (window.revelorDataLayer) {
  2703.       window.revelorDataLayer.searchQuerySubmitted(this.currentQuery, 'results_page');
  2704.     }
  2705.  
  2706.     // Check cache first
  2707.     const cacheKey = `${this.currentQuery}_${this.projectId}_${this.currentPage}`;
  2708.     const cached = this.getFromCache(cacheKey);
  2709.     if (cached) {
  2710.       console.log('📦 Using cached results for:', this.currentQuery, 'page:', this.currentPage);
  2711.       this.processSearchResults(cached);
  2712.       this.isLoading = false;
  2713.       this.hideLoading();
  2714.       return;
  2715.     }
  2716.  
  2717.     // Cancel previous request
  2718.     if (this.abortController) {
  2719.       this.abortController.abort();
  2720.     }
  2721.     this.abortController = new AbortController();
  2722.  
  2723.     const startTime = Date.now();
  2724.  
  2725.     try {
  2726.       const response = await this.fetchWithRetry(
  2727.         `${this.apiBaseUrl}/search?q=${encodeURIComponent(this.currentQuery)}&projectId=${this.projectId}&page=${this.currentPage}&page_size=${this.pageSize}`,
  2728.         {
  2729.           signal: this.abortController.signal,
  2730.           headers: {
  2731.             'X-Tenant': this.projectId,
  2732.           },
  2733.         }
  2734.       );
  2735.  
  2736.       if (!response.ok) {
  2737.         throw new Error(`HTTP error! status: ${response.status}`);
  2738.       }
  2739.  
  2740.       const data = await response.json();
  2741.       const responseTime = Date.now() - startTime;
  2742.      
  2743.       // Save to cache
  2744.       this.saveToCache(cacheKey, data);
  2745.      
  2746.       // DataLayer: Search Results Displayed or No Results
  2747.       if (window.revelorDataLayer) {
  2748.         const hasResults = data.products?.length > 0 || data.categories?.length > 0 || data.articles?.length > 0;
  2749.        
  2750.         if (hasResults) {
  2751.           window.revelorDataLayer.searchResultsDisplayed(
  2752.             this.currentQuery,
  2753.             data,
  2754.             responseTime,
  2755.             this.currentPage
  2756.           );
  2757.         } else {
  2758.           window.revelorDataLayer.searchNoResults(this.currentQuery);
  2759.         }
  2760.       }
  2761.  
  2762.       // Backend API: Track search event
  2763.       if (window.searchTracker && this.currentQuery && this.currentQuery.length >= 3) {
  2764.         const totalResults = data.total_products || 0;
  2765.         window.searchTracker.trackSearch({
  2766.           query: this.currentQuery,
  2767.           results_count: totalResults,
  2768.           search_type: this.currentPage === 1 ? 'manual' : 'live_search'
  2769.         });
  2770.       }
  2771.      
  2772.       this.processSearchResults(data);
  2773.     } catch (error) {
  2774.       if (error.name === 'AbortError') {
  2775.         console.log('🚫 Request cancelled');
  2776.         return;
  2777.       }
  2778.       console.error('Search API Error:', error);
  2779.      
  2780.       // Try to use cached results as fallback
  2781.       const fallbackCached = this.getFromCache(cacheKey, true);
  2782.       if (fallbackCached) {
  2783.         console.log('📦 Using stale cache as fallback');
  2784.         this.processSearchResults(fallbackCached);
  2785.         this.showToast('Zobrazuji uložené výsledky (offline)', 'warning');
  2786.         return;
  2787.       }
  2788.      
  2789.       this.showError();
  2790.     } finally {
  2791.       this.isLoading = false;
  2792.       this.hideLoading();
  2793.     }
  2794.   }
  2795.  
  2796.   /**
  2797.    * Save to cache
  2798.    */
  2799.   saveToCache(key, data) {
  2800.     this.cache.set(key, {
  2801.       data,
  2802.       timestamp: Date.now()
  2803.     });
  2804.   }
  2805.  
  2806.   /**
  2807.    * Get from cache
  2808.    */
  2809.   getFromCache(key, ignoreExpiry = false) {
  2810.     const cached = this.cache.get(key);
  2811.     if (!cached) return null;
  2812.    
  2813.     // Check TTL (unless ignoreExpiry for fallback)
  2814.     if (!ignoreExpiry && Date.now() - cached.timestamp > this.cacheTTL) {
  2815.       this.cache.delete(key);
  2816.       return null;
  2817.     }
  2818.    
  2819.     return cached.data;
  2820.   }
  2821.  
  2822.   /**
  2823.    * Escape HTML to prevent XSS
  2824.    */
  2825.   escapeHtml(text) {
  2826.     const div = document.createElement('div');
  2827.     div.textContent = text;
  2828.     return div.innerHTML;
  2829.   }
  2830.  
  2831.   /**
  2832.    * Show toast notification
  2833.    */
  2834.   showToast(message, type = 'info', duration = 3000) {
  2835.     // Remove existing toasts
  2836.     document.querySelectorAll('.revelor-toast').forEach(t => t.remove());
  2837.    
  2838.     const icons = {
  2839.       success: '✅',
  2840.       error: '❌',
  2841.       warning: '⚠️',
  2842.       info: 'ℹ️'
  2843.     };
  2844.    
  2845.     const toast = document.createElement('div');
  2846.     toast.className = `revelor-toast revelor-toast--${type}`;
  2847.     toast.setAttribute('role', 'status');
  2848.     toast.setAttribute('aria-live', 'polite');
  2849.     toast.innerHTML = `
  2850.       <div class="revelor-toast__content">
  2851.         <span class="revelor-toast__icon" aria-hidden="true">${icons[type] || icons.info}</span>
  2852.         <span class="revelor-toast__message">${this.escapeHtml(message)}</span>
  2853.         <button class="revelor-toast__close" aria-label="Zavřít">&times;</button>
  2854.       </div>
  2855.     `;
  2856.    
  2857.     document.body.appendChild(toast);
  2858.    
  2859.     // Animate in
  2860.     setTimeout(() => toast.classList.add('revelor-toast--visible'), 10);
  2861.    
  2862.     // Close button
  2863.     toast.querySelector('.revelor-toast__close').addEventListener('click', () => {
  2864.       toast.classList.remove('revelor-toast--visible');
  2865.       setTimeout(() => toast.remove(), 300);
  2866.     });
  2867.    
  2868.     // Auto remove
  2869.     setTimeout(() => {
  2870.       toast.classList.remove('revelor-toast--visible');
  2871.       setTimeout(() => toast.remove(), 300);
  2872.     }, duration);
  2873.   }
  2874.  
  2875.   /**
  2876.    * Fetch with retry logic
  2877.    */
  2878.   async fetchWithRetry(url, options = {}, retries = 3) {
  2879.     for (let i = 0; i < retries; i++) {
  2880.       try {
  2881.         const response = await fetch(url, options);
  2882.        
  2883.         if (!response.ok && i < retries - 1) {
  2884.           await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
  2885.           continue;
  2886.         }
  2887.        
  2888.         return response;
  2889.       } catch (error) {
  2890.         if (error.name === 'AbortError') {
  2891.           throw error;
  2892.         }
  2893.        
  2894.         if (i === retries - 1) {
  2895.           throw error;
  2896.         }
  2897.        
  2898.         await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
  2899.       }
  2900.     }
  2901.   }
  2902.  
  2903.   /**
  2904.    * Announce message to screen readers
  2905.    */
  2906.   announceToScreenReader(message) {
  2907.     // Create or get live region
  2908.     let liveRegion = document.getElementById('search-results-live-region');
  2909.     if (!liveRegion) {
  2910.       liveRegion = document.createElement('div');
  2911.       liveRegion.id = 'search-results-live-region';
  2912.       liveRegion.className = 'sr-only';
  2913.       liveRegion.setAttribute('role', 'status');
  2914.       liveRegion.setAttribute('aria-live', 'polite');
  2915.       liveRegion.setAttribute('aria-atomic', 'true');
  2916.       document.body.appendChild(liveRegion);
  2917.     }
  2918.    
  2919.     liveRegion.textContent = message;
  2920.     // Clear after announcement
  2921.     setTimeout(() => {
  2922.       liveRegion.textContent = '';
  2923.     }, 1000);
  2924.   }
  2925.  
  2926.   /**
  2927.    * Process search results from API
  2928.    */
  2929.   processSearchResults(data, isLoadMore = false) {
  2930.     this.hasMoreResults = data.pagination?.has_next || false;
  2931.  
  2932.     const hasResults =
  2933.       data.products?.length > 0 ||
  2934.       data.categories?.length > 0 ||
  2935.       data.articles?.length > 0 ||
  2936.       data.brands?.length > 0;
  2937.  
  2938.     if (!hasResults && this.currentPage === 1) {
  2939.       this.showNoResults();
  2940.       this.announceToScreenReader('Žádné výsledky nebyly nalezeny');
  2941.       return;
  2942.     }
  2943.  
  2944.     this.showResults();
  2945.  
  2946.     // Announce results for screen readers
  2947.     if (!isLoadMore) {
  2948.       const totalResults = data.total_products || 0;
  2949.       this.announceToScreenReader(`Nalezeno ${totalResults} výsledků pro dotaz ${this.currentQuery}`);
  2950.      
  2951.       // Update meta tags for AI crawlers
  2952.       this.updateMetaTags(this.currentQuery, totalResults);
  2953.      
  2954.       // Create ItemList schema for AI agents
  2955.       this.createItemListSchema(data);
  2956.     } else {
  2957.       this.announceToScreenReader(`Načteny další výsledky`);
  2958.     }
  2959.  
  2960.     // Display results by type - Kategorie a články nahoru
  2961.     // Kategorie a články se zobrazují pouze na první stránce
  2962.     if (!isLoadMore) {
  2963.       if (data.categories?.length > 0) {
  2964.         this.displayCategories(data.categories);
  2965.       }
  2966.  
  2967.       if (data.articles?.length > 0) {
  2968.         this.displayArticles(data.articles);
  2969.       }
  2970.  
  2971.       if (data.brands?.length > 0) {
  2972.         this.displayBrands(data.brands);
  2973.       }
  2974.      
  2975.       // Ensure correct order after displaying categories and articles
  2976.       this.reorderSections();
  2977.     }
  2978.  
  2979.     // Produkty se zobrazují vždy
  2980.     if (data.products?.length > 0) {
  2981.       this.displayProducts(data.products);
  2982.     }
  2983.  
  2984.     // Update results count
  2985.     this.updateResultsCount(data);
  2986.  
  2987.     // Show/hide load more button
  2988.     this.updateLoadMoreButton();
  2989.   }
  2990.  
  2991.   /**
  2992.    * Create ItemList schema for AI agents
  2993.    */
  2994.   createItemListSchema(data) {
  2995.     // Remove existing ItemList schema if any
  2996.     const existingSchema = document.querySelector('script[data-schema="itemlist"]');
  2997.     if (existingSchema) {
  2998.       existingSchema.remove();
  2999.     }
  3000.  
  3001.     const itemListSchema = {
  3002.       "@context": "https://schema.org",
  3003.       "@type": "ItemList",
  3004.       "name": `Výsledky vyhledávání pro "${this.currentQuery}"`,
  3005.       "numberOfItems": data.total_products || 0,
  3006.       "itemListElement": []
  3007.     };
  3008.  
  3009.     // Add products to ItemList
  3010.     if (data.products && data.products.length > 0) {
  3011.       data.products.forEach((product, index) => {
  3012.         itemListSchema.itemListElement.push({
  3013.           "@type": "ListItem",
  3014.           "position": index + 1,
  3015.           "item": {
  3016.             "@type": "Product",
  3017.             "@id": product._id,
  3018.             "name": product.title,
  3019.             "url": product.url,
  3020.             "image": product.img || '/img/no-image.png',
  3021.             "sku": product.kod || 'N/A',
  3022.             "offers": {
  3023.               "@type": "Offer",
  3024.               "price": product.price_s_dph || 0,
  3025.               "priceCurrency": "CZK",
  3026.               "availability": product.skladem
  3027.                 ? "https://schema.org/InStock"
  3028.                 : "https://schema.org/OutOfStock"
  3029.             }
  3030.           }
  3031.         });
  3032.       });
  3033.     }
  3034.  
  3035.     // Create and append script tag
  3036.     const script = document.createElement('script');
  3037.     script.type = 'application/ld+json';
  3038.     script.setAttribute('data-schema', 'itemlist');
  3039.     script.textContent = JSON.stringify(itemListSchema);
  3040.     document.head.appendChild(script);
  3041.   }
  3042.  
  3043.   /**
  3044.    * Update meta tags for AI crawlers
  3045.    */
  3046.   updateMetaTags(query, totalResults) {
  3047.     const description = `Výsledky vyhledávání pro "${query}" - Nalezeno ${totalResults} produktů, kategorií a článků`;
  3048.     const title = `${query} - Výsledky vyhledávání`;
  3049.  
  3050.     // Update or create description meta tag
  3051.     let descMeta = document.querySelector('meta[name="description"]');
  3052.     if (!descMeta) {
  3053.       descMeta = document.createElement('meta');
  3054.       descMeta.setAttribute('name', 'description');
  3055.       document.head.appendChild(descMeta);
  3056.     }
  3057.     descMeta.setAttribute('content', description);
  3058.  
  3059.     // Update or create keywords meta tag
  3060.     let keywordsMeta = document.querySelector('meta[name="keywords"]');
  3061.     if (!keywordsMeta) {
  3062.       keywordsMeta = document.createElement('meta');
  3063.       keywordsMeta.setAttribute('name', 'keywords');
  3064.       document.head.appendChild(keywordsMeta);
  3065.     }
  3066.     keywordsMeta.setAttribute('content', `${query}, produkty, kategorie, články, e-shop, vyhledávání`);
  3067.  
  3068.     // Update page title
  3069.     document.title = title;
  3070.  
  3071.     // OpenGraph tags for AI agents
  3072.     this.updateOrCreateMetaTag('property', 'og:type', 'website');
  3073.     this.updateOrCreateMetaTag('property', 'og:title', title);
  3074.     this.updateOrCreateMetaTag('property', 'og:description', description);
  3075.     this.updateOrCreateMetaTag('property', 'og:url', window.location.href);
  3076.  
  3077.     // Twitter Card tags
  3078.     this.updateOrCreateMetaTag('name', 'twitter:card', 'summary_large_image');
  3079.     this.updateOrCreateMetaTag('name', 'twitter:title', title);
  3080.     this.updateOrCreateMetaTag('name', 'twitter:description', description);
  3081.  
  3082.     // AI crawler specific tags
  3083.     this.updateOrCreateMetaTag('name', 'robots', 'index, follow, max-snippet:-1, max-image-preview:large');
  3084.     this.updateOrCreateMetaTag('name', 'googlebot', 'index, follow');
  3085.     this.updateOrCreateMetaTag('name', 'bingbot', 'index, follow');
  3086.  
  3087.     // Canonical URL
  3088.     let canonical = document.querySelector('link[rel="canonical"]');
  3089.     if (!canonical) {
  3090.       canonical = document.createElement('link');
  3091.       canonical.setAttribute('rel', 'canonical');
  3092.       document.head.appendChild(canonical);
  3093.     }
  3094.     canonical.setAttribute('href', window.location.href);
  3095.   }
  3096.  
  3097.   /**
  3098.    * Helper: Update or create meta tag
  3099.    */
  3100.   updateOrCreateMetaTag(attributeName, attributeValue, content) {
  3101.     let meta = document.querySelector(`meta[${attributeName}="${attributeValue}"]`);
  3102.     if (!meta) {
  3103.       meta = document.createElement('meta');
  3104.       meta.setAttribute(attributeName, attributeValue);
  3105.       document.head.appendChild(meta);
  3106.     }
  3107.     meta.setAttribute('content', content);
  3108.   }
  3109.  
  3110.   /**
  3111.    * Display products
  3112.    */
  3113.   displayProducts(products) {
  3114.     console.log('displayProducts called with:', products); // Debug log
  3115.     const container = document.getElementById('search-products-container');
  3116.     const section = document.getElementById('search-results-products');
  3117.     const filters = document.getElementById('search-filters');
  3118.  
  3119.     console.log('Container:', container); // Debug log
  3120.     console.log('Section:', section); // Debug log
  3121.  
  3122.     if (!container || !section) {
  3123.       console.error('Container or section not found!');
  3124.       return;
  3125.     }
  3126.  
  3127.     section.style.display = 'block';
  3128.    
  3129.     // Show filters
  3130.     if (filters) {
  3131.       filters.style.display = 'flex';
  3132.     }
  3133.    
  3134.     // Store products for filtering
  3135.     this.allProducts.push(...products);
  3136.  
  3137.     products.forEach((product, index) => {
  3138.       const productElement = this.createProductElement(product, index);
  3139.       container.appendChild(productElement);
  3140.     });
  3141.   }
  3142.  
  3143.   /**
  3144.    * Quick add product to cart (Shoptet API)
  3145.    */
  3146.   async quickAddToCart(product, event) {
  3147.     // If product has variants, redirect to product detail
  3148.     if (product.has_variants || (product.variants && product.variants.length > 0)) {
  3149.       window.location.href = product.url;
  3150.       return;
  3151.     }
  3152.    
  3153.     const button = event.target;
  3154.     const originalText = button.innerHTML;
  3155.    
  3156.     // Show loading state
  3157.     button.disabled = true;
  3158.     button.innerHTML = '<span class="spinner-small" aria-hidden="true">⏳</span> Přidávám...';
  3159.    
  3160.     try {
  3161.       // Check if Shoptet API is available
  3162.       if (typeof shoptet === 'undefined' || !shoptet.cartShared) {
  3163.         throw new Error('Shoptet API není k dispozici');
  3164.       }
  3165.      
  3166.       // Use Shoptet API according to documentation
  3167.       // Priority: priceId > productCode > productId
  3168.       let addParams = {};
  3169.      
  3170.       if (product.priceId) {
  3171.         // Best option: priceId
  3172.         addParams = { priceId: product.priceId };
  3173.       } else if (product.kod || product.code) {
  3174.         // Fallback: product code
  3175.         addParams = { productCode: product.kod || product.code };
  3176.       } else if (product._id || product.id) {
  3177.         // Last resort: product ID
  3178.         addParams = { productId: product._id || product.id };
  3179.       } else {
  3180.         throw new Error('Nelze identifikovat produkt');
  3181.       }
  3182.      
  3183.       // Add to cart (without silent mode - show Shoptet's default behavior)
  3184.       await shoptet.cartShared.addToCart(addParams);
  3185.      
  3186.       // Success feedback
  3187.       button.innerHTML = '✅ Přidáno';
  3188.       button.classList.add('success');
  3189.      
  3190.       // DataLayer event
  3191.       if (window.revelorDataLayer) {
  3192.         window.dataLayer.push({
  3193.           event: 'revelor_quick_add_to_cart',
  3194.           product_id: product._id || product.id,
  3195.           product_name: product.title,
  3196.           price: product.price_s_dph,
  3197.           source: 'search_results'
  3198.         });
  3199.       }
  3200.      
  3201.       // Reset button after 2s
  3202.       setTimeout(() => {
  3203.         button.disabled = false;
  3204.         button.innerHTML = originalText;
  3205.         button.classList.remove('success');
  3206.       }, 2000);
  3207.      
  3208.     } catch (error) {
  3209.       console.error('Quick Add to Cart Error:', error);
  3210.       button.innerHTML = originalText;
  3211.       button.disabled = false;
  3212.     }
  3213.   }
  3214.  
  3215.   /**
  3216.    * Create product element - Shoptet category format
  3217.    * @param {Object} product - Product data
  3218.    * @param {number} index - Product position in list (0-based)
  3219.    */
  3220.   createProductElement(product, index = 0) {
  3221.     const productDiv = document.createElement('div');
  3222.     productDiv.className = 'product';
  3223.    
  3224.     // Format price - remove decimals if whole number, use space as thousands separator
  3225.     const formattedPrice = product.price_s_dph
  3226.       ? Math.floor(product.price_s_dph).toLocaleString('cs-CZ', {
  3227.           minimumFractionDigits: 0,
  3228.           maximumFractionDigits: 0
  3229.         }) + ' Kč'
  3230.       : 'Cena na dotaz';
  3231.    
  3232.     const availabilityText = product.skladem ? 'Skladem' : '10 dní';
  3233.     const availabilityClass = product.skladem ? 'availability skladem' : 'availability';
  3234.     const availabilityStyle = product.skladem ? 'style="color:#37A672"' : '';
  3235.     const availabilitySchema = product.skladem ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock';
  3236.    
  3237.     // Generate unique identifier
  3238.     const microIdentifier = product._id || product.id || `product_${Date.now()}_${index}`;
  3239.    
  3240.     // Get CSRF token if available (from Shoptet)
  3241.     const csrfToken = this.getCsrfToken();
  3242.    
  3243.     // Build product HTML matching Shoptet structure exactly
  3244.     const productInner = document.createElement('div');
  3245.     productInner.className = 'p swap-images';
  3246.     productInner.setAttribute('data-micro', 'product');
  3247.     productInner.setAttribute('data-micro-product-id', product._id || product.id || '');
  3248.     productInner.setAttribute('data-micro-identifier', microIdentifier);
  3249.     productInner.setAttribute('data-testid', 'productItem');
  3250.    
  3251.     productInner.innerHTML = `
  3252.       <a href="${product.url}" class="image">
  3253.         <img src="${product.img || '/img/no-image.png'}"
  3254.              alt="${this.escapeHtml(product.title)}"
  3255.              data-src="${product.img || '/img/no-image.png'}"
  3256.              class="swap-image"
  3257.              data-micro-image="${product.img || '/img/no-image.png'}"
  3258.              width="423"
  3259.              height="318"
  3260.              fetchpriority="${index < 4 ? 'high' : 'low'}"
  3261.              loading="${index < 4 ? 'eager' : 'lazy'}">
  3262.         <span class="imgCover"></span>
  3263.       </a>
  3264.      
  3265.       <div class="p-in">
  3266.         <div class="p-in-in">
  3267.           <a href="${product.url}" class="name" data-micro="url">
  3268.             <span data-micro="name" data-testid="productCardName">${this.escapeHtml(product.title)}</span>
  3269.           </a>
  3270.           <div class="dkLabFavouriteProductDiv onlyIcon">
  3271.             <span class="dkLabFavProductSpan dkLabFavouriteProductLinkAdd hasIcon"></span>
  3272.           </div>
  3273.          
  3274.           <div class="ratings-wrapper">
  3275.             <div class="${availabilityClass}">
  3276.               <span class="show-tooltip" title="" ${availabilityStyle} data-original-title="${product.skladem ? 'Produkt máme skladem u nás v Brně a odešleme Vám ho co nejdříve!' : 'Tento produkt je skladem u dodavatele a můžeme Vám ho doručit zhruba do 10 dní.'}">
  3277.                 ${availabilityText}
  3278.               </span>
  3279.               ${product.skladem ? `<span class="availability-amount" data-testid="numberAvailabilityAmount">(>10&nbsp;ks)</span>` : ''}
  3280.             </div>
  3281.           </div>
  3282.         </div>
  3283.  
  3284.         <div class="p-bottom">
  3285.           <div data-micro="offer" data-micro-price="${product.price_s_dph || 0}" data-micro-price-currency="CZK" ${product.skladem ? `data-micro-availability="${availabilitySchema}"` : ''}>
  3286.             <div class="prices">
  3287.               <div class="price price-final" data-testid="productCardPrice">
  3288.                 <strong>${formattedPrice}</strong>
  3289.               </div>
  3290.             </div>
  3291.            
  3292.             <div class="p-tools">
  3293.               ${product.has_variants || product.variants?.length > 0
  3294.                 ? `<a href="${product.url}" class="btn btn-cart add-to-cart-button" data-testid="buttonAddToCart" aria-label="Do košíku ${this.escapeHtml(product.title)}">
  3295.                     <span>Do košíku</span>
  3296.                   </a>`
  3297.                 : `<form action="/action/Cart/addCartItem/" method="post" class="pr-action csrf-enabled">
  3298.                     <input type="hidden" name="language" value="cs">
  3299.                     ${product.priceId ? `<input type="hidden" name="priceId" value="${product.priceId}">` : ''}
  3300.                     <input type="hidden" name="productId" value="${product._id || product.id}">
  3301.                     <input type="hidden" name="amount" value="1" autocomplete="off">
  3302.                     <button type="submit" class="btn btn-cart add-to-cart-button" data-testid="buttonAddToCart" aria-label="Do košíku ${this.escapeHtml(product.title)}">
  3303.                       <span>Do košíku</span>
  3304.                     </button>
  3305.                     ${csrfToken ? `<input type="hidden" name="__csrf__" value="${csrfToken}">` : ''}
  3306.                   </form>`
  3307.               }
  3308.             </div>
  3309.           </div>
  3310.         </div>
  3311.       </div>
  3312.      
  3313.       <span class="no-display" data-micro="sku">${this.escapeHtml(product.kod || 'N/A')}</span>
  3314.     `;
  3315.    
  3316.     productDiv.appendChild(productInner);
  3317.    
  3318.     // Bind add to cart form submission (only for products without variants)
  3319.     const addToCartForm = productInner.querySelector('form.pr-action');
  3320.     if (addToCartForm && !product.has_variants && (!product.variants || product.variants.length === 0)) {
  3321.       addToCartForm.addEventListener('submit', async (e) => {
  3322.         e.preventDefault();
  3323.        
  3324.         // Track click before adding to cart
  3325.         if (window.searchTracker && this.currentQuery) {
  3326.           const position = index + 1 + (this.currentPage - 1) * this.pageSize;
  3327.           window.searchTracker.trackClick({
  3328.             clicked_result: product._id || product.id,
  3329.             product_id: product._id || product.id,
  3330.             result_type: 'product',
  3331.             result_title: product.title,
  3332.             result_url: product.url,
  3333.             result_position: position,
  3334.             query: this.currentQuery
  3335.           });
  3336.         }
  3337.        
  3338.         // Get button for feedback
  3339.         const button = addToCartForm.querySelector('button[type="submit"]');
  3340.         const originalHTML = button.innerHTML;
  3341.        
  3342.         // Show loading
  3343.         button.disabled = true;
  3344.         button.innerHTML = '<span>Přidávám...</span>';
  3345.        
  3346.         try {
  3347.           if (typeof shoptet === 'undefined' || !shoptet.cartShared) {
  3348.             throw new Error('Shoptet API není k dispozici');
  3349.           }
  3350.          
  3351.           // Use Shoptet API according to documentation
  3352.           // Priority: priceId > productCode > productId
  3353.           const addParams = {};
  3354.           if (product.priceId) {
  3355.             addParams.priceId = product.priceId;
  3356.           } else if (product.kod || product.code) {
  3357.             addParams.productCode = product.kod || product.code;
  3358.           } else if (product._id || product.id) {
  3359.             addParams.productId = product._id || product.id;
  3360.           } else {
  3361.             throw new Error('Nelze identifikovat produkt');
  3362.           }
  3363.          
  3364.           // Add to cart (without silent mode - show Shoptet's default behavior)
  3365.           await shoptet.cartShared.addToCart(addParams);
  3366.          
  3367.           // Success
  3368.           button.innerHTML = '<span>Přidáno</span>';
  3369.          
  3370.           // Reset after 2s
  3371.           setTimeout(() => {
  3372.             button.disabled = false;
  3373.             button.innerHTML = originalHTML;
  3374.           }, 2000);
  3375.          
  3376.           // Tracking
  3377.           if (window.revelorDataLayer) {
  3378.             window.dataLayer.push({
  3379.               event: 'revelor_quick_add_to_cart',
  3380.               product_id: product._id || product.id,
  3381.               product_name: product.title,
  3382.               price: product.price_s_dph,
  3383.               source: 'search_results'
  3384.             });
  3385.           }
  3386.         } catch (error) {
  3387.           console.error('Add to Cart Error:', error);
  3388.           button.disabled = false;
  3389.           button.innerHTML = originalHTML;
  3390.         }
  3391.       });
  3392.     }
  3393.  
  3394.     // Add click tracking for product links (image, name)
  3395.     const productLinks = productDiv.querySelectorAll('a[href="' + product.url + '"]');
  3396.     if (productLinks.length > 0 && window.searchTracker && this.currentQuery) {
  3397.       productLinks.forEach(link => {
  3398.         link.addEventListener('click', () => {
  3399.           const position = index + 1 + (this.currentPage - 1) * this.pageSize;
  3400.          
  3401.           window.searchTracker.trackClick({
  3402.             clicked_result: product._id || product.id,
  3403.             product_id: product._id || product.id,
  3404.             result_type: 'product',
  3405.             result_title: product.title,
  3406.             result_url: product.url,
  3407.             result_position: position,
  3408.             query: this.currentQuery
  3409.           });
  3410.         });
  3411.       });
  3412.     }
  3413.  
  3414.     // Add click tracking for "Do košíku" button (products with variants - redirects to detail)
  3415.     const addToCartLink = productInner.querySelector('a.add-to-cart-button');
  3416.     if (addToCartLink && window.searchTracker && this.currentQuery) {
  3417.       addToCartLink.addEventListener('click', () => {
  3418.         const position = index + 1 + (this.currentPage - 1) * this.pageSize;
  3419.        
  3420.         window.searchTracker.trackClick({
  3421.           clicked_result: product._id || product.id,
  3422.           product_id: product._id || product.id,
  3423.           result_type: 'product',
  3424.           result_title: product.title,
  3425.           result_url: product.url,
  3426.           result_position: position,
  3427.           query: this.currentQuery
  3428.         });
  3429.       });
  3430.     }
  3431.  
  3432.  
  3433.     return productDiv;
  3434.   }
  3435.  
  3436.   /**
  3437.    * Get CSRF token from Shoptet if available
  3438.    */
  3439.   getCsrfToken() {
  3440.     // Try to find CSRF token from existing forms on page
  3441.     const existingForm = document.querySelector('form.csrf-enabled');
  3442.     if (existingForm) {
  3443.       const csrfInput = existingForm.querySelector('input[name="__csrf__"]');
  3444.       if (csrfInput) {
  3445.         return csrfInput.value;
  3446.       }
  3447.     }
  3448.     return null;
  3449.   }
  3450.  
  3451.   /**
  3452.    * Display categories
  3453.    */
  3454.   displayCategories(categories) {
  3455.     const container = document.getElementById('search-categories-container');
  3456.     const section = document.getElementById('search-results-categories');
  3457.  
  3458.     if (!container || !section) return;
  3459.  
  3460.     section.style.display = 'block';
  3461.  
  3462.     // Clear previous categories
  3463.     container.innerHTML = '';
  3464.    
  3465.     // Remove old expand-all button if exists
  3466.     const oldExpandAllBtn = section.querySelector('.subcategories-expand-all-btn');
  3467.     if (oldExpandAllBtn) {
  3468.       oldExpandAllBtn.remove();
  3469.     }
  3470.    
  3471.     // Reset enhanced state if exists
  3472.     const wrapper = container.closest('.subcategories-wrapper');
  3473.     if (wrapper) {
  3474.       const expandBtn = wrapper.nextElementSibling;
  3475.       if (expandBtn && expandBtn.classList.contains('subcategories-expand-btn')) {
  3476.         expandBtn.remove();
  3477.       }
  3478.       wrapper.dataset.enhanced = 'false';
  3479.       const parent = wrapper.parentElement;
  3480.       if (parent) {
  3481.         parent.insertBefore(container, wrapper);
  3482.         wrapper.remove();
  3483.       }
  3484.     }
  3485.     container.dataset.enhanced = 'false';
  3486.  
  3487.     // Store all categories for expand functionality
  3488.     this.allCategories = categories;
  3489.     const maxVisible = 8;
  3490.     const visibleCategories = categories.slice(0, maxVisible);
  3491.     const hasMore = categories.length > maxVisible;
  3492.  
  3493.     // Display visible categories
  3494.     visibleCategories.forEach((category, index) => {
  3495.       const categoryElement = this.createCategoryElement(category, index);
  3496.       container.appendChild(categoryElement);
  3497.     });
  3498.  
  3499.     // Add expand button if there are more categories
  3500.     if (hasMore) {
  3501.       const expandButton = document.createElement('button');
  3502.       expandButton.type = 'button';
  3503.       expandButton.className = 'subcategories-expand-all-btn btn btn--secondary';
  3504.       expandButton.setAttribute('aria-expanded', 'false');
  3505.       expandButton.setAttribute('aria-label', `Zobrazit všechny kategorie (celkem ${categories.length})`);
  3506.       expandButton.innerHTML = `<span>Zobrazit všechny kategorie (${categories.length})</span>`;
  3507.      
  3508.       expandButton.addEventListener('click', () => {
  3509.         const isExpanded = expandButton.getAttribute('aria-expanded') === 'true';
  3510.        
  3511.         if (!isExpanded) {
  3512.           // Show all categories
  3513.           container.innerHTML = '';
  3514.           this.allCategories.forEach((category, index) => {
  3515.             const categoryElement = this.createCategoryElement(category, index);
  3516.             container.appendChild(categoryElement);
  3517.           });
  3518.          
  3519.           expandButton.setAttribute('aria-expanded', 'true');
  3520.           expandButton.setAttribute('aria-label', 'Zobrazit méně kategorií');
  3521.           expandButton.innerHTML = '<span>Zobrazit méně kategorií</span>';
  3522.           this.announceToScreenReader(`Zobrazeno všech ${categories.length} kategorií`);
  3523.         } else {
  3524.           // Show only first 8
  3525.           container.innerHTML = '';
  3526.           visibleCategories.forEach((category, index) => {
  3527.             const categoryElement = this.createCategoryElement(category, index);
  3528.             container.appendChild(categoryElement);
  3529.           });
  3530.          
  3531.           expandButton.setAttribute('aria-expanded', 'false');
  3532.           expandButton.setAttribute('aria-label', `Zobrazit všechny kategorie (celkem ${categories.length})`);
  3533.           expandButton.innerHTML = `<span>Zobrazit všechny kategorie (${categories.length})</span>`;
  3534.           this.announceToScreenReader('Zobrazeno prvních 8 kategorií');
  3535.         }
  3536.       });
  3537.      
  3538.       // Insert button after container
  3539.       container.parentElement.insertBefore(expandButton, container.nextSibling);
  3540.     }
  3541.  
  3542.     // Enhance subcategories with expand/collapse functionality
  3543.     this.enhanceSubcategories();
  3544.   }
  3545.  
  3546.   /**
  3547.    * Create category element
  3548.    */
  3549.   createCategoryElement(category, index = 0) {
  3550.     const li = document.createElement('li');
  3551.     li.className = 'col-xs-6 col-sm-6 col-lg-3 col-xl-3';
  3552.     li.innerHTML = `
  3553.       <a href="${category.url}">
  3554.         <span class="image">
  3555.           <img src="${category.img || 'https://cdn.myshoptet.com/prj/dist/master/cms/templates/frontend_templates/00/img/folder.svg'}"
  3556.                alt="${category.title}"
  3557.                width="140"
  3558.                height="100"
  3559.                loading="lazy">
  3560.         </span>
  3561.         <span class="text">${category.title}</span>
  3562.       </a>
  3563.     `;
  3564.  
  3565.     // Add click tracking
  3566.     const link = li.querySelector('a');
  3567.     if (link && window.searchTracker && this.currentQuery) {
  3568.       link.addEventListener('click', () => {
  3569.         window.searchTracker.trackClick({
  3570.           clicked_result: category._id || category.id || category.url,
  3571.           result_type: 'category',
  3572.           result_title: category.title,
  3573.           result_url: category.url,
  3574.           result_position: index + 1,
  3575.           query: this.currentQuery
  3576.         });
  3577.       });
  3578.     }
  3579.  
  3580.     return li;
  3581.   }
  3582.  
  3583.   /**
  3584.    * Enhance subcategories to show only first row with expand button
  3585.    */
  3586.   enhanceSubcategories() {
  3587.     const subcategoriesList = document.querySelector(
  3588.       '.subcategories.with-image'
  3589.     );
  3590.  
  3591.     if (!subcategoriesList) {
  3592.       return;
  3593.     }
  3594.  
  3595.     // Check if already enhanced
  3596.     if (subcategoriesList.dataset.enhanced === 'true') {
  3597.       return;
  3598.     }
  3599.  
  3600.     // Get all subcategory items
  3601.     const items = Array.from(subcategoriesList.querySelectorAll('li'));
  3602.  
  3603.     if (items.length === 0) {
  3604.       return;
  3605.     }
  3606.  
  3607.     // Determine how many items to show in first row based on viewport
  3608.     const getItemsPerRow = () => {
  3609.       const viewportWidth = window.innerWidth;
  3610.       if (viewportWidth <= 480) return 1; // xs: 1 column
  3611.       if (viewportWidth <= 768) return 2; // sm: 2 columns
  3612.       if (viewportWidth <= 1200) return 3; // lg: 3 columns
  3613.       if (viewportWidth <= 1440) return 4; // xl: 4 columns
  3614.       return 6; // Default: 6 columns
  3615.     };
  3616.  
  3617.     const itemsPerRow = getItemsPerRow();
  3618.  
  3619.     // If all items fit in one row, no need for enhancement
  3620.     if (items.length <= itemsPerRow) {
  3621.       return;
  3622.     }
  3623.  
  3624.     // Add wrapper for collapsed state
  3625.     const wrapper = document.createElement('div');
  3626.     wrapper.className = 'subcategories-wrapper';
  3627.     wrapper.dataset.collapsed = 'true';
  3628.  
  3629.     // Insert wrapper and move list inside
  3630.     subcategoriesList.parentNode.insertBefore(wrapper, subcategoriesList);
  3631.     wrapper.appendChild(subcategoriesList);
  3632.  
  3633.     // Create expand button
  3634.     const expandButton = document.createElement('button');
  3635.     expandButton.type = 'button';
  3636.     expandButton.className = 'subcategories-expand-btn wt-btn-ghost';
  3637.     expandButton.setAttribute('aria-expanded', 'false');
  3638.     expandButton.setAttribute('aria-controls', 'search-categories-list');
  3639.     expandButton.setAttribute('aria-label', 'Zobrazit další kategorie');
  3640.     expandButton.innerHTML = `
  3641.       <span class="text" aria-hidden="true">Další kategorie</span>
  3642.     `;
  3643.  
  3644.     // Insert button after wrapper
  3645.     wrapper.insertAdjacentElement('afterend', expandButton);
  3646.  
  3647.     // Handle expand/collapse
  3648.     expandButton.addEventListener('click', () => {
  3649.       const isCollapsed = wrapper.dataset.collapsed === 'true';
  3650.       const visibleBefore = items.length;
  3651.       const visibleAfter = isCollapsed ? items.length : itemsPerRow;
  3652.  
  3653.       if (isCollapsed) {
  3654.         wrapper.dataset.collapsed = 'false';
  3655.         expandButton.classList.add('expanded');
  3656.         expandButton.setAttribute('aria-expanded', 'true');
  3657.         expandButton.setAttribute('aria-label', 'Zobrazit méně kategorií');
  3658.         expandButton.querySelector('.text').textContent = 'Méně kategorií';
  3659.         this.announceToScreenReader('Kategorie rozbaleny');
  3660.        
  3661.         // DataLayer: Search Category Expand
  3662.         if (window.revelorDataLayer) {
  3663.           window.revelorDataLayer.searchCategoryExpand(
  3664.             this.currentQuery,
  3665.             'categories',
  3666.             'expand',
  3667.             itemsPerRow,
  3668.             visibleBefore
  3669.           );
  3670.         }
  3671.       } else {
  3672.         wrapper.dataset.collapsed = 'true';
  3673.         expandButton.classList.remove('expanded');
  3674.         expandButton.setAttribute('aria-expanded', 'false');
  3675.         expandButton.setAttribute('aria-label', 'Zobrazit další kategorie');
  3676.         expandButton.querySelector('.text').textContent = 'Další kategorie';
  3677.         this.announceToScreenReader('Kategorie sbaleny');
  3678.        
  3679.         // DataLayer: Search Category Collapse
  3680.         if (window.revelorDataLayer) {
  3681.           window.revelorDataLayer.searchCategoryExpand(
  3682.             this.currentQuery,
  3683.             'categories',
  3684.             'collapse',
  3685.             visibleBefore,
  3686.             itemsPerRow
  3687.           );
  3688.         }
  3689.        
  3690.         // Scroll to subcategories if needed
  3691.         const wrapperTop = wrapper.getBoundingClientRect().top + window.scrollY;
  3692.         if (window.scrollY > wrapperTop) {
  3693.           window.scrollTo({
  3694.             top: wrapperTop - 100,
  3695.             behavior: 'smooth',
  3696.           });
  3697.         }
  3698.       }
  3699.     });
  3700.  
  3701.     // Update items per row on resize
  3702.     const updateItemsPerRow = () => {
  3703.       const newItemsPerRow = getItemsPerRow();
  3704.       wrapper.style.setProperty('--items-per-row', newItemsPerRow);
  3705.     };
  3706.  
  3707.     // Initial setup
  3708.     updateItemsPerRow();
  3709.  
  3710.     // Add resize listener
  3711.     let subcategoriesResizeTimeout;
  3712.     window.addEventListener('resize', () => {
  3713.       clearTimeout(subcategoriesResizeTimeout);
  3714.       subcategoriesResizeTimeout = setTimeout(updateItemsPerRow, 250);
  3715.     });
  3716.  
  3717.     // Mark as enhanced
  3718.     subcategoriesList.dataset.enhanced = 'true';
  3719.   }
  3720.  
  3721.   /**
  3722.    * Display articles
  3723.    */
  3724.   displayArticles(articles) {
  3725.     const container = document.getElementById('newsWrapper');
  3726.     const section = document.getElementById('search-results-articles');
  3727.  
  3728.     if (!container || !section) return;
  3729.  
  3730.     section.style.display = 'block';
  3731.  
  3732.     // Clear previous articles
  3733.     container.innerHTML = '';
  3734.    
  3735.     // Reset enhanced state if exists
  3736.     const wrapper = container.closest('.articles-wrapper');
  3737.     if (wrapper) {
  3738.       const expandBtn = wrapper.nextElementSibling;
  3739.       if (expandBtn && expandBtn.classList.contains('articles-expand-btn')) {
  3740.         expandBtn.remove();
  3741.       }
  3742.       wrapper.dataset.enhanced = 'false';
  3743.       const parent = wrapper.parentElement;
  3744.       if (parent) {
  3745.         parent.insertBefore(container, wrapper);
  3746.         wrapper.remove();
  3747.       }
  3748.     }
  3749.     container.dataset.enhanced = 'false';
  3750.  
  3751.     // Display only first 3 articles
  3752.     const maxVisible = 3;
  3753.     const visibleArticles = articles.slice(0, maxVisible);
  3754.  
  3755.     visibleArticles.forEach((article, index) => {
  3756.       const articleElement = this.createArticleElement(article, index);
  3757.       container.appendChild(articleElement);
  3758.     });
  3759.  
  3760.     // Don't use enhanceArticles - we show only 3 items, no expand needed
  3761.   }
  3762.  
  3763.   /**
  3764.    * Enhance articles to show only first row with expand button
  3765.    */
  3766.   enhanceArticles() {
  3767.     const articlesContainer = document.getElementById('newsWrapper');
  3768.  
  3769.     if (!articlesContainer) {
  3770.       return;
  3771.     }
  3772.  
  3773.     // Check if already enhanced
  3774.     if (articlesContainer.dataset.enhanced === 'true') {
  3775.       return;
  3776.     }
  3777.  
  3778.     // Get all article items
  3779.     const items = Array.from(
  3780.       articlesContainer.querySelectorAll('article.news-item')
  3781.     );
  3782.  
  3783.     if (items.length === 0) {
  3784.       return;
  3785.     }
  3786.  
  3787.     // Determine how many items to show in first row based on viewport
  3788.     const getItemsPerRow = () => {
  3789.       const viewportWidth = window.innerWidth;
  3790.       if (viewportWidth <= 480) return 1; // xs: 1 column
  3791.       if (viewportWidth <= 768) return 2; // sm: 2 columns
  3792.       if (viewportWidth <= 1200) return 3; // lg: 3 columns
  3793.       if (viewportWidth <= 1440) return 4; // xl: 4 columns
  3794.       return 6; // Default: 6 columns
  3795.     };
  3796.  
  3797.     const itemsPerRow = getItemsPerRow();
  3798.  
  3799.     // If all items fit in one row, no need for enhancement
  3800.     if (items.length <= itemsPerRow) {
  3801.       return;
  3802.     }
  3803.  
  3804.     // Add wrapper for collapsed state
  3805.     const wrapper = document.createElement('div');
  3806.     wrapper.className = 'articles-wrapper';
  3807.     wrapper.dataset.collapsed = 'true';
  3808.  
  3809.     // Insert wrapper and move articles inside
  3810.     articlesContainer.parentNode.insertBefore(wrapper, articlesContainer);
  3811.     wrapper.appendChild(articlesContainer);
  3812.  
  3813.     // Create expand button
  3814.     const expandButton = document.createElement('button');
  3815.     expandButton.type = 'button';
  3816.     expandButton.className = 'articles-expand-btn wt-btn-ghost';
  3817.     expandButton.setAttribute('aria-expanded', 'false');
  3818.     expandButton.setAttribute('aria-controls', 'newsWrapper');
  3819.     expandButton.setAttribute('aria-label', 'Zobrazit další články');
  3820.     expandButton.innerHTML = `
  3821.       <span class="text" aria-hidden="true">Další články</span>
  3822.     `;
  3823.  
  3824.     // Insert button after wrapper
  3825.     wrapper.insertAdjacentElement('afterend', expandButton);
  3826.  
  3827.     // Handle expand/collapse
  3828.     expandButton.addEventListener('click', () => {
  3829.       const isCollapsed = wrapper.dataset.collapsed === 'true';
  3830.       const visibleBefore = items.length;
  3831.  
  3832.       if (isCollapsed) {
  3833.         wrapper.dataset.collapsed = 'false';
  3834.         expandButton.classList.add('expanded');
  3835.         expandButton.setAttribute('aria-expanded', 'true');
  3836.         expandButton.setAttribute('aria-label', 'Zobrazit méně článků');
  3837.         expandButton.querySelector('.text').textContent = 'Méně článků';
  3838.         this.announceToScreenReader('Články rozbaleny');
  3839.        
  3840.         // DataLayer: Search Articles Expand
  3841.         if (window.revelorDataLayer) {
  3842.           window.revelorDataLayer.searchCategoryExpand(
  3843.             this.currentQuery,
  3844.             'articles',
  3845.             'expand',
  3846.             itemsPerRow,
  3847.             visibleBefore
  3848.           );
  3849.         }
  3850.       } else {
  3851.         wrapper.dataset.collapsed = 'true';
  3852.         expandButton.classList.remove('expanded');
  3853.         expandButton.setAttribute('aria-expanded', 'false');
  3854.         expandButton.setAttribute('aria-label', 'Zobrazit další články');
  3855.         expandButton.querySelector('.text').textContent = 'Další články';
  3856.         this.announceToScreenReader('Články sbaleny');
  3857.        
  3858.         // DataLayer: Search Articles Collapse
  3859.         if (window.revelorDataLayer) {
  3860.           window.revelorDataLayer.searchCategoryExpand(
  3861.             this.currentQuery,
  3862.             'articles',
  3863.             'collapse',
  3864.             visibleBefore,
  3865.             itemsPerRow
  3866.           );
  3867.         }
  3868.        
  3869.         // Scroll to articles if needed
  3870.         const wrapperTop = wrapper.getBoundingClientRect().top + window.scrollY;
  3871.         if (window.scrollY > wrapperTop) {
  3872.           window.scrollTo({
  3873.             top: wrapperTop - 100,
  3874.             behavior: 'smooth',
  3875.           });
  3876.         }
  3877.       }
  3878.     });
  3879.  
  3880.     // Update items per row on resize
  3881.     const updateItemsPerRow = () => {
  3882.       const newItemsPerRow = getItemsPerRow();
  3883.       wrapper.style.setProperty('--items-per-row', newItemsPerRow);
  3884.     };
  3885.  
  3886.     // Initial setup
  3887.     updateItemsPerRow();
  3888.  
  3889.     // Add resize listener
  3890.     let articlesResizeTimeout;
  3891.     window.addEventListener('resize', () => {
  3892.       clearTimeout(articlesResizeTimeout);
  3893.       articlesResizeTimeout = setTimeout(updateItemsPerRow, 250);
  3894.     });
  3895.  
  3896.     // Mark as enhanced
  3897.     articlesContainer.dataset.enhanced = 'true';
  3898.   }
  3899.  
  3900.   /**
  3901.    * Create article element
  3902.    */
  3903.   createArticleElement(article, index = 0) {
  3904.     const articleDiv = document.createElement('article');
  3905.     articleDiv.className = 'news-item';
  3906.    
  3907.     // Microdata for AI agents
  3908.     articleDiv.setAttribute('itemscope', '');
  3909.     articleDiv.setAttribute('itemtype', 'https://schema.org/Article');
  3910.    
  3911.     // Create JSON-LD structured data
  3912.     const jsonLd = {
  3913.       "@context": "https://schema.org",
  3914.       "@type": "Article",
  3915.       "headline": article.title,
  3916.       "description": article.description || article.title,
  3917.       "image": article.img || '/img/no-image.jpg',
  3918.       "url": article.url,
  3919.       "author": {
  3920.         "@type": "Organization",
  3921.         "name": "Your E-shop"
  3922.       }
  3923.     };
  3924.    
  3925.     // Compact article display - small image + title only
  3926.     articleDiv.innerHTML = `
  3927.       <!-- JSON-LD for AI agents -->
  3928.       <script type="application/ld+json">
  3929.         ${JSON.stringify(jsonLd)}
  3930.       </script>
  3931.      
  3932.       <a href="${article.url}" title="${article.title}" class="news-item-compact" itemprop="url">
  3933.         <span class="news-item-compact-image">
  3934.           <img src="${article.img || '/img/no-image.jpg'}"
  3935.                alt="${article.title}"
  3936.                itemprop="image"
  3937.                class="news-image-compact"
  3938.                width="100"
  3939.                height="100"
  3940.                loading="lazy">
  3941.         </span>
  3942.         <span class="news-item-compact-title" itemprop="headline">
  3943.           ${this.escapeHtml(article.title)}
  3944.         </span>
  3945.       </a>
  3946.     `;
  3947.  
  3948.     // Add click tracking for article links
  3949.     const articleLinks = articleDiv.querySelectorAll('a[href="' + article.url + '"]');
  3950.     if (articleLinks.length > 0 && window.searchTracker && this.currentQuery) {
  3951.       articleLinks.forEach(link => {
  3952.         link.addEventListener('click', () => {
  3953.           window.searchTracker.trackClick({
  3954.             clicked_result: article._id || article.id || article.url,
  3955.             result_type: 'article',
  3956.             result_title: article.title,
  3957.             result_url: article.url,
  3958.             result_position: index + 1,
  3959.             query: this.currentQuery
  3960.           });
  3961.         });
  3962.       });
  3963.     }
  3964.  
  3965.     return articleDiv;
  3966.   }
  3967.  
  3968.   /**
  3969.    * Display brands
  3970.    */
  3971.   displayBrands(brands) {
  3972.     const container = document.getElementById('search-brands-container');
  3973.     const section = document.getElementById('search-results-brands');
  3974.  
  3975.     if (!container || !section) return;
  3976.  
  3977.     section.style.display = 'block';
  3978.  
  3979.     brands.forEach((brand, index) => {
  3980.       const brandElement = this.createBrandElement(brand, index);
  3981.       container.appendChild(brandElement);
  3982.     });
  3983.   }
  3984.  
  3985.   /**
  3986.    * Create brand element
  3987.    */
  3988.   createBrandElement(brand, index = 0) {
  3989.     const li = document.createElement('li');
  3990.     li.className = 'search-brand-item';
  3991.     li.innerHTML = `
  3992.       <a href="${brand.url}">
  3993.         <div class="search-brand-content">
  3994.           <h3>${brand.title}</h3>
  3995.           <p>${brand.description || ''}</p>
  3996.         </div>
  3997.       </a>
  3998.     `;
  3999.  
  4000.     // Add click tracking (brands tracked as categories)
  4001.     const link = li.querySelector('a');
  4002.     if (link && window.searchTracker && this.currentQuery) {
  4003.       link.addEventListener('click', () => {
  4004.         window.searchTracker.trackClick({
  4005.           clicked_result: brand._id || brand.id || brand.url,
  4006.           result_type: 'category', // Brands tracked as categories
  4007.           result_title: brand.title,
  4008.           result_url: brand.url,
  4009.           result_position: index + 1,
  4010.           query: this.currentQuery
  4011.         });
  4012.       });
  4013.     }
  4014.  
  4015.     return li;
  4016.   }
  4017.  
  4018.   /**
  4019.    * Load more results
  4020.    */
  4021.   async loadMoreResults() {
  4022.     if (this.isLoading || !this.hasMoreResults) return;
  4023.  
  4024.     this.currentPage++;
  4025.     this.showLoadMoreLoading();
  4026.  
  4027.     // DataLayer: Search Load More
  4028.     if (window.revelorDataLayer) {
  4029.       const currentProductsCount = document.querySelectorAll('#search-products-container .product').length;
  4030.       window.revelorDataLayer.searchLoadMore(
  4031.         this.currentQuery,
  4032.         this.currentPage - 1,
  4033.         Math.ceil((this.latestTotalProducts || 0) / this.pageSize),
  4034.         this.pageSize,
  4035.         currentProductsCount + this.pageSize
  4036.       );
  4037.     }
  4038.  
  4039.     try {
  4040.       const response = await fetch(
  4041.         `${this.apiBaseUrl}/search?q=${encodeURIComponent(this.currentQuery)}&projectId=${this.projectId}&page=${this.currentPage}&page_size=${this.pageSize}`,
  4042.         {
  4043.           headers: {
  4044.             'X-Tenant': this.projectId,
  4045.           },
  4046.         }
  4047.       );
  4048.  
  4049.       if (!response.ok) {
  4050.         throw new Error(`HTTP error! status: ${response.status}`);
  4051.       }
  4052.  
  4053.       const data = await response.json();
  4054.       this.processSearchResults(data, true);
  4055.     } catch (error) {
  4056.       console.error('Load more error:', error);
  4057.       this.currentPage--; // Revert page increment on error
  4058.     } finally {
  4059.       this.hideLoadMoreLoading();
  4060.     }
  4061.   }
  4062.  
  4063.   /**
  4064.    * Update results count
  4065.    */
  4066.   updateResultsCount(data) {
  4067.     const countElement = document.getElementById('search-results-count');
  4068.     if (countElement) {
  4069.       const totalProducts = data.total_products || 0;
  4070.       this.latestTotalProducts = totalProducts;
  4071.       countElement.textContent = `Nalezeno ${totalProducts} výsledků`;
  4072.     }
  4073.   }
  4074.  
  4075.   /**
  4076.    * Update load more button
  4077.    */
  4078.   updateLoadMoreButton() {
  4079.     const loadMoreContainer = document.getElementById(
  4080.       'search-results-load-more'
  4081.     );
  4082.     if (loadMoreContainer) {
  4083.       loadMoreContainer.style.display = this.hasMoreResults ? 'block' : 'none';
  4084.     }
  4085.   }
  4086.  
  4087.   /**
  4088.    * Show loading state
  4089.    */
  4090.   showLoading() {
  4091.     const loading = document.getElementById('search-results-loading');
  4092.     if (loading) loading.style.display = 'block';
  4093.   }
  4094.  
  4095.   /**
  4096.    * Hide loading state
  4097.    */
  4098.   hideLoading() {
  4099.     const loading = document.getElementById('search-results-loading');
  4100.     if (loading) loading.style.display = 'none';
  4101.   }
  4102.  
  4103.   /**
  4104.    * Show load more loading
  4105.    */
  4106.   showLoadMoreLoading() {
  4107.     const button = document.getElementById('load-more-button');
  4108.     const text = button?.querySelector('.load-more-text');
  4109.     const spinner = button?.querySelector('.load-more-spinner');
  4110.  
  4111.     if (text) text.style.display = 'none';
  4112.     if (spinner) spinner.style.display = 'inline';
  4113.     if (button) {
  4114.       button.disabled = true;
  4115.       button.setAttribute('aria-busy', 'true');
  4116.       button.setAttribute('aria-label', 'Načítám další výsledky');
  4117.     }
  4118.     this.announceToScreenReader('Načítám další výsledky');
  4119.   }
  4120.  
  4121.   /**
  4122.    * Hide load more loading
  4123.    */
  4124.   hideLoadMoreLoading() {
  4125.     const button = document.getElementById('load-more-button');
  4126.     const text = button?.querySelector('.load-more-text');
  4127.     const spinner = button?.querySelector('.load-more-spinner');
  4128.  
  4129.     if (text) text.style.display = 'inline';
  4130.     if (spinner) spinner.style.display = 'none';
  4131.     if (button) {
  4132.       button.disabled = false;
  4133.       button.setAttribute('aria-busy', 'false');
  4134.       button.setAttribute('aria-label', 'Načíst více výsledků');
  4135.     }
  4136.   }
  4137.  
  4138.   /**
  4139.    * Show results
  4140.    */
  4141.   showResults() {
  4142.     const content = document.getElementById('search-results-content');
  4143.     if (content) content.style.display = 'block';
  4144.   }
  4145.  
  4146.   /**
  4147.    * Show no results
  4148.    */
  4149.   showNoResults(message = 'Žádné výsledky') {
  4150.     const noResults = document.getElementById('search-results-no-results');
  4151.     if (noResults) {
  4152.       noResults.style.display = 'block';
  4153.       const title = noResults.querySelector('h2');
  4154.       if (title) title.textContent = message;
  4155.     }
  4156.   }
  4157.  
  4158.   /**
  4159.    * Show error
  4160.    */
  4161.   showError() {
  4162.     this.showNoResults('Chyba při načítání výsledků');
  4163.   }
  4164. }
  4165.  
  4166. // Initialize when DOM is ready - only on search results page
  4167. document.addEventListener('DOMContentLoaded', () => {
  4168.   // Only initialize if body has 'in-vyhledavani' class
  4169.   if (document.body.classList.contains('in-vyhledavani')) {
  4170.     new SearchResultsPage();
  4171.   }
  4172. });
  4173.  
  4174.  
  4175.  
Advertisement
Add Comment
Please, Sign In to add comment