Files
CGW-Quote-Builder/app/static/js/script.js
T

2097 lines
78 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 = {};
// In-page order list for the current browser session
let orderItems = [];
// 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 = `
<div class="result-container">
<div class="result-title">Error Loading Application</div>
<div class="result-details">
Failed to load data files. Please refresh the page or contact support.<br>
<small>Error: ${error}</small>
</div>
</div>
`;
});
}
// 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 0: Direct product from Flask route (e.g., /quiz/product/404)
if (window.INITIAL_PRODUCT && window.INITIAL_PRODUCT !== '') {
showProductByCode(window.INITIAL_PRODUCT);
return;
}
// Priority 1: Direct product view from query param
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 `
<div class="notes-section">
<button class="notes-toggle" onclick="toggleNotes('${notesId}')" type="button">
<span class="notes-icon">️</span>
<span class="notes-label">Additional Information</span>
<span class="notes-arrow" id="${notesId}-arrow">▼</span>
</button>
<div class="notes-content" id="${notesId}" style="display: none;">
${notes}
</div>
</div>
`;
}
// 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 = `
<div class="question-container">
<div class="question-title">${data.title}</div>
<div class="question-subtitle">${data.subtitle}</div>
</div>
`;
html += `
<div class="buttons-wrapper">
<div class="buttons-container">
`;
data.answers.forEach((answer, index) => {
html += `
<button class="answer-button" onclick="handleAnswerByIndex('${currentKey}', ${index})">
<div class="answer-image">${answer.image}</div>
<div class="answer-caption">${answer.caption}</div>
</button>
`;
});
html += `
</div>
</div>
`;
// 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 = `
<div class="question-container">
<div class="question-title">${data.title}</div>
<div class="question-subtitle">${data.subtitle}</div>
</div>
`;
// Render measurement type toggle buttons if configuration exists
if (data.measurementType && data.measurementType.options) {
const currentMeasurementType = userAnswers[`${currentKey}.measurementType`] || data.measurementType.defaultValue;
html += `<div class="measurement-type-container">`;
data.measurementType.options.forEach(option => {
const isSelected = currentMeasurementType === option.value;
const disabledClass = option.disabled ? 'disabled' : '';
const disabledAttr = option.disabled ? 'disabled' : '';
html += `
<button type="button"
class="measurement-toggle ${isSelected ? 'selected' : ''} ${disabledClass}"
data-value="${option.value}"
${disabledAttr}
onclick="handleMeasurementTypeToggle('${option.value}', '${currentKey}')">
<span class="toggle-icon">📏</span>
<span class="toggle-label">${option.label}</span>
</button>
`;
});
html += `</div>`;
}
html += `
<div class="form-wrapper">
<form id="dynamicForm" class="dynamic-form" onsubmit="handleFormSubmit(event, '${currentKey}')">
`;
data.fields.forEach((field, index) => {
html += `<div class="form-group">`;
html += `<label for="${field.name}">${field.label}${field.required ? ' *' : ''}</label>`;
if (field.type === 'select') {
html += `<select id="${field.name}" name="${field.name}" ${field.required ? 'required' : ''}>`;
field.options.forEach(option => {
const selected = userAnswers[`${currentKey}.${field.name}`] === option.value ? 'selected' : '';
html += `<option value="${option.value}" ${selected}>${option.label}</option>`;
});
html += `</select>`;
} else if (field.type === 'textarea') {
const value = userAnswers[`${currentKey}.${field.name}`] || '';
html += `<textarea id="${field.name}" name="${field.name}"
placeholder="${field.placeholder || ''}"
${field.required ? 'required' : ''}>${value}</textarea>`;
} else {
const value = userAnswers[`${currentKey}.${field.name}`] || '';
html += `<input type="${field.type}"
id="${field.name}"
name="${field.name}"
placeholder="${field.placeholder || ''}"
value="${value}"
${field.required ? 'required' : ''}>`;
}
html += `</div>`;
});
html += `
<div class="form-group">
<button type="submit" class="submit-button">Continue →</button>
</div>
</form>
</div>
`;
// 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 = '<strong>Your Specifications:</strong><br>';
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}<br>`;
}
}
specsHtml += '<br>';
}
// 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 = `
<div class="result-container">
<div class="result-title">${data.title}</div>
<div class="result-content">
<div class="result-image-section">
<img src="${productImage}" alt="${data.title}" class="product-image" onerror="this.src='https://via.placeholder.com/400x400/e0e0e0/666?text=No+Image'">
</div>
<div class="result-details">
<strong>Product Code:</strong> ${data.code}<br>
<strong>Category:</strong> ${data.category}<br><br>
${specsHtml}
${data.description}<br><br>
<strong>Key Features:</strong>
<ul style="list-style-position: inside; text-align: left;">
${data.features.map(f => `<li>${f}</li>`).join('')}
</ul>
</div>
</div>
<div class="result-actions">
<button class="back-button" onclick="goBack()">← Go Back</button>
<button class="back-button" onclick="startOver()" style="margin-left: 10px;">Start Over</button>
</div>
</div>
`;
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 order list
orderItems = [];
// 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 = `
<div class="result-container">
<div class="result-title">Products Not Loaded</div>
<div class="result-details">
Product data not available yet. Please generate JSON files first.
</div>
<button class="back-button" onclick="startOver()">Start Over</button>
</div>
`;
return;
}
const product = productData.find(p => p.productCode === productCode || p.id === productCode);
if (!product) {
document.getElementById('content').innerHTML = `
<div class="result-container">
<div class="result-title">Product Not Found</div>
<div class="result-details">
Product ${productCode} not found.
</div>
<button class="back-button" onclick="startOver()">Start Over</button>
</div>
`;
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 = '<strong>Your Specifications:</strong><br>';
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}<br>`;
}
}
specsHtml += '<br>';
}
// 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 useCanvasAPI = product.imageConfig && product.imageConfig.useCanvasAPI === true;
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 = '<option value="">N/A</option>';
} else if (sortedMaterials.length === 1) {
materialOptionsHtml = `<option value="${sortedMaterials[0].toLowerCase()}" selected>${sortedMaterials[0]}</option>`;
} else {
materialOptionsHtml = '<option value="">Select Material</option>';
sortedMaterials.forEach(material => {
const materialLower = material.toLowerCase();
const selected = (selectedMaterial && materialLower === selectedMaterial) ? 'selected' : '';
materialOptionsHtml += `<option value="${materialLower}" ${selected}>${material}</option>`;
});
}
// Build color options
let colorOptionsHtml = '';
if (sortedColors.length === 0) {
colorOptionsHtml = '<option value="">N/A</option>';
} else if (sortedColors.length === 1) {
colorOptionsHtml = `<option value="${sortedColors[0].toLowerCase()}" selected>${sortedColors[0]}</option>`;
} else {
colorOptionsHtml = '<option value="">Select Color</option>';
sortedColors.forEach(color => {
const colorLower = color.toLowerCase();
const selected = (selectedColor && colorLower === selectedColor) ? 'selected' : '';
colorOptionsHtml += `<option value="${colorLower}" ${selected}>${color}</option>`;
});
}
// 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 = '<option value="">Select Size</option>';
if (availableSizes.length > 0) {
availableSizes.forEach(size => {
if (typeof size === 'object' && size.value && size.label) {
const selected = (selectedSize && size.value === selectedSize) ? 'selected' : '';
sizeOptionsHtml += `<option value="${size.value}" ${selected}>${size.label}</option>`;
} else if (typeof size === 'string') {
const selected = (selectedSize && size === selectedSize) ? 'selected' : '';
sizeOptionsHtml += `<option value="${size}" ${selected}>${size}</option>`;
}
});
} else {
// Default generic sizes if none specified
sizeOptionsHtml += `<option value="30x80" ${selectedSize === '30x80' ? 'selected' : ''}>30" x 80"</option>`;
sizeOptionsHtml += `<option value="32x80" ${selectedSize === '32x80' ? 'selected' : ''}>32" x 80"</option>`;
sizeOptionsHtml += `<option value="36x80" ${selectedSize === '36x80' ? 'selected' : ''}>36" x 80"</option>`;
}
sizeOptionsHtml += '<option value="custom">Custom Size</option>';
const contentDiv = document.getElementById('content');
// Build the image display (Canvas API, layered, flat, or dynamic API)
let imageDisplayHtml = '';
if (useCanvasAPI || (hasLayeredImages && !hasFlatImages && !useDynamicAPI)) {
// Use layered image system (Canvas API or static paths)
const basePath = product.imageConfig?.basePath || '';
const layers = product.imageConfig?.layers || {};
imageDisplayHtml = `
<div class="door-configurator" id="door-configurator">
<img src="" class="layer-base" alt="Base">
<img src="" class="layer-door" id="door-layer" alt="Door" style="display:none;">
<img src="" class="layer-hardware" id="hardware-layer" alt="Hardware" style="display:none;">
<img src="" class="layer-overlay" id="overlay-layer" alt="View" style="display:none;">
<img src="" class="layer-foreground" alt="Foreground" style="display:none;">
<div class="config-preview-notice" id="config-notice" style="display:none;">
⚠️ Preview not available for this configuration
</div>
</div>
`;
} 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 = `
<img id="product-detail-image"
src="${initialImage}"
alt="${title}"
class="product-image"
onerror="this.src='${productImage}'">
`;
}
let html = `
<div class="result-container">
<div class="result-title">${title}</div>
<div class="result-content">
<div class="result-image-section">
${imageDisplayHtml}
<div class="product-config-form">
<h3>Configure Your Product</h3>
<div class="config-field">
<label for="config-material">Material:</label>
<select id="config-material" class="config-select" ${availableMaterials.length <= 1 ? 'disabled' : ''} onchange="updateProductPreview('${prodCode}')">
${materialOptionsHtml}
</select>
</div>
<div class="config-field">
<label for="config-color">Color:</label>
<select id="config-color" class="config-select" ${availableColors.length <= 1 ? 'disabled' : ''} onchange="updateProductPreview('${prodCode}')">
${colorOptionsHtml}
</select>
</div>
<div class="config-field">
<label for="config-size">Size:</label>
<select id="config-size" class="config-select" onchange="handleSizeChange('${prodCode}')">
${sizeOptionsHtml}
</select>
</div>
<div id="custom-size-fields" class="config-field" style="display: none; margin-left: 20px;">
<div style="display: flex; gap: 10px; align-items: center;">
<div style="flex: 1;">
<label for="custom-width">Width (inches):</label>
<input type="number" id="custom-width" class="config-input" placeholder="e.g., 36" min="1" max="120">
</div>
<div style="flex: 1;">
<label for="custom-height">Height (inches):</label>
<input type="number" id="custom-height" class="config-input" placeholder="e.g., 80" min="1" max="120">
</div>
</div>
</div>
<div class="config-field">
<label>Hinge Location:</label>
<div class="hinge-options">
<label class="radio-label">
<input type="radio" name="hinge-location" value="right" onchange="updateHingeLocation(true)">
<span>Right Hinge Door</span>
</label>
<label class="radio-label">
<input type="radio" name="hinge-location" value="left" checked onchange="updateHingeLocation(false)">
<span>Left Hinge Door</span>
</label>
</div>
</div>
<div class="config-field" style="margin-top: 20px;">
<button class="back-button" onclick="addToOrder('${prodCode}')" style="width: 100%; padding: 12px; font-size: 16px;">Add to Order</button>
</div>
<div class="config-field" style="margin-top: 12px;">
<div style="font-weight: 600; margin-bottom: 8px;">Current Order</div>
<div id="order-items-list" style="background: #f8f9fb; border: 1px solid #e3e5ea; border-radius: 8px; padding: 10px;"></div>
<button class="back-button" onclick="submitQuoteOrder()" style="width: 100%; margin-top: 10px; padding: 12px; font-size: 16px;">Quote</button>
</div>
</div>
</div>
<div class="result-details">
<strong>Product Code:</strong> ${prodCode}<br>
<strong>Category:</strong> ${category}<br><br>
${specsHtml}
${description}<br><br>
`;
// Add features if available
if (features.length > 0) {
html += `
<strong>Key Features:</strong>
<ul style="list-style-position: inside; text-align: left;">
${features.map(f => `<li>${f}</li>`).join('')}
</ul>
`;
}
if (compatibleAccessories.length > 0) {
html += `
<h3 style="margin-top: 20px;">Compatible Accessories</h3>
<div class="accessories-list">
`;
compatibleAccessories.forEach(acc => {
html += `
<div class="accessory-item">
<strong>${acc.description}</strong><br>
<small>Code: ${acc.accessoryCode} | ${acc.materials ? acc.materials.join(', ') : 'N/A'}</small>
</div>
`;
});
html += `</div>`;
}
html += `
</div>
</div>
<div class="result-actions" style="justify-content: center;">
<button class="back-button" onclick="goBackToResults()">← Go Back</button>
<button class="back-button" onclick="startOver()">Start Over</button>
<button class="back-button" onclick="shareCurrentPage()">🔗 Share</button>
</div>
</div>
`;
contentDiv.innerHTML = html;
updateBreadcrumb();
renderOrderList();
// Initialize images based on type
if (useCanvasAPI) {
// Use Canvas API with hierarchical fallback
updateCanvasAPIPreview(prodCode);
} else if (hasLayeredImages && !hasFlatImages && !useDynamicAPI) {
// Use static layered images
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);
}
}
}
// ============================================================================
// Canvas API Functions - Hierarchical Image Fallback System
// ============================================================================
/**
* Build Canvas API URL for a specific layer
* @param {string} productCode - Product code
* @param {string} layer - Layer name (base, door, hardware, overlay, foreground)
* @param {string} color - Optional color
* @param {string} material - Optional material
* @returns {string} Canvas API URL
*/
function buildCanvasAPIUrl(productCode, layer, color = null, material = null) {
let url = `${BASE_URL}/api/canvas/${productCode}/${layer}`;
const params = new URLSearchParams();
if (color) params.append('color', color.toLowerCase());
if (material) params.append('material', material.toLowerCase());
const queryString = params.toString();
return queryString ? `${url}?${queryString}` : url;
}
/**
* Update layered image preview using Canvas API
* @param {string} productCode - Product code
*/
function updateCanvasAPIPreview(productCode) {
const product = productData.find(p => p.productCode === productCode || p.id === productCode);
if (!product) return;
// Get selected values
const selectedColor = document.getElementById('config-color')?.value || '';
const selectedMaterial = document.getElementById('config-material')?.value || '';
// Get layer transform configuration (for flipping, etc.)
const layerTransforms = product.imageConfig?.layerTransforms || {};
// Helper function to apply transform classes
function applyTransform(element, layerName) {
if (!element) return;
// Remove any existing transform classes
element.classList.remove('layer-flip-horizontal', 'layer-flip-vertical', 'layer-flip-both');
// Apply new transform if configured
const transform = layerTransforms[layerName];
if (transform) {
element.classList.add(`layer-${transform}`);
}
}
// Update each layer
const baseLayer = document.querySelector('.layer-base');
const doorLayer = document.getElementById('door-layer');
const hardwareLayer = document.getElementById('hardware-layer');
const overlayLayer = document.getElementById('overlay-layer');
const foregroundLayer = document.querySelector('.layer-foreground');
// Base layer (no color variation)
if (baseLayer && baseLayer.tagName === 'IMG') {
baseLayer.src = buildCanvasAPIUrl(productCode, 'base');
applyTransform(baseLayer, 'base');
baseLayer.onerror = function() {
console.warn('Base layer not found via Canvas API');
};
}
// Door layer (with color)
if (doorLayer) {
applyTransform(doorLayer, 'door');
if (selectedColor) {
doorLayer.src = buildCanvasAPIUrl(productCode, 'door', selectedColor);
doorLayer.style.display = 'block';
doorLayer.onerror = function() {
console.warn(`Door layer not found for color: ${selectedColor}`);
this.style.display = 'none';
showConfigNotice(true);
};
doorLayer.onload = function() {
showConfigNotice(false);
};
} else {
doorLayer.style.display = 'none';
}
}
// Hardware layer
if (hardwareLayer) {
hardwareLayer.src = buildCanvasAPIUrl(productCode, 'hardware');
applyTransform(hardwareLayer, 'hardware');
hardwareLayer.onerror = function() {
console.warn('Hardware layer not found via Canvas API');
this.style.display = 'none';
};
hardwareLayer.onload = function() {
this.style.display = 'block';
};
}
// Foreground layer
if (foregroundLayer && foregroundLayer.tagName === 'IMG') {
foregroundLayer.src = buildCanvasAPIUrl(productCode, 'foreground');
applyTransform(foregroundLayer, 'foreground');
foregroundLayer.onerror = function() {
console.warn('Foreground layer not found via Canvas API');
// This is fine - foreground is optional
};
}
}
// ============================================================================
// 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 - Canvas API has priority
const useCanvasAPI = product.imageConfig && product.imageConfig.useCanvasAPI === true;
const hasFlatImages = product.flatImages === true || product.imagePattern;
const useDynamicAPI = product.useDynamicAPI === true;
console.log('Product flags - useCanvasAPI:', useCanvasAPI, 'hasFlatImages:', hasFlatImages, 'useDynamicAPI:', useDynamicAPI);
// Priority: Canvas API > Flat Images > Dynamic API > Layered Static
if (useCanvasAPI) {
// Use Canvas API with hierarchical fallback
updateCanvasAPIPreview(productCode);
return;
}
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);
}
}
function getSelectedSizeLabel() {
const sizeSelect = document.getElementById('config-size');
if (!sizeSelect) return '';
if (sizeSelect.value === 'custom') {
const customWidth = document.getElementById('custom-width')?.value || '';
const customHeight = document.getElementById('custom-height')?.value || '';
if (customWidth && customHeight) {
return `Custom ${customWidth}" x ${customHeight}"`;
}
return 'Custom Size';
}
const selectedOption = sizeSelect.options[sizeSelect.selectedIndex];
return selectedOption ? selectedOption.textContent.trim() : '';
}
function buildOrderItemKey(item) {
return [
item.productCode,
item.material,
item.color,
item.size,
item.hingeLocation
].join('|');
}
function addToOrder(productCode) {
const product = productData.find(p => p.productCode === productCode || p.id === productCode);
if (!product) {
showNotification('Unable to add item: product not found.');
return;
}
const material = document.getElementById('config-material')?.value || 'N/A';
const color = document.getElementById('config-color')?.value || 'N/A';
const hingeLocation = document.querySelector('[name="hinge-location"]:checked')?.value || 'left';
const size = getSelectedSizeLabel() || 'N/A';
const item = {
productCode,
description: product.description || product.DESCRIPTION || product.title || productCode,
material,
color,
size,
hingeLocation,
quantity: 1
};
const itemKey = buildOrderItemKey(item);
const existingIndex = orderItems.findIndex(existing => buildOrderItemKey(existing) === itemKey);
if (existingIndex >= 0) {
orderItems[existingIndex].quantity += 1;
} else {
orderItems.push(item);
}
renderOrderList();
showNotification('Added to order.');
}
async function submitQuoteOrder() {
if (!Array.isArray(orderItems) || orderItems.length === 0) {
showNotification('Add at least one item before submitting a quote.');
return;
}
try {
const response = await fetch('/quiz/api/submit-quote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: orderItems,
bitValue: accumulatedBitValue,
answers: userAnswers,
route: window.location.pathname,
url: window.location.href
})
});
const data = await response.json();
if (!response.ok || data.status !== 'success') {
showNotification(data.message || 'Quote submission failed.');
return;
}
const submissionId = data.submissionId || 'unknown';
orderItems = [];
renderOrderList();
showNotification(`Quote submitted (${submissionId}).`);
} catch (error) {
showNotification('Quote submission failed.');
}
}
function removeOrderItem(index) {
if (index < 0 || index >= orderItems.length) return;
orderItems.splice(index, 1);
renderOrderList();
}
function renderOrderList() {
const container = document.getElementById('order-items-list');
if (!container) return;
if (orderItems.length === 0) {
container.innerHTML = '<div style="color: #666; font-size: 0.95em;">No items added yet.</div>';
return;
}
const totalQuantity = orderItems.reduce((total, item) => total + item.quantity, 0);
container.innerHTML = `
<div style="display: grid; gap: 8px;">
${orderItems.map((item, index) => `
<div style="background: #fff; border: 1px solid #dfe3ea; border-radius: 6px; padding: 8px;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 8px;">
<div>
<div style="font-weight: 600;">${item.productCode} (${item.quantity})</div>
<div style="font-size: 0.9em; color: #444;">${item.description}</div>
<div style="font-size: 0.85em; color: #666; margin-top: 4px;">
Material: ${item.material} | Color: ${item.color}<br>
Size: ${item.size} | Hinge: ${item.hingeLocation}
</div>
</div>
<button class="back-button" onclick="removeOrderItem(${index})" style="padding: 6px 10px; font-size: 0.85em;">Remove</button>
</div>
</div>
`).join('')}
<div style="font-size: 0.9em; color: #333; font-weight: 600; padding-top: 4px;">
Total Items: ${totalQuantity}
</div>
</div>
`;
}
// 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 = `
<div class="result-container">
<div class="result-title">No Products Found</div>
<div class="result-details">
<p>We couldn't find any products matching your criteria:</p>
<ul style="text-align: left;">
${baseType ? `<li>Type: ${baseType}</li>` : ''}
${subType ? `<li>Sub-type: ${subType}</li>` : ''}
${material ? `<li>Material: ${material}</li>` : ''}
${color ? `<li>Color: ${color}</li>` : ''}
${width ? `<li>Width: ${width}"</li>` : ''}
${height ? `<li>Height: ${height}"</li>` : ''}
</ul>
<p>Please try different options or contact us for assistance.</p>
</div>
<div class="result-actions">
<button class="back-button" onclick="goBack()">← Go Back</button>
<button class="back-button" onclick="startOver()">Start Over</button>
</div>
</div>
`;
return;
}
// Display product grid
let html = `
<div class="result-container">
<div class="result-title">Found ${filtered.length} Product${filtered.length > 1 ? 's' : ''}</div>
<div class="result-details">
<p style="margin-bottom: 20px;">Your selections:
${baseType || ''} ${subType ? ' ' + subType : ''}
${material ? ' ' + material : ''}
${color ? ' ' + color : ''}
${width && height ? ` ${width}" × ${height}"` : ''}
</p>
</div>
<div class="products-grid">
`;
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 += `
<div class="product-card" onclick="showProductByCode('${prodCode}')">
<h3>${desc}</h3>
<p style="color: #999; font-size: 0.95em; margin-top: 5px;">${prodCode}</p>
<div style="font-size: 0.9em; color: #666; margin-top: 10px;">
<strong>Materials:</strong> ${materials}<br>
<strong>Colors:</strong> ${colors}
</div>
</div>
`;
});
html += `
</div>
<div class="result-actions">
<button class="back-button" onclick="goBack()">← Go Back</button>
<button class="back-button" onclick="startOver()">Start Over</button>
<button class="back-button" onclick="shareCurrentPage()">🔗 Share</button>
</div>
</div>
`;
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 `<span style="cursor: pointer; text-decoration: underline;" onclick="navigateToHistory(${index})">${label}</span>`;
} else {
return `<strong>${label}</strong>`;
}
});
breadcrumb.innerHTML = trail.join(' <span>→</span> ');
}
// 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 `
<div class="search-dropdown-item ${isSelected}"
data-index="${index}"
data-product-code="${prodCode}"
onclick="selectSearchItem(${index})">
<div class="search-dropdown-item-title">${desc}</div>
<div class="search-dropdown-item-code">${prodCode}</div>
</div>
`;
}).join('');
if (matches.length > 10) {
dropdown.innerHTML += `
<div class="search-dropdown-item" style="font-style: italic; color: #999; cursor: pointer;"
onclick="handleSearchResults()">
+ ${matches.length - 10} more results... (click to view all)
</div>
`;
}
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 = `
<div class="result-container">
<div class="result-title">Found ${matches.length} Product${matches.length > 1 ? 's' : ''}</div>
<div class="result-details">
<p style="margin-bottom: 20px;">Search results for: <strong>"${searchTerm}"</strong></p>
</div>
<div class="products-grid">
`;
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 += `
<div class="product-card" onclick="showProductByCode('${prodCode}')">
<h3>${desc}</h3>
<p style="color: #999; font-size: 0.95em; margin-top: 5px;">${prodCode}</p>
<div style="font-size: 0.9em; color: #666; margin-top: 10px;">
<strong>Type:</strong> ${baseType}${subType !== 'N/A' ? ' - ' + subType : ''}<br>
<strong>Materials:</strong> ${materials}<br>
<strong>Colors:</strong> ${colors}
</div>
</div>
`;
});
html += `
</div>
<div class="result-actions">
<button class="back-button" onclick="startOver()">← Back to Start</button>
<button class="back-button" onclick="shareCurrentPage()">🔗 Share</button>
</div>
</div>
`;
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() {
init(); // Load quiz data and initialize
initSearchBar();
toggleSearchBar();
});