Commit: 4617f76

Commit Details

SHA4617f76246bf039920879ae5497436d31494ed7c
Treee78bcb155880b8c167af124554a6db724c41264e
Author<f69e50@finnacloud.com> 1766442953 +0300
Committer<f69e50@finnacloud.com> 1766442953 +0300
Message
rename branch from main to master oops
GPG Signature
-----BEGIN PGP SIGNATURE-----

iQJSBAABCAA8FiEEWJb139mJI+vZ81KkoAIVSUsXI0oFAmlJx8keHHNvcGhpYS5l
cmFzbGFuQGZpbm5hY2xvdWQuY29tAAoJEKACFUlLFyNK2BQP/3EfkXtKsmQoqa4E
e9jdO5BKWUnHwF31PdpxExXwoogkZg2hSjSc0jB9htAKSbQspx+Pst7f9yj2Gf2u
ENGTEQHqqVeLEve2IPc1YJ+F+yedI3NE2PCZJ0+rh1/S14vQTT0kWMSqs6d8Te4K
x4hiTNNjfWidOXQ1vHMXl9iUnevmnko8XqNe3aBZ3JUSRLhhCvehsiPwSjfKGqB8
Sm9i1Y/HaTizFfl4WG5f6MppDgzV2I7Bm/c6K1oDIviO/Wken5vk4TXgLUWDfHJ5
d9m9gh4N9unX4Ivf5G22JVRzxPgox0Y0yFQwpj4IqQ9LzjT2Vz2s+hXoV6HZkfUW
BCLc/6oRmImcSflxOgV/TGaZrvysQ0pz32H8lyLoOI1QAS3o4u1icPFjfHlc5298
zpjGAqPjlVEx4Sjghrow4pb7sb/OyFihTwfjxlLgVfR4tXfb/l5rRt2f9vsm1HJB
qc9F/Qqc6xpb9ECyYZtuuCMToS0MRNkqy2KifrvOaKi2EznhcJu2VcLmjQJookFY
meevIkRenUvjwbs+aoDOa76HmycWkw1NtTtE9M+NSZ9SCBOD+Gp1rqO66z758bUQ
NmISh50GPDiZw5AKDrtsygWRZumLbrWHBleHm935V5O3gYkEKo9kb40Zf2sDsvlS
/wXQCTVz3XuSby3pyaMGgd3ol6gk
=YOD1
-----END PGP SIGNATURE-----

✓ Verified

File: src/main/resources/static/js/cart.js

1 const API_BASE = '/api';
2
3 function getOrCreateSessionId() {
4 let sessionId = localStorage.getItem('cart_session_id');
5 if (!sessionId) {
6 sessionId = 'sess_' + Math.random().toString(36).substr(2, 9) + Date.now().toString(36);
7 localStorage.setItem('cart_session_id', sessionId);
8 }
9 return sessionId;
10 }
11
12 function getSelectedRegion() {
13 const stored = localStorage.getItem('selectedRegion');
14 return stored ? JSON.parse(stored) : null;
15 }
16
17 async function getExchangeRate(fromCurrency, toCurrency) {
18 if (fromCurrency === toCurrency) {
19 return 1.0;
20 }
21
22 try {
23 const response = await fetch(`${API_BASE}/currency/rate/${fromCurrency}/${toCurrency}`);
24 const data = await response.json();
25
26 if (data.success && data.rate) {
27 return data.rate;
28 }
29 } catch (err) {
30 console.error('Failed to get exchange rate:', err);
31 }
32
33 return 1.0; // Fallback
34 }
35
36
37 // Cart management (same as app.js)
38 const cart = {
39 get() {
40 try {
41 return JSON.parse(localStorage.getItem('cart') || '[]');
42 } catch {
43 return [];
44 }
45 },
46 set(items) {
47 localStorage.setItem('cart', JSON.stringify(items));
48 updateCartCount();
49 },
50 update(productId, quantity) {
51 const items = this.get();
52 // Convert both to strings for comparison to handle type mismatches
53 const item = items.find(i => String(i.productId) === String(productId));
54 if (item) {
55 if (quantity <= 0) {
56 this.remove(productId);
57 } else {
58 item.quantity = quantity;
59 this.set(items);
60 }
61 }
62 },
63 remove(productId) {
64 const items = this.get().filter(item => String(item.productId) !== String(productId));
65 this.set(items);
66 },
67 clear() {
68 this.set([]);
69 }
70 };
71
72 function updateCartCount() {
73 const count = cart.get().reduce((sum, item) => sum + item.quantity, 0);
74 const cartBadge = document.getElementById('cartCount');
75 if (cartBadge) {
76 cartBadge.textContent = count;
77 cartBadge.style.display = count > 0 ? 'flex' : 'none';
78 }
79 }
80
81 async function renderCart() {
82 const content = document.getElementById('cartContent');
83 const items = cart.get();
84
85 if (items.length === 0) {
86 content.innerHTML = `
87 <div class="empty-cart">
88 <div class="empty-cart-icon">🛒</div>
89 <h3>Your cart is empty</h3>
90 <p>Add some products to get started</p>
91 <a href="/" class="btn btn-success">Continue Shopping</a>
92 </div>
93 `;
94 return;
95 }
96
97 // Get user's selected region for currency
98 const selectedRegion = getSelectedRegion();
99 const targetCurrency = selectedRegion ? selectedRegion.currency : 'USD';
100
101 // Get exchange rate once for all products
102 const exchangeRate = await getExchangeRate('USD', targetCurrency);
103
104 // Fetch product details and calculate totals (all in cents)
105 // Use current prices from API, not stored prices
106 let subtotal = 0;
107 const itemsHtml = await Promise.all(items.map(async (item) => {
108 try {
109 const headers = {};
110 if (selectedRegion) {
111 headers['X-User-Region'] = selectedRegion.code;
112 }
113
114 const response = await fetch(`${API_BASE}/products/${item.productId}`, { headers });
115 const data = await response.json();
116
117 if (data.success) {
118 const product = data.product;
119
120 // Convert price to target currency
121 const currentPriceUSD = product.price; // In cents
122 const currentPrice = Math.round(currentPriceUSD * exchangeRate); // Converted to target currency
123 const itemTotal = currentPrice * item.quantity;
124 subtotal += itemTotal;
125
126 const image = product.image || '';
127 const imageElement = image && image.trim() && (image.startsWith('http') || image.startsWith('/'))
128 ? `<img src="${image}" alt="${product.name}" class="cart-item-image" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">`
129 : '';
130
131 return `
132 <div class="cart-item">
133 ${imageElement || '<div class="cart-item-image">No Image</div>'}
134 <div class="cart-item-info">
135 <div class="cart-item-name">${escapeHtml(product.name)}</div>
136 <div class="cart-item-price">${targetCurrency} ${(currentPrice / 100).toFixed(2)} each</div>
137 <div class="cart-item-quantity">
138 <label>Quantity</label>
139 <input type="number" class="quantity-input" min="1" value="${item.quantity}"
140 oninput="updateQuantity('${item.productId}', this.value)">
141 <button class="btn" onclick="removeItem('${item.productId}')">Remove</button>
142 </div>
143 </div>
144 <div class="cart-item-total">
145 ${targetCurrency} ${(itemTotal / 100).toFixed(2)}
146 </div>
147 </div>
148 `;
149 }
150 } catch (error) {
151 console.error('Error loading product:', error);
152 return '';
153 }
154 }));
155
156 // Tax cannot be calculated without country - show as uncalculated
157 const shipping = 0; // Can be calculated based on shipping info (in cents)
158 const tax = 0; // Tax cannot be calculated without country
159 const total = subtotal + shipping; // Tax will be calculated at checkout
160
161 content.innerHTML = `
162 <div>
163 <div class="cart-items">
164 ${itemsHtml.join('')}
165 </div>
166
167 <div class="cart-summary">
168 <h3>Order Summary</h3>
169 <div class="summary-row">
170 <span>Subtotal</span>
171 <span id="cart-subtotal">${targetCurrency} ${(subtotal / 100).toFixed(2)}</span>
172 </div>
173 <div class="summary-row">
174 <span>Shipping</span>
175 <span>${targetCurrency} ${(shipping / 100).toFixed(2)}</span>
176 </div>
177 <div class="summary-row">
178 <span>Tax</span>
179 <span id="cart-tax">Uncalculated</span>
180 </div>
181 <div class="summary-row total">
182 <span>Total</span>
183 <span id="cart-total">${targetCurrency} ${(total / 100).toFixed(2)}</span>
184 </div>
185 <a href="/checkout" class="btn btn-success">
186 Proceed to Checkout
187 </a>
188 <a href="/" class="btn">
189 Continue Shopping
190 </a>
191 </div>
192 </div>
193 `;
194 }
195
196 // Debounce timer for quantity updates
197 let updateQuantityTimer = null;
198
199 async function updateQuantity(productId, quantity) {
200 const qty = parseInt(quantity) || 1;
201
202 // Update localStorage immediately
203 cart.update(productId, qty);
204
205 // Re-render immediately to show the updated quantity and price
206 await renderCart();
207
208 // Clear existing timer for API call
209 if (updateQuantityTimer) {
210 clearTimeout(updateQuantityTimer);
211 }
212
213 // Debounce the API call for stock reservation
214 updateQuantityTimer = setTimeout(async () => {
215 try {
216 const sessionId = getOrCreateSessionId();
217 // Try to reserve stock
218 const response = await fetchWithCsrf(`${API_BASE}/cart/reserve`, {
219 method: 'POST',
220 headers: {
221 'Content-Type': 'application/json',
222 'x-session-id': sessionId
223 },
224 body: JSON.stringify({ productId, quantity: qty })
225 });
226
227 const data = await response.json();
228 if (!data.success) {
229 showNotification(data.error || 'Failed to update quantity', 'error');
230 // Reload cart to reset to available stock
231 await renderCart();
232 }
233 } catch (error) {
234 showNotification(`Error: ${error.message}`, 'error');
235 }
236 }, 500); // Wait 500ms after last change before making API call
237 }
238
239 async function removeItem(productId) {
240 const confirmed = await showConfirm('Remove this item from cart?', 'Remove Item');
241 if (confirmed) {
242 try {
243 const sessionId = getOrCreateSessionId();
244 await fetchWithCsrf(`${API_BASE}/cart/release`, {
245 method: 'POST',
246 headers: {
247 'Content-Type': 'application/json',
248 'x-session-id': sessionId
249 },
250 body: JSON.stringify({ productId })
251 });
252 } catch (error) {
253 console.error('Error releasing stock:', error);
254 }
255
256 cart.remove(productId);
257 await renderCart();
258 showNotification('Item removed from cart', 'success', 3000);
259 }
260 }
261
262 function escapeHtml(text) {
263 const div = document.createElement('div');
264 div.textContent = text;
265 return div.innerHTML;
266 }
267
268 // Initialize
269 document.addEventListener('DOMContentLoaded', () => {
270 updateCartCount();
271 renderCart();
272 refreshReservations();
273 });
274
275 async function refreshReservations() {
276 const items = cart.get();
277 if (items.length === 0) return;
278
279 const sessionId = getOrCreateSessionId();
280
281 // Refresh reservations for all items in cart
282 // We don't block UI for this, just try to secure the stock
283 items.forEach(item => {
284 fetchWithCsrf(`${API_BASE}/cart/reserve`, {
285 method: 'POST',
286 headers: {
287 'Content-Type': 'application/json',
288 'x-session-id': sessionId
289 },
290 body: JSON.stringify({ productId: item.productId, quantity: item.quantity })
291 }).catch(console.error);
292 });
293 }
294
295
296 window.updateQuantity = updateQuantity;
297 window.removeItem = removeItem;
298