// Question and answer data structure // Loaded from JSON file via AJAX // Store all user answers const userAnswers = {}; // Question data loaded from JSON let questionData = {}; let productData = []; let accessoryData = []; let bitwiseData = {}; // Track accumulated bit value for URL state let accumulatedBitValue = 0; // Bit definitions (must match generate_bitwise_helper.py) const BIT_DEFINITIONS = { 'base_door': 0, // 1 'base_window': 1, // 2 'is_accessory': 2, // 4 'specific_item': 3, // 8 'material_aluminum': 4, // 16 'material_vinyl': 5, // 32 'color_black': 6, // 64 'color_white': 7, // 128 'color_bronze': 8, // 256 'color_tan': 9, // 512 'color_mill': 10, // 1024 'color_sandstone': 11, // 2048 'subtype_patio_door': 12, // 4096 'subtype_primary_window': 13, // 8192 'subtype_storm_door': 14, // 16384 'subtype_storm_window': 15 // 32768 }; // Initialize the quiz function init() { // Load question data from JSON file loadQuestionData(); } // Load question data and other JSON files function loadQuestionData() { // Use the data path from the blueprint const dataPath = '/quiz/data/'; // Load all data files in parallel Promise.all([ fetch(dataPath + 'navigation.json').then(r => r.ok ? r.json() : Promise.reject('navigation.json not found')), fetch(dataPath + 'products.json').then(r => r.ok ? r.json() : []).catch(() => []), fetch(dataPath + 'accessories.json').then(r => r.ok ? r.json() : []).catch(() => []), fetch(dataPath + 'product_bitwise.json').then(r => r.ok ? r.json() : []).catch(() => []) ]) .then(([questions, products, accessories, bitwise]) => { questionData = questions; productData = products; accessoryData = accessories; // Index bitwise data by product code bitwiseData = {}; bitwise.forEach(item => { bitwiseData[item.PROD_CODE] = item.BIT_VALUE; }); console.log('Loaded:', { questions: Object.keys(questionData).length, products: productData.length, accessories: accessoryData.length, bitwise: Object.keys(bitwiseData).length }); // Check if there's state in URL initFromURL(); }) .catch(error => { console.error('Error loading data:', error); document.getElementById('content').innerHTML = `
Error Loading Application
Failed to load data files. Please refresh the page or contact support.
Error: ${error}
`; }); } // Handle conditional logic for q-material function getNextForMaterial(answers) { const productType = answers['start']; if (productType === 'Window') { return 'q-window-type'; } else if (productType === 'Door') { return 'q-door-type'; } else if (productType === 'Patio Door') { return 'q-patio'; } return 'q-window-type'; // default } // Navigation history let history = ['start']; // Initialize from URL parameters function initFromURL() { const params = new URLSearchParams(window.location.search); // Priority 1: Direct product view const productCode = params.get('p'); if (productCode) { accumulatedBitValue = parseInt(params.get('b') || '0', 10); showProductByCode(productCode); return; } // Priority 2: Restore navigation state const bitValue = params.get('b'); const questionKey = params.get('q'); if (bitValue) { accumulatedBitValue = parseInt(bitValue, 10); if (questionKey) { // Special case: results page if (questionKey === 'results') { restoreStateFromBitValue(accumulatedBitValue, null); history.push('results'); showFilteredProducts(); return; } restoreStateFromBitValue(accumulatedBitValue, questionKey); return; } } // Priority 3: Start fresh loadContent('start'); } // Restore state from bit value function restoreStateFromBitValue(bitValue, questionKey) { // Decode bit value back to user selections // Base type if (bitValue & 1) { userAnswers['start'] = 'Door'; } else if (bitValue & 2) { userAnswers['start'] = 'Window'; } // Subtypes if (bitValue & (1 << BIT_DEFINITIONS['subtype_patio_door'])) { if (bitValue & 1) userAnswers['q-door-type'] = 'Patio Door'; } if (bitValue & (1 << BIT_DEFINITIONS['subtype_storm_door'])) { if (bitValue & 1) userAnswers['q-door-type'] = 'Storm Door'; } if (bitValue & (1 << BIT_DEFINITIONS['subtype_primary_window'])) { if (bitValue & 2) userAnswers['q-window-type'] = 'Primary Window'; } if (bitValue & (1 << BIT_DEFINITIONS['subtype_storm_window'])) { if (bitValue & 2) userAnswers['q-window-type'] = 'Storm Window'; } // Materials if (bitValue & 16) { userAnswers['material'] = 'Aluminum'; } else if (bitValue & 32) { userAnswers['material'] = 'Vinyl'; } // Colors const colorMap = { 64: 'Black', 128: 'White', 256: 'Bronze', 512: 'Tan', 1024: 'Mill', 2048: 'Sandstone' }; for (const [bit, color] of Object.entries(colorMap)) { if (bitValue & parseInt(bit)) { userAnswers['color'] = color; break; } } // Navigate to the saved question history = ['start']; if (questionKey && questionKey !== 'start') { history.push(questionKey); } loadContent(questionKey || 'start'); } // Update URL with current state function updateURL(questionKey = null) { const params = new URLSearchParams(); if (accumulatedBitValue > 0) { params.set('b', accumulatedBitValue.toString()); } const currentKey = questionKey || (history.length > 0 ? history[history.length - 1] : 'start'); if (currentKey && currentKey !== 'start') { params.set('q', currentKey); } const newURL = window.location.pathname + (params.toString() ? '?' + params.toString() : ''); window.history.replaceState( { bitValue: accumulatedBitValue, questionKey: currentKey }, '', newURL ); } // Update bit value based on selection function updateBitValue(answerObject) { if (!answerObject || !answerObject.filter) return; const filter = answerObject.filter; // Base type if (filter.baseType === 'Door') { accumulatedBitValue |= (1 << BIT_DEFINITIONS['base_door']); } else if (filter.baseType === 'Window') { accumulatedBitValue |= (1 << BIT_DEFINITIONS['base_window']); } // Materials if (filter.material === 'Aluminum') { accumulatedBitValue |= (1 << BIT_DEFINITIONS['material_aluminum']); } else if (filter.material === 'Vinyl') { accumulatedBitValue |= (1 << BIT_DEFINITIONS['material_vinyl']); } // Colors const colorKey = filter.color ? 'color_' + filter.color.toLowerCase() : null; if (colorKey && BIT_DEFINITIONS[colorKey]) { accumulatedBitValue |= (1 << BIT_DEFINITIONS[colorKey]); } // Subtypes if (filter.subType) { const subtypeKey = 'subtype_' + filter.subType.toLowerCase().replace(/ /g, '_'); if (BIT_DEFINITIONS[subtypeKey]) { accumulatedBitValue |= (1 << BIT_DEFINITIONS[subtypeKey]); } } } // Load content based on key function loadContent(key) { // Special handling for results if (key === 'results') { showFilteredProducts(); return; } const data = questionData[key]; if (!data) { console.error('Content not found for key:', key); return; } const contentDiv = document.getElementById('content'); if (data.type === 'question') { if (data.inputType === 'form') { renderForm(data, key); } else { renderQuestion(data, key); } } else if (data.type === 'product') { renderProduct(data); } updateBreadcrumb(); } // Helper function to render notes section function renderNotesSection(notes) { const notesId = 'notes-' + Date.now(); return `
`; } // Toggle notes visibility function toggleNotes(notesId) { const notesContent = document.getElementById(notesId); const arrow = document.getElementById(notesId + '-arrow'); if (notesContent.style.display === 'none') { notesContent.style.display = 'block'; arrow.textContent = '▲'; } else { notesContent.style.display = 'none'; arrow.textContent = '▼'; } } // Render a question with answer buttons function renderQuestion(data, currentKey) { const contentDiv = document.getElementById('content'); let html = `
${data.title}
${data.subtitle}
`; html += `
`; data.answers.forEach((answer, index) => { html += ` `; }); html += `
`; // Add notes section if available if (data.notes) { html += renderNotesSection(data.notes); } contentDiv.innerHTML = html; } // Render a form with text inputs function renderForm(data, currentKey) { const contentDiv = document.getElementById('content'); let html = `
${data.title}
${data.subtitle}
`; // Render measurement type toggle buttons if configuration exists if (data.measurementType && data.measurementType.options) { const currentMeasurementType = userAnswers[`${currentKey}.measurementType`] || data.measurementType.defaultValue; html += `
`; data.measurementType.options.forEach(option => { const isSelected = currentMeasurementType === option.value; const disabledClass = option.disabled ? 'disabled' : ''; const disabledAttr = option.disabled ? 'disabled' : ''; html += ` `; }); html += `
`; } html += `
`; data.fields.forEach((field, index) => { html += `
`; html += ``; if (field.type === 'select') { html += ``; } else if (field.type === 'textarea') { const value = userAnswers[`${currentKey}.${field.name}`] || ''; html += ``; } else { const value = userAnswers[`${currentKey}.${field.name}`] || ''; html += ``; } html += `
`; }); html += `
`; // Add notes section if available if (data.notes) { html += renderNotesSection(data.notes); } contentDiv.innerHTML = html; } // Render a product result function renderProduct(data) { const contentDiv = document.getElementById('content'); // Build specifications from stored answers let specsHtml = ''; if (Object.keys(userAnswers).length > 0) { specsHtml = 'Your Specifications:
'; for (const [key, value] of Object.entries(userAnswers)) { if (value && key !== 'start') { const displayKey = key.split('.').pop().replace(/([A-Z])/g, ' $1').trim(); specsHtml += `${displayKey.charAt(0).toUpperCase() + displayKey.slice(1)}: ${value}
`; } } specsHtml += '
'; } // Get product image (use placeholder if not specified) const productImage = data.image || 'https://via.placeholder.com/400x400/e0e0e0/666?text=' + encodeURIComponent(data.code); let html = `
${data.title}
${data.title}
Product Code: ${data.code}
Category: ${data.category}

${specsHtml} ${data.description}

Key Features:
    ${data.features.map(f => `
  • ${f}
  • `).join('')}
`; contentDiv.innerHTML = html; } // Handle answer selection from buttons function handleAnswer(currentKey, answerValue, nextKeyOrIndex) { // Store the answer userAnswers[currentKey] = answerValue; // Check if this is an index (new format) or key (old format) let nextKey = nextKeyOrIndex; const currentQuestion = questionData[currentKey]; if (currentQuestion && currentQuestion.answers && typeof nextKeyOrIndex === 'number') { // New format: index provided const answerObject = currentQuestion.answers[nextKeyOrIndex]; nextKey = answerObject.next; // Store dimensions if provided (for preset sizes) if (answerObject.dimensions) { userAnswers['q-dimensions.width'] = answerObject.dimensions.width; userAnswers['q-dimensions.height'] = answerObject.dimensions.height; } // Update bit value if filter exists if (answerObject.filter) { updateBitValue(answerObject); } } history.push(nextKey); updateURL(nextKey); loadContent(nextKey); } // Handle answer by index only (used by buttons to avoid escaping issues) function handleAnswerByIndex(currentKey, answerIndex) { const currentQuestion = questionData[currentKey]; if (!currentQuestion || !currentQuestion.answers) { console.error('Invalid question:', currentKey); return; } const answerObject = currentQuestion.answers[answerIndex]; if (!answerObject) { console.error('Invalid answer index:', answerIndex); return; } // Store the answer caption userAnswers[currentKey] = answerObject.caption; // Store dimensions if provided (for preset sizes) if (answerObject.dimensions) { userAnswers['q-dimensions.width'] = answerObject.dimensions.width; userAnswers['q-dimensions.height'] = answerObject.dimensions.height; } // Update bit value if filter exists if (answerObject.filter) { updateBitValue(answerObject); } // Navigate to next question history.push(answerObject.next); updateURL(answerObject.next); loadContent(answerObject.next); } // Handle measurement type toggle button click function handleMeasurementTypeToggle(value, currentKey) { // Store the selected measurement type userAnswers[`${currentKey}.measurementType`] = value; // Update the UI to reflect the selection const toggleButtons = document.querySelectorAll('.measurement-toggle'); toggleButtons.forEach(button => { if (button.dataset.value === value) { button.classList.add('selected'); } else { button.classList.remove('selected'); } }); } // Handle form submission function handleFormSubmit(event, currentKey) { event.preventDefault(); const form = event.target; const formData = new FormData(form); // Store all form field values for (const [key, value] of formData.entries()) { userAnswers[`${currentKey}.${key}`] = value; } // Ensure measurement type is saved if it was selected const data = questionData[currentKey]; if (data.measurementType && !userAnswers[`${currentKey}.measurementType`]) { userAnswers[`${currentKey}.measurementType`] = data.measurementType.defaultValue; } // Get the next key (handle conditional logic) let nextKey; // Check if this is the q-material question that needs conditional logic if (currentKey === 'q-material' && data.next === 'q-material-conditional') { nextKey = getNextForMaterial(userAnswers); } else { nextKey = data.next; } // Special handling for results if (nextKey === 'results') { history.push(nextKey); updateURL(nextKey); showFilteredProducts(); return; } history.push(nextKey); updateURL(nextKey); loadContent(nextKey); } // Go back to previous question function goBack() { if (history.length > 1) { history.pop(); const previousKey = history[history.length - 1]; loadContent(previousKey); } } // Start over function startOver() { history = ['start']; // Clear all stored answers for (const key in userAnswers) { delete userAnswers[key]; } // Clear bit value accumulatedBitValue = 0; // Clear URL window.history.replaceState({}, '', window.location.pathname); loadContent('start'); } // Show product by code (from URL) function showProductByCode(productCode) { if (productData.length === 0) { document.getElementById('content').innerHTML = `
Products Not Loaded
Product data not available yet. Please generate JSON files first.
`; return; } const product = productData.find(p => p.productCode === productCode || p.id === productCode); if (!product) { document.getElementById('content').innerHTML = `
Product Not Found
Product ${productCode} not found.
`; return; } showProductDetail(product); } // Show product detail function showProductDetail(product) { const prodCode = product.productCode || product.id; // Add to history if not already there const productKey = `product-${prodCode}`; if (history[history.length - 1] !== productKey) { history.push(productKey); } // Update URL with product code const params = new URLSearchParams(); params.set('p', prodCode); if (accumulatedBitValue > 0) { params.set('b', accumulatedBitValue.toString()); } window.history.pushState( { productCode: prodCode }, '', '?' + params.toString() ); // Get compatible accessories if available const compatibleAccessories = accessoryData.filter(acc => product.compatibleAccessories && product.compatibleAccessories.includes(acc.id) ); // Build specifications from stored answers let specsHtml = ''; if (Object.keys(userAnswers).length > 0) { specsHtml = 'Your Specifications:
'; for (const [key, value] of Object.entries(userAnswers)) { if (value && key !== 'start') { const displayKey = key.split('.').pop().replace(/([A-Z])/g, ' $1').trim(); specsHtml += `${displayKey.charAt(0).toUpperCase() + displayKey.slice(1)}: ${value}
`; } } specsHtml += '
'; } // Get product details const title = product.title || product.description || product.DESCRIPTION || prodCode; const category = product.category || product.CATEGORY || 'N/A'; const description = product.detailedDescription || product.description || product.DESCRIPTION || 'No description available'; const features = product.features || []; // Get product image configuration const productImage = product.image || `https://via.placeholder.com/400x400/e0e0e0/666?text=${encodeURIComponent(prodCode)}`; const hasLayeredImages = product.imageConfig && product.imageConfig.layered === true; const hasFlatImages = product.flatImages === true || product.imagePattern; const useDynamicAPI = product.useDynamicAPI === true; // Get available materials and colors from product data const availableMaterials = product.materials || []; const availableColors = product.colors || []; const availableSizes = product.sizes || []; // Sort materials alphabetically const sortedMaterials = [...availableMaterials].sort((a, b) => a.localeCompare(b)); // Sort colors alphabetically, but put White at the bottom const sortedColors = [...availableColors].sort((a, b) => { if (a === 'White') return 1; if (b === 'White') return -1; return a.localeCompare(b); }); // Get user's quiz answers to pre-select dropdowns // Check multiple possible keys for material and color let selectedMaterial = null; let selectedColor = null; for (const key in userAnswers) { if (key.includes('material') && userAnswers[key]) { selectedMaterial = userAnswers[key].toLowerCase(); } if (key.includes('color') && userAnswers[key]) { selectedColor = userAnswers[key].toLowerCase(); } } // Build material options let materialOptionsHtml = ''; if (sortedMaterials.length === 0) { materialOptionsHtml = ''; } else if (sortedMaterials.length === 1) { materialOptionsHtml = ``; } else { materialOptionsHtml = ''; sortedMaterials.forEach(material => { const materialLower = material.toLowerCase(); const selected = (selectedMaterial && materialLower === selectedMaterial) ? 'selected' : ''; materialOptionsHtml += ``; }); } // Build color options let colorOptionsHtml = ''; if (sortedColors.length === 0) { colorOptionsHtml = ''; } else if (sortedColors.length === 1) { colorOptionsHtml = ``; } else { colorOptionsHtml = ''; sortedColors.forEach(color => { const colorLower = color.toLowerCase(); const selected = (selectedColor && colorLower === selectedColor) ? 'selected' : ''; colorOptionsHtml += ``; }); } // Get dimensions from quiz to pre-select size const quizWidth = userAnswers['q-dimensions.width']; const quizHeight = userAnswers['q-dimensions.height']; const selectedSize = (quizWidth && quizHeight) ? `${quizWidth}x${quizHeight}` : null; // Build size options let sizeOptionsHtml = ''; if (availableSizes.length > 0) { availableSizes.forEach(size => { if (typeof size === 'object' && size.value && size.label) { const selected = (selectedSize && size.value === selectedSize) ? 'selected' : ''; sizeOptionsHtml += ``; } else if (typeof size === 'string') { const selected = (selectedSize && size === selectedSize) ? 'selected' : ''; sizeOptionsHtml += ``; } }); } else { // Default generic sizes if none specified sizeOptionsHtml += ``; sizeOptionsHtml += ``; sizeOptionsHtml += ``; } sizeOptionsHtml += ''; const contentDiv = document.getElementById('content'); // Build the image display (layered, flat, or dynamic API) let imageDisplayHtml = ''; if (hasLayeredImages && !hasFlatImages && !useDynamicAPI) { // Use layered image system const basePath = product.imageConfig.basePath || ''; const layers = product.imageConfig.layers || {}; imageDisplayHtml = `
${layers.base ? `Base` : ''} ${layers.hardware ? `Hardware` : ''}
`; } else { // Use flat image (for pre-rendered images or dynamic API) // Determine initial image based on user's quiz answer, or default/first color let initialImage = productImage; if (hasFlatImages && sortedColors.length > 0) { // Priority: 1) User's quiz selection, 2) Single color auto-select, 3) Default to 'white' let defaultColor; if (selectedColor && sortedColors.some(c => c.toLowerCase() === selectedColor)) { defaultColor = selectedColor; } else if (sortedColors.length === 1) { defaultColor = sortedColors[0].toLowerCase(); } else { defaultColor = 'white'; } // Determine handle color based on door color let handleColor = product.handleColor || 'silver'; if (prodCode.toUpperCase() === 'COBRAI') { const handleMap = { 'black': 'black', 'white': 'white', 'bronze': 'bronze', 'sandstone': 'bronze' }; handleColor = handleMap[defaultColor] || defaultColor; } initialImage = buildFlatImagePath(product, defaultColor, handleColor); } imageDisplayHtml = ` ${title} `; } let html = `
${title}
${imageDisplayHtml}

Configure Your Product

Product Code: ${prodCode}
Category: ${category}

${specsHtml} ${description}

`; // Add features if available if (features.length > 0) { html += ` Key Features:
    ${features.map(f => `
  • ${f}
  • `).join('')}
`; } if (compatibleAccessories.length > 0) { html += `

Compatible Accessories

`; compatibleAccessories.forEach(acc => { html += `
${acc.description}
Code: ${acc.accessoryCode} | ${acc.materials ? acc.materials.join(', ') : 'N/A'}
`; }); html += `
`; } html += `
`; contentDiv.innerHTML = html; updateBreadcrumb(); // Initialize images based on type if (hasLayeredImages && !hasFlatImages && !useDynamicAPI) { updateProductPreview(prodCode); } else if (hasFlatImages || useDynamicAPI) { // Flat images or dynamic API - will update on dropdown change // Initialize with first color if flat images if (hasFlatImages && sortedColors.length > 0) { updateFlatImagePreview(prodCode); } } } // Update product preview based on configuration function updateProductPreview(productCode) { console.log('updateProductPreview called with:', productCode); const product = productData.find(p => p.productCode === productCode || p.id === productCode); if (!product) return; // Check which system to use const hasFlatImages = product.flatImages === true || product.imagePattern; const useDynamicAPI = product.useDynamicAPI === true; console.log('Product flags - hasFlatImages:', hasFlatImages, 'useDynamicAPI:', useDynamicAPI); if (hasFlatImages) { // Use pre-rendered flat images updateFlatImagePreview(productCode); return; } if (useDynamicAPI) { // Use dynamic image generation API updateDynamicImagePreview(productCode); return; } // Otherwise use layered image system if (!product.imageConfig || !product.imageConfig.layered) return; const basePath = product.imageConfig.basePath || ''; const layers = product.imageConfig.layers || {}; // Get selected values const selectedColor = document.getElementById('config-color')?.value || ''; const selectedSize = document.getElementById('config-size')?.value || ''; const doorLayer = document.getElementById('door-layer'); const overlayLayer = document.getElementById('overlay-layer'); const notice = document.getElementById('config-notice'); let hasPreview = true; // Update door color layer if (doorLayer && layers.door) { if (selectedColor && layers.door[selectedColor]) { doorLayer.src = basePath + layers.door[selectedColor]; doorLayer.style.display = 'block'; doorLayer.onerror = function() { this.style.display = 'none'; showConfigNotice(true); }; } else if (selectedColor) { doorLayer.style.display = 'none'; hasPreview = false; } } // Update overlay layer (if configuration dependent) if (overlayLayer && layers.overlay) { // You can add logic here based on door type, view, etc. // For now, keep it hidden unless specifically needed overlayLayer.style.display = 'none'; } // Show/hide notice showConfigNotice(!hasPreview && selectedColor); } // Build flat image path based on product configuration and selections function buildFlatImagePath(product, doorColor, handleColor = 'silver') { const prodCode = product.productCode || product.id; // Option 1: Use custom image pattern if defined if (product.imagePattern) { return product.imagePattern .replace('{productCode}', prodCode) .replace('{doorColor}', doorColor) .replace('{handleColor}', handleColor); } // Option 2: Use custom image map if defined if (product.imageMap && product.imageMap[doorColor]) { return product.imageMap[doorColor]; } // Option 3: Default naming convention: productCode-doorColor-handleColor // Use relative path (Flask serves from app/images/) return `images/${prodCode.toLowerCase()}-${doorColor.toLowerCase()}-${handleColor.toLowerCase()}.png`; } // Update preview using pre-rendered flat images function updateFlatImagePreview(productCode) { const product = productData.find(p => p.productCode === productCode || p.id === productCode); if (!product) return; const img = document.getElementById('product-detail-image'); if (!img) return; // Get selected configuration const doorColor = document.getElementById('config-color')?.value || 'white'; const material = document.getElementById('config-material')?.value || 'aluminum'; console.log('updateFlatImagePreview - productCode:', productCode, 'doorColor:', doorColor); // Determine handle color based on door color (matching hardware to door) let handleColor = product.handleColor || 'silver'; // For cobrai, map door colors to their matching handle colors if (productCode.toUpperCase() === 'COBRAI') { const handleMap = { 'black': 'black', 'white': 'white', 'bronze': 'bronze', 'sandstone': 'bronze' }; handleColor = handleMap[doorColor.toLowerCase()] || doorColor; } // Build image path const imagePath = buildFlatImagePath(product, doorColor, handleColor); console.log('Image path:', imagePath); // Update image source (with fallback to original product image) const fallbackImage = product.image || `https://via.placeholder.com/400x400/e0e0e0/666?text=${encodeURIComponent(productCode)}`; img.onerror = function() { console.log('Image failed to load:', imagePath); this.onerror = null; // Prevent infinite loop this.src = fallbackImage; }; img.src = imagePath; console.log('Image src set to:', img.src); } // Update preview using dynamic image API function updateDynamicImagePreview(productCode) { const img = document.getElementById('product-detail-image'); if (!img) return; // Get selected configuration const color = document.getElementById('config-color')?.value || 'white'; const hinge = document.querySelector('[name="hinge-location"]:checked')?.value || 'right'; const material = document.getElementById('config-material')?.value || 'aluminum'; // Build API URL const apiUrl = `/api/product-image/${productCode}?color=${color}&hinge=${hinge}&material=${material}`; // Update image source img.src = apiUrl; } // Show or hide configuration notice function showConfigNotice(show) { const notice = document.getElementById('config-notice'); if (notice) { notice.style.display = show ? 'block' : 'none'; } } // Update hinge location (handles both flat and layered images) function updateHingeLocation(flipLeft) { // Check if current product uses dynamic API or flat images const urlParams = new URLSearchParams(window.location.search); const productCode = urlParams.get('p'); if (productCode) { const product = productData.find(p => p.productCode === productCode || p.id === productCode); if (product) { // For dynamic API, regenerate image with new hinge if (product.useDynamicAPI === true) { updateDynamicImagePreview(productCode); return; } // For flat images, just flip the image visually if (product.flatImages === true || product.imagePattern) { const flatImg = document.getElementById('product-detail-image'); if (flatImg) { flatImg.style.transform = flipLeft ? 'scaleX(-1)' : 'scaleX(1)'; } return; } } } // Try flat image element const flatImg = document.getElementById('product-detail-image'); if (flatImg) { flatImg.style.transform = flipLeft ? 'scaleX(-1)' : 'scaleX(1)'; return; } // Try layered images const hardwareLayer = document.getElementById('hardware-layer'); const doorLayer = document.getElementById('door-layer'); if (hardwareLayer) { hardwareLayer.style.transform = flipLeft ? 'scaleX(-1)' : 'scaleX(1)'; } if (doorLayer) { doorLayer.style.transform = flipLeft ? 'scaleX(-1)' : 'scaleX(1)'; } } // Flip product image based on hinge location (deprecated - use updateHingeLocation) function flipProductImage(flip) { updateHingeLocation(flip); } // Handle size dropdown change to show/hide custom size fields function handleSizeChange(productCode) { const sizeSelect = document.getElementById('config-size'); const customFields = document.getElementById('custom-size-fields'); if (sizeSelect && customFields) { if (sizeSelect.value === 'custom') { customFields.style.display = 'block'; } else { customFields.style.display = 'none'; } } // Update preview if not custom (custom doesn't affect image) if (sizeSelect && sizeSelect.value !== 'custom') { updateProductPreview(productCode); } } // Go back to results from product detail function goBackToResults() { // Find results in history or load it directly const resultsIndex = history.indexOf('results'); if (resultsIndex >= 0) { history = history.slice(0, resultsIndex + 1); } else { // If not in history, add it history.push('results'); } // Update URL to results state updateURL('results'); showFilteredProducts(); } // Share current page function shareCurrentPage() { const url = window.location.href; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(url).then(() => { showNotification('Link copied to clipboard!'); }).catch(() => { prompt('Copy this URL to share:', url); }); } else { prompt('Copy this URL to share:', url); } } // Show filtered products based on user selections function showFilteredProducts() { const contentDiv = document.getElementById('content'); // Get filter criteria from userAnswers let baseType = userAnswers['start']; let subType = userAnswers['q-door-type'] || userAnswers['q-window-type']; let material = null; let color = null; // Extract material from any material questions for (const key in userAnswers) { if (key.includes('material') && userAnswers[key]) { material = userAnswers[key]; } if (key.includes('color') && userAnswers[key]) { color = userAnswers[key]; } } // Get dimensions const width = userAnswers['q-dimensions.width']; const height = userAnswers['q-dimensions.height']; console.log('Filtering with:', { baseType, subType, material, color, width, height }); // Filter products let filtered = productData.filter(product => { // Check base type if (baseType && product.baseType !== baseType) return false; // Check sub-type if (subType) { const prodSubType = product.subType?.door || product.subType?.window; if (prodSubType !== subType) return false; } // Check material if (material && !product.materials.includes(material)) return false; // Check color if (color && !product.colors.includes(color)) return false; return true; }); console.log('Filtered products:', filtered.length); // Display results if (filtered.length === 0) { contentDiv.innerHTML = `
No Products Found

We couldn't find any products matching your criteria:

Please try different options or contact us for assistance.

`; return; } // Display product grid let html = `
Found ${filtered.length} Product${filtered.length > 1 ? 's' : ''}

Your selections: ${baseType || ''} ${subType ? '› ' + subType : ''} ${material ? '› ' + material : ''} ${color ? '› ' + color : ''} ${width && height ? `› ${width}" × ${height}"` : ''}

`; filtered.forEach(product => { const prodCode = product.productCode || product.id; const desc = product.description || product.DESCRIPTION || 'No description'; const materials = product.materials ? product.materials.join(', ') : 'N/A'; const colors = product.colors ? product.colors.join(', ') : 'N/A'; html += `

${desc}

${prodCode}

Materials: ${materials}
Colors: ${colors}
`; }); html += `
`; contentDiv.innerHTML = html; updateBreadcrumb(); } // Show notification function showNotification(message) { const notification = document.createElement('div'); notification.className = 'notification'; notification.textContent = message; notification.style.cssText = ` position: fixed; top: 20px; right: 20px; background: #4CAF50; color: white; padding: 15px 20px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); z-index: 10000; animation: slideIn 0.3s ease-out; `; document.body.appendChild(notification); setTimeout(() => { notification.style.animation = 'slideOut 0.3s ease-in'; setTimeout(() => notification.remove(), 300); }, 3000); } // Handle browser back/forward buttons window.addEventListener('popstate', function(event) { if (event.state) { if (event.state.productCode) { showProductByCode(event.state.productCode); } else if (event.state.questionKey) { accumulatedBitValue = event.state.bitValue || 0; loadContent(event.state.questionKey); } else { startOver(); } } else { // No state, check URL initFromURL(); } }); // Update breadcrumb trail function updateBreadcrumb() { const breadcrumb = document.getElementById('breadcrumb'); const trail = history.map((key, index) => { const data = questionData[key]; let label = 'Start'; if (key === 'results') { label = 'Results'; } else if (key !== 'start') { if (key.startsWith('product-')) { // Product detail page const productCode = key.replace('product-', ''); label = productCode; } else if (data && data.type === 'product') { label = data.code; } else if (data && data.title) { // Extract meaningful part from question title let title = data.title; // Remove common question prefixes and clean up title = title.replace(/^What (are you looking for|type of|is your|do you)[\s?]*/i, ''); title = title.replace(/^(Select|Choose|Enter)[\s]*/i, ''); title = title.replace(/\?$/, ''); // Capitalize if needed if (title.length > 0) { title = title.charAt(0).toUpperCase() + title.slice(1); } label = title || 'Question ' + index; } else { label = `Question ${index}`; } } if (index < history.length - 1) { return `${label}`; } else { return `${label}`; } }); breadcrumb.innerHTML = trail.join(' '); } // Navigate to a specific point in history function navigateToHistory(index) { history = history.slice(0, index + 1); loadContent(history[history.length - 1]); } // Start the quiz when page loads window.addEventListener('DOMContentLoaded', init); // ===== SEARCH BAR FUNCTIONALITY ===== // Search state variables let searchMatches = []; let selectedSearchIndex = -1; // Toggle search bar visibility based on current screen function toggleSearchBar() { const searchContainer = document.getElementById('searchContainer'); const currentKey = history.length > 0 ? history[history.length - 1] : 'start'; if (currentKey === 'start') { searchContainer.style.display = 'block'; } else { searchContainer.style.display = 'none'; // Clear search when hiding clearSearch(); } } // Clear search state function clearSearch() { const searchInput = document.getElementById('searchInput'); const searchDropdown = document.getElementById('searchDropdown'); if (searchInput) searchInput.value = ''; if (searchDropdown) { searchDropdown.style.display = 'none'; searchDropdown.innerHTML = ''; } searchMatches = []; selectedSearchIndex = -1; } // Helper function to normalize text for searching function normalizeText(text) { if (!text) return ''; return text.toString().toLowerCase() .replace(/[-\s_]/g, '') // Remove hyphens, spaces, underscores .replace(/[^a-z0-9]/g, ''); // Remove other special characters } // Helper function to check if a value matches a search term function matchesTerm(value, term) { if (!value) return false; // For arrays (like colors, materials) if (Array.isArray(value)) { return value.some(item => normalizeText(item).includes(term)); } // For objects (like subType) if (typeof value === 'object' && value !== null) { return Object.values(value).some(v => v && normalizeText(v).includes(term) ); } // For strings and numbers return normalizeText(value).includes(term); } // Filter products by search term function filterProductsBySearch(searchTerm) { if (!searchTerm || searchTerm.trim().length < 2) { return []; } // Split search query into individual terms const searchTerms = searchTerm.toLowerCase() .split(/\s+/) .filter(term => term.length > 0) .map(term => normalizeText(term)); // Filter products that match ALL search terms return productData.filter(product => { // Each product must match ALL search terms return searchTerms.every(term => { // Check if the term matches any of these fields return ( matchesTerm(product.productCode, term) || matchesTerm(product.id, term) || matchesTerm(product.description, term) || matchesTerm(product.category, term) || matchesTerm(product.baseType, term) || matchesTerm(product.subType, term) || matchesTerm(product.materials, term) || matchesTerm(product.colors, term) || matchesTerm(product.location, term) ); }); }); } // Update search dropdown with matches function updateSearchDropdown(matches) { const dropdown = document.getElementById('searchDropdown'); if (!matches || matches.length === 0) { dropdown.style.display = 'none'; dropdown.innerHTML = ''; selectedSearchIndex = -1; return; } // Show only first 10 matches const displayMatches = matches.slice(0, 10); dropdown.innerHTML = displayMatches.map((product, index) => { const prodCode = product.productCode || product.id; const desc = product.description || 'No description'; const isSelected = index === selectedSearchIndex ? 'selected' : ''; return `
${desc}
${prodCode}
`; }).join(''); if (matches.length > 10) { dropdown.innerHTML += `
+ ${matches.length - 10} more results... (click to view all)
`; } dropdown.style.display = 'block'; } // Select a search item function selectSearchItem(index, closeDropdown = true) { selectedSearchIndex = index; const searchInput = document.getElementById('searchInput'); const searchDropdown = document.getElementById('searchDropdown'); // Update search input with selected product description if (searchMatches[index]) { const selectedProduct = searchMatches[index]; const desc = selectedProduct.description || selectedProduct.DESCRIPTION || 'No description'; searchInput.value = desc; } // Hide dropdown after selection (unless using keyboard navigation) if (closeDropdown) { searchDropdown.style.display = 'none'; } // Update visual selection const items = document.querySelectorAll('.search-dropdown-item'); items.forEach((item, i) => { if (i === index) { item.classList.add('selected'); } else { item.classList.remove('selected'); } }); } // Handle View button click (kept for backward compatibility) function handleViewProduct() { if (selectedSearchIndex >= 0 && selectedSearchIndex < searchMatches.length) { const product = searchMatches[selectedSearchIndex]; const prodCode = product.productCode || product.id; // Clear search and hide dropdown clearSearch(); // Navigate to product showProductByCode(prodCode); } } // Handle Search/Go button click with smart behavior function handleSearchGo() { const searchInput = document.getElementById('searchInput'); const searchTerm = searchInput.value.trim(); // If an item is selected from dropdown, go directly to that product if (selectedSearchIndex >= 0 && selectedSearchIndex < searchMatches.length) { handleViewProduct(); return; } // If search term is empty or too short, go to advanced search if (!searchTerm || searchTerm.length < 2) { handleAdvancedSearch(); return; } // Get matches for the search term const matches = filterProductsBySearch(searchTerm); // If exactly one match, go directly to that product if (matches.length === 1) { const product = matches[0]; const prodCode = product.productCode || product.id; clearSearch(); showProductByCode(prodCode); return; } // Otherwise, go to advanced search with the search term handleAdvancedSearch(); } // Handle Search button click (show results grid) - kept for "+ more" functionality function handleSearchResults() { const searchInput = document.getElementById('searchInput'); const searchTerm = searchInput.value.trim(); // If no search term, go to advanced search if (!searchTerm || searchTerm.length < 2) { handleAdvancedSearch(); return; } // Navigate to advanced search with search term handleAdvancedSearch(); } // Show search results in a grid (kept for potential future use) function showSearchResultsGrid(matches, searchTerm) { const contentDiv = document.getElementById('content'); let html = `
Found ${matches.length} Product${matches.length > 1 ? 's' : ''}

Search results for: "${searchTerm}"

`; matches.forEach(product => { const prodCode = product.productCode || product.id; const desc = product.description || product.DESCRIPTION || 'No description'; const materials = product.materials ? product.materials.join(', ') : 'N/A'; const colors = product.colors ? product.colors.join(', ') : 'N/A'; const baseType = product.baseType || 'N/A'; const subType = product.subType?.window || product.subType?.door || 'N/A'; html += `

${desc}

${prodCode}

Type: ${baseType}${subType !== 'N/A' ? ' - ' + subType : ''}
Materials: ${materials}
Colors: ${colors}
`; }); html += `
`; contentDiv.innerHTML = html; // Update history to track search results history.push('search-results'); updateBreadcrumb(); } // Handle Advanced Search button click function handleAdvancedSearch() { // Get search text from input field const searchInput = document.getElementById('searchInput'); const searchText = searchInput ? searchInput.value.trim() : ''; // Navigate to advanced search page with search query parameter if (searchText) { window.location.href = '/quiz/advanced-search?q=' + encodeURIComponent(searchText); } else { window.location.href = '/quiz/advanced-search'; } } // Set up search input event listeners function initSearchBar() { const searchInput = document.getElementById('searchInput'); const searchDropdown = document.getElementById('searchDropdown'); if (!searchInput) return; // Input event for real-time filtering searchInput.addEventListener('input', function(e) { const searchTerm = e.target.value.trim(); if (searchTerm.length < 2) { searchMatches = []; updateSearchDropdown([]); return; } searchMatches = filterProductsBySearch(searchTerm); updateSearchDropdown(searchMatches); // Reset selection selectedSearchIndex = -1; }); // Keyboard navigation (arrow keys and enter) searchInput.addEventListener('keydown', function(e) { if (searchMatches.length === 0) return; if (e.key === 'ArrowDown') { e.preventDefault(); selectedSearchIndex = Math.min(selectedSearchIndex + 1, searchMatches.length - 1); updateSearchDropdown(searchMatches); selectSearchItem(selectedSearchIndex, false); // Keep dropdown open } else if (e.key === 'ArrowUp') { e.preventDefault(); selectedSearchIndex = Math.max(selectedSearchIndex - 1, -1); if (selectedSearchIndex >= 0) { updateSearchDropdown(searchMatches); selectSearchItem(selectedSearchIndex, false); // Keep dropdown open } } else if (e.key === 'Enter') { e.preventDefault(); handleSearchGo(); } else if (e.key === 'Escape') { clearSearch(); } }); // Click outside to close dropdown document.addEventListener('click', function(e) { if (!searchInput.contains(e.target) && !searchDropdown.contains(e.target)) { searchDropdown.style.display = 'none'; } }); } // Override loadContent to toggle search bar const originalLoadContent = loadContent; loadContent = function(key) { originalLoadContent(key); toggleSearchBar(); }; // Override startOver to show search bar const originalStartOver = startOver; startOver = function() { originalStartOver(); toggleSearchBar(); }; // Initialize search bar when page loads window.addEventListener('DOMContentLoaded', function() { initSearchBar(); toggleSearchBar(); });