422 lines
12 KiB
JavaScript
422 lines
12 KiB
JavaScript
/**
|
|
* Shopping Cart Component
|
|
* Handles cart dropdown, updates, and interactions
|
|
*/
|
|
|
|
(function () {
|
|
"use strict";
|
|
|
|
// Base Dropdown Component
|
|
class BaseDropdown {
|
|
constructor(config) {
|
|
this.toggleBtn = document.getElementById(config.toggleId);
|
|
this.panel = document.getElementById(config.panelId);
|
|
this.content = document.getElementById(config.contentId);
|
|
this.closeBtn = document.getElementById(config.closeId);
|
|
this.wrapperClass = config.wrapperClass;
|
|
this.eventName = config.eventName;
|
|
this.emptyMessage = config.emptyMessage;
|
|
this.isOpen = false;
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.setupEventListeners();
|
|
this.render();
|
|
}
|
|
|
|
setupEventListeners() {
|
|
if (this.toggleBtn) {
|
|
this.toggleBtn.addEventListener("click", () => this.toggle());
|
|
}
|
|
|
|
if (this.closeBtn) {
|
|
this.closeBtn.addEventListener("click", () => this.close());
|
|
}
|
|
|
|
document.addEventListener("click", (e) => {
|
|
if (this.isOpen && !e.target.closest(this.wrapperClass)) {
|
|
this.close();
|
|
}
|
|
});
|
|
|
|
window.addEventListener(this.eventName, () => {
|
|
console.log(`[${this.constructor.name}] ${this.eventName} received`);
|
|
this.render();
|
|
});
|
|
}
|
|
|
|
toggle() {
|
|
this.isOpen ? this.close() : this.open();
|
|
}
|
|
|
|
open() {
|
|
if (this.panel) {
|
|
this.panel.classList.add("active");
|
|
this.panel.setAttribute("aria-hidden", "false");
|
|
this.isOpen = true;
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
close() {
|
|
if (this.panel) {
|
|
this.panel.classList.remove("active");
|
|
this.panel.setAttribute("aria-hidden", "true");
|
|
this.isOpen = false;
|
|
}
|
|
}
|
|
|
|
renderEmpty() {
|
|
if (this.content) {
|
|
this.content.innerHTML = this.emptyMessage;
|
|
}
|
|
}
|
|
}
|
|
|
|
class ShoppingCart extends BaseDropdown {
|
|
constructor() {
|
|
super({
|
|
toggleId: "cartToggle",
|
|
panelId: "cartPanel",
|
|
contentId: "cartContent",
|
|
closeId: "cartClose",
|
|
wrapperClass: ".cart-dropdown-wrapper",
|
|
eventName: "cart-updated",
|
|
emptyMessage:
|
|
'<p class="empty-state"><i class="bi bi-cart-x"></i><br>Your cart is empty</p>',
|
|
});
|
|
}
|
|
|
|
render() {
|
|
if (!this.content) return;
|
|
|
|
try {
|
|
if (!window.AppState) {
|
|
return;
|
|
}
|
|
|
|
const cart = window.AppState.cart;
|
|
|
|
if (!Array.isArray(cart)) {
|
|
this.content.innerHTML =
|
|
'<p class="empty-state">Error loading cart</p>';
|
|
return;
|
|
}
|
|
|
|
if (cart.length === 0) {
|
|
this.renderEmpty();
|
|
this.updateFooter(null);
|
|
return;
|
|
}
|
|
|
|
const validItems = this._filterValidItems(cart);
|
|
if (validItems.length === 0) {
|
|
this.renderEmpty();
|
|
this.updateFooter(null);
|
|
return;
|
|
}
|
|
|
|
this.content.innerHTML = validItems
|
|
.map((item) => this.renderCartItem(item))
|
|
.join("");
|
|
this.setupCartItemListeners();
|
|
|
|
const total = this._calculateTotal(validItems);
|
|
this.updateFooter(total);
|
|
} catch (error) {
|
|
this.content.innerHTML =
|
|
'<p class="empty-state">Error loading cart</p>';
|
|
}
|
|
}
|
|
|
|
_filterValidItems(items) {
|
|
return items.filter(
|
|
(item) => item && item.id && typeof item.price !== "undefined"
|
|
);
|
|
}
|
|
|
|
_calculateTotal(items) {
|
|
if (window.AppState.getCartTotal) {
|
|
return window.AppState.getCartTotal();
|
|
}
|
|
return items.reduce((sum, item) => {
|
|
const price = parseFloat(item.price) || 0;
|
|
const quantity = parseInt(item.quantity) || 0;
|
|
return sum + price * quantity;
|
|
}, 0);
|
|
}
|
|
|
|
renderCartItem(item) {
|
|
try {
|
|
// Validate item and Utils availability
|
|
if (!item || !item.id) {
|
|
return "";
|
|
}
|
|
|
|
if (!window.Utils) {
|
|
return '<p class="error-message">Error loading item</p>';
|
|
}
|
|
|
|
// Sanitize and validate item data with defensive checks
|
|
const imageUrl =
|
|
item.image ||
|
|
item.imageurl ||
|
|
item.imageUrl ||
|
|
item.image_url ||
|
|
"/assets/images/placeholder.svg";
|
|
const title = window.Utils.escapeHtml(
|
|
item.title || item.name || "Product"
|
|
);
|
|
const color = item.color ? window.Utils.escapeHtml(item.color) : null;
|
|
const price = parseFloat(item.price) || 0;
|
|
const quantity = Math.max(1, parseInt(item.quantity) || 1);
|
|
const subtotal = price * quantity;
|
|
|
|
const priceFormatted = window.Utils.formatCurrency(price);
|
|
const subtotalFormatted = window.Utils.formatCurrency(subtotal);
|
|
|
|
return `
|
|
<div class="cart-item" data-id="${item.id}">
|
|
<img src="${imageUrl}" alt="${title}" class="cart-item-image" loading="lazy" onerror="this.src='/assets/images/placeholder.svg'">
|
|
<div class="cart-item-details">
|
|
<h4 class="cart-item-title">${title}</h4>
|
|
${
|
|
color
|
|
? `<p class="cart-item-color" style="font-size: 0.85rem; color: #666; margin: 2px 0;">Color: ${color}</p>`
|
|
: ""
|
|
}
|
|
<p class="cart-item-price">${priceFormatted}</p>
|
|
<div class="cart-item-quantity">
|
|
<button class="quantity-btn quantity-minus" data-id="${
|
|
item.id
|
|
}" aria-label="Decrease quantity">
|
|
<i class="bi bi-dash"></i>
|
|
</button>
|
|
<span class="quantity-value">${quantity}</span>
|
|
<button class="quantity-btn quantity-plus" data-id="${
|
|
item.id
|
|
}" aria-label="Increase quantity">
|
|
<i class="bi bi-plus"></i>
|
|
</button>
|
|
</div>
|
|
<p class="cart-item-subtotal">Subtotal: ${subtotalFormatted}</p>
|
|
</div>
|
|
<button class="cart-item-remove" data-id="${
|
|
item.id
|
|
}" aria-label="Remove from cart">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
setupCartItemListeners() {
|
|
try {
|
|
this._setupRemoveButtons();
|
|
this._setupQuantityButtons();
|
|
} catch (error) {
|
|
console.error("[ShoppingCart] Error setting up listeners:", error);
|
|
}
|
|
}
|
|
|
|
_setupRemoveButtons() {
|
|
this.content.querySelectorAll(".cart-item-remove").forEach((btn) => {
|
|
btn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
this._handleAction(e, () => {
|
|
const id = e.currentTarget.dataset.id;
|
|
if (id && window.AppState?.removeFromCart) {
|
|
window.AppState.removeFromCart(id);
|
|
this.render();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
_setupQuantityButtons() {
|
|
this._setupQuantityButton(".quantity-minus", -1);
|
|
this._setupQuantityButton(".quantity-plus", 1);
|
|
}
|
|
|
|
_setupQuantityButton(selector, delta) {
|
|
this.content.querySelectorAll(selector).forEach((btn) => {
|
|
btn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
this._handleAction(e, () => {
|
|
const id = e.currentTarget.dataset.id;
|
|
if (!window.AppState?.cart) return;
|
|
|
|
const item = window.AppState.cart.find(
|
|
(item) => String(item.id) === String(id)
|
|
);
|
|
|
|
if (!item || !window.AppState.updateCartQuantity) return;
|
|
|
|
const newQuantity =
|
|
delta > 0
|
|
? Math.min(item.quantity + delta, 999)
|
|
: Math.max(item.quantity + delta, 1);
|
|
|
|
if (delta < 0 && item.quantity <= 1) return;
|
|
|
|
window.AppState.updateCartQuantity(id, newQuantity);
|
|
this.render();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
_handleAction(event, callback) {
|
|
try {
|
|
callback();
|
|
} catch (error) {
|
|
console.error("[ShoppingCart] Action error:", error);
|
|
}
|
|
}
|
|
|
|
updateFooter(total) {
|
|
const footer = this.cartPanel?.querySelector(".dropdown-foot");
|
|
if (!footer) return;
|
|
|
|
if (total === null) {
|
|
footer.innerHTML =
|
|
'<a href="/shop" class="btn-outline">Continue Shopping</a>';
|
|
} else {
|
|
footer.innerHTML = `
|
|
<a href="/shop" class="btn-outline">Continue Shopping</a>
|
|
`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Wishlist Component
|
|
class Wishlist extends BaseDropdown {
|
|
constructor() {
|
|
super({
|
|
toggleId: "wishlistToggle",
|
|
panelId: "wishlistPanel",
|
|
contentId: "wishlistContent",
|
|
closeId: "wishlistClose",
|
|
wrapperClass: ".wishlist-dropdown-wrapper",
|
|
eventName: "wishlist-updated",
|
|
emptyMessage:
|
|
'<p class="empty-state"><i class="bi bi-heart"></i><br>Your wishlist is empty</p>',
|
|
});
|
|
}
|
|
|
|
render() {
|
|
if (!this.content) return;
|
|
|
|
if (!window.AppState) {
|
|
console.warn("[Wishlist] AppState not available yet");
|
|
return;
|
|
}
|
|
|
|
const wishlist = window.AppState.wishlist;
|
|
|
|
if (wishlist.length === 0) {
|
|
this.renderEmpty();
|
|
return;
|
|
}
|
|
|
|
this.content.innerHTML = wishlist
|
|
.map((item) => this.renderWishlistItem(item))
|
|
.join("");
|
|
|
|
this.setupWishlistItemListeners();
|
|
}
|
|
|
|
renderWishlistItem(item) {
|
|
if (!window.Utils) {
|
|
console.error("[Wishlist] Utils not available");
|
|
return '<p class="error-message">Error loading item</p>';
|
|
}
|
|
|
|
const imageUrl =
|
|
item.imageurl ||
|
|
item.imageUrl ||
|
|
item.image_url ||
|
|
"/assets/images/placeholder.jpg";
|
|
const title = window.Utils.escapeHtml(
|
|
item.title || item.name || "Product"
|
|
);
|
|
const price = window.Utils.formatCurrency(parseFloat(item.price) || 0);
|
|
|
|
return `
|
|
<div class="wishlist-item" data-id="${item.id}">
|
|
<img src="${imageUrl}" alt="${title}" class="wishlist-item-image" loading="lazy" onerror="this.src='/assets/images/placeholder.svg'">
|
|
<div class="wishlist-item-details">
|
|
<h4 class="wishlist-item-title">${title}</h4>
|
|
<p class="wishlist-item-price">${price}</p>
|
|
<button class="btn-add-to-cart" data-id="${item.id}">
|
|
<i class="bi bi-cart-plus"></i> Add to Cart
|
|
</button>
|
|
</div>
|
|
<button class="wishlist-item-remove" data-id="${item.id}" aria-label="Remove from wishlist">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
setupWishlistItemListeners() {
|
|
this._setupRemoveButtons();
|
|
this._setupAddToCartButtons();
|
|
}
|
|
|
|
_setupRemoveButtons() {
|
|
this.content.querySelectorAll(".wishlist-item-remove").forEach((btn) => {
|
|
btn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
const id = e.currentTarget.dataset.id;
|
|
if (window.AppState?.removeFromWishlist) {
|
|
window.AppState.removeFromWishlist(id);
|
|
this.render();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
_setupAddToCartButtons() {
|
|
this.content.querySelectorAll(".btn-add-to-cart").forEach((btn) => {
|
|
btn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
const id = e.currentTarget.dataset.id;
|
|
const item = window.AppState?.wishlist.find(
|
|
(item) => String(item.id) === String(id)
|
|
);
|
|
if (item && window.AppState?.addToCart) {
|
|
window.AppState.addToCart(item);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
// Initialize when DOM is ready
|
|
const initializeComponents = () => {
|
|
// Skip if shop-system.js already initialized
|
|
if (window.ShopSystem?.isInitialized) {
|
|
console.log(
|
|
"[cart.js] Skipping initialization - shop-system.js already loaded"
|
|
);
|
|
return;
|
|
}
|
|
console.log("[cart.js] Initializing ShoppingCart and Wishlist components");
|
|
new ShoppingCart();
|
|
new Wishlist();
|
|
};
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", initializeComponents);
|
|
} else {
|
|
initializeComponents();
|
|
}
|
|
})();
|