webupdatev1
This commit is contained in:
@@ -6,12 +6,16 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
class ShoppingCart {
|
||||
constructor() {
|
||||
this.cartToggle = document.getElementById("cartToggle");
|
||||
this.cartPanel = document.getElementById("cartPanel");
|
||||
this.cartContent = document.getElementById("cartContent");
|
||||
this.cartClose = document.getElementById("cartClose");
|
||||
// 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();
|
||||
@@ -23,23 +27,24 @@
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
if (this.cartToggle) {
|
||||
this.cartToggle.addEventListener("click", () => this.toggle());
|
||||
if (this.toggleBtn) {
|
||||
this.toggleBtn.addEventListener("click", () => this.toggle());
|
||||
}
|
||||
|
||||
if (this.cartClose) {
|
||||
this.cartClose.addEventListener("click", () => this.close());
|
||||
if (this.closeBtn) {
|
||||
this.closeBtn.addEventListener("click", () => this.close());
|
||||
}
|
||||
|
||||
// Close when clicking outside
|
||||
document.addEventListener("click", (e) => {
|
||||
if (this.isOpen && !e.target.closest(".cart-dropdown-wrapper")) {
|
||||
if (this.isOpen && !e.target.closest(this.wrapperClass)) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for cart updates
|
||||
window.addEventListener("cart-updated", () => this.render());
|
||||
window.addEventListener(this.eventName, () => {
|
||||
console.log(`[${this.constructor.name}] ${this.eventName} received`);
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
toggle() {
|
||||
@@ -47,113 +52,213 @@
|
||||
}
|
||||
|
||||
open() {
|
||||
if (this.cartPanel) {
|
||||
this.cartPanel.classList.add("active");
|
||||
this.cartPanel.setAttribute("aria-hidden", "false");
|
||||
if (this.panel) {
|
||||
this.panel.classList.add("active");
|
||||
this.panel.setAttribute("aria-hidden", "false");
|
||||
this.isOpen = true;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.cartPanel) {
|
||||
this.cartPanel.classList.remove("active");
|
||||
this.cartPanel.setAttribute("aria-hidden", "true");
|
||||
if (this.panel) {
|
||||
this.panel.classList.remove("active");
|
||||
this.panel.setAttribute("aria-hidden", "true");
|
||||
this.isOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.cartContent) return;
|
||||
|
||||
const cart = window.AppState.cart;
|
||||
|
||||
if (cart.length === 0) {
|
||||
this.cartContent.innerHTML =
|
||||
'<p class="empty-state">Your cart is empty</p>';
|
||||
this.updateFooter(null);
|
||||
return;
|
||||
renderEmpty() {
|
||||
if (this.content) {
|
||||
this.content.innerHTML = this.emptyMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const html = cart.map((item) => this.renderCartItem(item)).join("");
|
||||
this.cartContent.innerHTML = html;
|
||||
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>'
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listeners to cart items
|
||||
this.setupCartItemListeners();
|
||||
render() {
|
||||
if (!this.content) return;
|
||||
|
||||
// Update footer with total
|
||||
this.updateFooter(window.AppState.getCartTotal());
|
||||
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) {
|
||||
const 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(item.price || 0);
|
||||
const subtotal = window.Utils.formatCurrency(
|
||||
(item.price || 0) * item.quantity
|
||||
);
|
||||
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.imageurl ||
|
||||
item.imageUrl ||
|
||||
item.image_url ||
|
||||
"/assets/images/placeholder.svg";
|
||||
const title = window.Utils.escapeHtml(
|
||||
item.title || item.name || "Product"
|
||||
);
|
||||
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">
|
||||
<div class="cart-item-details">
|
||||
<h4 class="cart-item-title">${title}</h4>
|
||||
<p class="cart-item-price">${price}</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">${item.quantity}</span>
|
||||
<button class="quantity-btn quantity-plus" data-id="${item.id}" aria-label="Increase quantity">
|
||||
<i class="bi bi-plus"></i>
|
||||
</button>
|
||||
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>
|
||||
<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>
|
||||
<p class="cart-item-subtotal">${subtotal}</p>
|
||||
<button class="cart-item-remove" data-id="${item.id}" aria-label="Remove from cart">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</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() {
|
||||
// Remove buttons
|
||||
this.cartContent.querySelectorAll(".cart-item-remove").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
const id = parseInt(e.currentTarget.dataset.id);
|
||||
window.AppState.removeFromCart(id);
|
||||
this.render();
|
||||
});
|
||||
});
|
||||
try {
|
||||
this._setupRemoveButtons();
|
||||
this._setupQuantityButtons();
|
||||
} catch (error) {
|
||||
console.error("[ShoppingCart] Error setting up listeners:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Quantity buttons
|
||||
this.cartContent.querySelectorAll(".quantity-minus").forEach((btn) => {
|
||||
_setupRemoveButtons() {
|
||||
this.content.querySelectorAll(".cart-item-remove").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
const id = parseInt(e.currentTarget.dataset.id);
|
||||
const item = window.AppState.cart.find((item) => item.id === id);
|
||||
if (item && item.quantity > 1) {
|
||||
window.AppState.updateCartQuantity(id, item.quantity - 1);
|
||||
this.render();
|
||||
}
|
||||
e.stopPropagation();
|
||||
this._handleAction(e, () => {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
if (id && window.AppState?.removeFromCart) {
|
||||
window.AppState.removeFromCart(id);
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.cartContent.querySelectorAll(".quantity-plus").forEach((btn) => {
|
||||
_setupQuantityButtons() {
|
||||
this._setupQuantityButton(".quantity-minus", -1);
|
||||
this._setupQuantityButton(".quantity-plus", 1);
|
||||
}
|
||||
|
||||
_setupQuantityButton(selector, delta) {
|
||||
this.content.querySelectorAll(selector).forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
const id = parseInt(e.currentTarget.dataset.id);
|
||||
const item = window.AppState.cart.find((item) => item.id === id);
|
||||
if (item) {
|
||||
window.AppState.updateCartQuantity(id, item.quantity + 1);
|
||||
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;
|
||||
@@ -177,43 +282,18 @@
|
||||
}
|
||||
|
||||
// Wishlist Component
|
||||
class Wishlist {
|
||||
class Wishlist extends BaseDropdown {
|
||||
constructor() {
|
||||
this.wishlistToggle = document.getElementById("wishlistToggle");
|
||||
this.wishlistPanel = document.getElementById("wishlistPanel");
|
||||
this.wishlistContent = document.getElementById("wishlistContent");
|
||||
this.wishlistClose = document.getElementById("wishlistClose");
|
||||
this.isOpen = false;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.render();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
if (this.wishlistToggle) {
|
||||
this.wishlistToggle.addEventListener("click", () => this.toggle());
|
||||
}
|
||||
|
||||
if (this.wishlistClose) {
|
||||
this.wishlistClose.addEventListener("click", () => this.close());
|
||||
}
|
||||
|
||||
// Close when clicking outside
|
||||
document.addEventListener("click", (e) => {
|
||||
if (this.isOpen && !e.target.closest(".wishlist-dropdown-wrapper")) {
|
||||
this.close();
|
||||
}
|
||||
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>'
|
||||
});
|
||||
|
||||
// Listen for wishlist updates
|
||||
window.addEventListener("wishlist-updated", () => this.render());
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.isOpen ? this.close() : this.open();
|
||||
}
|
||||
|
||||
@@ -235,40 +315,52 @@
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.wishlistContent) return;
|
||||
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.wishlistContent.innerHTML =
|
||||
'<p class="empty-state">Your wishlist is empty</p>';
|
||||
this.renderEmpty();
|
||||
return;
|
||||
}
|
||||
|
||||
const html = wishlist
|
||||
this.content.innerHTML = wishlist
|
||||
.map((item) => this.renderWishlistItem(item))
|
||||
.join("");
|
||||
this.wishlistContent.innerHTML = html;
|
||||
|
||||
// Add event listeners
|
||||
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.image_url || "/assets/images/placeholder.jpg";
|
||||
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(item.price || 0);
|
||||
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">
|
||||
<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}">Add to Cart</button>
|
||||
<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>
|
||||
@@ -278,42 +370,49 @@
|
||||
}
|
||||
|
||||
setupWishlistItemListeners() {
|
||||
// Remove buttons
|
||||
this.wishlistContent
|
||||
.querySelectorAll(".wishlist-item-remove")
|
||||
.forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
const id = parseInt(e.currentTarget.dataset.id);
|
||||
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();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Add to cart buttons
|
||||
this.wishlistContent
|
||||
.querySelectorAll(".btn-add-to-cart")
|
||||
.forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
const id = parseInt(e.currentTarget.dataset.id);
|
||||
const item = window.AppState.wishlist.find(
|
||||
(item) => item.id === id
|
||||
);
|
||||
if (item) {
|
||||
window.AppState.addToCart(item);
|
||||
}
|
||||
});
|
||||
_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
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
new ShoppingCart();
|
||||
new Wishlist();
|
||||
});
|
||||
} else {
|
||||
const initializeComponents = () => {
|
||||
console.log("[cart.js] Initializing ShoppingCart and Wishlist components");
|
||||
new ShoppingCart();
|
||||
new Wishlist();
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initializeComponents);
|
||||
} else {
|
||||
initializeComponents();
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user