init projectmycelium
This commit is contained in:
357
src/static/js/cart.js
Normal file
357
src/static/js/cart.js
Normal file
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
Cart interactions (CSP compliant)
|
||||
- Parses hydration JSON from #cart-hydration
|
||||
- Binds all cart-related events (increase/decrease qty, remove, clear, currency change)
|
||||
- Handles guest checkout modal buttons
|
||||
- Shows toasts via createToast helpers here
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
let hydration = {
|
||||
item_count: 0,
|
||||
redirect_login_url: '/login?checkout=true',
|
||||
redirect_register_url: '/register?checkout=true',
|
||||
redirect_after_auth: '/cart'
|
||||
};
|
||||
|
||||
function readHydration() {
|
||||
try {
|
||||
const el = document.getElementById('cart-hydration');
|
||||
if (!el) return;
|
||||
const parsed = JSON.parse(el.textContent || '{}');
|
||||
hydration = Object.assign(hydration, parsed || {});
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse cart hydration JSON', e);
|
||||
}
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
const overlay = document.getElementById('loadingOverlay');
|
||||
if (overlay) overlay.classList.remove('d-none');
|
||||
}
|
||||
function hideLoading() {
|
||||
const overlay = document.getElementById('loadingOverlay');
|
||||
if (overlay) overlay.classList.add('d-none');
|
||||
}
|
||||
|
||||
function createToast(message, type, icon) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast align-items-center text-white bg-${type} border-0 position-fixed end-0 m-3`;
|
||||
toast.style.top = '80px';
|
||||
toast.style.zIndex = '10000';
|
||||
toast.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<i class="bi ${icon} me-2"></i>${message}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(toast);
|
||||
if (window.bootstrap && window.bootstrap.Toast) {
|
||||
const bsToast = new window.bootstrap.Toast(toast);
|
||||
bsToast.show();
|
||||
toast.addEventListener('hidden.bs.toast', () => toast.remove());
|
||||
}
|
||||
}
|
||||
function showError(message) { createToast(message, 'danger', 'bi-exclamation-triangle'); }
|
||||
function showSuccess(message) { createToast(message, 'success', 'bi-check-circle'); }
|
||||
|
||||
async function parseResponse(response) {
|
||||
let json = {};
|
||||
try { json = await response.json(); } catch (_) {}
|
||||
const payload = (json && (json.data || json)) || {};
|
||||
const success = (typeof json.success === 'boolean') ? json.success : (typeof payload.success === 'boolean' ? payload.success : response.ok);
|
||||
return { json, payload, success };
|
||||
}
|
||||
|
||||
async function updateQuantity(productId, newQuantity) {
|
||||
if (newQuantity < 1) {
|
||||
await removeFromCart(productId);
|
||||
return;
|
||||
}
|
||||
showLoading();
|
||||
try {
|
||||
await window.apiJson(`/api/cart/item/${encodeURIComponent(productId)}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ quantity: newQuantity })
|
||||
});
|
||||
{
|
||||
showSuccess('Quantity updated successfully');
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
}
|
||||
} catch (e) {
|
||||
showError(e && e.message ? e.message : 'Network error occurred');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
let productIdToRemove = null;
|
||||
|
||||
function showClearCartModal() {
|
||||
const el = document.getElementById('clearCartModal');
|
||||
if (!el || !window.bootstrap) return;
|
||||
const modal = new window.bootstrap.Modal(el);
|
||||
modal.show();
|
||||
}
|
||||
function showRemoveItemModal(productId) {
|
||||
productIdToRemove = productId;
|
||||
const el = document.getElementById('removeItemModal');
|
||||
if (!el || !window.bootstrap) return;
|
||||
const modal = new window.bootstrap.Modal(el);
|
||||
modal.show();
|
||||
}
|
||||
async function confirmRemoveItem() {
|
||||
if (!productIdToRemove) return;
|
||||
const el = document.getElementById('removeItemModal');
|
||||
if (el && window.bootstrap) {
|
||||
const modal = window.bootstrap.Modal.getInstance(el);
|
||||
if (modal) modal.hide();
|
||||
}
|
||||
await removeFromCart(productIdToRemove);
|
||||
productIdToRemove = null;
|
||||
}
|
||||
|
||||
async function removeFromCart(productId) {
|
||||
showLoading();
|
||||
try {
|
||||
await window.apiJson(`/api/cart/item/${encodeURIComponent(productId)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
{
|
||||
showSuccess('Item removed from cart');
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
}
|
||||
} catch (e) {
|
||||
showError(e && e.message ? e.message : 'Network error occurred');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
async function clearCart() {
|
||||
const clearEl = document.getElementById('clearCartModal');
|
||||
if (clearEl && window.bootstrap) {
|
||||
const modal = window.bootstrap.Modal.getInstance(clearEl);
|
||||
if (modal) modal.hide();
|
||||
}
|
||||
showLoading();
|
||||
try {
|
||||
await window.apiJson('/api/cart', { method: 'DELETE' });
|
||||
{
|
||||
showSuccess('Cart cleared successfully');
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
}
|
||||
} catch (e) {
|
||||
showError(e && e.message ? e.message : 'Network error occurred');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
async function changeCurrency() {
|
||||
const currencySelector = document.getElementById('currencySelector');
|
||||
if (!currencySelector) return;
|
||||
const selectedCurrency = currencySelector.value;
|
||||
showLoading();
|
||||
try {
|
||||
await window.apiJson('/api/user/currency', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ currency: selectedCurrency })
|
||||
});
|
||||
{
|
||||
showSuccess('Currency updated');
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
}
|
||||
} catch (e) {
|
||||
showError(e && e.message ? e.message : 'Network error occurred');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
function saveForLater() {
|
||||
showSuccess('Cart saved for later');
|
||||
}
|
||||
function shareCart() {
|
||||
const shareData = {
|
||||
title: 'My ThreeFold Cart',
|
||||
text: 'Check out my ThreeFold marketplace cart',
|
||||
url: window.location.href
|
||||
};
|
||||
if (navigator.share) {
|
||||
navigator.share(shareData).catch(() => {});
|
||||
return;
|
||||
}
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(window.location.href)
|
||||
.then(() => showSuccess('Cart link copied to clipboard'))
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh cart contents after small delay (used by recommended add)
|
||||
function refreshCartContents() {
|
||||
setTimeout(() => { window.location.reload(); }, 500);
|
||||
}
|
||||
|
||||
// Initialize recommended products add-to-cart buttons
|
||||
function initializeRecommendedProducts() {
|
||||
document.querySelectorAll('.add-recommended-btn').forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
const productId = this.getAttribute('data-product-id');
|
||||
const productName = this.getAttribute('data-product-name') || 'Product';
|
||||
const productCategory = this.getAttribute('data-product-category') || '';
|
||||
addRecommendedToCart(productId, productName, productCategory, this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function addRecommendedToCart(productId, productName, productCategory, buttonEl) {
|
||||
if (!productId || !buttonEl) return;
|
||||
const original = buttonEl.innerHTML;
|
||||
buttonEl.disabled = true;
|
||||
buttonEl.innerHTML = '<i class="bi bi-hourglass-split me-1"></i>Adding...';
|
||||
try {
|
||||
await window.apiJson('/api/cart/add', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ product_id: productId, quantity: 1, specifications: {} })
|
||||
});
|
||||
buttonEl.innerHTML = '<i class="bi bi-check me-1"></i>Added!';
|
||||
buttonEl.classList.remove('btn-primary', 'btn-success', 'btn-info', 'btn-warning');
|
||||
buttonEl.classList.add('btn-success');
|
||||
createToast(`${productName} added to cart!`, 'success', 'bi-check-circle');
|
||||
if (typeof window.updateCartCount === 'function') window.updateCartCount();
|
||||
try { if (typeof window.emitCartUpdated === 'function') window.emitCartUpdated(undefined); } catch (_) {}
|
||||
updateMyOrdersVisibility();
|
||||
refreshCartContents();
|
||||
setTimeout(() => {
|
||||
buttonEl.innerHTML = original;
|
||||
buttonEl.disabled = false;
|
||||
buttonEl.classList.remove('btn-success');
|
||||
if (productCategory === 'compute') buttonEl.classList.add('btn-primary');
|
||||
else if (productCategory === 'storage') buttonEl.classList.add('btn-success');
|
||||
else if (productCategory === 'gateways') buttonEl.classList.add('btn-info');
|
||||
else if (productCategory === 'applications') buttonEl.classList.add('btn-warning');
|
||||
else buttonEl.classList.add('btn-primary');
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
console.error('Error adding recommended product:', e);
|
||||
if (e && e.status === 402) {
|
||||
return;
|
||||
}
|
||||
buttonEl.innerHTML = '<i class="bi bi-exclamation-triangle me-1"></i>Error';
|
||||
buttonEl.classList.add('btn-danger');
|
||||
createToast(`Failed to add ${productName} to cart. Please try again.`, 'danger', 'bi-exclamation-triangle');
|
||||
setTimeout(() => {
|
||||
buttonEl.innerHTML = original;
|
||||
buttonEl.disabled = false;
|
||||
buttonEl.classList.remove('btn-danger');
|
||||
if (productCategory === 'compute') buttonEl.classList.add('btn-primary');
|
||||
else if (productCategory === 'storage') buttonEl.classList.add('btn-success');
|
||||
else if (productCategory === 'gateways') buttonEl.classList.add('btn-info');
|
||||
else if (productCategory === 'applications') buttonEl.classList.add('btn-warning');
|
||||
else buttonEl.classList.add('btn-primary');
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle visibility of My Orders link based on API
|
||||
async function updateMyOrdersVisibility() {
|
||||
const myOrdersLink = document.getElementById('myOrdersLink');
|
||||
if (!myOrdersLink) return;
|
||||
try {
|
||||
const data = await window.apiJson('/api/orders');
|
||||
const hasOrders = !!(data && Array.isArray(data.orders) && data.orders.length > 0);
|
||||
myOrdersLink.style.display = hasOrders ? 'inline-block' : 'none';
|
||||
} catch (e) {
|
||||
myOrdersLink.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
// Quantity controls
|
||||
document.querySelectorAll('[data-action="increase"], [data-action="decrease"]').forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
const productId = this.getAttribute('data-product-id');
|
||||
const action = this.getAttribute('data-action');
|
||||
const qtySpan = this.parentElement.querySelector('span');
|
||||
const currentQuantity = parseInt(qtySpan && qtySpan.textContent, 10) || 0;
|
||||
if (action === 'increase') updateQuantity(productId, currentQuantity + 1);
|
||||
else if (action === 'decrease') updateQuantity(productId, currentQuantity - 1);
|
||||
});
|
||||
});
|
||||
|
||||
// Remove buttons
|
||||
document.querySelectorAll('[data-action="remove"]').forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
const productId = this.getAttribute('data-product-id');
|
||||
showRemoveItemModal(productId);
|
||||
});
|
||||
});
|
||||
|
||||
// Clear cart
|
||||
const clearCartBtn = document.getElementById('clearCartBtn');
|
||||
if (clearCartBtn) clearCartBtn.addEventListener('click', showClearCartModal);
|
||||
|
||||
const confirmClearCartBtn = document.getElementById('confirmClearCartBtn');
|
||||
if (confirmClearCartBtn) confirmClearCartBtn.addEventListener('click', clearCart);
|
||||
|
||||
const confirmRemoveItemBtn = document.getElementById('confirmRemoveItemBtn');
|
||||
if (confirmRemoveItemBtn) confirmRemoveItemBtn.addEventListener('click', confirmRemoveItem);
|
||||
|
||||
// Currency selector
|
||||
const currencySelector = document.getElementById('currencySelector');
|
||||
if (currencySelector) currencySelector.addEventListener('change', changeCurrency);
|
||||
|
||||
// Extra actions
|
||||
document.querySelectorAll('[data-action="save-for-later"]').forEach(btn => btn.addEventListener('click', saveForLater));
|
||||
document.querySelectorAll('[data-action="share-cart"]').forEach(btn => btn.addEventListener('click', shareCart));
|
||||
|
||||
// Guest checkout modal buttons
|
||||
const loginBtn = document.getElementById('guestLoginBtn');
|
||||
const registerBtn = document.getElementById('guestRegisterBtn');
|
||||
if (loginBtn) {
|
||||
loginBtn.addEventListener('click', function () {
|
||||
try { sessionStorage.setItem('redirectAfterLogin', hydration.redirect_after_auth || '/cart'); } catch (_) {}
|
||||
window.location.href = hydration.redirect_login_url || '/login?checkout=true';
|
||||
});
|
||||
}
|
||||
if (registerBtn) {
|
||||
registerBtn.addEventListener('click', function () {
|
||||
try { sessionStorage.setItem('redirectAfterRegister', hydration.redirect_after_auth || '/cart'); } catch (_) {}
|
||||
window.location.href = hydration.redirect_register_url || '/register?checkout=true';
|
||||
});
|
||||
}
|
||||
|
||||
// Checkout CTA (guest) fallback: open modal if button present
|
||||
const checkoutBtn = document.getElementById('checkoutBtn');
|
||||
if (checkoutBtn) {
|
||||
checkoutBtn.addEventListener('click', function () {
|
||||
const el = document.getElementById('guestCheckoutModal');
|
||||
if (el && window.bootstrap) {
|
||||
const modal = new window.bootstrap.Modal(el);
|
||||
modal.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
readHydration();
|
||||
bindEvents();
|
||||
initializeRecommendedProducts();
|
||||
updateMyOrdersVisibility();
|
||||
});
|
||||
})();
|
Reference in New Issue
Block a user