const API_BASE = '/api'; function getOrCreateSessionId() { let sessionId = localStorage.getItem('cart_session_id'); if (!sessionId) { sessionId = 'sess_' + Math.random().toString(36).substr(2, 9) + Date.now().toString(36); localStorage.setItem('cart_session_id', sessionId); } return sessionId; } // Cart management (localStorage) const cart = { get() { try { return JSON.parse(localStorage.getItem('cart') || '[]'); } catch { return []; } }, set(items) { localStorage.setItem('cart', JSON.stringify(items)); updateCartCount(); }, add(product, quantity = 1) { const items = this.get(); // Convert both to strings for comparison to handle type mismatches const existing = items.find(item => String(item.productId) === String(product.id)); if (existing) { existing.quantity += quantity; } else { items.push({ productId: product.id, productName: product.name, price: product.price, currency: product.currency, image: product.image, quantity }); } this.set(items); }, remove(productId) { const items = this.get().filter(item => String(item.productId) !== String(productId)); this.set(items); }, update(productId, quantity) { const items = this.get(); // Convert both to strings for comparison to handle type mismatches const item = items.find(i => String(i.productId) === String(productId)); if (item) { if (quantity <= 0) { this.remove(productId); } else { item.quantity = quantity; this.set(items); } } }, clear() { this.set([]); } }; // Current page for pagination let currentPage = 1; const pageSize = 12; // Load products on page load document.addEventListener('DOMContentLoaded', async () => { await checkAndShowRegionSelector(); await loadProducts(currentPage); setupModal(); updateCartCount(); }); function updateCartCount() { const count = cart.get().reduce((sum, item) => sum + item.quantity, 0); const cartBadge = document.getElementById('cartCount'); if (cartBadge) { cartBadge.textContent = count; cartBadge.style.display = count > 0 ? 'flex' : 'none'; } } async function loadProducts(page = 1) { const loading = document.getElementById('loading'); const error = document.getElementById('error'); const grid = document.getElementById('productsGrid'); try { loading.style.display = 'block'; error.style.display = 'none'; grid.innerHTML = ''; // Get user's selected region for currency conversion const selectedRegion = getSelectedRegion(); const headers = {}; if (selectedRegion) { headers['X-User-Region'] = selectedRegion.code; } const response = await fetch(`${API_BASE}/products?page=${page}&size=${pageSize}`, { headers }); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to load products'); } currentPage = page; // Convert prices if needed if (selectedRegion && data.products) { data.products = await convertProductPrices(data.products, selectedRegion.currency); } if (data.products.length === 0) { grid.innerHTML = '

No products available. Check back soon!

'; } else { data.products.forEach(product => { grid.appendChild(createProductCard(product)); }); } loading.style.display = 'none'; } catch (err) { loading.style.display = 'none'; error.style.display = 'block'; error.textContent = `Error: ${err.message}`; } } function createProductCard(product) { const card = document.createElement('div'); card.className = 'product-card'; const image = product.image || ''; const imageElement = image && image.trim() && (image.startsWith('http') || image.startsWith('/')) ? `${product.name}` : ''; const stockInfo = getStockInfo(product.stock); card.innerHTML = ` ${imageElement} ${!imageElement ? '
No Image
' : ''}

${escapeHtml(product.name)}

${escapeHtml(product.description || '')}

${product.currency} ${(product.price / 100).toFixed(2)}
${stockInfo}
`; return card; } function getStockInfo(stock) { if (stock === null) return ''; if (stock === 0) return '
Out of Stock
'; if (stock < 10) return `
Only ${stock} left
`; return `
${stock} in stock
`; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } async function addToCart(productId, element) { try { const sessionId = getOrCreateSessionId(); // Calculate new total quantity const currentCart = cart.get(); const existingItem = currentCart.find(i => i.productId === productId); const newQuantity = (existingItem ? existingItem.quantity : 0) + 1; // Try to reserve stock first const reserveResponse = await fetchWithCsrf(`${API_BASE}/cart/reserve`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-session-id': sessionId }, body: JSON.stringify({ productId, quantity: newQuantity }) }); const reserveData = await reserveResponse.json(); if (!reserveData.success) { throw new Error(reserveData.error || 'Failed to reserve stock'); } const response = await fetch(`${API_BASE}/products/${productId}`); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Product not found'); } const product = data.product; // Check stock if (product.stock !== null && product.stock <= 0) { showNotification('This product is out of stock', 'warning'); return; } cart.add(product, 1); // Show feedback const btn = element; const originalText = btn.textContent; btn.textContent = 'Added!'; btn.disabled = true; setTimeout(() => { btn.textContent = originalText; btn.disabled = false; }, 1000); showNotification('Product added to cart!', 'success', 3000); } catch (err) { showNotification(`Error: ${err.message}`, 'error'); throw err; } } let currentProduct = null; async function openProductModal(productId) { const modal = document.getElementById('productModal'); const content = document.getElementById('modalContent'); try { const response = await fetch(`${API_BASE}/products/${productId}`); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Product not found'); } currentProduct = data.product; content.innerHTML = createModalContent(data.product); modal.style.display = 'block'; // Re-attach close button after content update const closeBtn = modal.querySelector('.close'); if (closeBtn) { closeBtn.onclick = closeModal; } } catch (err) { showNotification(`Error: ${err.message}`, 'error'); } } function createModalContent(product) { const image = product.image || ''; const imageElement = image && image.trim() && (image.startsWith('http') || image.startsWith('/')) ? `${product.name}` : ''; return `

${escapeHtml(product.name)}

${imageElement} ${!imageElement ? '
No Image
' : ''}

${escapeHtml(product.description || '')}

${product.currency} ${(product.price / 100).toFixed(2)}
`; } async function addToCartFromModal(productId) { const quantity = parseInt(document.getElementById('productQuantity').value) || 1; try { const sessionId = getOrCreateSessionId(); // Calculate new total quantity const currentCart = cart.get(); const existingItem = currentCart.find(i => i.productId === productId); const newQuantity = (existingItem ? existingItem.quantity : 0) + quantity; // Try to reserve stock first const reserveResponse = await fetchWithCsrf(`${API_BASE}/cart/reserve`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-session-id': sessionId }, body: JSON.stringify({ productId, quantity: newQuantity }) }); const reserveData = await reserveResponse.json(); if (!reserveData.success) { throw new Error(reserveData.error || 'Failed to reserve stock'); } const response = await fetch(`${API_BASE}/products/${productId}`); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Product not found'); } const product = data.product; // Check stock if (product.stock !== null && product.stock < quantity) { showNotification(`Only ${product.stock} available in stock`, 'warning'); return; } cart.add(product, quantity); closeModal(); // Show notification showNotification(`Added ${quantity} item(s) to cart!`, 'success', 3000); } catch (err) { showNotification(`Error: ${err.message}`, 'error'); } } function setupModal() { const modal = document.getElementById('productModal'); const closeBtn = modal?.querySelector('.close'); if (closeBtn) { closeBtn.onclick = closeModal; } if (modal) { // Close on overlay click (outside modal content) modal.addEventListener('click', function(event) { if (event.target === modal) { closeModal(); } }); // Close on Escape key document.addEventListener('keydown', function(event) { if (event.key === 'Escape' && modal.style.display === 'block') { closeModal(); } }); } } function closeModal() { const modal = document.getElementById('productModal'); if (modal) { modal.style.display = 'none'; currentProduct = null; } } // Region Selector async function checkAndShowRegionSelector() { const selectedRegion = getSelectedRegion(); if (!selectedRegion) { // First visit, show region selector await showRegionSelector(); } else { // Update current region display updateRegionDisplay(selectedRegion); } } // Store regions globally for filtering let allRegions = []; async function showRegionSelector() { const modal = document.getElementById('regionSelectorModal'); const selector = document.getElementById('regionSelector'); modal.style.display = 'flex'; try { const response = await fetch(`${API_BASE}/regions/enabled`); const data = await response.json(); if (!data.success) { throw new Error('Failed to load regions'); } allRegions = data.regions; if (allRegions.length === 0) { selector.innerHTML = '

No regions available at the moment.

'; return; } // Sort regions alphabetically allRegions.sort((a, b) => a.countryName.localeCompare(b.countryName)); // Render the region selector with search renderRegionSelector(allRegions); } catch (err) { selector.innerHTML = `
Error: ${err.message}
`; } } async function renderRegionSelector(regions) { const selector = document.getElementById('regionSelector'); // SVG search icon const searchIcon = ` `; // Fetch flags for all regions const countryCodes = regions.map(r => r.countryCode); const flags = await fetchFlags(countryCodes); selector.innerHTML = `
${searchIcon}
${regions.map(region => createRegionOption(region, flags[region.countryCode])).join('')}
`; // Add search functionality const searchInput = document.getElementById('regionSearch'); searchInput.addEventListener('input', (e) => filterRegions(e.target.value)); // Focus search input setTimeout(() => searchInput.focus(), 100); } // Fetch flags from API async function fetchFlags(countryCodes) { try { const response = await fetch(`${API_BASE}/flags/batch`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ countryCodes }) }); const data = await response.json(); if (data.success) { return data.flags; } } catch (err) { console.error('Failed to fetch flags:', err); } return {}; } function createRegionOption(region, flagData) { const arrowSvg = ` `; // Use flag if available, otherwise fallback SVG const flagContent = flagData && flagData.startsWith('data:image') ? `${region.countryCode}` : flagData || ` `; return `
${flagContent}
${escapeHtml(region.countryName)}
${region.currencyCode} ยท ${region.languageCode.toUpperCase()}
${arrowSvg}
`; } function filterRegions(searchTerm) { const term = searchTerm.toLowerCase().trim(); const allOptions = document.querySelectorAll('.region-option'); const noResults = document.getElementById('noResults'); const regionList = document.getElementById('allRegionsList'); let visibleCount = 0; if (!term) { // Show all regions allOptions.forEach(option => option.style.display = 'flex'); noResults.style.display = 'none'; if (regionList) regionList.style.display = 'flex'; return; } // Filter regions allOptions.forEach(option => { const country = option.dataset.country || ''; const currency = option.dataset.currency || ''; const code = option.dataset.code || ''; const matches = country.includes(term) || currency.includes(term) || code.includes(term); if (matches) { option.style.display = 'flex'; visibleCount++; } else { option.style.display = 'none'; } }); // Show/hide no results message if (visibleCount === 0) { noResults.style.display = 'block'; if (regionList) regionList.style.display = 'none'; } else { noResults.style.display = 'none'; if (regionList) regionList.style.display = 'flex'; } } function getSelectedRegion() { const stored = localStorage.getItem('selectedRegion'); return stored ? JSON.parse(stored) : null; } async function convertProductPrices(products, targetCurrency) { if (!targetCurrency || targetCurrency === 'USD') { return products; // No conversion needed } // Get exchange rate try { const response = await fetch(`${API_BASE}/currency/rate/USD/${targetCurrency}`); const data = await response.json(); if (data.success && data.rate) { // Convert each product price return products.map(product => ({ ...product, price: Math.round(product.price * data.rate), currency: targetCurrency, originalPrice: product.price, originalCurrency: 'USD' })); } } catch (err) { console.error('Failed to convert prices:', err); } return products; } function selectRegion(code, name, language, currency) { const region = { code, name, language, currency }; localStorage.setItem('selectedRegion', JSON.stringify(region)); // Close modal document.getElementById('regionSelectorModal').style.display = 'none'; // Update display updateRegionDisplay(region); // Show notification showNotification(`Welcome! Shopping in ${name} (${currency})`, 'success', 4000); // Reload products with new region loadProducts(1); } function updateRegionDisplay(region) { const display = document.getElementById('currentRegion'); if (display) { display.textContent = `${region.name} (${region.currency})`; } } // Make functions available globally window.cart = cart; window.addToCart = addToCart; window.openProductModal = openProductModal; window.addToCartFromModal = addToCartFromModal; window.closeModal = closeModal; window.showRegionSelector = showRegionSelector; window.selectRegion = selectRegion; window.loadProducts = loadProducts;