1816 lines
67 KiB
JavaScript
1816 lines
67 KiB
JavaScript
// 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 = `
|
||
<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 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 `
|
||
<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 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 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 (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 = `
|
||
<div class="door-configurator" id="door-configurator">
|
||
${layers.base ? `<img src="${basePath}${layers.base}" class="layer-base" alt="Base">` : ''}
|
||
<img src="" class="layer-door" id="door-layer" alt="Door" style="display:none;">
|
||
${layers.hardware ? `<img src="${basePath}${layers.hardware}" class="layer-hardware" id="hardware-layer" alt="Hardware">` : ''}
|
||
<img src="" class="layer-overlay" id="overlay-layer" alt="View" 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>
|
||
</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();
|
||
|
||
// 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 = `
|
||
<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() {
|
||
initSearchBar();
|
||
toggleSearchBar();
|
||
});
|