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('/'))
? `
`
: '';
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('/'))
? `
`
: '';
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 = `
${regions.map(region => createRegionOption(region, flags[region.countryCode])).join('')}
No regions found matching your search
Try searching by country name or currency code
`;
// 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')
? `
`
: 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;