refactor: rename shop to storefront for consistency
Rename all "shop" directories and references to "storefront" to match the API and route naming convention already in use. Renamed directories: - app/templates/shop/ → app/templates/storefront/ - static/shop/ → static/storefront/ - app/templates/shared/macros/shop/ → .../macros/storefront/ - docs/frontend/shop/ → docs/frontend/storefront/ Renamed files: - shop.css → storefront.css - shop-layout.js → storefront-layout.js Updated references in: - app/routes/storefront_pages.py (21 template references) - app/modules/cms/routes/pages/vendor.py - app/templates/storefront/base.html (static paths) - All storefront templates (extends/includes) - docs/architecture/frontend-structure.md This aligns the template/static naming with: - Route file: storefront_pages.py - API directory: app/api/v1/storefront/ - Module routes: */routes/api/storefront.py - URL paths: /storefront/* Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
295
static/storefront/js/storefront-layout.js
Normal file
295
static/storefront/js/storefront-layout.js
Normal file
@@ -0,0 +1,295 @@
|
||||
// static/storefront/js/storefront-layout.js
|
||||
/**
|
||||
* Shop Layout Component
|
||||
* Provides base functionality for vendor shop pages
|
||||
* Works with vendor-specific themes
|
||||
*/
|
||||
|
||||
const shopLog = {
|
||||
info: (...args) => console.info('🛒 [SHOP]', ...args),
|
||||
warn: (...args) => console.warn('⚠️ [SHOP]', ...args),
|
||||
error: (...args) => console.error('❌ [SHOP]', ...args),
|
||||
debug: (...args) => console.log('🔍 [SHOP]', ...args)
|
||||
};
|
||||
|
||||
/**
|
||||
* Shop Layout Data
|
||||
* Base Alpine.js component for shop pages
|
||||
*/
|
||||
function shopLayoutData() {
|
||||
return {
|
||||
// Theme state
|
||||
dark: localStorage.getItem('shop-theme') === 'dark',
|
||||
|
||||
// UI state
|
||||
mobileMenuOpen: false,
|
||||
searchOpen: false,
|
||||
loading: false,
|
||||
cartCount: 0,
|
||||
|
||||
// Cart state
|
||||
cart: [],
|
||||
sessionId: null,
|
||||
|
||||
// Initialize
|
||||
init() {
|
||||
shopLog.info('Shop layout initializing...');
|
||||
|
||||
// Get or create session ID
|
||||
this.sessionId = this.getOrCreateSessionId();
|
||||
shopLog.debug('Session ID:', this.sessionId);
|
||||
|
||||
// Load cart from localStorage
|
||||
this.loadCart();
|
||||
|
||||
// Listen for cart updates
|
||||
window.addEventListener('cart-updated', () => {
|
||||
this.loadCart();
|
||||
});
|
||||
|
||||
shopLog.info('Shop layout initialized');
|
||||
},
|
||||
|
||||
// Get or create session ID
|
||||
getOrCreateSessionId() {
|
||||
let sessionId = localStorage.getItem('cart_session_id');
|
||||
if (!sessionId) {
|
||||
sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
localStorage.setItem('cart_session_id', sessionId);
|
||||
shopLog.info('Created new session ID:', sessionId);
|
||||
}
|
||||
return sessionId;
|
||||
},
|
||||
|
||||
// Theme management
|
||||
toggleTheme() {
|
||||
this.dark = !this.dark;
|
||||
localStorage.setItem('shop-theme', this.dark ? 'dark' : 'light');
|
||||
shopLog.debug('Theme toggled:', this.dark ? 'dark' : 'light');
|
||||
},
|
||||
|
||||
// Mobile menu
|
||||
toggleMobileMenu() {
|
||||
this.mobileMenuOpen = !this.mobileMenuOpen;
|
||||
if (this.mobileMenuOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
},
|
||||
|
||||
closeMobileMenu() {
|
||||
this.mobileMenuOpen = false;
|
||||
document.body.style.overflow = '';
|
||||
},
|
||||
|
||||
// Search
|
||||
openSearch() {
|
||||
this.searchOpen = true;
|
||||
shopLog.debug('Search opened');
|
||||
// Focus search input after a short delay
|
||||
setTimeout(() => {
|
||||
const input = document.querySelector('#search-input');
|
||||
if (input) input.focus();
|
||||
}, 100);
|
||||
},
|
||||
|
||||
closeSearch() {
|
||||
this.searchOpen = false;
|
||||
},
|
||||
|
||||
// Cart management
|
||||
loadCart() {
|
||||
try {
|
||||
const cartData = localStorage.getItem('shop-cart');
|
||||
if (cartData) {
|
||||
this.cart = JSON.parse(cartData);
|
||||
this.cartCount = this.cart.reduce((sum, item) => sum + item.quantity, 0);
|
||||
}
|
||||
} catch (error) {
|
||||
shopLog.error('Failed to load cart:', error);
|
||||
this.cart = [];
|
||||
this.cartCount = 0;
|
||||
}
|
||||
},
|
||||
|
||||
addToCart(product, quantity = 1) {
|
||||
shopLog.info('Adding to cart:', product.name, 'x', quantity);
|
||||
|
||||
// Find existing item
|
||||
const existingIndex = this.cart.findIndex(item => item.id === product.id);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// Update quantity
|
||||
this.cart[existingIndex].quantity += quantity;
|
||||
} else {
|
||||
// Add new item
|
||||
this.cart.push({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
image: product.image,
|
||||
quantity: quantity
|
||||
});
|
||||
}
|
||||
|
||||
// Save and update
|
||||
this.saveCart();
|
||||
this.showToast(`${product.name} added to cart`, 'success');
|
||||
},
|
||||
|
||||
updateCartItem(productId, quantity) {
|
||||
const index = this.cart.findIndex(item => item.id === productId);
|
||||
if (index !== -1) {
|
||||
if (quantity <= 0) {
|
||||
this.cart.splice(index, 1);
|
||||
} else {
|
||||
this.cart[index].quantity = quantity;
|
||||
}
|
||||
this.saveCart();
|
||||
}
|
||||
},
|
||||
|
||||
removeFromCart(productId) {
|
||||
this.cart = this.cart.filter(item => item.id !== productId);
|
||||
this.saveCart();
|
||||
this.showToast('Item removed from cart', 'info');
|
||||
},
|
||||
|
||||
clearCart() {
|
||||
this.cart = [];
|
||||
this.saveCart();
|
||||
this.showToast('Cart cleared', 'info');
|
||||
},
|
||||
|
||||
saveCart() {
|
||||
try {
|
||||
localStorage.setItem('shop-cart', JSON.stringify(this.cart));
|
||||
this.cartCount = this.cart.reduce((sum, item) => sum + item.quantity, 0);
|
||||
|
||||
// Dispatch custom event
|
||||
window.dispatchEvent(new CustomEvent('cart-updated'));
|
||||
|
||||
shopLog.debug('Cart saved:', this.cart.length, 'items');
|
||||
} catch (error) {
|
||||
shopLog.error('Failed to save cart:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// Get cart total
|
||||
get cartTotal() {
|
||||
return this.cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
},
|
||||
|
||||
// Toast notifications
|
||||
showToast(message, type = 'info') {
|
||||
const container = document.getElementById('toast-container');
|
||||
if (!container) return;
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type} transform transition-all duration-300 mb-2`;
|
||||
|
||||
// Color based on type
|
||||
const colors = {
|
||||
success: 'bg-green-500',
|
||||
error: 'bg-red-500',
|
||||
warning: 'bg-yellow-500',
|
||||
info: 'bg-blue-500'
|
||||
};
|
||||
|
||||
// noqa: SEC-015 - message is application-controlled
|
||||
toast.innerHTML = `
|
||||
<div class="${colors[type]} text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-3">
|
||||
<span>${message}</span>
|
||||
<button onclick="this.parentElement.parentElement.remove()"
|
||||
class="ml-4 hover:opacity-75">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
// Auto-remove after 3 seconds
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
},
|
||||
|
||||
// Format currency using configured locale
|
||||
formatPrice(amount) {
|
||||
if (!amount && amount !== 0) return '';
|
||||
const locale = window.SHOP_CONFIG?.locale || 'fr-LU';
|
||||
const currency = window.SHOP_CONFIG?.currency || 'EUR';
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: currency
|
||||
}).format(amount);
|
||||
},
|
||||
|
||||
// Format date
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Make available globally
|
||||
window.shopLayoutData = shopLayoutData;
|
||||
|
||||
/**
|
||||
* Language Selector Component
|
||||
* Alpine.js component for language switching
|
||||
*/
|
||||
function languageSelector(currentLang, enabledLanguages) {
|
||||
return {
|
||||
isLangOpen: false,
|
||||
currentLang: currentLang || 'fr',
|
||||
languages: enabledLanguages || ['fr', 'de', 'en'],
|
||||
languageNames: {
|
||||
'en': 'English',
|
||||
'fr': 'Français',
|
||||
'de': 'Deutsch',
|
||||
'lb': 'Lëtzebuergesch'
|
||||
},
|
||||
languageFlags: {
|
||||
'en': 'gb',
|
||||
'fr': 'fr',
|
||||
'de': 'de',
|
||||
'lb': 'lu'
|
||||
},
|
||||
async setLanguage(lang) {
|
||||
if (lang === this.currentLang) {
|
||||
this.isLangOpen = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch('/api/v1/language/set', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ language: lang })
|
||||
});
|
||||
if (response.ok) {
|
||||
this.currentLang = lang;
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to set language:', error);
|
||||
}
|
||||
this.isLangOpen = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
window.languageSelector = languageSelector;
|
||||
|
||||
shopLog.info('Shop layout module loaded');
|
||||
Reference in New Issue
Block a user