358 lines
13 KiB
JavaScript
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();
|
|
});
|
|
})();
|