refactor: migrate templates and static files to self-contained modules
Templates Migration: - Migrate admin templates to modules (tenancy, billing, monitoring, marketplace, etc.) - Migrate vendor templates to modules (tenancy, billing, orders, messaging, etc.) - Migrate storefront templates to modules (catalog, customers, orders, cart, checkout, cms) - Migrate public templates to modules (billing, marketplace, cms) - Keep shared templates in app/templates/ (base.html, errors/, partials/, macros/) - Migrate letzshop partials to marketplace module Static Files Migration: - Migrate admin JS to modules: tenancy (23 files), core (5 files), monitoring (1 file) - Migrate vendor JS to modules: tenancy (4 files), core (2 files) - Migrate shared JS: vendor-selector.js to core, media-picker.js to cms - Migrate storefront JS: storefront-layout.js to core - Keep framework JS in static/ (api-client, utils, money, icons, log-config, lib/) - Update all template references to use module_static paths Naming Consistency: - Rename static/platform/ to static/public/ - Rename app/templates/platform/ to app/templates/public/ - Update all extends and static references Documentation: - Update module-system.md with shared templates documentation - Update frontend-structure.md with new module JS organization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,295 +0,0 @@
|
||||
// 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