// 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() {
// Load all data files in parallel
Promise.all([
fetch('data/navigation.json').then(r => r.ok ? r.json() : Promise.reject('navigation.json not found')),
fetch('data/products.json').then(r => r.ok ? r.json() : []).catch(() => []),
fetch('data/accessories.json').then(r => r.ok ? r.json() : []).catch(() => []),
fetch('data/product_bitwise.json').then(r => r.ok ? r.json() : []).catch(() => [])
])
.then(([questions, products, accessories, bitwise]) => {
questionData = questions;
productData = products;
accessoryData = accessories;
// Index bitwise data by product code
bitwiseData = {};
bitwise.forEach(item => {
bitwiseData[item.PROD_CODE] = item.BIT_VALUE;
});
console.log('Loaded:', {
questions: Object.keys(questionData).length,
products: productData.length,
accessories: accessoryData.length,
bitwise: Object.keys(bitwiseData).length
});
// Check if there's state in URL
initFromURL();
})
.catch(error => {
console.error('Error loading data:', error);
document.getElementById('content').innerHTML = `
Error Loading Application
Failed to load data files. Please refresh the page or contact support.
Error: ${error}
`;
});
}
// Handle conditional logic for q-material
function getNextForMaterial(answers) {
const productType = answers['start'];
if (productType === 'Window') {
return 'q-window-type';
} else if (productType === 'Door') {
return 'q-door-type';
} else if (productType === 'Patio Door') {
return 'q-patio';
}
return 'q-window-type'; // default
}
// Navigation history
let history = ['start'];
// Initialize from URL parameters
function initFromURL() {
const params = new URLSearchParams(window.location.search);
// Priority 1: Direct product view
const productCode = params.get('p');
if (productCode) {
accumulatedBitValue = parseInt(params.get('b') || '0', 10);
showProductByCode(productCode);
return;
}
// Priority 2: Restore navigation state
const bitValue = params.get('b');
const questionKey = params.get('q');
if (bitValue) {
accumulatedBitValue = parseInt(bitValue, 10);
if (questionKey) {
// Special case: results page
if (questionKey === 'results') {
restoreStateFromBitValue(accumulatedBitValue, null);
history.push('results');
showFilteredProducts();
return;
}
restoreStateFromBitValue(accumulatedBitValue, questionKey);
return;
}
}
// Priority 3: Start fresh
loadContent('start');
}
// Restore state from bit value
function restoreStateFromBitValue(bitValue, questionKey) {
// Decode bit value back to user selections
// Base type
if (bitValue & 1) {
userAnswers['start'] = 'Door';
} else if (bitValue & 2) {
userAnswers['start'] = 'Window';
}
// Subtypes
if (bitValue & (1 << BIT_DEFINITIONS['subtype_patio_door'])) {
if (bitValue & 1) userAnswers['q-door-type'] = 'Patio Door';
}
if (bitValue & (1 << BIT_DEFINITIONS['subtype_storm_door'])) {
if (bitValue & 1) userAnswers['q-door-type'] = 'Storm Door';
}
if (bitValue & (1 << BIT_DEFINITIONS['subtype_primary_window'])) {
if (bitValue & 2) userAnswers['q-window-type'] = 'Primary Window';
}
if (bitValue & (1 << BIT_DEFINITIONS['subtype_storm_window'])) {
if (bitValue & 2) userAnswers['q-window-type'] = 'Storm Window';
}
// Materials
if (bitValue & 16) {
userAnswers['material'] = 'Aluminum';
} else if (bitValue & 32) {
userAnswers['material'] = 'Vinyl';
}
// Colors
const colorMap = {
64: 'Black', 128: 'White', 256: 'Bronze',
512: 'Tan', 1024: 'Mill', 2048: 'Sandstone'
};
for (const [bit, color] of Object.entries(colorMap)) {
if (bitValue & parseInt(bit)) {
userAnswers['color'] = color;
break;
}
}
// Navigate to the saved question
history = ['start'];
if (questionKey && questionKey !== 'start') {
history.push(questionKey);
}
loadContent(questionKey || 'start');
}
// Update URL with current state
function updateURL(questionKey = null) {
const params = new URLSearchParams();
if (accumulatedBitValue > 0) {
params.set('b', accumulatedBitValue.toString());
}
const currentKey = questionKey || (history.length > 0 ? history[history.length - 1] : 'start');
if (currentKey && currentKey !== 'start') {
params.set('q', currentKey);
}
const newURL = window.location.pathname + (params.toString() ? '?' + params.toString() : '');
window.history.replaceState(
{ bitValue: accumulatedBitValue, questionKey: currentKey },
'',
newURL
);
}
// Update bit value based on selection
function updateBitValue(answerObject) {
if (!answerObject || !answerObject.filter) return;
const filter = answerObject.filter;
// Base type
if (filter.baseType === 'Door') {
accumulatedBitValue |= (1 << BIT_DEFINITIONS['base_door']);
} else if (filter.baseType === 'Window') {
accumulatedBitValue |= (1 << BIT_DEFINITIONS['base_window']);
}
// Materials
if (filter.material === 'Aluminum') {
accumulatedBitValue |= (1 << BIT_DEFINITIONS['material_aluminum']);
} else if (filter.material === 'Vinyl') {
accumulatedBitValue |= (1 << BIT_DEFINITIONS['material_vinyl']);
}
// Colors
const colorKey = filter.color ? 'color_' + filter.color.toLowerCase() : null;
if (colorKey && BIT_DEFINITIONS[colorKey]) {
accumulatedBitValue |= (1 << BIT_DEFINITIONS[colorKey]);
}
// Subtypes
if (filter.subType) {
const subtypeKey = 'subtype_' + filter.subType.toLowerCase().replace(/ /g, '_');
if (BIT_DEFINITIONS[subtypeKey]) {
accumulatedBitValue |= (1 << BIT_DEFINITIONS[subtypeKey]);
}
}
}
// Load content based on key
function loadContent(key) {
// Special handling for results
if (key === 'results') {
showFilteredProducts();
return;
}
const data = questionData[key];
if (!data) {
console.error('Content not found for key:', key);
return;
}
const contentDiv = document.getElementById('content');
if (data.type === 'question') {
if (data.inputType === 'form') {
renderForm(data, key);
} else {
renderQuestion(data, key);
}
} else if (data.type === 'product') {
renderProduct(data);
}
updateBreadcrumb();
}
// Helper function to render notes section
function renderNotesSection(notes) {
const notesId = 'notes-' + Date.now();
return `
${notes}
`;
}
// Toggle notes visibility
function toggleNotes(notesId) {
const notesContent = document.getElementById(notesId);
const arrow = document.getElementById(notesId + '-arrow');
if (notesContent.style.display === 'none') {
notesContent.style.display = 'block';
arrow.textContent = '▲';
} else {
notesContent.style.display = 'none';
arrow.textContent = '▼';
}
}
// Render a question with answer buttons
function renderQuestion(data, currentKey) {
const contentDiv = document.getElementById('content');
let html = `
${data.title}
${data.subtitle}
`;
html += `
`;
data.answers.forEach((answer, index) => {
html += `
`;
});
html += `
`;
// Add notes section if available
if (data.notes) {
html += renderNotesSection(data.notes);
}
contentDiv.innerHTML = html;
}
// Render a form with text inputs
function renderForm(data, currentKey) {
const contentDiv = document.getElementById('content');
let html = `
${data.title}
${data.subtitle}
`;
// Render measurement type toggle buttons if configuration exists
if (data.measurementType && data.measurementType.options) {
const currentMeasurementType = userAnswers[`${currentKey}.measurementType`] || data.measurementType.defaultValue;
html += `
`;
contentDiv.innerHTML = html;
}
// Handle answer selection from buttons
function handleAnswer(currentKey, answerValue, nextKeyOrIndex) {
// Store the answer
userAnswers[currentKey] = answerValue;
// Check if this is an index (new format) or key (old format)
let nextKey = nextKeyOrIndex;
const currentQuestion = questionData[currentKey];
if (currentQuestion && currentQuestion.answers && typeof nextKeyOrIndex === 'number') {
// New format: index provided
const answerObject = currentQuestion.answers[nextKeyOrIndex];
nextKey = answerObject.next;
// Store dimensions if provided (for preset sizes)
if (answerObject.dimensions) {
userAnswers['q-dimensions.width'] = answerObject.dimensions.width;
userAnswers['q-dimensions.height'] = answerObject.dimensions.height;
}
// Update bit value if filter exists
if (answerObject.filter) {
updateBitValue(answerObject);
}
}
history.push(nextKey);
updateURL(nextKey);
loadContent(nextKey);
}
// Handle answer by index only (used by buttons to avoid escaping issues)
function handleAnswerByIndex(currentKey, answerIndex) {
const currentQuestion = questionData[currentKey];
if (!currentQuestion || !currentQuestion.answers) {
console.error('Invalid question:', currentKey);
return;
}
const answerObject = currentQuestion.answers[answerIndex];
if (!answerObject) {
console.error('Invalid answer index:', answerIndex);
return;
}
// Store the answer caption
userAnswers[currentKey] = answerObject.caption;
// Store dimensions if provided (for preset sizes)
if (answerObject.dimensions) {
userAnswers['q-dimensions.width'] = answerObject.dimensions.width;
userAnswers['q-dimensions.height'] = answerObject.dimensions.height;
}
// Update bit value if filter exists
if (answerObject.filter) {
updateBitValue(answerObject);
}
// Navigate to next question
history.push(answerObject.next);
updateURL(answerObject.next);
loadContent(answerObject.next);
}
// Handle measurement type toggle button click
function handleMeasurementTypeToggle(value, currentKey) {
// Store the selected measurement type
userAnswers[`${currentKey}.measurementType`] = value;
// Update the UI to reflect the selection
const toggleButtons = document.querySelectorAll('.measurement-toggle');
toggleButtons.forEach(button => {
if (button.dataset.value === value) {
button.classList.add('selected');
} else {
button.classList.remove('selected');
}
});
}
// Handle form submission
function handleFormSubmit(event, currentKey) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
// Store all form field values
for (const [key, value] of formData.entries()) {
userAnswers[`${currentKey}.${key}`] = value;
}
// Ensure measurement type is saved if it was selected
const data = questionData[currentKey];
if (data.measurementType && !userAnswers[`${currentKey}.measurementType`]) {
userAnswers[`${currentKey}.measurementType`] = data.measurementType.defaultValue;
}
// Get the next key (handle conditional logic)
let nextKey;
// Check if this is the q-material question that needs conditional logic
if (currentKey === 'q-material' && data.next === 'q-material-conditional') {
nextKey = getNextForMaterial(userAnswers);
} else {
nextKey = data.next;
}
// Special handling for results
if (nextKey === 'results') {
history.push(nextKey);
updateURL(nextKey);
showFilteredProducts();
return;
}
history.push(nextKey);
updateURL(nextKey);
loadContent(nextKey);
}
// Go back to previous question
function goBack() {
if (history.length > 1) {
history.pop();
const previousKey = history[history.length - 1];
loadContent(previousKey);
}
}
// Start over
function startOver() {
history = ['start'];
// Clear all stored answers
for (const key in userAnswers) {
delete userAnswers[key];
}
// Clear bit value
accumulatedBitValue = 0;
// Clear URL
window.history.replaceState({}, '', window.location.pathname);
loadContent('start');
}
// Show product by code (from URL)
function showProductByCode(productCode) {
if (productData.length === 0) {
document.getElementById('content').innerHTML = `
Products Not Loaded
Product data not available yet. Please generate JSON files first.