Files
CGW-Quote-Builder/information/URL_STATE_MANAGEMENT.md
T
2026-04-11 00:04:09 -05:00

515 lines
15 KiB
Markdown

# 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)
```javascript
// 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
```javascript
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
```javascript
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
```javascript
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
```javascript
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
```javascript
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
```javascript
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
```javascript
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
```javascript
// 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
```css
@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
### Test 2: Product Deep Link
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