Initial
This commit is contained in:
@@ -0,0 +1,514 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user