Files
2026-04-11 00:04:09 -05:00

15 KiB

URL State Management Implementation

Overview

This feature allows users to:

  1. Bookmark their progress through the navigation flow
  2. Refresh the page without losing their selections
  3. Share URLs with specific products or navigation states
  4. Deep link directly to products

URL Parameter Structure

Query Parameters

  • b - Bitwise value representing all selections (integer)
  • q - Current question ID (string)
  • p - Product code for direct product view (string)

Examples

# At start
https://yoursite.com/

# After selecting Door > Storm Door > Aluminum
https://yoursite.com/?b=16401&q=q-dimensions

# Viewing a specific product
https://yoursite.com/?p=BGIST&b=16401

# Bit value 16401 decodes to:
# - Door (bit 0)
# - Aluminum (bit 4)
# - Storm Door subtype (bit 14)

Implementation Code

1. Add to Global Variables (top of script.js)

// Add these to existing global variables
let accumulatedBitValue = 0;
let bitwiseData = {};

// Bit definitions (must match generate_bitwise_helper.py)
const BIT_DEFINITIONS = {
    // Base Types
    'base_door': 0,          // 1
    'base_window': 1,        // 2
    
    // Flags
    'is_accessory': 2,       // 4
    'specific_item': 3,      // 8
    
    // Materials
    'material_aluminum': 4,   // 16
    'material_vinyl': 5,      // 32
    
    // Colors
    'color_black': 6,         // 64
    'color_white': 7,         // 128
    'color_bronze': 8,        // 256
    'color_tan': 9,           // 512
    'color_mill': 10,         // 1024
    'color_sandstone': 11,    // 2048
    
    // Subtypes (from bitwise_legend.json)
    'subtype_patio_door': 12,      // 4096
    'subtype_primary_window': 13,  // 8192
    'subtype_storm_door': 14,      // 16384
    'subtype_storm_window': 15     // 32768
};

2. Update init() Function

function init() {
    // Load all data files (including bitwise)
    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;
        });
        
        // Initialize from URL or start fresh
        initFromURL();
    })
    .catch(error => {
        console.error('Error loading data:', error);
        showError('Failed to load application data.');
    });
}

3. URL Initialization

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 && questionKey) {
        accumulatedBitValue = parseInt(bitValue, 10);
        restoreStateFromBitValue(accumulatedBitValue, questionKey);
        return;
    }
    
    // Priority 3: Start fresh
    loadContent('start');
}

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';
    }
    
    // Materials
    if (bitValue & 16) {
        userAnswers['q-material.material'] = 'Aluminum';
    } else if (bitValue & 32) {
        userAnswers['q-material.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['q-color.color'] = color;
            break; // Only store first color found
        }
    }
    
    // Subtypes
    const subtypeMap = {
        4096: 'Patio Door',
        8192: 'Primary Window',
        16384: 'Storm Door',
        32768: 'Storm Window'
    };
    
    for (const [bit, subtype] of Object.entries(subtypeMap)) {
        if (bitValue & parseInt(bit)) {
            userAnswers['q-subtype'] = subtype;
            break; // Only store first subtype found
        }
    }
    
    // Navigate to the saved question
    history = ['start'];
    if (questionKey !== 'start') {
        history.push(questionKey);
    }
    loadContent(questionKey);
}

4. Update URL on Each Selection

function updateURL(questionKey = null) {
    const params = new URLSearchParams();
    
    if (accumulatedBitValue > 0) {
        params.set('b', accumulatedBitValue.toString());
    }
    
    if (questionKey) {
        params.set('q', questionKey);
    } else if (history.length > 0) {
        params.set('q', history[history.length - 1]);
    }
    
    const newURL = window.location.pathname + (params.toString() ? '?' + params.toString() : '');
    window.history.replaceState(
        { 
            bitValue: accumulatedBitValue,
            questionKey: questionKey 
        }, 
        '', 
        newURL
    );
}

function updateBitValue(answerObject) {
    if (!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
    const subtypeKey = filter.subType ? 'subtype_' + filter.subType.toLowerCase().replace(/ /g, '_') : null;
    if (subtypeKey && BIT_DEFINITIONS[subtypeKey]) {
        accumulatedBitValue |= (1 << BIT_DEFINITIONS[subtypeKey]);
    }
}

5. Update 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);
    
    // Update URL with new state
    updateURL(nextKey);
    
    loadContent(nextKey);
}

6. Product View with URL

function showProductByCode(productCode) {
    const product = productData.find(p => p.productCode === productCode);
    if (!product) {
        showError(`Product ${productCode} not found`);
        return;
    }
    
    showProductDetail(product);
}

function showProductDetail(product) {
    // Update URL with product code
    const params = new URLSearchParams();
    params.set('p', product.productCode);
    if (accumulatedBitValue > 0) {
        params.set('b', accumulatedBitValue.toString());
    }
    
    window.history.pushState(
        { productCode: product.productCode }, 
        '', 
        '?' + params.toString()
    );
    
    // Render product detail
    const compatibleAccessories = accessoryData.filter(acc => 
        product.compatibleAccessories && product.compatibleAccessories.includes(acc.id)
    );
    
    const contentDiv = document.getElementById('content');
    let html = `
        <div class="result-container">
            <div class="result-title">${product.description}</div>
            <div class="result-content">
                <div class="result-details">
                    <p><strong>Product Code:</strong> ${product.productCode}</p>
                    <p><strong>Category:</strong> ${product.category}</p>
                    <p><strong>Base Type:</strong> ${product.baseType || 'N/A'}</p>
                    <p><strong>Materials:</strong> ${product.materials.join(', ') || 'N/A'}</p>
                    <p><strong>Colors:</strong> ${product.colors.join(', ') || 'N/A'}</p>
                    
                    ${compatibleAccessories.length > 0 ? `
                        <h3 style="margin-top: 20px;">Compatible Accessories</h3>
                        <div class="accessories-list">
                            ${compatibleAccessories.map(acc => `
                                <div class="accessory-item">
                                    <strong>${acc.description}</strong><br>
                                    <small>Code: ${acc.accessoryCode} | ${acc.materials.join(', ')}</small>
                                </div>
                            `).join('')}
                        </div>
                    ` : ''}
                </div>
            </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 Product</button>
            </div>
        </div>
    `;
    
    contentDiv.innerHTML = html;
}

7. Share Functionality

function shareCurrentPage() {
    const url = window.location.href;
    
    if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard.writeText(url).then(() => {
            showNotification('Link copied to clipboard!');
        }).catch(() => {
            showUrlPrompt(url);
        });
    } else {
        showUrlPrompt(url);
    }
}

function showUrlPrompt(url) {
    const message = prompt('Copy this URL to share:', url);
}

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);
}

8. Update startOver Function

function startOver() {
    history = ['start'];
    
    // Clear all stored answers
    for (const key in userAnswers) {
        delete userAnswers[key];
    }
    
    // Clear bit value
    accumulatedBitValue = 0;
    
    // Clear URL (return to root)
    window.history.replaceState({}, '', window.location.pathname);
    
    loadContent('start');
}

9. Handle Browser Back Button

// Add this to init() or as separate event listener
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();
    }
});

10. Add CSS for Notification

@keyframes slideIn {
    from {
        transform: translateX(400px);
        opacity: 0;
    }
    to {
        transform: translateX(0);
        opacity: 1;
    }
}

@keyframes slideOut {
    from {
        transform: translateX(0);
        opacity: 1;
    }
    to {
        transform: translateX(400px);
        opacity: 0;
    }
}

.accessory-item {
    padding: 10px;
    margin: 5px 0;
    background: #f5f5f5;
    border-left: 3px solid #2196F3;
    border-radius: 3px;
}

.product-card {
    cursor: pointer;
    transition: transform 0.2s, box-shadow 0.2s;
}

.product-card:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}

Testing Scenarios

Test 1: Basic Navigation

  1. Start quiz
  2. Select Door → Storm Door → Aluminum → White
  3. Check URL contains ?b=XXXX&q=YYYY
  4. Copy URL
  5. Open in new tab → Should restore to same point
  1. Navigate to product BGIST
  2. Check URL contains ?p=BGIST&b=XXXX
  3. Copy URL
  4. Open in new tab → Should show product directly

Test 3: Browser Refresh

  1. Navigate through several questions
  2. Press F5 to refresh
  3. Should restore to same question with selections intact

Test 4: Browser Back Button

  1. Navigate forward through questions
  2. Press browser back button
  3. Should step backward through questions
  4. URL should update accordingly

Test 5: Share Button

  1. Complete navigation flow
  2. Click "Share" button
  3. Should copy URL to clipboard
  4. Paste in new browser → Should restore state

Benefits

  1. User Experience: Users don't lose progress on refresh
  2. Shareability: Users can share specific configurations
  3. Bookmarking: Useful configurations can be saved
  4. SEO: Product pages are directly linkable
  5. Analytics: Track specific navigation paths via URL parameters
  6. Support: Users can share URLs when asking for help

URL Encoding Notes

  • Bit values are stored as decimal integers (more compact than hex for small values)
  • Product codes are URL-safe (no special encoding needed)
  • Question IDs are alphanumeric (q-dimensions, etc.)
  • Special characters in product codes should be URL-encoded if present