Files
projectmycelium/src/static/js/cart.js
2025-09-01 21:37:01 -04:00

358 lines
13 KiB
JavaScript

/*
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();
});
})();