# Required Code Changes for Conditional Navigation ## Overview To support conditional material/color questions and dynamic navigation, the following changes are needed in your application code. --- ## 1. JavaScript Changes (`js/script.js`) ### Current State - Loads `questions.json` statically - Has hardcoded conditional for `q-material-conditional` - No product data loading ### Required Changes #### A. Load Multiple Data Files ```javascript // At the top of script.js - modify loadQuestionData() to load both files let questionData = {}; let productData = []; let accessoryData = []; let bitwiseData = {}; let accumulatedBitValue = 0; function init() { // Load all data files Promise.all([ fetch('data/navigation.json').then(r => r.json()), fetch('data/products.json').then(r => r.json()), fetch('data/accessories.json').then(r => r.json()), fetch('data/product_bitwise.json').then(r => r.json()) ]) .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; }); // Check if there's state in URL initFromURL(); // If no URL state, start normally if (!window.location.search) { loadContent('start'); } }) .catch(error => { console.error('Error loading data:', error); // Show error message }); } ``` #### B. Add Dynamic Next Resolution Function ```javascript // Add this new function to handle conditional navigation function resolveNextQuestion(currentKey, selectedAnswer, answerObject) { // Get the current accumulated filters const currentFilters = buildFilterFromAnswers(); // Add the new filter from this answer if (answerObject.filter) { Object.assign(currentFilters, answerObject.filter); } // Check if next question in chain should be shown const nextKey = answerObject.next; const nextQuestion = questionData[nextKey]; // If next question has conditional flag, evaluate it if (nextQuestion && nextQuestion.conditional) { return evaluateConditional(nextQuestion, currentFilters); } return nextKey; } // Evaluate if a conditional question should be shown function evaluateConditional(question, filters) { if (question.conditional.type === 'material') { // Check if products matching current filters have multiple materials const matchingProducts = filterProducts(filters); const materials = getAvailableMaterials(matchingProducts); if (materials.length <= 1) { // Skip this question, auto-apply material if exists if (materials.length === 1) { userAnswers[question.id + '.material'] = materials[0]; } // Return the fallback next return question.conditional.fallbackNext || question.next; } } if (question.conditional.type === 'color') { // Similar logic for colors const matchingProducts = filterProducts(filters); const colors = getAvailableColors(matchingProducts); if (colors.length <= 1) { if (colors.length === 1) { userAnswers[question.id + '.color'] = colors[0]; } return question.conditional.fallbackNext || question.next; } } // Show the question return question.id; } // Build current filter object from user answers function buildFilterFromAnswers() { const filters = {}; // Extract filter info from answers for (const [key, value] of Object.entries(userAnswers)) { if (key === 'start') { filters.baseType = value; } else if (key.includes('type')) { filters.subType = value; } else if (key.includes('material')) { filters.material = value; } else if (key.includes('color')) { filters.color = value; } } return filters; } // Filter products based on criteria function filterProducts(filters) { return productData.filter(product => { if (filters.baseType && product.baseType !== filters.baseType) { return false; } if (filters.subType) { const subType = product.subType.door || product.subType.window; if (subType !== filters.subType) { return false; } } if (filters.material && !product.materials.includes(filters.material)) { return false; } if (filters.color && !product.colors.includes(filters.color)) { return false; } return true; }); } // Get available materials from product set function getAvailableMaterials(products) { const materials = new Set(); products.forEach(p => { p.materials.forEach(m => materials.add(m)); }); return Array.from(materials); } // Get available colors from product set function getAvailableColors(products) { const colors = new Set(); products.forEach(p => { p.colors.forEach(c => colors.add(c)); }); return Array.from(colors); } ``` #### C. Update handleAnswer Function ```javascript // Replace the existing handleAnswer function function handleAnswer(currentKey, answerValue, answerIndex) { const currentQuestion = questionData[currentKey]; const answerObject = currentQuestion.answers[answerIndex]; // Store the answer userAnswers[currentKey] = answerValue; // Update bit value based on selection updateBitValue(answerObject); // Resolve the next question (handles conditionals) const nextKey = resolveNextQuestion(currentKey, answerValue, answerObject); history.push(nextKey); loadContent(nextKey); } ``` #### D. Update renderQuestion to Pass Answer Index ```javascript // In renderQuestion function, change the button onclick // FROM: // onclick="handleAnswer('${currentKey}', '${answer.caption}', '${answer.next}')" // TO: data.answers.forEach((answer, index) => { html += ` `; }); ``` #### D2. Add URL State Management ```javascript // Track accumulated bit value from user selections let accumulatedBitValue = 0; // Load bitwise helper data let bitwiseData = {}; function loadBitwiseData() { return fetch('data/product_bitwise.json') .then(r => r.json()) .then(data => { bitwiseData = {}; data.forEach(item => { bitwiseData[item.PROD_CODE] = item.BIT_VALUE; }); return bitwiseData; }); } // Initialize from URL on page load function initFromURL() { const params = new URLSearchParams(window.location.search); // Check for bit value in URL const bitValue = params.get('b'); if (bitValue) { accumulatedBitValue = parseInt(bitValue, 10); // Restore state based on bit value restoreStateFromBitValue(accumulatedBitValue); } // Check for product code in URL const productCode = params.get('p'); if (productCode) { showProductByCode(productCode); } } // Update URL with current state function updateURL() { const params = new URLSearchParams(); if (accumulatedBitValue > 0) { params.set('b', accumulatedBitValue); } // Get current question if (history.length > 0) { const currentKey = history[history.length - 1]; params.set('q', currentKey); } // Update URL without reloading page const newURL = window.location.pathname + '?' + params.toString(); window.history.replaceState({ bitValue: accumulatedBitValue }, '', newURL); } // Update bit value based on user selection function updateBitValue(answerObject) { if (answerObject.filter) { // Add bits based on filter if (answerObject.filter.baseType === 'Door') { accumulatedBitValue |= 1; // Bit 0 } else if (answerObject.filter.baseType === 'Window') { accumulatedBitValue |= 2; // Bit 1 } if (answerObject.filter.material === 'Aluminum') { accumulatedBitValue |= 16; // Bit 4 } else if (answerObject.filter.material === 'Vinyl') { accumulatedBitValue |= 32; // Bit 5 } // Add color bits const colorBits = { 'Black': 64, 'White': 128, 'Bronze': 256, 'Tan': 512, 'Mill': 1024, 'Sandstone': 2048 }; if (answerObject.filter.color && colorBits[answerObject.filter.color]) { accumulatedBitValue |= colorBits[answerObject.filter.color]; } // Add subtype bits (based on bitwise_legend.json) const subtypeBits = { 'Patio Door': 4096, 'Primary Window': 8192, 'Storm Door': 16384, 'Storm Window': 32768 }; if (answerObject.filter.subType && subtypeBits[answerObject.filter.subType]) { accumulatedBitValue |= subtypeBits[answerObject.filter.subType]; } } updateURL(); } // Restore state from bit value function restoreStateFromBitValue(bitValue) { // Decode bit value back to user selections const selections = {}; if (bitValue & 1) selections.baseType = 'Door'; if (bitValue & 2) selections.baseType = 'Window'; if (bitValue & 16) selections.material = 'Aluminum'; if (bitValue & 32) selections.material = 'Vinyl'; const colors = ['Black', 'White', 'Bronze', 'Tan', 'Mill', 'Sandstone']; const colorBits = [64, 128, 256, 512, 1024, 2048]; colors.forEach((color, idx) => { if (bitValue & colorBits[idx]) { selections.color = color; } }); // Store in userAnswers if (selections.baseType) userAnswers['start'] = selections.baseType; if (selections.material) userAnswers['q-material.material'] = selections.material; if (selections.color) userAnswers['q-color.color'] = selections.color; // Navigate to appropriate question from URL param const params = new URLSearchParams(window.location.search); const questionKey = params.get('q') || 'start'; loadContent(questionKey); } // Share current state function shareCurrentState() { const url = window.location.href; // Copy to clipboard if (navigator.clipboard) { navigator.clipboard.writeText(url).then(() => { alert('Link copied to clipboard! Share this link to return to this exact state.'); }); } else { // Fallback: show URL prompt('Copy this URL to share:', url); } } // Add share button to breadcrumb function updateBreadcrumbWithShare() { const breadcrumbDiv = document.getElementById('breadcrumb'); const shareButton = ` `; breadcrumbDiv.innerHTML += shareButton; } ``` #### E. Add Product Results Page ```javascript // Add new function to show filtered products function showProductResults(filters) { const matchingProducts = filterProducts(filters); const contentDiv = document.getElementById('content'); if (matchingProducts.length === 0) { contentDiv.innerHTML = `
No Products Found
No products match your specifications. Please try different options.
`; return; } let html = `
Found ${matchingProducts.length} Product(s)
`; matchingProducts.forEach(product => { // Get compatible accessories const compatibleAccessories = accessoryData.filter(acc => product.compatibleAccessories.includes(acc.id) ); html += `

${product.description}

Code: ${product.productCode}

Materials: ${product.materials.join(', ')}

Colors: ${product.colors.join(', ')}

${compatibleAccessories.length > 0 ? `

Available Options:

    ${compatibleAccessories.map(acc => `
  • ${acc.description}
  • ` ).join('')}
` : ''}
`; }); html += `
`; contentDiv.innerHTML = html; } // Show individual product detail with URL update function showProductDetail(productCode) { const product = productData.find(p => p.productCode === productCode); if (!product) return; // Update URL with product code const params = new URLSearchParams(); params.set('p', productCode); if (accumulatedBitValue > 0) { params.set('b', accumulatedBitValue); } window.history.pushState({ productCode }, '', '?' + params.toString()); // Get compatible accessories const compatibleAccessories = accessoryData.filter(acc => product.compatibleAccessories.includes(acc.id) ); const contentDiv = document.getElementById('content'); let html = `
${product.description}

Product Code: ${product.productCode}

Category: ${product.category}

Base Type: ${product.baseType || 'N/A'}

Materials: ${product.materials.join(', ') || 'N/A'}

Colors: ${product.colors.join(', ') || 'N/A'}

${compatibleAccessories.length > 0 ? `

Compatible Accessories

${compatibleAccessories.map(acc => `
${acc.description}
Code: ${acc.accessoryCode}
Materials: ${acc.materials.join(', ')}
`).join('')}
` : ''}
`; contentDiv.innerHTML = html; } // Show product by code (from URL) function showProductByCode(productCode) { const product = productData.find(p => p.productCode === productCode); if (product) { showProductDetail(productCode); } } ``` --- ## 2. JSON Structure Changes (`data/navigation.json`) ### Add Conditional Metadata to Questions Questions that might be skipped need a `conditional` property: ```json { "q-material-storm-door": { "type": "question", "inputType": "button", "title": "Select Material", "subtitle": "Choose your preferred material", "conditional": { "type": "material", "fallbackNext": "q-dimensions", "autoApply": true }, "answers": [ { "caption": "Aluminum", "next": "q-color", "filter": { "material": "Aluminum" } }, { "caption": "Vinyl", "next": "q-color", "filter": { "material": "Vinyl" } } ] } } ``` ### Add Filter Properties to Answers Each answer should include filter criteria: ```json { "caption": "Storm Door", "image": "🚪", "next": "q-material-storm-door", "filter": { "baseType": "Door", "subType": "Storm Door" } } ``` --- ## 3. CSS Changes (`css/styles.css`) Add styles for product grid: ```css .products-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin: 20px 0; padding: 20px; } .product-card { background: white; border: 1px solid #ddd; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .product-card h3 { margin-top: 0; color: #333; font-size: 1.1em; } .product-card p { margin: 10px 0; line-height: 1.6; } .product-card ul { list-style-position: inside; padding-left: 0; } ``` --- ## 4. Python Changes (OPTIONAL) If you want server-side filtering: ```python # Add to app.py import json import csv @app.route('/api/filter-products', methods=['POST']) def filter_products(): """Filter products based on criteria""" filters = request.json # Load products with open('data/products.json', 'r') as f: products = json.load(f) # Apply filters filtered = [] for product in products: if filters.get('baseType') and product['baseType'] != filters['baseType']: continue if filters.get('subType'): sub = product['subType'].get('door') or product['subType'].get('window') if sub != filters['subType']: continue if filters.get('material') and filters['material'] not in product['materials']: continue if filters.get('color') and filters['color'] not in product['colors']: continue filtered.append(product) return jsonify({ 'status': 'success', 'count': len(filtered), 'products': filtered }) ``` --- ## 5. CSV Parsing Script Changes Create a Python script to generate the JSON files: ```python # create_json_files.py import csv import json from collections import defaultdict def parse_products_csv(csv_path): """Parse products.csv and generate three JSON files""" products = [] accessories = [] nav_data = { "start": { "type": "question", "inputType": "button", "title": "What are you looking for?", "subtitle": "Select the product category", "answers": [] } } # Track unique values for navigation base_types = set() door_subtypes = defaultdict(int) # Track material diversity per subtype window_subtypes = defaultdict(int) with open(csv_path, 'r', encoding='utf-8') as f: reader = csv.DictReader(f, skipinitialspace=True) for row in reader: # Skip discontinued if row['DISCONT'].upper() == 'TRUE': continue # Parse materials materials = [] if row['Aluminum'].upper() == 'TRUE': materials.append('Aluminum') if row['Vinyl'].upper() == 'TRUE': materials.append('Vinyl') # Parse colors colors = [] for color in ['Black', 'White', 'Bronze', 'Tan', 'Mill', 'Sandstone']: if row.get(color, '').upper() == 'TRUE': colors.append(color) # Determine if accessory is_accessory = row['Accessory\\nYes'].upper() == 'TRUE' # Build sub-type object sub_type = { "door": row['Sub-type\\nDoor'].strip() if row['Sub-type\\nDoor'] else None, "window": row['Sub-type\\nWindow'].strip() if row['Sub-type\\nWindow'] else None } # Common data item_data = { "id": row['PROD_CODE'], "productCode": row['PROD_CODE'], "category": row['CATEGORY'], "description": row['DESCRIPTION'], "discontinued": False, "location": row['LOC_CODE'], "baseType": row['Base\\nType'].strip() if row['Base\\nType'] else None, "subType": sub_type, "materials": materials, "colors": colors } if is_accessory: # Add to accessories accessories.append({ **item_data, "compatibilityRules": { "type": "subType", "subTypeDoor": sub_type['door'], "subTypeWindow": sub_type['window'], # ... more rules } }) else: # Add to products products.append({ **item_data, "isAccessory": False, "compatibleAccessories": [] }) # Track for navigation if item_data['baseType']: base_types.add(item_data['baseType']) # Build navigation structure # ... (logic to create navigation.json based on analysis) # Write JSON files with open('data/products.json', 'w') as f: json.dump(products, f, indent=2) with open('data/accessories.json', 'w') as f: json.dump(accessories, f, indent=2) with open('data/navigation.json', 'w') as f: json.dump(nav_data, f, indent=2) if __name__ == '__main__': parse_products_csv('data/products.csv') print("JSON files generated successfully!") ``` --- ## Implementation Checklist - [ ] Update `js/script.js` with conditional navigation logic - [ ] Create parsing script to generate JSON files from CSV - [ ] Run parsing script to create `navigation.json`, `products.json`, `accessories.json` - [ ] Update CSS for product display - [ ] Test navigation flow with different product types - [ ] Verify material questions are skipped when appropriate - [ ] Verify color questions are skipped when appropriate - [ ] Test product filtering and results display - [ ] Add accessory display on product pages --- ## Testing Scenarios 1. **Storm Door Selection**: Should skip material question (all Aluminum) 2. **Window Selection**: Should show material question (mixed materials) 3. **Single Color Products**: Should skip color question 4. **Multi-Color Products**: Should show color question 5. **Product Results**: Should show filtered products with compatible accessories --- ## Summary **CSV**: No changes needed (unless adding bit values) **JavaScript**: Major refactoring for conditional logic **JSON**: New structure with filter objects and conditional flags **Python**: Optional parsing script to generate JSON from CSV