Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /**
- * Revelor Search Bundle
- * Combined and bundled JavaScript for Shoptet
- *
- * Contains:
- * - Console Wrapper (admin-only logging)
- * - DataLayer Helper (GTM analytics)
- * - Search Tracker (backend tracking)
- * - Search Overlay (main search functionality)
- * - Search Results Page (results page logic)
- *
- * Build date: 2025-11-01T00:12:21.753Z
- * Project: Revelor Textilomanie
- */
- // =============================================================================
- // console-wrapper.js
- // =============================================================================
- /**
- * @fileoverview Console Wrapper for Admin-Only Logging
- * Přepisuje console metody - výstupy se zobrazí pouze pokud je admin přihlášený
- * @version 1.0.0
- * @author Revelor by Webotvůrci
- */
- (function() {
- 'use strict';
- /**
- * Zkontroluje, jestli je admin přihlášený
- * Kontroluje dynamicky při každém volání (BODY může být ještě ne načtený)
- * @returns {boolean}
- */
- function isAdminLoggedIn() {
- // Pokud BODY ještě není načtené, vrať false (bezpečnější)
- if (!document.body) {
- return false;
- }
- // Zkontroluj, jestli má BODY class 'admin-logged'
- return document.body.classList.contains('admin-logged');
- }
- /**
- * Uložit původní console metody
- */
- const originalConsole = {
- log: console.log.bind(console),
- error: console.error.bind(console),
- warn: console.warn.bind(console),
- info: console.info.bind(console),
- debug: console.debug.bind(console),
- trace: console.trace.bind(console),
- table: console.table.bind(console),
- group: console.group.bind(console),
- groupEnd: console.groupEnd.bind(console),
- groupCollapsed: console.groupCollapsed.bind(console),
- time: console.time.bind(console),
- timeEnd: console.timeEnd.bind(console),
- assert: console.assert.bind(console)
- };
- /**
- * No-op funkce (nic nedělá)
- */
- function noop() {}
- /**
- * Wrapper funkce, která zkontroluje admin status
- */
- function createConsoleWrapper(originalMethod) {
- return function(...args) {
- if (isAdminLoggedIn()) {
- originalMethod.apply(console, args);
- }
- // Pokud není admin, nic se nestane (no-op)
- };
- }
- // Přepiš console metody pouze pokud ještě nejsou přepsané
- if (!window._revelorConsoleWrapped) {
- console.log = createConsoleWrapper(originalConsole.log);
- console.error = createConsoleWrapper(originalConsole.error);
- console.warn = createConsoleWrapper(originalConsole.warn);
- console.info = createConsoleWrapper(originalConsole.info);
- console.debug = createConsoleWrapper(originalConsole.debug);
- console.trace = createConsoleWrapper(originalConsole.trace);
- console.table = createConsoleWrapper(originalConsole.table);
- console.group = createConsoleWrapper(originalConsole.group);
- console.groupEnd = createConsoleWrapper(originalConsole.groupEnd);
- console.groupCollapsed = createConsoleWrapper(originalConsole.groupCollapsed);
- console.time = createConsoleWrapper(originalConsole.time);
- console.timeEnd = createConsoleWrapper(originalConsole.timeEnd);
- console.assert = createConsoleWrapper(originalConsole.assert);
- // Označit, že jsme už wrapper aplikovali
- window._revelorConsoleWrapped = true;
- }
- })();
- // =============================================================================
- // datalayer-helper.js
- // =============================================================================
- /**
- * @fileoverview DataLayer Helper for Revelor Search Analytics
- * Manages all dataLayer pushes for Google Tag Manager
- * @version 2.0.0
- * @author Revelor by Webotvůrci
- * @contact [email protected] | +420 728 089 029
- */
- class RevelorDataLayer {
- constructor() {
- // Initialize dataLayer if not exists
- window.dataLayer = window.dataLayer || [];
- // Session storage for tracking search journey
- this.sessionKey = 'revelor_search_session';
- this.initSession();
- }
- /**
- * Initialize search session
- */
- initSession() {
- if (!sessionStorage.getItem(this.sessionKey)) {
- sessionStorage.setItem(this.sessionKey, JSON.stringify({
- searchCount: 0,
- lastSearchTerm: null,
- lastSearchTime: null,
- searchStartTime: null
- }));
- }
- }
- /**
- * Get search session data
- */
- getSession() {
- return JSON.parse(sessionStorage.getItem(this.sessionKey) || '{}');
- }
- /**
- * Update search session
- */
- updateSession(updates) {
- const session = this.getSession();
- const newSession = { ...session, ...updates };
- sessionStorage.setItem(this.sessionKey, JSON.stringify(newSession));
- }
- /**
- * Get current timestamp in ISO format
- */
- getTimestamp() {
- return new Date().toISOString();
- }
- /**
- * Get user type (new vs returning)
- */
- getUserType() {
- // Check if user has visited before (using cookie or localStorage)
- return localStorage.getItem('user_visited') ? 'returning' : 'new';
- }
- /**
- * Get page type from URL or body class
- */
- getPageType() {
- const path = window.location.pathname;
- if (path === '/' || path === '') return 'homepage';
- if (path.includes('/produkt/') || path.includes('/product/')) return 'product';
- if (path.includes('/kategorie/') || path.includes('/category/')) return 'category';
- if (path.includes('/vysledky-hledani') || path.includes('/search')) return 'search_results';
- return 'other';
- }
- /**
- * Calculate time difference in seconds
- */
- getTimeDiff(startTime) {
- if (!startTime) return 0;
- return ((Date.now() - new Date(startTime).getTime()) / 1000).toFixed(1);
- }
- /**
- * Push event to dataLayer
- */
- push(eventData) {
- console.log('📊 Revelor DataLayer Push:', eventData);
- window.dataLayer.push(eventData);
- }
- /**
- * 1. Search Started
- */
- searchStarted(context = 'overlay') {
- const session = this.getSession();
- this.push({
- event: 'revelor_search_started',
- search_context: context,
- timestamp: this.getTimestamp(),
- user_type: this.getUserType(),
- page_type: this.getPageType(),
- session_search_count: session.searchCount || 0
- });
- // Update session
- this.updateSession({
- searchStartTime: this.getTimestamp(),
- searchCount: (session.searchCount || 0) + 1
- });
- }
- /**
- * 2. Search Query Submitted
- */
- searchQuerySubmitted(searchTerm, context = 'overlay') {
- const session = this.getSession();
- const isRefinement = session.lastSearchTerm && session.lastSearchTerm !== searchTerm;
- this.push({
- event: 'revelor_search_query_submitted',
- search_term: searchTerm,
- search_term_length: searchTerm.length,
- search_context: context,
- is_refinement: isRefinement,
- previous_search_term: isRefinement ? session.lastSearchTerm : null,
- timestamp: this.getTimestamp()
- });
- // Update session
- this.updateSession({
- lastSearchTerm: searchTerm,
- lastSearchTime: this.getTimestamp()
- });
- }
- /**
- * 3. Search Results Displayed
- */
- searchResultsDisplayed(searchTerm, resultsData, responseTime, page = 1) {
- const totalResults = resultsData.total_products || 0;
- const productsCount = resultsData.products?.length || 0;
- const categoriesCount = resultsData.categories?.length || 0;
- const articlesCount = resultsData.articles?.length || 0;
- const brandsCount = resultsData.brands?.length || 0;
- this.push({
- event: 'revelor_search_results_displayed',
- search_term: searchTerm,
- results_count: {
- total: totalResults,
- products: productsCount,
- categories: categoriesCount,
- articles: articlesCount,
- brands: brandsCount
- },
- has_results: totalResults > 0,
- page: page,
- response_time_ms: responseTime,
- timestamp: this.getTimestamp()
- });
- }
- /**
- * 4. Search No Results
- */
- searchNoResults(searchTerm) {
- // Simple typo detection (contains numbers mixed with letters, etc.)
- const potentialTypo = /\d/.test(searchTerm) || searchTerm.length < 3;
- this.push({
- event: 'revelor_search_no_results',
- search_term: searchTerm,
- search_term_length: searchTerm.length,
- potential_typo: potentialTypo,
- timestamp: this.getTimestamp()
- });
- }
- /**
- * 5. Search Result Click
- */
- searchResultClick(searchTerm, resultType, resultData, position, totalResults) {
- const session = this.getSession();
- const clickTimeFromSearch = this.getTimeDiff(session.lastSearchTime);
- const eventData = {
- event: 'revelor_search_result_click',
- search_term: searchTerm,
- result_type: resultType,
- result_position: position,
- total_results: totalResults,
- click_time_from_search: parseFloat(clickTimeFromSearch),
- timestamp: this.getTimestamp()
- };
- // Add result-specific data
- if (resultType === 'product') {
- eventData.result_data = {
- item_id: resultData._id || resultData.id,
- item_name: resultData.title,
- item_category: resultData.category || 'Neznámá kategorie',
- price: resultData.price_s_dph || 0,
- currency: 'CZK',
- availability: resultData.skladem ? 'in_stock' : 'out_of_stock',
- item_url: resultData.url
- };
- // GA4 Enhanced Ecommerce format
- eventData.ecommerce = {
- currency: 'CZK',
- value: resultData.price_s_dph || 0,
- items: [{
- item_id: resultData._id || resultData.id,
- item_name: resultData.title,
- item_list_name: 'Search Results',
- item_list_id: 'search_results',
- price: resultData.price_s_dph || 0,
- index: position,
- quantity: 1
- }]
- };
- } else if (resultType === 'category') {
- eventData.result_data = {
- category_name: resultData.title,
- category_url: resultData.url
- };
- } else if (resultType === 'article') {
- eventData.result_data = {
- article_title: resultData.title,
- article_url: resultData.url
- };
- }
- this.push(eventData);
- }
- /**
- * 6. Search Load More
- */
- searchLoadMore(searchTerm, currentPage, totalPages, productsLoaded, totalProductsShown) {
- this.push({
- event: 'revelor_search_load_more',
- search_term: searchTerm,
- current_page: currentPage,
- next_page: currentPage + 1,
- total_pages: totalPages,
- products_loaded: productsLoaded,
- total_products_shown: totalProductsShown,
- timestamp: this.getTimestamp()
- });
- }
- /**
- * 7. Search Refinement
- */
- searchRefinement(originalTerm, refinedTerm, originalResults, refinedResults) {
- // Determine refinement type
- let refinementType = 'modification';
- if (refinedTerm.length > originalTerm.length) {
- refinementType = 'expansion';
- } else if (refinedTerm.length < originalTerm.length) {
- refinementType = 'narrowing';
- } else if (Math.abs(refinedTerm.length - originalTerm.length) <= 2) {
- refinementType = 'correction';
- }
- this.push({
- event: 'revelor_search_refinement',
- original_search_term: originalTerm,
- refined_search_term: refinedTerm,
- refinement_type: refinementType,
- original_results_count: originalResults,
- refined_results_count: refinedResults,
- timestamp: this.getTimestamp()
- });
- }
- /**
- * 8. Search Suggestion Click
- */
- searchSuggestionClick(typedQuery, suggestedTerm, position, suggestionType) {
- this.push({
- event: 'revelor_search_suggestion_click',
- typed_query: typedQuery,
- suggested_term: suggestedTerm,
- suggestion_position: position,
- suggestion_type: suggestionType,
- timestamp: this.getTimestamp()
- });
- }
- /**
- * 9. Search Recommendations Shown
- */
- searchRecommendationsShown(recommendationsData) {
- const products = recommendationsData.products || [];
- const categories = recommendationsData.categories || [];
- this.push({
- event: 'revelor_search_recommendations_shown',
- recommendations_count: products.length + categories.length,
- products_count: products.length,
- categories_count: categories.length,
- recommendation_type: 'default', // trending/popular/personalized
- timestamp: this.getTimestamp()
- });
- }
- /**
- * 10. Search Category Expand
- */
- searchCategoryExpand(searchTerm, expandType, action, visibleBefore, visibleAfter) {
- this.push({
- event: 'revelor_search_category_expand',
- search_term: searchTerm,
- expand_type: expandType, // 'categories' | 'articles'
- action: action, // 'expand' | 'collapse'
- visible_items_before: visibleBefore,
- visible_items_after: visibleAfter,
- timestamp: this.getTimestamp()
- });
- }
- /**
- * 11. Search Abandoned
- */
- searchAbandoned(searchTerm, resultsCount, timeSpent, reason = 'closed_overlay') {
- this.push({
- event: 'revelor_search_abandoned',
- search_term: searchTerm,
- results_count: resultsCount,
- time_spent: parseFloat(timeSpent),
- reason: reason, // 'closed_overlay' | 'navigated_away' | 'no_interaction'
- timestamp: this.getTimestamp()
- });
- }
- }
- // Initialize global instance
- window.revelorDataLayer = new RevelorDataLayer();
- // =============================================================================
- // search-tracker.js
- // =============================================================================
- /**
- * @fileoverview Search Tracker for Backend Analytics
- * Tracks search events and clicks to backend API according to metrics-tracking.md
- * @version 1.0.0
- * @author Revelor by Webotvůrci
- * @contact [email protected] | +420 728 089 029
- */
- class SearchTracker {
- constructor(config = {}) {
- this.apiEndpoint = config.apiEndpoint || 'https://textilomanie.revelor.cz/api/search/track';
- this.projectId = config.projectId || '312030';
- this.sessionTimeout = config.sessionTimeout || 1800000; // 30 minut v ms
- this.sessionKey = 'search_session_id';
- this.lastSearchTimeKey = 'search_last_time';
- // Generuj session ID pokud neexistuje nebo vypršela
- this.getOrCreateSession();
- }
- /**
- * Get or create session ID
- */
- getOrCreateSession() {
- const sessionId = sessionStorage.getItem(this.sessionKey);
- const lastSearchTime = sessionStorage.getItem(this.lastSearchTimeKey);
- const now = Date.now();
- // Pokud session neexistuje nebo vypršela (30 min), vytvoř novou
- if (!sessionId || !lastSearchTime || (now - parseInt(lastSearchTime)) > this.sessionTimeout) {
- const newSessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
- sessionStorage.setItem(this.sessionKey, newSessionId);
- sessionStorage.setItem(this.lastSearchTimeKey, now.toString());
- return newSessionId;
- }
- return sessionId;
- }
- /**
- * Get current session ID
- */
- getSessionId() {
- return this.getOrCreateSession();
- }
- /**
- * Update last search time
- */
- updateLastSearchTime() {
- sessionStorage.setItem(this.lastSearchTimeKey, Date.now().toString());
- }
- /**
- * Track search event
- * @param {Object} data - Search tracking data
- * @param {string} data.query - Search query (min 3 chars)
- * @param {number} [data.results_count] - Number of results
- * @param {string} [data.search_type] - 'manual' | 'suggestion' | 'live_search'
- */
- async trackSearch(data) {
- const { query, results_count, search_type = 'live_search' } = data;
- // Backend požaduje minimálně 3 znaky
- if (!query || query.length < 3) {
- console.log('🔇 Search tracking skipped - query too short:', query);
- return;
- }
- const sessionId = this.getSessionId();
- this.updateLastSearchTime();
- const eventData = {
- event_type: 'search',
- query: query,
- session_id: sessionId,
- url: window.location.href,
- referrer: document.referrer || '',
- timestamp: new Date().toISOString(),
- user_agent: navigator.userAgent,
- search_type: search_type
- };
- if (results_count !== undefined) {
- eventData.results_count = results_count;
- }
- await this.sendEvent(eventData);
- }
- /**
- * Track click event
- * @param {Object} data - Click tracking data
- * @param {string} data.clicked_result - Product/article/category ID (required)
- * @param {string} data.result_type - 'product' | 'article' | 'category'
- * @param {string} data.result_title - Title of the result
- * @param {string} data.result_url - URL of the result
- * @param {number} data.result_position - Position in results (1-based)
- * @param {string} data.query - Search query
- */
- async trackClick(data) {
- const { clicked_result, product_id, result_type, result_title, result_url, result_position, query } = data;
- // Backend vyžaduje clicked_result nebo product_id
- const resultId = clicked_result || product_id;
- if (!resultId) {
- console.error('❌ Click tracking error - missing clicked_result or product_id');
- return;
- }
- const sessionId = this.getSessionId();
- const eventData = {
- event_type: 'click',
- clicked_result: resultId,
- result_type: result_type || 'product',
- query: query || '',
- session_id: sessionId,
- url: window.location.href,
- referrer: document.referrer || '',
- timestamp: new Date().toISOString(),
- user_agent: navigator.userAgent
- };
- if (result_title) {
- eventData.result_title = result_title;
- }
- if (result_url) {
- eventData.result_url = result_url;
- }
- if (result_position !== undefined) {
- eventData.result_position = result_position;
- }
- // Alternativní název pro product_id (pro kompatibilitu)
- if (product_id) {
- eventData.product_id = product_id;
- }
- await this.sendEvent(eventData);
- }
- /**
- * Send event to backend API
- * @param {Object} eventData - Event data to send
- */
- async sendEvent(eventData) {
- try {
- const response = await fetch(this.apiEndpoint, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Tenant': this.projectId
- },
- body: JSON.stringify(eventData)
- });
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({}));
- console.error('❌ Search tracking error:', response.status, errorData);
- return;
- }
- const responseData = await response.json().catch(() => ({}));
- if (responseData.status === 'skipped') {
- console.log('⏭️ Search tracking skipped:', responseData.message);
- } else {
- console.log('✅ Search tracking success:', responseData);
- }
- } catch (error) {
- // Fail silently - tracking should not break the app
- console.warn('⚠️ Search tracking failed:', error);
- }
- }
- }
- // Create global instance
- window.searchTracker = new SearchTracker({
- projectId: (typeof dataLayer !== 'undefined' &&
- dataLayer[0] &&
- dataLayer[0].shoptet &&
- dataLayer[0].shoptet.projectId) || '312030'
- });
- // =============================================================================
- // searching.js
- // =============================================================================
- /**
- * @fileoverview Search Overlay Component
- * Implements search functionality using Shoptet API
- * @version 2.0.0
- * @author Revelor by Webotvůrci
- * @contact [email protected] | +420 728 089 029
- */
- class SearchOverlay {
- constructor() {
- this.isOpen = false;
- this.apiBaseUrl = 'https://textilomanie.revelor.cz/api/public';
- this.projectId = this.getProjectId();
- this.minSearchLength = 1;
- this.previousActiveElement = null; // Store previous focus
- this.focusableElements = []; // Focusable elements in overlay
- this.currentFocusIndex = -1; // Current focus index for keyboard navigation
- // Performance optimizations
- this.abortController = null; // Request cancellation
- this.cache = new Map(); // Simple cache
- this.cacheTTL = 300000; // 5 minutes
- // Search history
- this.historyKey = 'revelor_search_history';
- this.maxHistoryItems = 10;
- this.init();
- }
- /**
- * Initialize search overlay
- */
- init() {
- this.createOverlay();
- this.bindEvents();
- this.loadPlaceholders();
- }
- /**
- * Get Project ID from dataLayer or use default
- */
- getProjectId() {
- if (
- typeof dataLayer !== 'undefined' &&
- dataLayer[0] &&
- dataLayer[0].shoptet
- ) {
- return dataLayer[0].shoptet.projectId;
- }
- return '312030'; // Default fallback
- }
- /**
- * Create and inject search overlay HTML
- */
- createOverlay() {
- const overlayHTML = `
- <div id="search-overlay" class="search-overlay" role="dialog" aria-modal="true" aria-labelledby="search-dialog-title" aria-describedby="search-dialog-description">
- <div class="search-overlay__content">
- <span id="search-dialog-title" class="sr-only">Vyhledávací dialog</span>
- <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>
- <div class="search-overlay__header">
- <div class="search-overlay__input-wrapper">
- <label for="search-input" class="sr-only">Vyhledávací dotaz</label>
- <input
- type="text"
- id="search-input"
- class="search-overlay__input"
- placeholder="Hledejte produkty, články, kategorie..."
- autocomplete="off"
- aria-label="Vyhledávací pole"
- aria-describedby="search-instructions"
- aria-autocomplete="list"
- aria-controls="search-content-results"
- role="combobox"
- aria-expanded="false"
- >
- <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>
- <button type="button" id="search-submit" class="search-overlay__submit" aria-label="Vyhledat">
- <span class="search-overlay__submit-icon" aria-hidden="true"></span>
- </button>
- </div>
- <button type="button" id="search-close" class="search-overlay__close" aria-label="Zavřít vyhledávání">
- <span class="search-overlay__close-icon" aria-hidden="true">×</span>
- </button>
- </div>
- <div class="search-overlay__results" id="search-results" role="region" aria-live="polite">
- <!-- Live region for screen reader announcements -->
- <div id="search-live-region" class="sr-only" role="status" aria-live="polite" aria-atomic="true"></div>
- <div class="search-overlay__loading" id="search-loading" style="display: none;" role="status" aria-live="polite">
- <div class="search-overlay__spinner" aria-hidden="true"></div>
- <p>Vyhledávám...</p>
- </div>
- <div class="search-overlay__empty" id="search-empty" role="status">
- <div class="search-overlay__empty-icon" aria-hidden="true">🔍</div>
- <h3>Zadejte hledaný výraz</h3>
- <p>Začněte psát a uvidíte návrhy</p>
- </div>
- <div class="search-overlay__no-results" id="search-no-results" style="display: none;" role="status" aria-live="polite">
- <div class="search-overlay__no-results-icon" aria-hidden="true">😔</div>
- <h3>Žádné výsledky</h3>
- <p>Zkuste jiný hledaný výraz</p>
- </div>
- <div class="search-overlay__content-results" id="search-content-results" style="display: none;">
- <div class="search-overlay__section" id="search-categories" style="display: none;">
- <h3 class="search-overlay__section-title" id="search-categories-title">Kategorie</h3>
- <div class="search-overlay__categories" id="search-categories-list" role="list" aria-labelledby="search-categories-title"></div>
- <div class="search-overlay__show-all" id="search-categories-show-all" style="display: none;">
- <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>
- </div>
- </div>
- <div class="search-overlay__section" id="search-products" style="display: none;">
- <h3 class="search-overlay__section-title" id="search-products-title">Produkty</h3>
- <div class="search-overlay__products" id="search-products-list" role="list" aria-labelledby="search-products-title"></div>
- <div class="search-overlay__show-all" id="search-products-show-all" style="display: none;">
- <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>
- </div>
- </div>
- <div class="search-overlay__section" id="search-articles" style="display: none;">
- <h3 class="search-overlay__section-title" id="search-articles-title">Články</h3>
- <div class="search-overlay__articles" id="search-articles-list" role="list" aria-labelledby="search-articles-title"></div>
- <div class="search-overlay__show-all" id="search-articles-show-all" style="display: none;">
- <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>
- </div>
- </div>
- <div class="search-overlay__section" id="search-brands" style="display: none;">
- <h3 class="search-overlay__section-title" id="search-brands-title">Značky</h3>
- <div class="search-overlay__brands" id="search-brands-list" role="list" aria-labelledby="search-brands-title"></div>
- <div class="search-overlay__show-all" id="search-brands-show-all" style="display: none;">
- <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>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- `;
- // Insert before #content-wrapper or at the beginning of body
- const contentWrapper = document.getElementById('content-wrapper');
- if (contentWrapper) {
- contentWrapper.insertAdjacentHTML('beforebegin', overlayHTML);
- } else {
- document.body.insertAdjacentHTML('afterbegin', overlayHTML);
- }
- }
- /**
- * Bind event listeners
- */
- bindEvents() {
- const formSearchForm = document.getElementById('formSearchForm');
- const overlay = document.getElementById('search-overlay');
- const input = document.getElementById('search-input');
- const close = document.getElementById('search-close');
- const submit = document.getElementById('search-submit');
- // Open search from header form
- if (formSearchForm) {
- console.log(
- '✅ SEARCH EVENTS - formSearchForm found, adding click listener'
- );
- formSearchForm.addEventListener('click', async e => {
- console.log('🖱️ SEARCH EVENTS - formSearchForm clicked!');
- e.preventDefault();
- await this.open();
- });
- } else {
- console.log('❌ SEARCH EVENTS - formSearchForm NOT FOUND!');
- }
- // Close search
- close?.addEventListener('click', () => this.close());
- overlay?.addEventListener('click', e => {
- if (e.target === overlay) this.close();
- });
- // Submit search
- submit?.addEventListener('click', () => {
- const query = this.getCurrentQuery().trim();
- if (query.length >= this.minSearchLength) {
- this.redirectToSearchResults(query);
- }
- });
- // Search input
- input?.addEventListener('input', e => this.handleSearch(e.target.value));
- input?.addEventListener('keydown', e => {
- if (e.key === 'Escape') {
- this.close();
- } else if (e.key === 'Enter') {
- e.preventDefault();
- const query = this.getCurrentQuery().trim();
- if (query.length >= this.minSearchLength) {
- this.redirectToSearchResults(query);
- }
- } else if (e.key === 'ArrowDown') {
- e.preventDefault();
- this.focusNextResult();
- } else if (e.key === 'ArrowUp') {
- e.preventDefault();
- this.focusPreviousResult();
- }
- });
- // Keyboard navigation - global
- document.addEventListener('keydown', e => {
- if (e.key === '/' && !this.isOpen && document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
- e.preventDefault();
- this.open();
- }
- // Handle Tab key for focus trap when overlay is open
- if (this.isOpen && e.key === 'Tab') {
- this.handleTabKey(e);
- }
- });
- // Setup swipe to close (mobile)
- this.setupSwipeToClose();
- }
- /**
- * Setup swipe to close for mobile
- */
- setupSwipeToClose() {
- const overlay = document.getElementById('search-overlay');
- if (!overlay) return;
- let startY = 0;
- let currentY = 0;
- let isDragging = false;
- overlay.addEventListener('touchstart', (e) => {
- // Only if touch starts at the top of content
- if (e.target.closest('.search-overlay__header')) {
- startY = e.touches[0].clientY;
- isDragging = true;
- }
- }, { passive: true });
- overlay.addEventListener('touchmove', (e) => {
- if (!isDragging) return;
- currentY = e.touches[0].clientY;
- const diff = currentY - startY;
- // Only allow swipe down
- if (diff > 0) {
- const content = overlay.querySelector('.search-overlay__content');
- if (content) {
- content.style.transform = `translateX(-50%) translateY(${diff}px)`;
- content.style.opacity = Math.max(0.5, 1 - diff / 400);
- }
- }
- }, { passive: true });
- overlay.addEventListener('touchend', () => {
- if (!isDragging) return;
- const diff = currentY - startY;
- const content = overlay.querySelector('.search-overlay__content');
- if (diff > 100) {
- // Swipe down threshold - close overlay
- this.close();
- } else {
- // Return to position
- if (content) {
- content.style.transform = 'translateX(-50%)';
- content.style.opacity = '1';
- }
- }
- isDragging = false;
- startY = 0;
- currentY = 0;
- });
- }
- /**
- * Handle Tab key for focus trap
- */
- handleTabKey(e) {
- const overlay = document.getElementById('search-overlay');
- if (!overlay) return;
- const focusableElements = overlay.querySelectorAll(
- 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
- );
- const firstFocusable = focusableElements[0];
- const lastFocusable = focusableElements[focusableElements.length - 1];
- if (e.shiftKey) {
- // Shift + Tab
- if (document.activeElement === firstFocusable) {
- e.preventDefault();
- lastFocusable.focus();
- }
- } else {
- // Tab
- if (document.activeElement === lastFocusable) {
- e.preventDefault();
- firstFocusable.focus();
- }
- }
- }
- /**
- * Focus next result in list
- */
- focusNextResult() {
- const results = this.getFocusableResults();
- if (results.length === 0) return;
- this.currentFocusIndex = (this.currentFocusIndex + 1) % results.length;
- results[this.currentFocusIndex].focus();
- }
- /**
- * Focus previous result in list
- */
- focusPreviousResult() {
- const results = this.getFocusableResults();
- if (results.length === 0) return;
- this.currentFocusIndex = this.currentFocusIndex <= 0 ? results.length - 1 : this.currentFocusIndex - 1;
- results[this.currentFocusIndex].focus();
- }
- /**
- * Get all focusable results
- */
- getFocusableResults() {
- const resultsContainer = document.getElementById('search-content-results');
- if (!resultsContainer || resultsContainer.style.display === 'none') {
- return [];
- }
- return Array.from(resultsContainer.querySelectorAll('a[href], button:not([disabled])'));
- }
- /**
- * Load dynamic placeholders
- */
- async loadPlaceholders() {
- try {
- const response = await fetch(
- `${this.apiBaseUrl}/placeholders?projectId=${this.projectId}`,
- {
- headers: {
- 'X-Tenant': this.projectId,
- },
- }
- );
- const data = await response.json();
- if (data.placeholders && data.placeholders.length > 0) {
- const input = document.getElementById('search-input');
- if (input) {
- input.placeholder = data.placeholders[0];
- }
- }
- } catch (error) {
- console.warn('Failed to load placeholders:', error);
- }
- }
- /**
- * Load recommendations for empty state
- */
- async loadRecommendations() {
- console.log('🚀 Starting to load recommendations...');
- console.log('API Base URL:', this.apiBaseUrl);
- console.log('Project ID:', this.projectId);
- try {
- const url = `${this.apiBaseUrl}/recommendations?projectId=${this.projectId}`;
- console.log('🔗 Fetching from URL:', url);
- const response = await fetch(url, {
- headers: {
- 'X-Tenant': this.projectId,
- },
- });
- console.log('📡 Response status:', response.status);
- console.log('📡 Response ok:', response.ok);
- const data = await response.json();
- console.log('🔍 SEARCH RECOMMENDATIONS API - Data loaded:', data);
- return data;
- } catch (error) {
- console.error('❌ Failed to load recommendations:', error);
- return null;
- }
- }
- /**
- * Open search overlay
- */
- async open() {
- console.log('🚀 SEARCH OVERLAY - Opening search overlay...');
- // Store current focus to return later
- this.previousActiveElement = document.activeElement;
- // DataLayer: Search Started
- if (window.revelorDataLayer) {
- window.revelorDataLayer.searchStarted('overlay');
- }
- // Store start time for abandoned tracking
- this.searchStartTime = Date.now();
- this.lastSearchTerm = null;
- this.lastResultsCount = 0;
- this.isOpen = true;
- const overlay = document.getElementById('search-overlay');
- const input = document.getElementById('search-input');
- if (overlay) {
- overlay.classList.add('search-overlay--active');
- document.body.classList.add('search-overlay-open');
- console.log('🚀 SEARCH OVERLAY - Overlay activated');
- } else {
- console.log('❌ SEARCH OVERLAY - Overlay element not found!');
- }
- // Update aria-expanded on input
- if (input) {
- input.setAttribute('aria-expanded', 'true');
- }
- // Focus input after animation
- setTimeout(async () => {
- input?.focus();
- console.log('🚀 SEARCH OVERLAY - Input focused');
- // Show empty state with recommendations
- await this.showEmptyState();
- }, 100);
- }
- /**
- * Close search overlay
- */
- close() {
- // DataLayer: Search Abandoned (if there was a search term but no click)
- if (window.revelorDataLayer && this.lastSearchTerm && this.searchStartTime) {
- const timeSpent = ((Date.now() - this.searchStartTime) / 1000).toFixed(1);
- window.revelorDataLayer.searchAbandoned(
- this.lastSearchTerm,
- this.lastResultsCount,
- timeSpent,
- 'closed_overlay'
- );
- }
- this.isOpen = false;
- const overlay = document.getElementById('search-overlay');
- const input = document.getElementById('search-input');
- if (overlay) {
- overlay.classList.remove('search-overlay--active');
- document.body.classList.remove('search-overlay-open');
- }
- // Update aria-expanded on input
- if (input) {
- input.setAttribute('aria-expanded', 'false');
- }
- // Return focus to previous element
- setTimeout(() => {
- if (this.previousActiveElement && this.previousActiveElement.focus) {
- this.previousActiveElement.focus();
- }
- }, 100);
- this.clearSearch();
- }
- /**
- * Clear search input and results
- */
- async clearSearch() {
- const input = document.getElementById('search-input');
- if (input) {
- input.value = '';
- input.focus();
- }
- await this.showEmptyState();
- }
- /**
- * Get current search query
- */
- getCurrentQuery() {
- const input = document.getElementById('search-input');
- return input ? input.value : '';
- }
- /**
- * Format price with dot as thousands separator and remove unnecessary decimals
- */
- formatPrice(price) {
- if (!price) return '';
- // Convert to number
- const numPrice = parseFloat(price);
- // Format as integer (bez desetinných míst) s tečkou jako oddělovač tisíců
- return Math.floor(numPrice).toLocaleString('cs-CZ', {
- minimumFractionDigits: 0,
- maximumFractionDigits: 0,
- });
- }
- /**
- * Redirect to search results page
- */
- redirectToSearchResults(query) {
- const url = `/vyhledavani/?string=${encodeURIComponent(query)}`;
- console.log('Redirecting to search results:', url);
- // Create temporary link and click it
- const tempLink = document.createElement('a');
- tempLink.href = url;
- tempLink.style.display = 'none';
- document.body.appendChild(tempLink);
- tempLink.click();
- document.body.removeChild(tempLink);
- }
- /**
- * Add product to cart (Shoptet API) - for Search Overlay
- * @param {Object} product - Product data
- * @param {HTMLElement} button - Button element
- */
- async addToCart(product, button) {
- const originalHTML = button.innerHTML;
- // Show loading state
- button.disabled = true;
- button.classList.add('loading');
- button.style.opacity = '0.5';
- try {
- // Check if Shoptet API is available
- if (typeof shoptet === 'undefined' || !shoptet.cartShared) {
- throw new Error('Shoptet API není k dispozici');
- }
- // Determine which method to use (podle Shoptet dokumentace)
- let addParams = {};
- if (product.priceId) {
- // Nejlepší varianta: priceId
- addParams = { priceId: product.priceId };
- } else if (product.kod || product.code) {
- // Fallback: product code
- addParams = { productCode: product.kod || product.code };
- } else if (product._id || product.id) {
- // Poslední varianta: product ID
- addParams = { productId: product._id || product.id };
- } else {
- throw new Error('Nelze identifikovat produkt');
- }
- // Add to cart (silent mode = true to prevent Shoptet's modal)
- await shoptet.cartShared.addToCart(addParams, true);
- // Success feedback
- button.classList.remove('loading');
- button.classList.add('success');
- button.style.opacity = '1';
- // TODO: Add toast notification if available
- // Tracking
- if (window.revelorDataLayer) {
- window.dataLayer.push({
- event: 'revelor_quick_add_to_cart',
- product_id: product._id || product.id,
- product_name: product.title,
- price: product.price_s_dph,
- source: 'search_overlay'
- });
- }
- // Reset button after 2s
- setTimeout(() => {
- button.disabled = false;
- button.classList.remove('success');
- button.style.opacity = '';
- }, 2000);
- } catch (error) {
- console.error('Add to Cart Error:', error);
- button.innerHTML = originalHTML;
- button.disabled = false;
- button.classList.remove('loading');
- button.style.opacity = '';
- // TODO: Add error notification
- }
- }
- /**
- * Handle search input
- */
- async handleSearch(query) {
- if (query.length < this.minSearchLength) {
- await this.showEmptyState();
- return;
- }
- // Only show loading if no results are currently displayed
- const resultsContainer = document.getElementById('search-content-results');
- const isShowingResults =
- resultsContainer && resultsContainer.style.display !== 'none';
- if (!isShowingResults) {
- this.showLoadingState();
- }
- // Perform search immediately
- this.performSearch(query);
- }
- /**
- * Perform search using API
- */
- async performSearch(query) {
- const searchUrl = `${this.apiBaseUrl}/suggestions?q=${encodeURIComponent(query)}&projectId=${this.projectId}&products_limit=6&categories_limit=6&articles_limit=6&brands_limit=0`;
- // DataLayer: Search Query Submitted
- if (window.revelorDataLayer) {
- window.revelorDataLayer.searchQuerySubmitted(query, 'overlay');
- }
- // Store for refinement tracking
- this.lastSearchTerm = query;
- // Check cache first
- const cacheKey = `${query}_${this.projectId}_suggestions`;
- const cached = this.getFromCache(cacheKey);
- if (cached) {
- console.log('📦 Using cached results for:', query);
- this.displayResults(cached, 0);
- return;
- }
- // Cancel previous request
- if (this.abortController) {
- this.abortController.abort();
- }
- this.abortController = new AbortController();
- const startTime = Date.now();
- try {
- const response = await this.fetchWithRetry(searchUrl, {
- signal: this.abortController.signal,
- headers: {
- 'X-Tenant': this.projectId,
- },
- });
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- const data = await response.json();
- const responseTime = Date.now() - startTime;
- // Save to cache
- this.saveToCache(cacheKey, data);
- // Add to search history if has results
- const hasResults = data.products?.length > 0 || data.categories?.length > 0 || data.articles?.length > 0;
- if (hasResults) {
- this.addToSearchHistory(query);
- }
- this.displayResults(data, responseTime);
- } catch (error) {
- if (error.name === 'AbortError') {
- console.log('🚫 Request cancelled for:', query);
- return;
- }
- console.error('Search API Error:', error);
- // Try to use cached results as fallback
- const fallbackCached = this.getFromCache(cacheKey, true);
- if (fallbackCached) {
- console.log('📦 Using stale cache as fallback');
- this.displayResults(fallbackCached, 0);
- this.announceToScreenReader('Zobrazuji uložené výsledky (offline)');
- return;
- }
- this.showErrorState();
- }
- }
- /**
- * Save to cache
- */
- saveToCache(key, data) {
- this.cache.set(key, {
- data,
- timestamp: Date.now()
- });
- }
- /**
- * Get from cache
- */
- getFromCache(key, ignoreExpiry = false) {
- const cached = this.cache.get(key);
- if (!cached) return null;
- // Check TTL (unless ignoreExpiry for fallback)
- if (!ignoreExpiry && Date.now() - cached.timestamp > this.cacheTTL) {
- this.cache.delete(key);
- return null;
- }
- return cached.data;
- }
- /**
- * Announce message to screen readers
- */
- announceToScreenReader(message) {
- const liveRegion = document.getElementById('search-live-region');
- if (liveRegion) {
- liveRegion.textContent = message;
- // Clear after announcement
- setTimeout(() => {
- liveRegion.textContent = '';
- }, 1000);
- }
- }
- /**
- * Display search results
- */
- displayResults(data, responseTime = 0) {
- this.hideAllStates();
- const hasResults =
- data.products?.length > 0 ||
- data.categories?.length > 0 ||
- data.articles?.length > 0;
- if (!hasResults) {
- this.showNoResultsState();
- this.announceToScreenReader('Žádné výsledky nebyly nalezeny');
- // DataLayer: Search No Results
- if (window.revelorDataLayer && this.lastSearchTerm) {
- window.revelorDataLayer.searchNoResults(this.lastSearchTerm);
- }
- this.lastResultsCount = 0;
- return;
- }
- this.showResultsState();
- // Count results
- const totalResults =
- (data.products?.length || 0) +
- (data.categories?.length || 0) +
- (data.articles?.length || 0);
- // Store for abandoned tracking
- this.lastResultsCount = totalResults;
- // Announce results
- const resultTypes = [];
- if (data.products?.length) resultTypes.push(`${data.products.length} ${data.products.length === 1 ? 'produkt' : 'produkty'}`);
- if (data.categories?.length) resultTypes.push(`${data.categories.length} ${data.categories.length === 1 ? 'kategorie' : 'kategorie'}`);
- if (data.articles?.length) resultTypes.push(`${data.articles.length} ${data.articles.length === 1 ? 'článek' : 'články'}`);
- this.announceToScreenReader(`Nalezeno ${totalResults} výsledků: ${resultTypes.join(', ')}`);
- // DataLayer: Search Results Displayed
- if (window.revelorDataLayer && this.lastSearchTerm) {
- const resultsData = {
- products: data.products || [],
- categories: data.categories || [],
- articles: data.articles || [],
- total_products: totalResults
- };
- window.revelorDataLayer.searchResultsDisplayed(
- this.lastSearchTerm,
- resultsData,
- responseTime,
- 1
- );
- }
- // Backend API: Track search event
- if (window.searchTracker && this.lastSearchTerm && this.lastSearchTerm.length >= 3) {
- window.searchTracker.trackSearch({
- query: this.lastSearchTerm,
- results_count: totalResults,
- search_type: 'live_search'
- });
- }
- // Display products
- if (data.products?.length > 0) {
- this.displayProducts(data.products);
- }
- // Display categories
- if (data.categories?.length > 0) {
- this.displayCategories(data.categories);
- }
- // Display articles
- if (data.articles?.length > 0) {
- this.displayArticles(data.articles);
- }
- // Reset focus index
- this.currentFocusIndex = -1;
- }
- /**
- * Display products
- */
- displayProducts(products) {
- const container = document.getElementById('search-products');
- const list = document.getElementById('search-products-list');
- const showAll = document.getElementById('search-products-show-all');
- if (!container || !list) return;
- container.style.display = 'block';
- list.innerHTML = '';
- products.forEach((product, index) => {
- const item = document.createElement('div');
- item.className = 'search-overlay__product';
- item.setAttribute('role', 'listitem');
- // Microdata attributes for AI agents
- item.setAttribute('itemscope', '');
- item.setAttribute('itemtype', 'https://schema.org/Product');
- const priceText = product.price_s_dph ? `${this.formatPrice(product.price_s_dph)} Kč` : 'Cena na dotaz';
- const availabilityText = product.skladem ? 'Skladem' : 'Není skladem';
- const availabilitySchema = product.skladem ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock';
- // Create JSON-LD structured data
- const jsonLd = {
- "@context": "https://schema.org/",
- "@type": "Product",
- "@id": product._id,
- "name": product.title,
- "description": product.description || product.title,
- "image": product.img,
- "url": product.url,
- "sku": product.kod,
- "offers": {
- "@type": "Offer",
- "price": product.price_s_dph || 0,
- "priceCurrency": "CZK",
- "availability": availabilitySchema,
- "url": product.url
- }
- };
- item.innerHTML = `
- <!-- JSON-LD for AI agents -->
- <script type="application/ld+json">
- ${JSON.stringify(jsonLd)}
- </script>
- <a href="${product.url}" class="search-overlay__product-link" itemprop="url" aria-label="Produkt ${product.title}, ${priceText}, ${availabilityText}" data-product-index="${index}">
- <meta itemprop="sku" content="${product.kod || 'N/A'}">
- <link itemprop="availability" href="${availabilitySchema}">
- <div class="search-overlay__product-image" aria-hidden="true">
- ${product.img ? `<img src="${product.img}" alt="" itemprop="image" loading="lazy">` : ''}
- </div>
- <div class="search-overlay__product-content">
- <h4 class="search-overlay__product-title" itemprop="name">${this.highlightSearchTerm(product.title, this.lastSearchTerm)}</h4>
- <div class="search-overlay__product-price" itemprop="offers" itemscope itemtype="https://schema.org/Offer" aria-label="Cena ${priceText}">
- <meta itemprop="priceCurrency" content="CZK">
- <meta itemprop="price" content="${product.price_s_dph || 0}">
- ${product.price_s_dph ? this.formatPrice(product.price_s_dph) + ' Kč' : 'Cena na dotaz'}
- </div>
- </div>
- </a>
- ${product.skladem && !product.has_variants ? `
- <button type="button" class="search-overlay__product-add-btn" aria-label="Přidat ${product.title} do košíku" data-product-id="${product._id}">
- <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>
- </button>
- ` : ''}
- `;
- // Add click event for tracking
- const link = item.querySelector('.search-overlay__product-link');
- if (link) {
- link.addEventListener('click', () => {
- // DataLayer: Search Result Click
- if (window.revelorDataLayer && this.lastSearchTerm) {
- window.revelorDataLayer.searchResultClick(
- this.lastSearchTerm,
- 'product',
- product,
- index + 1,
- this.lastResultsCount
- );
- }
- // Backend API: Track click event
- if (window.searchTracker && this.lastSearchTerm) {
- window.searchTracker.trackClick({
- clicked_result: product._id || product.id,
- product_id: product._id || product.id,
- result_type: 'product',
- result_title: product.title,
- result_url: product.url,
- result_position: index + 1,
- query: this.lastSearchTerm
- });
- }
- });
- }
- // Add click event for "Add to Cart" button
- const addBtn = item.querySelector('.search-overlay__product-add-btn');
- if (addBtn) {
- addBtn.addEventListener('click', async (e) => {
- e.preventDefault();
- e.stopPropagation();
- await this.addToCart(product, addBtn);
- });
- }
- list.appendChild(item);
- });
- if (showAll) {
- showAll.style.display = 'block';
- // Set up click handler for "Show all" link
- const showAllLink = showAll.querySelector(
- '.search-overlay__show-all-link'
- );
- if (showAllLink) {
- showAllLink.addEventListener('click', e => {
- e.preventDefault();
- e.stopPropagation();
- const url = `/vyhledavani/?string=${encodeURIComponent(this.getCurrentQuery())}`;
- console.log('Redirecting to:', url);
- // Create temporary link and click it
- const tempLink = document.createElement('a');
- tempLink.href = url;
- tempLink.style.display = 'none';
- document.body.appendChild(tempLink);
- tempLink.click();
- document.body.removeChild(tempLink);
- return false;
- });
- }
- }
- }
- /**
- * Display categories
- */
- displayCategories(categories) {
- const container = document.getElementById('search-categories');
- const list = document.getElementById('search-categories-list');
- const showAll = document.getElementById('search-categories-show-all');
- if (!container || !list) return;
- container.style.display = 'block';
- list.innerHTML = '';
- categories.forEach((category, index) => {
- const item = document.createElement('div');
- item.className = 'search-overlay__category';
- item.setAttribute('role', 'listitem');
- item.innerHTML = `
- <a href="${category.url}" class="search-overlay__category-link" aria-label="Kategorie ${category.title}">
- <div class="search-overlay__category-content">
- <h4 class="search-overlay__category-title">${category.title}</h4>
- </div>
- </a>
- `;
- // Add click tracking for category
- const link = item.querySelector('.search-overlay__category-link');
- if (link && window.searchTracker && this.lastSearchTerm) {
- link.addEventListener('click', () => {
- window.searchTracker.trackClick({
- clicked_result: category._id || category.id || category.url,
- result_type: 'category',
- result_title: category.title,
- result_url: category.url,
- result_position: index + 1,
- query: this.lastSearchTerm
- });
- });
- }
- list.appendChild(item);
- });
- if (showAll) {
- showAll.style.display = 'block';
- // Set up click handler for "Show all" link
- const showAllLink = showAll.querySelector(
- '.search-overlay__show-all-link'
- );
- if (showAllLink) {
- showAllLink.addEventListener('click', e => {
- e.preventDefault();
- e.stopPropagation();
- const url = `/vyhledavani/?string=${encodeURIComponent(this.getCurrentQuery())}`;
- console.log('Redirecting to:', url);
- // Create temporary link and click it
- const tempLink = document.createElement('a');
- tempLink.href = url;
- tempLink.style.display = 'none';
- document.body.appendChild(tempLink);
- tempLink.click();
- document.body.removeChild(tempLink);
- return false;
- });
- }
- }
- }
- /**
- * Display articles
- */
- displayArticles(articles) {
- const container = document.getElementById('search-articles');
- const list = document.getElementById('search-articles-list');
- const showAll = document.getElementById('search-articles-show-all');
- if (!container || !list) return;
- container.style.display = 'block';
- list.innerHTML = '';
- articles.forEach((article, index) => {
- const item = document.createElement('div');
- item.className = 'search-overlay__article';
- item.setAttribute('role', 'listitem');
- const description = article.description || '';
- const ariaLabel = description ? `Článek ${article.title}: ${description}` : `Článek ${article.title}`;
- item.innerHTML = `
- <a href="${article.url}" class="search-overlay__article-link" aria-label="${ariaLabel}">
- <div class="search-overlay__article-image" aria-hidden="true">
- ${article.img ? `<img src="${article.img}" alt="" loading="lazy">` : ''}
- </div>
- <div class="search-overlay__article-content">
- <h4 class="search-overlay__article-title">${article.title}</h4>
- ${description ? `<p class="search-overlay__article-description">${description}</p>` : ''}
- </div>
- </a>
- `;
- // Add click tracking for article
- const link = item.querySelector('.search-overlay__article-link');
- if (link && window.searchTracker && this.lastSearchTerm) {
- link.addEventListener('click', () => {
- window.searchTracker.trackClick({
- clicked_result: article._id || article.id || article.url,
- result_type: 'article',
- result_title: article.title,
- result_url: article.url,
- result_position: index + 1,
- query: this.lastSearchTerm
- });
- });
- }
- list.appendChild(item);
- });
- if (showAll) {
- showAll.style.display = 'block';
- // Set up click handler for "Show all" link
- const showAllLink = showAll.querySelector(
- '.search-overlay__show-all-link'
- );
- if (showAllLink) {
- showAllLink.addEventListener('click', e => {
- e.preventDefault();
- e.stopPropagation();
- const url = `/vyhledavani/?string=${encodeURIComponent(this.getCurrentQuery())}`;
- console.log('Redirecting to:', url);
- // Create temporary link and click it
- const tempLink = document.createElement('a');
- tempLink.href = url;
- tempLink.style.display = 'none';
- document.body.appendChild(tempLink);
- tempLink.click();
- document.body.removeChild(tempLink);
- return false;
- });
- }
- }
- }
- /**
- * Display brands
- */
- displayBrands(brands) {
- const container = document.getElementById('search-brands');
- const list = document.getElementById('search-brands-list');
- const showAll = document.getElementById('search-brands-show-all');
- if (!container || !list) return;
- container.style.display = 'block';
- list.innerHTML = '';
- brands.forEach((brand, index) => {
- console.log('Brand data:', brand); // Debug log
- const item = document.createElement('div');
- item.className = 'search-overlay__brand';
- if (brand.url) {
- // Brand má URL - vytvoř proklik
- item.innerHTML = `
- <a href="${brand.url}" class="search-overlay__brand-link">
- <div class="search-overlay__brand-content">
- <h4 class="search-overlay__brand-title">${brand.title}</h4>
- <p class="search-overlay__brand-description">${brand.description || ''}</p>
- </div>
- </a>
- `;
- // Add click tracking for brand (treat as category for now)
- const link = item.querySelector('.search-overlay__brand-link');
- if (link && window.searchTracker && this.lastSearchTerm) {
- link.addEventListener('click', () => {
- window.searchTracker.trackClick({
- clicked_result: brand._id || brand.id || brand.url,
- result_type: 'category', // Brands are tracked as categories
- result_title: brand.title,
- result_url: brand.url,
- result_position: index + 1,
- query: this.lastSearchTerm
- });
- });
- }
- } else {
- // Brand nemá URL - zobraz bez prokliku
- item.innerHTML = `
- <div class="search-overlay__brand-content">
- <h4 class="search-overlay__brand-title">${brand.title}</h4>
- <p class="search-overlay__brand-description">${brand.description || ''}</p>
- </div>
- `;
- }
- list.appendChild(item);
- });
- if (showAll) {
- showAll.style.display = 'block';
- // Set up click handler for "Show all" link
- const showAllLink = showAll.querySelector(
- '.search-overlay__show-all-link'
- );
- if (showAllLink) {
- showAllLink.addEventListener('click', e => {
- e.preventDefault();
- e.stopPropagation();
- const url = `/vyhledavani/?string=${encodeURIComponent(this.getCurrentQuery())}`;
- console.log('Redirecting to:', url);
- // Create temporary link and click it
- const tempLink = document.createElement('a');
- tempLink.href = url;
- tempLink.style.display = 'none';
- document.body.appendChild(tempLink);
- tempLink.click();
- document.body.removeChild(tempLink);
- return false;
- });
- }
- }
- }
- /**
- * Show loading state
- */
- showLoadingState() {
- this.hideAllStates();
- const loading = document.getElementById('search-loading');
- if (loading) loading.style.display = 'block';
- }
- /**
- * Show empty state
- */
- async showEmptyState() {
- console.log('📋 SEARCH EMPTY STATE - Starting showEmptyState()');
- this.hideAllStates();
- const empty = document.getElementById('search-empty');
- if (empty) {
- empty.style.display = 'block';
- console.log('📋 SEARCH EMPTY STATE - Empty element displayed');
- // Show search history if available
- this.displaySearchHistory();
- } else {
- console.log('❌ SEARCH EMPTY STATE - Empty element not found!');
- }
- // Load recommendations for empty state
- console.log('📋 SEARCH EMPTY STATE - About to load recommendations...');
- const recommendations = await this.loadRecommendations();
- if (recommendations) {
- console.log(
- '📋 SEARCH EMPTY STATE - Recommendations loaded:',
- recommendations
- );
- this.displayRecommendations(recommendations);
- } else {
- console.log('❌ SEARCH EMPTY STATE - No recommendations received');
- }
- }
- /**
- * Add search term to history
- */
- addToSearchHistory(query) {
- if (!query || query.length < 2) return;
- let history = this.getSearchHistory();
- // Remove duplicates and partial matches
- // If new query is part of existing query or vice versa, keep the longer one
- const mergedQuery = query;
- history = history.filter(item => {
- const existing = item.query.toLowerCase();
- const newQuery = query.toLowerCase();
- // If new query is longer and contains existing, remove existing
- if (newQuery.length > existing.length && newQuery.includes(existing)) {
- return false;
- }
- // If existing is longer and contains new query, keep existing (don't add new)
- if (existing.length > newQuery.length && existing.includes(newQuery)) {
- return false;
- }
- // If they're identical, remove existing (will add new)
- if (existing === newQuery) {
- return false;
- }
- return true;
- });
- // Add to beginning
- history.unshift({
- query,
- timestamp: Date.now(),
- count: 1
- });
- // Limit to maxHistoryItems
- history = history.slice(0, this.maxHistoryItems);
- localStorage.setItem(this.historyKey, JSON.stringify(history));
- }
- /**
- * Get search history from localStorage
- */
- getSearchHistory() {
- try {
- return JSON.parse(localStorage.getItem(this.historyKey) || '[]');
- } catch {
- return [];
- }
- }
- /**
- * Display search history in empty state
- */
- displaySearchHistory() {
- const history = this.getSearchHistory();
- if (history.length === 0) return;
- const empty = document.getElementById('search-empty');
- if (!empty) return;
- // Check if history is already displayed
- if (empty.querySelector('.search-history')) return;
- const historyHTML = `
- <div class="search-history">
- <h4 class="search-history__title">Nedávné hledání</h4>
- <div class="search-history__items">
- ${history.map(item => `
- <button type="button"
- class="search-history__item"
- data-query="${this.escapeHtml(item.query)}"
- aria-label="Hledat ${item.query}">
- <span class="search-history__text">${this.escapeHtml(item.query)}</span>
- </button>
- `).join('')}
- </div>
- </div>
- `;
- empty.insertAdjacentHTML('beforeend', historyHTML);
- // Bind click events
- empty.querySelectorAll('.search-history__item').forEach(button => {
- button.addEventListener('click', () => {
- const query = button.dataset.query;
- const input = document.getElementById('search-input');
- if (input) {
- input.value = query;
- this.handleSearch(query);
- }
- });
- });
- }
- /**
- * Escape HTML to prevent XSS
- */
- escapeHtml(text) {
- const div = document.createElement('div');
- div.textContent = text;
- return div.innerHTML;
- }
- /**
- * Highlight search term in text
- */
- highlightSearchTerm(text, searchTerm) {
- if (!searchTerm || !text) return this.escapeHtml(text);
- const escaped = this.escapeHtml(text);
- const escapedTerm = this.escapeHtml(searchTerm);
- const regex = new RegExp(`(${escapedTerm})`, 'gi');
- return escaped.replace(regex, '<mark class="search-highlight">$1</mark>');
- }
- /**
- * Fetch with retry logic (3 attempts with exponential backoff)
- */
- async fetchWithRetry(url, options = {}, retries = 3) {
- for (let i = 0; i < retries; i++) {
- try {
- const response = await fetch(url, options);
- if (!response.ok && i < retries - 1) {
- // Wait before retry (exponential backoff: 1s, 2s, 4s)
- await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
- continue;
- }
- return response;
- } catch (error) {
- // If it's AbortError, don't retry
- if (error.name === 'AbortError') {
- throw error;
- }
- // Last attempt - throw error
- if (i === retries - 1) {
- throw error;
- }
- // Wait before retry
- await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
- }
- }
- }
- /**
- * Display recommendations in empty state
- */
- displayRecommendations(data) {
- console.log(
- '📋 RECOMMENDATIONS - Displaying recommendations using existing format'
- );
- // Check if we have any recommendations
- const hasProducts = data.products && data.products.length > 0;
- const hasCategories = data.categories && data.categories.length > 0;
- const hasArticles = data.articles && data.articles.length > 0;
- const hasBrands = data.brands && data.brands.length > 0;
- if (!hasProducts && !hasCategories && !hasArticles && !hasBrands) {
- console.log('📋 RECOMMENDATIONS - No recommendations to display');
- return;
- }
- // DataLayer: Search Recommendations Shown
- if (window.revelorDataLayer) {
- window.revelorDataLayer.searchRecommendationsShown(data);
- }
- // Hide empty state and show results state
- this.hideAllStates();
- this.showResultsState();
- // Display products using existing method
- if (hasProducts) {
- this.displayProducts(data.products);
- }
- // Display categories using existing method
- if (hasCategories) {
- this.displayCategories(data.categories);
- }
- // Display articles using existing method
- if (hasArticles) {
- this.displayArticles(data.articles);
- }
- // Display brands using existing method
- if (hasBrands) {
- this.displayBrands(data.brands);
- }
- console.log(
- '✅ RECOMMENDATIONS - Displayed successfully using existing format'
- );
- }
- /**
- * Show no results state
- */
- showNoResultsState() {
- this.hideAllStates();
- const noResults = document.getElementById('search-no-results');
- if (noResults) noResults.style.display = 'block';
- }
- /**
- * Show results state
- */
- showResultsState() {
- this.hideAllStates();
- const results = document.getElementById('search-content-results');
- if (results) results.style.display = 'block';
- }
- /**
- * Show error state
- */
- showErrorState() {
- this.hideAllStates();
- const noResults = document.getElementById('search-no-results');
- if (noResults) {
- noResults.style.display = 'block';
- const title = noResults.querySelector('h3');
- const text = noResults.querySelector('p');
- if (title) title.textContent = 'Chyba při vyhledávání';
- if (text) text.textContent = 'Zkuste to prosím později';
- }
- }
- /**
- * Hide all states
- */
- hideAllStates() {
- const states = [
- 'search-loading',
- 'search-empty',
- 'search-no-results',
- 'search-content-results',
- ];
- states.forEach(stateId => {
- const element = document.getElementById(stateId);
- if (element) element.style.display = 'none';
- });
- // Hide all sections
- const sections = [
- 'search-products',
- 'search-categories',
- 'search-articles',
- ];
- sections.forEach(sectionId => {
- const element = document.getElementById(sectionId);
- if (element) element.style.display = 'none';
- });
- // Hide show all links
- const showAlls = [
- 'search-products-show-all',
- 'search-categories-show-all',
- 'search-articles-show-all',
- ];
- showAlls.forEach(showAllId => {
- const element = document.getElementById(showAllId);
- if (element) element.style.display = 'none';
- });
- }
- }
- // Initialize when DOM is ready
- document.addEventListener('DOMContentLoaded', () => {
- new SearchOverlay();
- });
- // =============================================================================
- // search-results.js
- // =============================================================================
- /**
- * @fileoverview Search Results Page
- * Handles search results page functionality with pagination
- * @version 2.0.0
- * @author Revelor by Webotvůrci
- * @contact [email protected] | +420 728 089 029
- */
- class SearchResultsPage {
- constructor() {
- this.apiBaseUrl = 'https://textilomanie.revelor.cz/api/public';
- this.projectId = this.getProjectId();
- this.currentPage = 1;
- this.pageSize = 20;
- this.hasMoreResults = false;
- this.isLoading = false;
- this.currentQuery = this.getQueryFromURL();
- // Performance optimizations
- this.abortController = null;
- this.cache = new Map();
- this.cacheTTL = 300000; // 5 minutes
- // Filters
- this.allProducts = []; // Store all products for client-side filtering
- this.allCategories = []; // Store all categories for expand functionality
- this.activeFilters = {
- inStock: false,
- sortBy: 'relevance'
- };
- this.init();
- }
- /**
- * Get Project ID from dataLayer or use default
- */
- getProjectId() {
- if (
- typeof dataLayer !== 'undefined' &&
- dataLayer[0] &&
- dataLayer[0].shoptet
- ) {
- return dataLayer[0].shoptet.projectId;
- }
- return '312030'; // Default fallback
- }
- /**
- * Get search query from URL parameters
- * Supports both 'q' and 'string' parameters (Shoptet uses 'string')
- */
- getQueryFromURL() {
- const urlParams = new URLSearchParams(window.location.search);
- return urlParams.get('string') || urlParams.get('q') || '';
- }
- /**
- * Initialize search results page
- */
- init() {
- // Create containers if they don't exist
- this.createContainersIfNeeded();
- // Hide Shoptet default search results
- this.hideShoptetResults();
- if (!this.currentQuery) {
- this.showNoResults('Zadejte hledaný výraz');
- return;
- }
- // DataLayer: Search Started (on results page)
- if (window.revelorDataLayer) {
- window.revelorDataLayer.searchStarted('results_page');
- }
- this.displayQuery();
- this.loadSearchResults();
- this.bindEvents();
- }
- /**
- * Create Revelor containers if they don't exist on the page
- */
- createContainersIfNeeded() {
- // Check if our container exists
- let container = document.getElementById('search-products-container');
- let section = document.getElementById('search-results-products');
- // If containers don't exist, create them
- if (!container || !section) {
- // Find where to insert - look for Shoptet products block or main content area
- const shoptetProductsBlock = document.querySelector('.products-block.products-searching, .products-block');
- const mainContent = document.querySelector('#content-wrapper, main, .content, #content');
- const insertTarget = shoptetProductsBlock?.parentElement || mainContent || document.body;
- // Create section wrapper
- if (!section) {
- section = document.createElement('section');
- section.id = 'search-results-products';
- section.className = 'search-results-section';
- section.setAttribute('aria-labelledby', 'products-heading');
- section.style.display = 'none';
- const heading = document.createElement('h2');
- heading.id = 'products-heading';
- heading.className = 'search-results-section-title';
- heading.textContent = 'Produkty';
- section.appendChild(heading);
- // Insert: replace Shoptet products block or insert at appropriate location
- if (shoptetProductsBlock) {
- // Replace Shoptet block with our section
- shoptetProductsBlock.parentElement.insertBefore(section, shoptetProductsBlock);
- } else if (insertTarget) {
- // Insert at beginning of content
- insertTarget.insertBefore(section, insertTarget.firstChild);
- } else {
- // Last resort: append to body
- document.body.appendChild(section);
- }
- }
- // Create products container - matching Shoptet structure
- if (!container) {
- container = document.createElement('div');
- container.id = 'search-products-container';
- container.className = 'products products-page products-block';
- container.setAttribute('data-testid', 'productCards');
- section.appendChild(container);
- }
- // Also create other necessary elements if missing
- if (!document.getElementById('search-results-header')) {
- const header = document.createElement('div');
- header.id = 'search-results-header';
- header.className = 'search-results-header';
- header.innerHTML = `
- <div class="search-results-query">
- <span class="search-results-query-label">Hledaný výraz:</span>
- <span class="search-results-query-value" id="search-query-display"></span>
- </div>
- <div class="search-results-count" id="search-results-count" role="status" aria-live="polite"> </div>
- `;
- // Insert header before section
- if (section.parentElement) {
- section.parentElement.insertBefore(header, section);
- } else if (insertTarget) {
- insertTarget.insertBefore(header, insertTarget.firstChild);
- } else {
- document.body.appendChild(header);
- }
- }
- if (!document.getElementById('search-filters')) {
- const filters = document.createElement('div');
- filters.id = 'search-filters';
- filters.className = 'search-filters';
- filters.style.display = 'none';
- filters.innerHTML = `
- <div class="search-filters__item">
- <label class="search-filter-checkbox">
- <input type="checkbox" id="filter-in-stock" aria-label="Filtrovat pouze produkty skladem">
- <span>Pouze skladem</span>
- </label>
- </div>
- <div class="search-filters__item">
- <label for="filter-sort" class="sr-only">Řadit podle</label>
- <select id="filter-sort" class="search-filter-select" aria-label="Řadit výsledky podle">
- <option value="relevance">Řadit: Relevance</option>
- <option value="price-asc">Řadit: Cena (↑)</option>
- <option value="price-desc">Řadit: Cena (↓)</option>
- <option value="name">Řadit: Název (A-Z)</option>
- </select>
- </div>
- `;
- section.insertBefore(filters, container);
- }
- if (!document.getElementById('search-results-load-more')) {
- const loadMore = document.createElement('div');
- loadMore.id = 'search-results-load-more';
- loadMore.className = 'search-results-load-more';
- loadMore.style.display = 'none';
- loadMore.innerHTML = `
- <button type="button" class="btn btn--primary" id="load-more-button" aria-label="Načíst více výsledků" aria-busy="false">
- <span class="load-more-text">Načíst více výsledků</span>
- <span class="load-more-spinner" style="display: none;" aria-hidden="true">⏳</span>
- </button>
- `;
- section.appendChild(loadMore);
- }
- // Create loading state if missing
- if (!document.getElementById('search-results-loading')) {
- const loading = document.createElement('div');
- loading.id = 'search-results-loading';
- loading.className = 'search-results-loading';
- loading.style.display = 'none';
- loading.setAttribute('role', 'status');
- loading.setAttribute('aria-live', 'polite');
- loading.setAttribute('aria-label', 'Načítám výsledky');
- loading.innerHTML = `
- <div class="search-results-spinner" aria-hidden="true"> </div>
- <p>Načítám výsledky...</p>
- `;
- if (section.parentElement) {
- section.parentElement.insertBefore(loading, section);
- }
- }
- // Create no results state if missing
- if (!document.getElementById('search-results-no-results')) {
- const noResults = document.createElement('div');
- noResults.id = 'search-results-no-results';
- noResults.className = 'search-results-no-results';
- noResults.style.display = 'none';
- noResults.setAttribute('role', 'status');
- noResults.setAttribute('aria-live', 'polite');
- noResults.innerHTML = `
- <div class="search-results-no-results-icon" aria-hidden="true">🔍</div>
- <h2>Žádné výsledky</h2>
- <p>Pro váš dotaz "<span id="no-results-query"></span>" nebyly nalezeny žádné výsledky.</p>
- <p>Zkuste jiný hledaný výraz nebo prohlédněte naše kategorie.</p>
- `;
- if (section.parentElement) {
- section.parentElement.insertBefore(noResults, section);
- }
- }
- // Create categories section if missing
- if (!document.getElementById('search-results-categories')) {
- const categoriesSection = document.createElement('section');
- categoriesSection.id = 'search-results-categories';
- categoriesSection.className = 'search-results-section';
- categoriesSection.style.display = 'none';
- categoriesSection.setAttribute('aria-labelledby', 'categories-heading');
- const categoriesHeading = document.createElement('h2');
- categoriesHeading.id = 'categories-heading';
- categoriesHeading.className = 'search-results-section-title';
- categoriesHeading.textContent = 'Kategorie';
- categoriesSection.appendChild(categoriesHeading);
- const categoriesList = document.createElement('ul');
- categoriesList.id = 'search-categories-container';
- categoriesList.className = 'subcategories with-image';
- categoriesList.setAttribute('role', 'list');
- categoriesSection.appendChild(categoriesList);
- // Insert after products section
- if (section.parentElement) {
- section.parentElement.insertBefore(categoriesSection, section.nextSibling);
- } else if (insertTarget) {
- insertTarget.appendChild(categoriesSection);
- }
- }
- // Create articles section if missing
- if (!document.getElementById('search-results-articles')) {
- const articlesSection = document.createElement('section');
- articlesSection.id = 'search-results-articles';
- articlesSection.className = 'search-results-section';
- articlesSection.style.display = 'none';
- articlesSection.setAttribute('aria-labelledby', 'articles-heading');
- const articlesHeading = document.createElement('h2');
- articlesHeading.id = 'articles-heading';
- articlesHeading.className = 'search-results-section-title';
- articlesHeading.textContent = 'Články';
- articlesSection.appendChild(articlesHeading);
- const articlesWrapper = document.createElement('div');
- articlesWrapper.id = 'newsWrapper';
- articlesWrapper.className = 'news-wrapper';
- articlesSection.appendChild(articlesWrapper);
- // Insert after categories section (before products section)
- const categoriesSection = document.getElementById('search-results-categories');
- const insertAfter = categoriesSection || section;
- if (insertAfter.parentElement) {
- insertAfter.parentElement.insertBefore(articlesSection, insertAfter.nextSibling);
- } else if (insertTarget) {
- insertTarget.appendChild(articlesSection);
- }
- }
- // Ensure correct order: categories -> articles -> products
- this.reorderSections();
- console.log('✅ Created Revelor search containers');
- }
- }
- /**
- * Reorder sections to ensure correct display order: categories -> articles -> products
- */
- reorderSections() {
- const categoriesSection = document.getElementById('search-results-categories');
- const articlesSection = document.getElementById('search-results-articles');
- const productsSection = document.getElementById('search-results-products');
- if (!productsSection || !productsSection.parentElement) return;
- const parent = productsSection.parentElement;
- // Ensure order: categories -> articles -> products
- // 1. Move categories first (if exists)
- if (categoriesSection && categoriesSection.parentElement === parent) {
- // Get the first relevant element (categories should be first)
- const firstRelevant = articlesSection || productsSection;
- if (categoriesSection.nextSibling !== firstRelevant) {
- parent.insertBefore(categoriesSection, firstRelevant);
- }
- }
- // 2. Move articles after categories (if exists)
- if (articlesSection && articlesSection.parentElement === parent) {
- const targetPosition = categoriesSection ? categoriesSection.nextSibling : productsSection;
- if (targetPosition !== articlesSection && targetPosition !== productsSection) {
- parent.insertBefore(articlesSection, targetPosition);
- } else if (!categoriesSection && articlesSection.nextSibling !== productsSection) {
- parent.insertBefore(articlesSection, productsSection);
- } else if (categoriesSection && articlesSection.nextSibling !== productsSection) {
- // Ensure articles are between categories and products
- parent.insertBefore(articlesSection, productsSection);
- }
- }
- }
- /**
- * Hide Shoptet default search results containers
- */
- hideShoptetResults() {
- // Hide Shoptet products container (but keep our container visible)
- const shoptetProducts = document.querySelectorAll('.products:not(#search-products-container), .products-block:not(#search-products-container), .products-searching:not(#search-products-container)');
- shoptetProducts.forEach(block => {
- block.style.display = 'none';
- });
- // Hide Shoptet listing controls (pagination, load more, etc.)
- const listingControls = document.querySelectorAll('.listingControls');
- listingControls.forEach(controls => {
- controls.style.display = 'none';
- });
- // Hide Shoptet pagination if exists
- const shoptetPagination = document.querySelectorAll('.pagination, .products-pagination');
- shoptetPagination.forEach(pagination => {
- pagination.style.display = 'none';
- });
- // Hide Shoptet "no results" messages
- const shoptetNoResults = document.querySelectorAll('.no-results:not(#search-results-no-results), .products-no-results');
- shoptetNoResults.forEach(noResults => {
- noResults.style.display = 'none';
- });
- // Hide category header sorting if exists (on search page)
- const categoryHeader = document.querySelector('#category-header');
- if (categoryHeader) {
- categoryHeader.style.display = 'none';
- }
- // Hide #content h2 element
- const contentH2 = document.querySelector('#content h2');
- if (contentH2) {
- contentH2.style.display = 'none';
- }
- // Hide #content.search-results-groups
- const contentSearchGroups = document.querySelector('#content.search-results-groups');
- if (contentSearchGroups) {
- contentSearchGroups.style.display = 'none';
- }
- // Hide sidebar with categories (left sidebar)
- const sidebar = document.querySelector('.sidebar.sidebar-left, aside.sidebar');
- if (sidebar) {
- sidebar.style.display = 'none';
- }
- }
- /**
- * Display current search query
- */
- displayQuery() {
- const queryDisplay = document.getElementById('search-query-display');
- const noResultsQuery = document.getElementById('no-results-query');
- if (queryDisplay) {
- queryDisplay.textContent = `"${this.currentQuery}"`;
- }
- if (noResultsQuery) {
- noResultsQuery.textContent = this.currentQuery;
- }
- }
- /**
- * Bind event listeners
- */
- bindEvents() {
- const loadMoreButton = document.getElementById('load-more-button');
- if (loadMoreButton) {
- loadMoreButton.addEventListener('click', () => this.loadMoreResults());
- }
- // Initialize infinite scroll
- this.setupInfiniteScroll();
- // Filter events
- const inStockFilter = document.getElementById('filter-in-stock');
- if (inStockFilter) {
- inStockFilter.addEventListener('change', (e) => {
- this.activeFilters.inStock = e.target.checked;
- this.applyFilters();
- });
- }
- const sortFilter = document.getElementById('filter-sort');
- if (sortFilter) {
- sortFilter.addEventListener('change', (e) => {
- this.activeFilters.sortBy = e.target.value;
- this.applyFilters();
- });
- }
- }
- /**
- * Apply filters to products
- */
- applyFilters() {
- let filtered = [...this.allProducts];
- // Filter: Only in stock
- if (this.activeFilters.inStock) {
- filtered = filtered.filter(p => p.skladem);
- }
- // Sort
- switch (this.activeFilters.sortBy) {
- case 'price-asc':
- filtered.sort((a, b) => (a.price_s_dph || 0) - (b.price_s_dph || 0));
- break;
- case 'price-desc':
- filtered.sort((a, b) => (b.price_s_dph || 0) - (a.price_s_dph || 0));
- break;
- case 'name':
- filtered.sort((a, b) => a.title.localeCompare(b.title, 'cs'));
- break;
- // 'relevance' - keep original order
- }
- // Clear and re-display products
- const container = document.getElementById('search-products-container');
- if (container) {
- container.innerHTML = '';
- filtered.forEach(product => {
- container.appendChild(this.createProductElement(product));
- });
- }
- // Show filters applied message
- const activeCount = (this.activeFilters.inStock ? 1 : 0);
- if (activeCount > 0 || this.activeFilters.sortBy !== 'relevance') {
- console.log(`🔍 Filters applied: ${filtered.length}/${this.allProducts.length} products`);
- }
- }
- /**
- * Setup infinite scroll with IntersectionObserver
- */
- setupInfiniteScroll() {
- const loadMoreContainer = document.getElementById('search-results-load-more');
- if (!loadMoreContainer) return;
- const observer = new IntersectionObserver((entries) => {
- entries.forEach(entry => {
- if (entry.isIntersecting && this.hasMoreResults && !this.isLoading) {
- console.log('📜 Infinite scroll triggered - loading more...');
- this.loadMoreResults();
- }
- });
- }, {
- rootMargin: '200px' // Load 200px before reaching bottom
- });
- observer.observe(loadMoreContainer);
- }
- /**
- * Load search results
- */
- async loadSearchResults() {
- this.showLoading();
- this.isLoading = true;
- // DataLayer: Search Query Submitted
- if (window.revelorDataLayer) {
- window.revelorDataLayer.searchQuerySubmitted(this.currentQuery, 'results_page');
- }
- // Check cache first
- const cacheKey = `${this.currentQuery}_${this.projectId}_${this.currentPage}`;
- const cached = this.getFromCache(cacheKey);
- if (cached) {
- console.log('📦 Using cached results for:', this.currentQuery, 'page:', this.currentPage);
- this.processSearchResults(cached);
- this.isLoading = false;
- this.hideLoading();
- return;
- }
- // Cancel previous request
- if (this.abortController) {
- this.abortController.abort();
- }
- this.abortController = new AbortController();
- const startTime = Date.now();
- try {
- const response = await this.fetchWithRetry(
- `${this.apiBaseUrl}/search?q=${encodeURIComponent(this.currentQuery)}&projectId=${this.projectId}&page=${this.currentPage}&page_size=${this.pageSize}`,
- {
- signal: this.abortController.signal,
- headers: {
- 'X-Tenant': this.projectId,
- },
- }
- );
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- const data = await response.json();
- const responseTime = Date.now() - startTime;
- // Save to cache
- this.saveToCache(cacheKey, data);
- // DataLayer: Search Results Displayed or No Results
- if (window.revelorDataLayer) {
- const hasResults = data.products?.length > 0 || data.categories?.length > 0 || data.articles?.length > 0;
- if (hasResults) {
- window.revelorDataLayer.searchResultsDisplayed(
- this.currentQuery,
- data,
- responseTime,
- this.currentPage
- );
- } else {
- window.revelorDataLayer.searchNoResults(this.currentQuery);
- }
- }
- // Backend API: Track search event
- if (window.searchTracker && this.currentQuery && this.currentQuery.length >= 3) {
- const totalResults = data.total_products || 0;
- window.searchTracker.trackSearch({
- query: this.currentQuery,
- results_count: totalResults,
- search_type: this.currentPage === 1 ? 'manual' : 'live_search'
- });
- }
- this.processSearchResults(data);
- } catch (error) {
- if (error.name === 'AbortError') {
- console.log('🚫 Request cancelled');
- return;
- }
- console.error('Search API Error:', error);
- // Try to use cached results as fallback
- const fallbackCached = this.getFromCache(cacheKey, true);
- if (fallbackCached) {
- console.log('📦 Using stale cache as fallback');
- this.processSearchResults(fallbackCached);
- this.showToast('Zobrazuji uložené výsledky (offline)', 'warning');
- return;
- }
- this.showError();
- } finally {
- this.isLoading = false;
- this.hideLoading();
- }
- }
- /**
- * Save to cache
- */
- saveToCache(key, data) {
- this.cache.set(key, {
- data,
- timestamp: Date.now()
- });
- }
- /**
- * Get from cache
- */
- getFromCache(key, ignoreExpiry = false) {
- const cached = this.cache.get(key);
- if (!cached) return null;
- // Check TTL (unless ignoreExpiry for fallback)
- if (!ignoreExpiry && Date.now() - cached.timestamp > this.cacheTTL) {
- this.cache.delete(key);
- return null;
- }
- return cached.data;
- }
- /**
- * Escape HTML to prevent XSS
- */
- escapeHtml(text) {
- const div = document.createElement('div');
- div.textContent = text;
- return div.innerHTML;
- }
- /**
- * Show toast notification
- */
- showToast(message, type = 'info', duration = 3000) {
- // Remove existing toasts
- document.querySelectorAll('.revelor-toast').forEach(t => t.remove());
- const icons = {
- success: '✅',
- error: '❌',
- warning: '⚠️',
- info: 'ℹ️'
- };
- const toast = document.createElement('div');
- toast.className = `revelor-toast revelor-toast--${type}`;
- toast.setAttribute('role', 'status');
- toast.setAttribute('aria-live', 'polite');
- toast.innerHTML = `
- <div class="revelor-toast__content">
- <span class="revelor-toast__icon" aria-hidden="true">${icons[type] || icons.info}</span>
- <span class="revelor-toast__message">${this.escapeHtml(message)}</span>
- <button class="revelor-toast__close" aria-label="Zavřít">×</button>
- </div>
- `;
- document.body.appendChild(toast);
- // Animate in
- setTimeout(() => toast.classList.add('revelor-toast--visible'), 10);
- // Close button
- toast.querySelector('.revelor-toast__close').addEventListener('click', () => {
- toast.classList.remove('revelor-toast--visible');
- setTimeout(() => toast.remove(), 300);
- });
- // Auto remove
- setTimeout(() => {
- toast.classList.remove('revelor-toast--visible');
- setTimeout(() => toast.remove(), 300);
- }, duration);
- }
- /**
- * Fetch with retry logic
- */
- async fetchWithRetry(url, options = {}, retries = 3) {
- for (let i = 0; i < retries; i++) {
- try {
- const response = await fetch(url, options);
- if (!response.ok && i < retries - 1) {
- await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
- continue;
- }
- return response;
- } catch (error) {
- if (error.name === 'AbortError') {
- throw error;
- }
- if (i === retries - 1) {
- throw error;
- }
- await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
- }
- }
- }
- /**
- * Announce message to screen readers
- */
- announceToScreenReader(message) {
- // Create or get live region
- let liveRegion = document.getElementById('search-results-live-region');
- if (!liveRegion) {
- liveRegion = document.createElement('div');
- liveRegion.id = 'search-results-live-region';
- liveRegion.className = 'sr-only';
- liveRegion.setAttribute('role', 'status');
- liveRegion.setAttribute('aria-live', 'polite');
- liveRegion.setAttribute('aria-atomic', 'true');
- document.body.appendChild(liveRegion);
- }
- liveRegion.textContent = message;
- // Clear after announcement
- setTimeout(() => {
- liveRegion.textContent = '';
- }, 1000);
- }
- /**
- * Process search results from API
- */
- processSearchResults(data, isLoadMore = false) {
- this.hasMoreResults = data.pagination?.has_next || false;
- const hasResults =
- data.products?.length > 0 ||
- data.categories?.length > 0 ||
- data.articles?.length > 0 ||
- data.brands?.length > 0;
- if (!hasResults && this.currentPage === 1) {
- this.showNoResults();
- this.announceToScreenReader('Žádné výsledky nebyly nalezeny');
- return;
- }
- this.showResults();
- // Announce results for screen readers
- if (!isLoadMore) {
- const totalResults = data.total_products || 0;
- this.announceToScreenReader(`Nalezeno ${totalResults} výsledků pro dotaz ${this.currentQuery}`);
- // Update meta tags for AI crawlers
- this.updateMetaTags(this.currentQuery, totalResults);
- // Create ItemList schema for AI agents
- this.createItemListSchema(data);
- } else {
- this.announceToScreenReader(`Načteny další výsledky`);
- }
- // Display results by type - Kategorie a články nahoru
- // Kategorie a články se zobrazují pouze na první stránce
- if (!isLoadMore) {
- if (data.categories?.length > 0) {
- this.displayCategories(data.categories);
- }
- if (data.articles?.length > 0) {
- this.displayArticles(data.articles);
- }
- if (data.brands?.length > 0) {
- this.displayBrands(data.brands);
- }
- // Ensure correct order after displaying categories and articles
- this.reorderSections();
- }
- // Produkty se zobrazují vždy
- if (data.products?.length > 0) {
- this.displayProducts(data.products);
- }
- // Update results count
- this.updateResultsCount(data);
- // Show/hide load more button
- this.updateLoadMoreButton();
- }
- /**
- * Create ItemList schema for AI agents
- */
- createItemListSchema(data) {
- // Remove existing ItemList schema if any
- const existingSchema = document.querySelector('script[data-schema="itemlist"]');
- if (existingSchema) {
- existingSchema.remove();
- }
- const itemListSchema = {
- "@context": "https://schema.org",
- "@type": "ItemList",
- "name": `Výsledky vyhledávání pro "${this.currentQuery}"`,
- "numberOfItems": data.total_products || 0,
- "itemListElement": []
- };
- // Add products to ItemList
- if (data.products && data.products.length > 0) {
- data.products.forEach((product, index) => {
- itemListSchema.itemListElement.push({
- "@type": "ListItem",
- "position": index + 1,
- "item": {
- "@type": "Product",
- "@id": product._id,
- "name": product.title,
- "url": product.url,
- "image": product.img || '/img/no-image.png',
- "sku": product.kod || 'N/A',
- "offers": {
- "@type": "Offer",
- "price": product.price_s_dph || 0,
- "priceCurrency": "CZK",
- "availability": product.skladem
- ? "https://schema.org/InStock"
- : "https://schema.org/OutOfStock"
- }
- }
- });
- });
- }
- // Create and append script tag
- const script = document.createElement('script');
- script.type = 'application/ld+json';
- script.setAttribute('data-schema', 'itemlist');
- script.textContent = JSON.stringify(itemListSchema);
- document.head.appendChild(script);
- }
- /**
- * Update meta tags for AI crawlers
- */
- updateMetaTags(query, totalResults) {
- const description = `Výsledky vyhledávání pro "${query}" - Nalezeno ${totalResults} produktů, kategorií a článků`;
- const title = `${query} - Výsledky vyhledávání`;
- // Update or create description meta tag
- let descMeta = document.querySelector('meta[name="description"]');
- if (!descMeta) {
- descMeta = document.createElement('meta');
- descMeta.setAttribute('name', 'description');
- document.head.appendChild(descMeta);
- }
- descMeta.setAttribute('content', description);
- // Update or create keywords meta tag
- let keywordsMeta = document.querySelector('meta[name="keywords"]');
- if (!keywordsMeta) {
- keywordsMeta = document.createElement('meta');
- keywordsMeta.setAttribute('name', 'keywords');
- document.head.appendChild(keywordsMeta);
- }
- keywordsMeta.setAttribute('content', `${query}, produkty, kategorie, články, e-shop, vyhledávání`);
- // Update page title
- document.title = title;
- // OpenGraph tags for AI agents
- this.updateOrCreateMetaTag('property', 'og:type', 'website');
- this.updateOrCreateMetaTag('property', 'og:title', title);
- this.updateOrCreateMetaTag('property', 'og:description', description);
- this.updateOrCreateMetaTag('property', 'og:url', window.location.href);
- // Twitter Card tags
- this.updateOrCreateMetaTag('name', 'twitter:card', 'summary_large_image');
- this.updateOrCreateMetaTag('name', 'twitter:title', title);
- this.updateOrCreateMetaTag('name', 'twitter:description', description);
- // AI crawler specific tags
- this.updateOrCreateMetaTag('name', 'robots', 'index, follow, max-snippet:-1, max-image-preview:large');
- this.updateOrCreateMetaTag('name', 'googlebot', 'index, follow');
- this.updateOrCreateMetaTag('name', 'bingbot', 'index, follow');
- // Canonical URL
- let canonical = document.querySelector('link[rel="canonical"]');
- if (!canonical) {
- canonical = document.createElement('link');
- canonical.setAttribute('rel', 'canonical');
- document.head.appendChild(canonical);
- }
- canonical.setAttribute('href', window.location.href);
- }
- /**
- * Helper: Update or create meta tag
- */
- updateOrCreateMetaTag(attributeName, attributeValue, content) {
- let meta = document.querySelector(`meta[${attributeName}="${attributeValue}"]`);
- if (!meta) {
- meta = document.createElement('meta');
- meta.setAttribute(attributeName, attributeValue);
- document.head.appendChild(meta);
- }
- meta.setAttribute('content', content);
- }
- /**
- * Display products
- */
- displayProducts(products) {
- console.log('displayProducts called with:', products); // Debug log
- const container = document.getElementById('search-products-container');
- const section = document.getElementById('search-results-products');
- const filters = document.getElementById('search-filters');
- console.log('Container:', container); // Debug log
- console.log('Section:', section); // Debug log
- if (!container || !section) {
- console.error('Container or section not found!');
- return;
- }
- section.style.display = 'block';
- // Show filters
- if (filters) {
- filters.style.display = 'flex';
- }
- // Store products for filtering
- this.allProducts.push(...products);
- products.forEach((product, index) => {
- const productElement = this.createProductElement(product, index);
- container.appendChild(productElement);
- });
- }
- /**
- * Quick add product to cart (Shoptet API)
- */
- async quickAddToCart(product, event) {
- // If product has variants, redirect to product detail
- if (product.has_variants || (product.variants && product.variants.length > 0)) {
- window.location.href = product.url;
- return;
- }
- const button = event.target;
- const originalText = button.innerHTML;
- // Show loading state
- button.disabled = true;
- button.innerHTML = '<span class="spinner-small" aria-hidden="true">⏳</span> Přidávám...';
- try {
- // Check if Shoptet API is available
- if (typeof shoptet === 'undefined' || !shoptet.cartShared) {
- throw new Error('Shoptet API není k dispozici');
- }
- // Use Shoptet API according to documentation
- // Priority: priceId > productCode > productId
- let addParams = {};
- if (product.priceId) {
- // Best option: priceId
- addParams = { priceId: product.priceId };
- } else if (product.kod || product.code) {
- // Fallback: product code
- addParams = { productCode: product.kod || product.code };
- } else if (product._id || product.id) {
- // Last resort: product ID
- addParams = { productId: product._id || product.id };
- } else {
- throw new Error('Nelze identifikovat produkt');
- }
- // Add to cart (without silent mode - show Shoptet's default behavior)
- await shoptet.cartShared.addToCart(addParams);
- // Success feedback
- button.innerHTML = '✅ Přidáno';
- button.classList.add('success');
- // DataLayer event
- if (window.revelorDataLayer) {
- window.dataLayer.push({
- event: 'revelor_quick_add_to_cart',
- product_id: product._id || product.id,
- product_name: product.title,
- price: product.price_s_dph,
- source: 'search_results'
- });
- }
- // Reset button after 2s
- setTimeout(() => {
- button.disabled = false;
- button.innerHTML = originalText;
- button.classList.remove('success');
- }, 2000);
- } catch (error) {
- console.error('Quick Add to Cart Error:', error);
- button.innerHTML = originalText;
- button.disabled = false;
- }
- }
- /**
- * Create product element - Shoptet category format
- * @param {Object} product - Product data
- * @param {number} index - Product position in list (0-based)
- */
- createProductElement(product, index = 0) {
- const productDiv = document.createElement('div');
- productDiv.className = 'product';
- // Format price - remove decimals if whole number, use space as thousands separator
- const formattedPrice = product.price_s_dph
- ? Math.floor(product.price_s_dph).toLocaleString('cs-CZ', {
- minimumFractionDigits: 0,
- maximumFractionDigits: 0
- }) + ' Kč'
- : 'Cena na dotaz';
- const availabilityText = product.skladem ? 'Skladem' : '10 dní';
- const availabilityClass = product.skladem ? 'availability skladem' : 'availability';
- const availabilityStyle = product.skladem ? 'style="color:#37A672"' : '';
- const availabilitySchema = product.skladem ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock';
- // Generate unique identifier
- const microIdentifier = product._id || product.id || `product_${Date.now()}_${index}`;
- // Get CSRF token if available (from Shoptet)
- const csrfToken = this.getCsrfToken();
- // Build product HTML matching Shoptet structure exactly
- const productInner = document.createElement('div');
- productInner.className = 'p swap-images';
- productInner.setAttribute('data-micro', 'product');
- productInner.setAttribute('data-micro-product-id', product._id || product.id || '');
- productInner.setAttribute('data-micro-identifier', microIdentifier);
- productInner.setAttribute('data-testid', 'productItem');
- productInner.innerHTML = `
- <a href="${product.url}" class="image">
- <img src="${product.img || '/img/no-image.png'}"
- alt="${this.escapeHtml(product.title)}"
- data-src="${product.img || '/img/no-image.png'}"
- class="swap-image"
- data-micro-image="${product.img || '/img/no-image.png'}"
- width="423"
- height="318"
- fetchpriority="${index < 4 ? 'high' : 'low'}"
- loading="${index < 4 ? 'eager' : 'lazy'}">
- <span class="imgCover"></span>
- </a>
- <div class="p-in">
- <div class="p-in-in">
- <a href="${product.url}" class="name" data-micro="url">
- <span data-micro="name" data-testid="productCardName">${this.escapeHtml(product.title)}</span>
- </a>
- <div class="dkLabFavouriteProductDiv onlyIcon">
- <span class="dkLabFavProductSpan dkLabFavouriteProductLinkAdd hasIcon"></span>
- </div>
- <div class="ratings-wrapper">
- <div class="${availabilityClass}">
- <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í.'}">
- ${availabilityText}
- </span>
- ${product.skladem ? `<span class="availability-amount" data-testid="numberAvailabilityAmount">(>10 ks)</span>` : ''}
- </div>
- </div>
- </div>
- <div class="p-bottom">
- <div data-micro="offer" data-micro-price="${product.price_s_dph || 0}" data-micro-price-currency="CZK" ${product.skladem ? `data-micro-availability="${availabilitySchema}"` : ''}>
- <div class="prices">
- <div class="price price-final" data-testid="productCardPrice">
- <strong>${formattedPrice}</strong>
- </div>
- </div>
- <div class="p-tools">
- ${product.has_variants || product.variants?.length > 0
- ? `<a href="${product.url}" class="btn btn-cart add-to-cart-button" data-testid="buttonAddToCart" aria-label="Do košíku ${this.escapeHtml(product.title)}">
- <span>Do košíku</span>
- </a>`
- : `<form action="/action/Cart/addCartItem/" method="post" class="pr-action csrf-enabled">
- <input type="hidden" name="language" value="cs">
- ${product.priceId ? `<input type="hidden" name="priceId" value="${product.priceId}">` : ''}
- <input type="hidden" name="productId" value="${product._id || product.id}">
- <input type="hidden" name="amount" value="1" autocomplete="off">
- <button type="submit" class="btn btn-cart add-to-cart-button" data-testid="buttonAddToCart" aria-label="Do košíku ${this.escapeHtml(product.title)}">
- <span>Do košíku</span>
- </button>
- ${csrfToken ? `<input type="hidden" name="__csrf__" value="${csrfToken}">` : ''}
- </form>`
- }
- </div>
- </div>
- </div>
- </div>
- <span class="no-display" data-micro="sku">${this.escapeHtml(product.kod || 'N/A')}</span>
- `;
- productDiv.appendChild(productInner);
- // Bind add to cart form submission (only for products without variants)
- const addToCartForm = productInner.querySelector('form.pr-action');
- if (addToCartForm && !product.has_variants && (!product.variants || product.variants.length === 0)) {
- addToCartForm.addEventListener('submit', async (e) => {
- e.preventDefault();
- // Track click before adding to cart
- if (window.searchTracker && this.currentQuery) {
- const position = index + 1 + (this.currentPage - 1) * this.pageSize;
- window.searchTracker.trackClick({
- clicked_result: product._id || product.id,
- product_id: product._id || product.id,
- result_type: 'product',
- result_title: product.title,
- result_url: product.url,
- result_position: position,
- query: this.currentQuery
- });
- }
- // Get button for feedback
- const button = addToCartForm.querySelector('button[type="submit"]');
- const originalHTML = button.innerHTML;
- // Show loading
- button.disabled = true;
- button.innerHTML = '<span>Přidávám...</span>';
- try {
- if (typeof shoptet === 'undefined' || !shoptet.cartShared) {
- throw new Error('Shoptet API není k dispozici');
- }
- // Use Shoptet API according to documentation
- // Priority: priceId > productCode > productId
- const addParams = {};
- if (product.priceId) {
- addParams.priceId = product.priceId;
- } else if (product.kod || product.code) {
- addParams.productCode = product.kod || product.code;
- } else if (product._id || product.id) {
- addParams.productId = product._id || product.id;
- } else {
- throw new Error('Nelze identifikovat produkt');
- }
- // Add to cart (without silent mode - show Shoptet's default behavior)
- await shoptet.cartShared.addToCart(addParams);
- // Success
- button.innerHTML = '<span>Přidáno</span>';
- // Reset after 2s
- setTimeout(() => {
- button.disabled = false;
- button.innerHTML = originalHTML;
- }, 2000);
- // Tracking
- if (window.revelorDataLayer) {
- window.dataLayer.push({
- event: 'revelor_quick_add_to_cart',
- product_id: product._id || product.id,
- product_name: product.title,
- price: product.price_s_dph,
- source: 'search_results'
- });
- }
- } catch (error) {
- console.error('Add to Cart Error:', error);
- button.disabled = false;
- button.innerHTML = originalHTML;
- }
- });
- }
- // Add click tracking for product links (image, name)
- const productLinks = productDiv.querySelectorAll('a[href="' + product.url + '"]');
- if (productLinks.length > 0 && window.searchTracker && this.currentQuery) {
- productLinks.forEach(link => {
- link.addEventListener('click', () => {
- const position = index + 1 + (this.currentPage - 1) * this.pageSize;
- window.searchTracker.trackClick({
- clicked_result: product._id || product.id,
- product_id: product._id || product.id,
- result_type: 'product',
- result_title: product.title,
- result_url: product.url,
- result_position: position,
- query: this.currentQuery
- });
- });
- });
- }
- // Add click tracking for "Do košíku" button (products with variants - redirects to detail)
- const addToCartLink = productInner.querySelector('a.add-to-cart-button');
- if (addToCartLink && window.searchTracker && this.currentQuery) {
- addToCartLink.addEventListener('click', () => {
- const position = index + 1 + (this.currentPage - 1) * this.pageSize;
- window.searchTracker.trackClick({
- clicked_result: product._id || product.id,
- product_id: product._id || product.id,
- result_type: 'product',
- result_title: product.title,
- result_url: product.url,
- result_position: position,
- query: this.currentQuery
- });
- });
- }
- return productDiv;
- }
- /**
- * Get CSRF token from Shoptet if available
- */
- getCsrfToken() {
- // Try to find CSRF token from existing forms on page
- const existingForm = document.querySelector('form.csrf-enabled');
- if (existingForm) {
- const csrfInput = existingForm.querySelector('input[name="__csrf__"]');
- if (csrfInput) {
- return csrfInput.value;
- }
- }
- return null;
- }
- /**
- * Display categories
- */
- displayCategories(categories) {
- const container = document.getElementById('search-categories-container');
- const section = document.getElementById('search-results-categories');
- if (!container || !section) return;
- section.style.display = 'block';
- // Clear previous categories
- container.innerHTML = '';
- // Remove old expand-all button if exists
- const oldExpandAllBtn = section.querySelector('.subcategories-expand-all-btn');
- if (oldExpandAllBtn) {
- oldExpandAllBtn.remove();
- }
- // Reset enhanced state if exists
- const wrapper = container.closest('.subcategories-wrapper');
- if (wrapper) {
- const expandBtn = wrapper.nextElementSibling;
- if (expandBtn && expandBtn.classList.contains('subcategories-expand-btn')) {
- expandBtn.remove();
- }
- wrapper.dataset.enhanced = 'false';
- const parent = wrapper.parentElement;
- if (parent) {
- parent.insertBefore(container, wrapper);
- wrapper.remove();
- }
- }
- container.dataset.enhanced = 'false';
- // Store all categories for expand functionality
- this.allCategories = categories;
- const maxVisible = 8;
- const visibleCategories = categories.slice(0, maxVisible);
- const hasMore = categories.length > maxVisible;
- // Display visible categories
- visibleCategories.forEach((category, index) => {
- const categoryElement = this.createCategoryElement(category, index);
- container.appendChild(categoryElement);
- });
- // Add expand button if there are more categories
- if (hasMore) {
- const expandButton = document.createElement('button');
- expandButton.type = 'button';
- expandButton.className = 'subcategories-expand-all-btn btn btn--secondary';
- expandButton.setAttribute('aria-expanded', 'false');
- expandButton.setAttribute('aria-label', `Zobrazit všechny kategorie (celkem ${categories.length})`);
- expandButton.innerHTML = `<span>Zobrazit všechny kategorie (${categories.length})</span>`;
- expandButton.addEventListener('click', () => {
- const isExpanded = expandButton.getAttribute('aria-expanded') === 'true';
- if (!isExpanded) {
- // Show all categories
- container.innerHTML = '';
- this.allCategories.forEach((category, index) => {
- const categoryElement = this.createCategoryElement(category, index);
- container.appendChild(categoryElement);
- });
- expandButton.setAttribute('aria-expanded', 'true');
- expandButton.setAttribute('aria-label', 'Zobrazit méně kategorií');
- expandButton.innerHTML = '<span>Zobrazit méně kategorií</span>';
- this.announceToScreenReader(`Zobrazeno všech ${categories.length} kategorií`);
- } else {
- // Show only first 8
- container.innerHTML = '';
- visibleCategories.forEach((category, index) => {
- const categoryElement = this.createCategoryElement(category, index);
- container.appendChild(categoryElement);
- });
- expandButton.setAttribute('aria-expanded', 'false');
- expandButton.setAttribute('aria-label', `Zobrazit všechny kategorie (celkem ${categories.length})`);
- expandButton.innerHTML = `<span>Zobrazit všechny kategorie (${categories.length})</span>`;
- this.announceToScreenReader('Zobrazeno prvních 8 kategorií');
- }
- });
- // Insert button after container
- container.parentElement.insertBefore(expandButton, container.nextSibling);
- }
- // Enhance subcategories with expand/collapse functionality
- this.enhanceSubcategories();
- }
- /**
- * Create category element
- */
- createCategoryElement(category, index = 0) {
- const li = document.createElement('li');
- li.className = 'col-xs-6 col-sm-6 col-lg-3 col-xl-3';
- li.innerHTML = `
- <a href="${category.url}">
- <span class="image">
- <img src="${category.img || 'https://cdn.myshoptet.com/prj/dist/master/cms/templates/frontend_templates/00/img/folder.svg'}"
- alt="${category.title}"
- width="140"
- height="100"
- loading="lazy">
- </span>
- <span class="text">${category.title}</span>
- </a>
- `;
- // Add click tracking
- const link = li.querySelector('a');
- if (link && window.searchTracker && this.currentQuery) {
- link.addEventListener('click', () => {
- window.searchTracker.trackClick({
- clicked_result: category._id || category.id || category.url,
- result_type: 'category',
- result_title: category.title,
- result_url: category.url,
- result_position: index + 1,
- query: this.currentQuery
- });
- });
- }
- return li;
- }
- /**
- * Enhance subcategories to show only first row with expand button
- */
- enhanceSubcategories() {
- const subcategoriesList = document.querySelector(
- '.subcategories.with-image'
- );
- if (!subcategoriesList) {
- return;
- }
- // Check if already enhanced
- if (subcategoriesList.dataset.enhanced === 'true') {
- return;
- }
- // Get all subcategory items
- const items = Array.from(subcategoriesList.querySelectorAll('li'));
- if (items.length === 0) {
- return;
- }
- // Determine how many items to show in first row based on viewport
- const getItemsPerRow = () => {
- const viewportWidth = window.innerWidth;
- if (viewportWidth <= 480) return 1; // xs: 1 column
- if (viewportWidth <= 768) return 2; // sm: 2 columns
- if (viewportWidth <= 1200) return 3; // lg: 3 columns
- if (viewportWidth <= 1440) return 4; // xl: 4 columns
- return 6; // Default: 6 columns
- };
- const itemsPerRow = getItemsPerRow();
- // If all items fit in one row, no need for enhancement
- if (items.length <= itemsPerRow) {
- return;
- }
- // Add wrapper for collapsed state
- const wrapper = document.createElement('div');
- wrapper.className = 'subcategories-wrapper';
- wrapper.dataset.collapsed = 'true';
- // Insert wrapper and move list inside
- subcategoriesList.parentNode.insertBefore(wrapper, subcategoriesList);
- wrapper.appendChild(subcategoriesList);
- // Create expand button
- const expandButton = document.createElement('button');
- expandButton.type = 'button';
- expandButton.className = 'subcategories-expand-btn wt-btn-ghost';
- expandButton.setAttribute('aria-expanded', 'false');
- expandButton.setAttribute('aria-controls', 'search-categories-list');
- expandButton.setAttribute('aria-label', 'Zobrazit další kategorie');
- expandButton.innerHTML = `
- <span class="text" aria-hidden="true">Další kategorie</span>
- `;
- // Insert button after wrapper
- wrapper.insertAdjacentElement('afterend', expandButton);
- // Handle expand/collapse
- expandButton.addEventListener('click', () => {
- const isCollapsed = wrapper.dataset.collapsed === 'true';
- const visibleBefore = items.length;
- const visibleAfter = isCollapsed ? items.length : itemsPerRow;
- if (isCollapsed) {
- wrapper.dataset.collapsed = 'false';
- expandButton.classList.add('expanded');
- expandButton.setAttribute('aria-expanded', 'true');
- expandButton.setAttribute('aria-label', 'Zobrazit méně kategorií');
- expandButton.querySelector('.text').textContent = 'Méně kategorií';
- this.announceToScreenReader('Kategorie rozbaleny');
- // DataLayer: Search Category Expand
- if (window.revelorDataLayer) {
- window.revelorDataLayer.searchCategoryExpand(
- this.currentQuery,
- 'categories',
- 'expand',
- itemsPerRow,
- visibleBefore
- );
- }
- } else {
- wrapper.dataset.collapsed = 'true';
- expandButton.classList.remove('expanded');
- expandButton.setAttribute('aria-expanded', 'false');
- expandButton.setAttribute('aria-label', 'Zobrazit další kategorie');
- expandButton.querySelector('.text').textContent = 'Další kategorie';
- this.announceToScreenReader('Kategorie sbaleny');
- // DataLayer: Search Category Collapse
- if (window.revelorDataLayer) {
- window.revelorDataLayer.searchCategoryExpand(
- this.currentQuery,
- 'categories',
- 'collapse',
- visibleBefore,
- itemsPerRow
- );
- }
- // Scroll to subcategories if needed
- const wrapperTop = wrapper.getBoundingClientRect().top + window.scrollY;
- if (window.scrollY > wrapperTop) {
- window.scrollTo({
- top: wrapperTop - 100,
- behavior: 'smooth',
- });
- }
- }
- });
- // Update items per row on resize
- const updateItemsPerRow = () => {
- const newItemsPerRow = getItemsPerRow();
- wrapper.style.setProperty('--items-per-row', newItemsPerRow);
- };
- // Initial setup
- updateItemsPerRow();
- // Add resize listener
- let subcategoriesResizeTimeout;
- window.addEventListener('resize', () => {
- clearTimeout(subcategoriesResizeTimeout);
- subcategoriesResizeTimeout = setTimeout(updateItemsPerRow, 250);
- });
- // Mark as enhanced
- subcategoriesList.dataset.enhanced = 'true';
- }
- /**
- * Display articles
- */
- displayArticles(articles) {
- const container = document.getElementById('newsWrapper');
- const section = document.getElementById('search-results-articles');
- if (!container || !section) return;
- section.style.display = 'block';
- // Clear previous articles
- container.innerHTML = '';
- // Reset enhanced state if exists
- const wrapper = container.closest('.articles-wrapper');
- if (wrapper) {
- const expandBtn = wrapper.nextElementSibling;
- if (expandBtn && expandBtn.classList.contains('articles-expand-btn')) {
- expandBtn.remove();
- }
- wrapper.dataset.enhanced = 'false';
- const parent = wrapper.parentElement;
- if (parent) {
- parent.insertBefore(container, wrapper);
- wrapper.remove();
- }
- }
- container.dataset.enhanced = 'false';
- // Display only first 3 articles
- const maxVisible = 3;
- const visibleArticles = articles.slice(0, maxVisible);
- visibleArticles.forEach((article, index) => {
- const articleElement = this.createArticleElement(article, index);
- container.appendChild(articleElement);
- });
- // Don't use enhanceArticles - we show only 3 items, no expand needed
- }
- /**
- * Enhance articles to show only first row with expand button
- */
- enhanceArticles() {
- const articlesContainer = document.getElementById('newsWrapper');
- if (!articlesContainer) {
- return;
- }
- // Check if already enhanced
- if (articlesContainer.dataset.enhanced === 'true') {
- return;
- }
- // Get all article items
- const items = Array.from(
- articlesContainer.querySelectorAll('article.news-item')
- );
- if (items.length === 0) {
- return;
- }
- // Determine how many items to show in first row based on viewport
- const getItemsPerRow = () => {
- const viewportWidth = window.innerWidth;
- if (viewportWidth <= 480) return 1; // xs: 1 column
- if (viewportWidth <= 768) return 2; // sm: 2 columns
- if (viewportWidth <= 1200) return 3; // lg: 3 columns
- if (viewportWidth <= 1440) return 4; // xl: 4 columns
- return 6; // Default: 6 columns
- };
- const itemsPerRow = getItemsPerRow();
- // If all items fit in one row, no need for enhancement
- if (items.length <= itemsPerRow) {
- return;
- }
- // Add wrapper for collapsed state
- const wrapper = document.createElement('div');
- wrapper.className = 'articles-wrapper';
- wrapper.dataset.collapsed = 'true';
- // Insert wrapper and move articles inside
- articlesContainer.parentNode.insertBefore(wrapper, articlesContainer);
- wrapper.appendChild(articlesContainer);
- // Create expand button
- const expandButton = document.createElement('button');
- expandButton.type = 'button';
- expandButton.className = 'articles-expand-btn wt-btn-ghost';
- expandButton.setAttribute('aria-expanded', 'false');
- expandButton.setAttribute('aria-controls', 'newsWrapper');
- expandButton.setAttribute('aria-label', 'Zobrazit další články');
- expandButton.innerHTML = `
- <span class="text" aria-hidden="true">Další články</span>
- `;
- // Insert button after wrapper
- wrapper.insertAdjacentElement('afterend', expandButton);
- // Handle expand/collapse
- expandButton.addEventListener('click', () => {
- const isCollapsed = wrapper.dataset.collapsed === 'true';
- const visibleBefore = items.length;
- if (isCollapsed) {
- wrapper.dataset.collapsed = 'false';
- expandButton.classList.add('expanded');
- expandButton.setAttribute('aria-expanded', 'true');
- expandButton.setAttribute('aria-label', 'Zobrazit méně článků');
- expandButton.querySelector('.text').textContent = 'Méně článků';
- this.announceToScreenReader('Články rozbaleny');
- // DataLayer: Search Articles Expand
- if (window.revelorDataLayer) {
- window.revelorDataLayer.searchCategoryExpand(
- this.currentQuery,
- 'articles',
- 'expand',
- itemsPerRow,
- visibleBefore
- );
- }
- } else {
- wrapper.dataset.collapsed = 'true';
- expandButton.classList.remove('expanded');
- expandButton.setAttribute('aria-expanded', 'false');
- expandButton.setAttribute('aria-label', 'Zobrazit další články');
- expandButton.querySelector('.text').textContent = 'Další články';
- this.announceToScreenReader('Články sbaleny');
- // DataLayer: Search Articles Collapse
- if (window.revelorDataLayer) {
- window.revelorDataLayer.searchCategoryExpand(
- this.currentQuery,
- 'articles',
- 'collapse',
- visibleBefore,
- itemsPerRow
- );
- }
- // Scroll to articles if needed
- const wrapperTop = wrapper.getBoundingClientRect().top + window.scrollY;
- if (window.scrollY > wrapperTop) {
- window.scrollTo({
- top: wrapperTop - 100,
- behavior: 'smooth',
- });
- }
- }
- });
- // Update items per row on resize
- const updateItemsPerRow = () => {
- const newItemsPerRow = getItemsPerRow();
- wrapper.style.setProperty('--items-per-row', newItemsPerRow);
- };
- // Initial setup
- updateItemsPerRow();
- // Add resize listener
- let articlesResizeTimeout;
- window.addEventListener('resize', () => {
- clearTimeout(articlesResizeTimeout);
- articlesResizeTimeout = setTimeout(updateItemsPerRow, 250);
- });
- // Mark as enhanced
- articlesContainer.dataset.enhanced = 'true';
- }
- /**
- * Create article element
- */
- createArticleElement(article, index = 0) {
- const articleDiv = document.createElement('article');
- articleDiv.className = 'news-item';
- // Microdata for AI agents
- articleDiv.setAttribute('itemscope', '');
- articleDiv.setAttribute('itemtype', 'https://schema.org/Article');
- // Create JSON-LD structured data
- const jsonLd = {
- "@context": "https://schema.org",
- "@type": "Article",
- "headline": article.title,
- "description": article.description || article.title,
- "image": article.img || '/img/no-image.jpg',
- "url": article.url,
- "author": {
- "@type": "Organization",
- "name": "Your E-shop"
- }
- };
- // Compact article display - small image + title only
- articleDiv.innerHTML = `
- <!-- JSON-LD for AI agents -->
- <script type="application/ld+json">
- ${JSON.stringify(jsonLd)}
- </script>
- <a href="${article.url}" title="${article.title}" class="news-item-compact" itemprop="url">
- <span class="news-item-compact-image">
- <img src="${article.img || '/img/no-image.jpg'}"
- alt="${article.title}"
- itemprop="image"
- class="news-image-compact"
- width="100"
- height="100"
- loading="lazy">
- </span>
- <span class="news-item-compact-title" itemprop="headline">
- ${this.escapeHtml(article.title)}
- </span>
- </a>
- `;
- // Add click tracking for article links
- const articleLinks = articleDiv.querySelectorAll('a[href="' + article.url + '"]');
- if (articleLinks.length > 0 && window.searchTracker && this.currentQuery) {
- articleLinks.forEach(link => {
- link.addEventListener('click', () => {
- window.searchTracker.trackClick({
- clicked_result: article._id || article.id || article.url,
- result_type: 'article',
- result_title: article.title,
- result_url: article.url,
- result_position: index + 1,
- query: this.currentQuery
- });
- });
- });
- }
- return articleDiv;
- }
- /**
- * Display brands
- */
- displayBrands(brands) {
- const container = document.getElementById('search-brands-container');
- const section = document.getElementById('search-results-brands');
- if (!container || !section) return;
- section.style.display = 'block';
- brands.forEach((brand, index) => {
- const brandElement = this.createBrandElement(brand, index);
- container.appendChild(brandElement);
- });
- }
- /**
- * Create brand element
- */
- createBrandElement(brand, index = 0) {
- const li = document.createElement('li');
- li.className = 'search-brand-item';
- li.innerHTML = `
- <a href="${brand.url}">
- <div class="search-brand-content">
- <h3>${brand.title}</h3>
- <p>${brand.description || ''}</p>
- </div>
- </a>
- `;
- // Add click tracking (brands tracked as categories)
- const link = li.querySelector('a');
- if (link && window.searchTracker && this.currentQuery) {
- link.addEventListener('click', () => {
- window.searchTracker.trackClick({
- clicked_result: brand._id || brand.id || brand.url,
- result_type: 'category', // Brands tracked as categories
- result_title: brand.title,
- result_url: brand.url,
- result_position: index + 1,
- query: this.currentQuery
- });
- });
- }
- return li;
- }
- /**
- * Load more results
- */
- async loadMoreResults() {
- if (this.isLoading || !this.hasMoreResults) return;
- this.currentPage++;
- this.showLoadMoreLoading();
- // DataLayer: Search Load More
- if (window.revelorDataLayer) {
- const currentProductsCount = document.querySelectorAll('#search-products-container .product').length;
- window.revelorDataLayer.searchLoadMore(
- this.currentQuery,
- this.currentPage - 1,
- Math.ceil((this.latestTotalProducts || 0) / this.pageSize),
- this.pageSize,
- currentProductsCount + this.pageSize
- );
- }
- try {
- const response = await fetch(
- `${this.apiBaseUrl}/search?q=${encodeURIComponent(this.currentQuery)}&projectId=${this.projectId}&page=${this.currentPage}&page_size=${this.pageSize}`,
- {
- headers: {
- 'X-Tenant': this.projectId,
- },
- }
- );
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- const data = await response.json();
- this.processSearchResults(data, true);
- } catch (error) {
- console.error('Load more error:', error);
- this.currentPage--; // Revert page increment on error
- } finally {
- this.hideLoadMoreLoading();
- }
- }
- /**
- * Update results count
- */
- updateResultsCount(data) {
- const countElement = document.getElementById('search-results-count');
- if (countElement) {
- const totalProducts = data.total_products || 0;
- this.latestTotalProducts = totalProducts;
- countElement.textContent = `Nalezeno ${totalProducts} výsledků`;
- }
- }
- /**
- * Update load more button
- */
- updateLoadMoreButton() {
- const loadMoreContainer = document.getElementById(
- 'search-results-load-more'
- );
- if (loadMoreContainer) {
- loadMoreContainer.style.display = this.hasMoreResults ? 'block' : 'none';
- }
- }
- /**
- * Show loading state
- */
- showLoading() {
- const loading = document.getElementById('search-results-loading');
- if (loading) loading.style.display = 'block';
- }
- /**
- * Hide loading state
- */
- hideLoading() {
- const loading = document.getElementById('search-results-loading');
- if (loading) loading.style.display = 'none';
- }
- /**
- * Show load more loading
- */
- showLoadMoreLoading() {
- const button = document.getElementById('load-more-button');
- const text = button?.querySelector('.load-more-text');
- const spinner = button?.querySelector('.load-more-spinner');
- if (text) text.style.display = 'none';
- if (spinner) spinner.style.display = 'inline';
- if (button) {
- button.disabled = true;
- button.setAttribute('aria-busy', 'true');
- button.setAttribute('aria-label', 'Načítám další výsledky');
- }
- this.announceToScreenReader('Načítám další výsledky');
- }
- /**
- * Hide load more loading
- */
- hideLoadMoreLoading() {
- const button = document.getElementById('load-more-button');
- const text = button?.querySelector('.load-more-text');
- const spinner = button?.querySelector('.load-more-spinner');
- if (text) text.style.display = 'inline';
- if (spinner) spinner.style.display = 'none';
- if (button) {
- button.disabled = false;
- button.setAttribute('aria-busy', 'false');
- button.setAttribute('aria-label', 'Načíst více výsledků');
- }
- }
- /**
- * Show results
- */
- showResults() {
- const content = document.getElementById('search-results-content');
- if (content) content.style.display = 'block';
- }
- /**
- * Show no results
- */
- showNoResults(message = 'Žádné výsledky') {
- const noResults = document.getElementById('search-results-no-results');
- if (noResults) {
- noResults.style.display = 'block';
- const title = noResults.querySelector('h2');
- if (title) title.textContent = message;
- }
- }
- /**
- * Show error
- */
- showError() {
- this.showNoResults('Chyba při načítání výsledků');
- }
- }
- // Initialize when DOM is ready - only on search results page
- document.addEventListener('DOMContentLoaded', () => {
- // Only initialize if body has 'in-vyhledavani' class
- if (document.body.classList.contains('in-vyhledavani')) {
- new SearchResultsPage();
- }
- });
Advertisement
Add Comment
Please, Sign In to add comment