Files
SkyArtShop/website/public/assets/js/shop-system.js

731 lines
22 KiB
JavaScript

/**
* Shopping Cart & Wishlist System
* Complete, simple, reliable implementation
*/
(function () {
"use strict";
console.log("[ShopSystem] Loading...");
// ========================================
// UTILS - Fallback if main.js not loaded
// ========================================
if (!window.Utils) {
window.Utils = {
formatCurrency(amount) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
},
escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
},
};
}
// ========================================
// VALIDATION UTILITIES
// ========================================
const ValidationUtils = {
validateProduct(product) {
if (!product || !product.id) {
return { valid: false, error: "Invalid product: missing ID" };
}
const price = parseFloat(product.price);
if (isNaN(price) || price < 0) {
return { valid: false, error: "Invalid product price" };
}
return { valid: true, price };
},
sanitizeProduct(product, price) {
return {
id: product.id,
name: product.name || product.title || "Product",
price: price,
imageurl:
product.imageurl || product.imageUrl || product.image_url || "",
};
},
validateQuantity(quantity) {
return Math.max(1, parseInt(quantity) || 1);
},
sanitizeItems(items, includeQuantity = false) {
return items
.filter(
(item) =>
item &&
item.id &&
typeof item.price !== "undefined" &&
(!includeQuantity || item.quantity > 0)
)
.map((item) => ({
...item,
price: parseFloat(item.price) || 0,
...(includeQuantity && {
quantity: Math.max(1, parseInt(item.quantity) || 1),
}),
}));
},
};
// ========================================
// CART & WISHLIST STATE MANAGEMENT
// ========================================
class ShopState {
constructor() {
this.cart = [];
this.wishlist = [];
this.init();
}
init() {
console.log("[ShopState] Initializing...");
this.loadFromStorage();
this.updateAllBadges();
console.log(
"[ShopState] Initialized - Cart:",
this.cart.length,
"Wishlist:",
this.wishlist.length
);
}
// Load data from localStorage
loadFromStorage() {
try {
const [cartData, wishlistData] = [
localStorage.getItem("skyart_cart"),
localStorage.getItem("skyart_wishlist"),
];
// Parse and validate data
this.cart = this._parseAndValidate(cartData, "cart");
this.wishlist = this._parseAndValidate(wishlistData, "wishlist");
// Sanitize items
this.cart = ValidationUtils.sanitizeItems(this.cart, true);
this.wishlist = ValidationUtils.sanitizeItems(this.wishlist, false);
} catch (e) {
console.error("[ShopState] Load error:", e);
this._clearCorruptedData();
}
}
_parseAndValidate(data, type) {
const parsed = data ? JSON.parse(data) : [];
if (!Array.isArray(parsed)) {
console.warn(`[ShopState] Invalid ${type} data, resetting`);
return [];
}
return parsed;
}
_clearCorruptedData() {
localStorage.removeItem("skyart_cart");
localStorage.removeItem("skyart_wishlist");
this.cart = [];
this.wishlist = [];
}
// Save data to localStorage
saveToStorage() {
try {
// Check localStorage availability
if (typeof localStorage === "undefined") {
console.error("[ShopState] localStorage not available");
return false;
}
const cartJson = JSON.stringify(this.cart);
const wishlistJson = JSON.stringify(this.wishlist);
// Check size (5MB limit for most browsers)
if (cartJson.length + wishlistJson.length > 4000000) {
console.warn(
"[ShopState] Storage data too large, trimming old items"
);
// Keep only last 50 cart items and 100 wishlist items
this.cart = this.cart.slice(-50);
this.wishlist = this.wishlist.slice(-100);
}
localStorage.setItem("skyart_cart", JSON.stringify(this.cart));
localStorage.setItem("skyart_wishlist", JSON.stringify(this.wishlist));
return true;
} catch (e) {
console.error("[ShopState] Save error:", e);
// Handle quota exceeded error
if (e.name === "QuotaExceededError" || e.code === 22) {
console.warn("[ShopState] Storage quota exceeded, clearing old data");
// Try to recover by keeping only essential items
this.cart = this.cart.slice(-20);
this.wishlist = this.wishlist.slice(-30);
try {
localStorage.setItem("skyart_cart", JSON.stringify(this.cart));
localStorage.setItem(
"skyart_wishlist",
JSON.stringify(this.wishlist)
);
this.showNotification(
"Storage limit reached. Older items removed.",
"info"
);
} catch (retryError) {
console.error("[ShopState] Failed to recover storage:", retryError);
}
}
return false;
}
}
// ========================================
// CART METHODS
// ========================================
addToCart(product, quantity = 1) {
console.log("[ShopState] Adding to cart:", product);
const validation = ValidationUtils.validateProduct(product);
if (!validation.valid) {
console.error("[ShopState] Invalid product:", product);
this.showNotification(validation.error, "error");
return false;
}
quantity = ValidationUtils.validateQuantity(quantity);
const existing = this._findById(this.cart, product.id);
if (existing) {
existing.quantity = Math.min(existing.quantity + quantity, 999);
} else {
const sanitized = ValidationUtils.sanitizeProduct(
product,
validation.price
);
this.cart.push({ ...sanitized, quantity });
}
return this._saveAndUpdate(
"cart",
product.name || product.title || "Item",
"added to cart"
);
}
removeFromCart(productId) {
console.log("[ShopState] Removing from cart:", productId);
this.cart = this.cart.filter(
(item) => String(item.id) !== String(productId)
);
this.saveToStorage();
this.updateAllBadges();
this.renderCartDropdown();
this.showNotification("Item removed from cart", "info");
// Dispatch event for cart.js compatibility
window.dispatchEvent(
new CustomEvent("cart-updated", { detail: this.cart })
);
}
updateCartQuantity(productId, quantity) {
const item = this.cart.find(
(item) => String(item.id) === String(productId)
);
if (item) {
item.quantity = Math.max(1, quantity);
this.saveToStorage();
this.updateAllBadges();
this.renderCartDropdown();
// Dispatch event for cart.js compatibility
window.dispatchEvent(
new CustomEvent("cart-updated", { detail: this.cart })
);
}
}
getCartTotal() {
return this.cart.reduce((sum, item) => {
const price = parseFloat(item.price) || 0;
const quantity = parseInt(item.quantity) || 0;
return sum + price * quantity;
}, 0);
}
getCartCount() {
return this.cart.reduce((sum, item) => {
const quantity = parseInt(item.quantity) || 0;
return sum + quantity;
}, 0);
}
// ========================================
// WISHLIST METHODS
// ========================================
addToWishlist(product) {
console.log("[ShopState] Adding to wishlist:", product);
const validation = ValidationUtils.validateProduct(product);
if (!validation.valid) {
console.error("[ShopState] Invalid product:", product);
this.showNotification(validation.error, "error");
return false;
}
if (this._findById(this.wishlist, product.id)) {
this.showNotification("Already in wishlist", "info");
return false;
}
const sanitized = ValidationUtils.sanitizeProduct(
product,
validation.price
);
this.wishlist.push(sanitized);
return this._saveAndUpdate(
"wishlist",
product.name || product.title || "Item",
"added to wishlist"
);
}
removeFromWishlist(productId) {
console.log("[ShopState] Removing from wishlist:", productId);
this.wishlist = this.wishlist.filter(
(item) => String(item.id) !== String(productId)
);
this.saveToStorage();
this.updateAllBadges();
this.renderWishlistDropdown();
this.showNotification("Item removed from wishlist", "info");
// Dispatch event for cart.js compatibility
window.dispatchEvent(
new CustomEvent("wishlist-updated", { detail: this.wishlist })
);
}
isInWishlist(productId) {
return !!this._findById(this.wishlist, productId);
}
isInCart(productId) {
return !!this._findById(this.cart, productId);
}
// Helper methods
_findById(collection, id) {
return collection.find((item) => String(item.id) === String(id));
}
_saveAndUpdate(type, productName, action) {
if (!this.saveToStorage()) return false;
this.updateAllBadges();
if (type === "cart") {
this.renderCartDropdown();
} else {
this.renderWishlistDropdown();
}
this.showNotification(`${productName} ${action}`, "success");
window.dispatchEvent(
new CustomEvent(`${type}-updated`, { detail: this[type] })
);
return true;
}
// ========================================
// UI UPDATE METHODS
// ========================================
updateAllBadges() {
// Update cart badge
const cartBadge = document.getElementById("cartCount");
if (cartBadge) {
const count = this.getCartCount();
cartBadge.textContent = count;
if (count > 0) {
cartBadge.classList.add("show");
} else {
cartBadge.classList.remove("show");
}
}
// Update wishlist badge
const wishlistBadge = document.getElementById("wishlistCount");
if (wishlistBadge) {
const count = this.wishlist.length;
wishlistBadge.textContent = count;
if (count > 0) {
wishlistBadge.classList.add("show");
} else {
wishlistBadge.classList.remove("show");
}
}
}
renderCartDropdown() {
const cartContent = document.getElementById("cartContent");
if (!cartContent) return;
if (this.cart.length === 0) {
cartContent.innerHTML =
'<p class="empty-state"><i class="bi bi-cart-x"></i><br>Your cart is empty</p>';
this.updateCartFooter(0);
return;
}
cartContent.innerHTML = this.cart
.map((item) => this.createCartItemHTML(item))
.join("");
this.updateCartFooter(this.getCartTotal());
this.attachCartEventListeners();
}
createCartItemHTML(item) {
const imageUrl =
item.imageurl ||
item.imageUrl ||
item.image_url ||
"/assets/images/placeholder.jpg";
const price = parseFloat(item.price || 0).toFixed(2);
const subtotal = (parseFloat(item.price || 0) * item.quantity).toFixed(2);
return `
<div class="cart-item" data-id="${item.id}">
<img src="${imageUrl}" alt="${this.escapeHtml(
item.name
)}" class="cart-item-image" loading="lazy" onerror="this.src='/assets/images/placeholder.svg'">
<div class="cart-item-details">
<h4 class="cart-item-title">${this.escapeHtml(item.name)}</h4>
<p class="cart-item-price">$${price}</p>
<div class="cart-item-quantity">
<button class="quantity-btn quantity-minus" data-id="${item.id}">
<i class="bi bi-dash"></i>
</button>
<span class="quantity-value">${item.quantity}</span>
<button class="quantity-btn quantity-plus" data-id="${item.id}">
<i class="bi bi-plus"></i>
</button>
</div>
<p class="cart-item-subtotal">Subtotal: $${subtotal}</p>
</div>
<button class="cart-item-remove" data-id="${item.id}">
<i class="bi bi-x-lg"></i>
</button>
</div>
`;
}
attachCartEventListeners() {
// Remove buttons
document.querySelectorAll(".cart-item-remove").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const id = e.currentTarget.dataset.id;
this.removeFromCart(id);
});
});
// Quantity minus
document.querySelectorAll(".quantity-minus").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const id = e.currentTarget.dataset.id;
const item = this.cart.find((i) => String(i.id) === String(id));
if (item && item.quantity > 1) {
this.updateCartQuantity(id, item.quantity - 1);
}
});
});
// Quantity plus
document.querySelectorAll(".quantity-plus").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const id = e.currentTarget.dataset.id;
const item = this.cart.find((i) => String(i.id) === String(id));
if (item) {
this.updateCartQuantity(id, item.quantity + 1);
}
});
});
}
updateCartFooter(total) {
const footer = document.querySelector("#cartPanel .dropdown-foot");
if (!footer) return;
if (total === 0 || 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>
`;
}
}
renderWishlistDropdown() {
const wishlistContent = document.getElementById("wishlistContent");
if (!wishlistContent) return;
if (this.wishlist.length === 0) {
wishlistContent.innerHTML =
'<p class="empty-state"><i class="bi bi-heart"></i><br>Your wishlist is empty</p>';
return;
}
wishlistContent.innerHTML = this.wishlist
.map((item) => this.createWishlistItemHTML(item))
.join("");
this.attachWishlistEventListeners();
}
createWishlistItemHTML(item) {
const imageUrl =
item.imageurl ||
item.imageUrl ||
item.image_url ||
"/assets/images/placeholder.jpg";
const price = parseFloat(item.price || 0).toFixed(2);
return `
<div class="wishlist-item" data-id="${item.id}">
<img src="${imageUrl}" alt="${this.escapeHtml(
item.name
)}" class="wishlist-item-image" loading="lazy" onerror="this.src='/assets/images/placeholder.svg'">
<div class="wishlist-item-details">
<h4 class="wishlist-item-title">${this.escapeHtml(item.name)}</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}">
<i class="bi bi-x-lg"></i>
</button>
</div>
`;
}
attachWishlistEventListeners() {
// Remove buttons
document.querySelectorAll(".wishlist-item-remove").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const id = e.currentTarget.dataset.id;
this.removeFromWishlist(id);
});
});
// Add to cart buttons
document.querySelectorAll(".btn-add-to-cart").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const id = e.currentTarget.dataset.id;
const item = this.wishlist.find((i) => String(i.id) === String(id));
if (item) {
this.addToCart(item, 1);
}
});
});
}
// ========================================
// DROPDOWN TOGGLE METHODS
// ========================================
setupDropdowns() {
// Cart dropdown
const cartToggle = document.getElementById("cartToggle");
const cartPanel = document.getElementById("cartPanel");
const cartClose = document.getElementById("cartClose");
if (cartToggle && cartPanel) {
cartToggle.addEventListener("click", () => {
cartPanel.classList.toggle("active");
this.renderCartDropdown();
});
}
if (cartClose) {
cartClose.addEventListener("click", () => {
cartPanel.classList.remove("active");
});
}
// Wishlist dropdown
const wishlistToggle = document.getElementById("wishlistToggle");
const wishlistPanel = document.getElementById("wishlistPanel");
const wishlistClose = document.getElementById("wishlistClose");
if (wishlistToggle && wishlistPanel) {
wishlistToggle.addEventListener("click", () => {
wishlistPanel.classList.toggle("active");
this.renderWishlistDropdown();
});
}
if (wishlistClose) {
wishlistClose.addEventListener("click", () => {
wishlistPanel.classList.remove("active");
});
}
// Close dropdowns when clicking outside
document.addEventListener("click", (e) => {
if (!e.target.closest(".cart-dropdown-wrapper") && cartPanel) {
cartPanel.classList.remove("active");
}
if (!e.target.closest(".wishlist-dropdown-wrapper") && wishlistPanel) {
wishlistPanel.classList.remove("active");
}
});
}
// ========================================
// NOTIFICATION SYSTEM
// ========================================
showNotification(message, type = "info") {
// Remove existing notifications
document
.querySelectorAll(".shop-notification")
.forEach((n) => n.remove());
const notification = document.createElement("div");
notification.className = `shop-notification notification-${type}`;
notification.textContent = message;
const bgColors = {
success: "#10b981",
error: "#ef4444",
info: "#3b82f6",
};
notification.style.cssText = `
position: fixed;
top: 80px;
right: 20px;
background: ${bgColors[type] || bgColors.info};
color: white;
padding: 12px 24px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10000;
animation: slideInRight 0.3s ease;
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = "slideOutRight 0.3s ease";
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// ========================================
// UTILITY METHODS
// ========================================
escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
}
// ========================================
// INITIALIZE SYSTEM
// ========================================
// Create global instance
window.ShopSystem = new ShopState();
// ========================================
// APPSTATE COMPATIBILITY LAYER
// ========================================
// Provide AppState interface for cart.js compatibility
window.AppState = {
get cart() {
return window.ShopSystem.cart;
},
get wishlist() {
return window.ShopSystem.wishlist;
},
addToCart: (product, quantity = 1) => {
window.ShopSystem.addToCart(product, quantity);
},
removeFromCart: (productId) => {
window.ShopSystem.removeFromCart(productId);
},
updateCartQuantity: (productId, quantity) => {
window.ShopSystem.updateCartQuantity(productId, quantity);
},
getCartTotal: () => {
return window.ShopSystem.getCartTotal();
},
getCartCount: () => {
return window.ShopSystem.getCartCount();
},
addToWishlist: (product) => {
window.ShopSystem.addToWishlist(product);
},
removeFromWishlist: (productId) => {
window.ShopSystem.removeFromWishlist(productId);
},
isInWishlist: (productId) => {
return window.ShopSystem.isInWishlist(productId);
},
showNotification: (message, type) => {
window.ShopSystem.showNotification(message, type);
},
};
console.log("[ShopSystem] AppState compatibility layer installed");
// Setup dropdowns when DOM is ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
window.ShopSystem.setupDropdowns();
});
} else {
window.ShopSystem.setupDropdowns();
}
// Add animation styles
if (!document.getElementById("shop-system-styles")) {
const style = document.createElement("style");
style.id = "shop-system-styles";
style.textContent = `
@keyframes slideInRight {
from { transform: translateX(400px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOutRight {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(400px); opacity: 0; }
}
`;
document.head.appendChild(style);
}
console.log("[ShopSystem] Ready!");
})();