Files
SkyArtShop/website/public/assets/js/main-enhanced.js

819 lines
23 KiB
JavaScript

/**
* Enhanced Main Application JavaScript
* Production-Ready with No Console Errors
* Proper State Management & API Integration
*/
(function () {
"use strict";
// Production mode check
const isDevelopment =
window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1";
// Safe console wrapper
const logger = {
log: (...args) => isDevelopment && console.log(...args),
error: (...args) => console.error(...args),
warn: (...args) => isDevelopment && console.warn(...args),
info: (...args) => isDevelopment && console.info(...args),
};
// ========================================
// GLOBAL STATE MANAGEMENT
// ========================================
window.AppState = {
cart: [],
wishlist: [],
products: [],
settings: null,
user: null,
_saveCartTimeout: null,
_saveWishlistTimeout: null,
_initialized: false,
// Initialize state
init() {
if (this._initialized) {
logger.warn("[AppState] Already initialized");
return;
}
logger.info("[AppState] Initializing...");
this.loadCart();
this.loadWishlist();
this.updateUI();
this._initialized = true;
logger.info(
"[AppState] Initialized - Cart:",
this.cart.length,
"items, Wishlist:",
this.wishlist.length,
"items"
);
// Dispatch ready event
window.dispatchEvent(new CustomEvent("appstate-ready"));
},
// ========================================
// CART MANAGEMENT
// ========================================
loadCart() {
try {
const saved = localStorage.getItem("cart");
this.cart = saved ? JSON.parse(saved) : [];
// Validate cart items
this.cart = this.cart.filter((item) => item && item.id && item.price);
} catch (error) {
logger.error("Error loading cart:", error);
this.cart = [];
}
},
saveCart() {
if (this._saveCartTimeout) {
clearTimeout(this._saveCartTimeout);
}
this._saveCartTimeout = setTimeout(() => {
try {
localStorage.setItem("cart", JSON.stringify(this.cart));
this.updateUI();
window.dispatchEvent(
new CustomEvent("cart-updated", { detail: this.cart })
);
} catch (error) {
logger.error("Error saving cart:", error);
this.showNotification("Error saving cart", "error");
}
}, 100);
},
addToCart(product, quantity = 1) {
if (!product || !product.id) {
logger.error("[AppState] Invalid product:", product);
this.showNotification("Invalid product", "error");
return false;
}
try {
const existing = this.cart.find((item) => item.id === product.id);
if (existing) {
existing.quantity = (existing.quantity || 1) + quantity;
logger.info("[AppState] Updated cart quantity:", existing);
} else {
this.cart.push({
...product,
quantity,
addedAt: new Date().toISOString(),
});
logger.info("[AppState] Added to cart:", product.name);
}
this.saveCart();
this.showNotification(
`${product.name || "Item"} added to cart`,
"success"
);
return true;
} catch (error) {
logger.error("[AppState] Error adding to cart:", error);
this.showNotification("Error adding to cart", "error");
return false;
}
},
removeFromCart(productId) {
if (!productId) {
logger.error("[AppState] Invalid productId");
return false;
}
const initialLength = this.cart.length;
this.cart = this.cart.filter((item) => item.id !== productId);
if (this.cart.length < initialLength) {
this.saveCart();
this.showNotification("Removed from cart", "info");
return true;
}
return false;
},
updateCartQuantity(productId, quantity) {
const item = this.cart.find((item) => item.id === productId);
if (item) {
item.quantity = Math.max(1, parseInt(quantity) || 1);
this.saveCart();
return true;
}
return false;
},
clearCart() {
this.cart = [];
this.saveCart();
this.showNotification("Cart cleared", "info");
},
getCartTotal() {
return this.cart.reduce((sum, item) => {
const price = parseFloat(item.price) || 0;
const quantity = parseInt(item.quantity) || 1;
return sum + price * quantity;
}, 0);
},
getCartCount() {
return this.cart.reduce(
(sum, item) => sum + (parseInt(item.quantity) || 1),
0
);
},
// ========================================
// WISHLIST MANAGEMENT
// ========================================
loadWishlist() {
try {
const saved = localStorage.getItem("wishlist");
this.wishlist = saved ? JSON.parse(saved) : [];
// Validate wishlist items
this.wishlist = this.wishlist.filter((item) => item && item.id);
} catch (error) {
logger.error("Error loading wishlist:", error);
this.wishlist = [];
}
},
saveWishlist() {
if (this._saveWishlistTimeout) {
clearTimeout(this._saveWishlistTimeout);
}
this._saveWishlistTimeout = setTimeout(() => {
try {
localStorage.setItem("wishlist", JSON.stringify(this.wishlist));
this.updateUI();
window.dispatchEvent(
new CustomEvent("wishlist-updated", { detail: this.wishlist })
);
} catch (error) {
logger.error("Error saving wishlist:", error);
}
}, 100);
},
addToWishlist(product) {
if (!product || !product.id) {
logger.error("[AppState] Invalid product:", product);
this.showNotification("Invalid product", "error");
return false;
}
try {
const exists = this.wishlist.some((item) => item.id === product.id);
if (exists) {
this.showNotification("Already in wishlist", "info");
return false;
}
this.wishlist.push({
...product,
addedAt: new Date().toISOString(),
});
this.saveWishlist();
this.showNotification(
`${product.name || "Item"} added to wishlist`,
"success"
);
return true;
} catch (error) {
logger.error("[AppState] Error adding to wishlist:", error);
this.showNotification("Error adding to wishlist", "error");
return false;
}
},
removeFromWishlist(productId) {
if (!productId) return false;
const initialLength = this.wishlist.length;
this.wishlist = this.wishlist.filter((item) => item.id !== productId);
if (this.wishlist.length < initialLength) {
this.saveWishlist();
this.showNotification("Removed from wishlist", "info");
return true;
}
return false;
},
isInWishlist(productId) {
return this.wishlist.some((item) => item.id === productId);
},
getWishlistCount() {
return this.wishlist.length;
},
// ========================================
// UI UPDATES
// ========================================
updateUI() {
this.updateCartBadge();
this.updateWishlistBadge();
this.updateCartDropdown();
this.updateWishlistDropdown();
},
updateCartBadge() {
const badges = document.querySelectorAll(
".cart-count, .cart-badge, #cartCount"
);
const count = this.getCartCount();
badges.forEach((badge) => {
badge.textContent = count;
if (count > 0) {
badge.classList.add("show");
} else {
badge.classList.remove("show");
}
});
},
updateWishlistBadge() {
const badges = document.querySelectorAll(
".wishlist-count, .wishlist-badge, #wishlistCount"
);
const count = this.getWishlistCount();
badges.forEach((badge) => {
badge.textContent = count;
if (count > 0) {
badge.classList.add("show");
} else {
badge.classList.remove("show");
}
});
},
updateCartDropdown() {
const container = document.querySelector("#cart-items");
if (!container) return;
if (this.cart.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="bi bi-cart-x"></i>
<p>Your cart is empty</p>
</div>
`;
const totalEl = document.querySelector(".cart-total-value");
if (totalEl) totalEl.textContent = "$0.00";
return;
}
container.innerHTML = this.cart
.map((item) => this.renderCartItem(item))
.join("");
const totalEl = document.querySelector(".cart-total-value");
if (totalEl) {
totalEl.textContent = `$${this.getCartTotal().toFixed(2)}`;
}
this.attachCartEventListeners();
},
updateWishlistDropdown() {
const container = document.querySelector("#wishlist-items");
if (!container) return;
if (this.wishlist.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="bi bi-heart"></i>
<p>Your wishlist is empty</p>
</div>
`;
return;
}
container.innerHTML = this.wishlist
.map((item) => this.renderWishlistItem(item))
.join("");
this.attachWishlistEventListeners();
},
renderCartItem(item) {
const price = parseFloat(item.price) || 0;
const quantity = parseInt(item.quantity) || 1;
const imageUrl = this.getProductImage(item);
const name = this.sanitizeHTML(item.name || "Product");
return `
<div class="cart-item" data-product-id="${item.id}">
<div class="cart-item-image">
<img src="${imageUrl}" alt="${name}" loading="lazy" onerror="this.src='/assets/img/placeholder.jpg'">
</div>
<div class="cart-item-info">
<div class="cart-item-title">${name}</div>
<div class="cart-item-price">$${price.toFixed(2)}</div>
<div class="cart-item-controls">
<button class="btn-quantity" data-action="decrease" aria-label="Decrease quantity">
<i class="bi bi-dash"></i>
</button>
<input type="number" class="quantity-input" value="${quantity}" min="1" max="99" aria-label="Quantity">
<button class="btn-quantity" data-action="increase" aria-label="Increase quantity">
<i class="bi bi-plus"></i>
</button>
</div>
</div>
<button class="btn-remove" data-action="remove" aria-label="Remove from cart">
<i class="bi bi-x-lg"></i>
</button>
</div>
`;
},
renderWishlistItem(item) {
const price = parseFloat(item.price) || 0;
const imageUrl = this.getProductImage(item);
const name = this.sanitizeHTML(item.name || "Product");
return `
<div class="wishlist-item" data-product-id="${item.id}">
<div class="wishlist-item-image">
<img src="${imageUrl}" alt="${name}" loading="lazy" onerror="this.src='/assets/img/placeholder.jpg'">
</div>
<div class="wishlist-item-info">
<div class="wishlist-item-title">${name}</div>
<div class="wishlist-item-price">$${price.toFixed(2)}</div>
<button class="btn-add-to-cart" data-product-id="${item.id}">
<i class="bi bi-cart-plus"></i> Add to Cart
</button>
</div>
<button class="btn-remove" data-action="remove" aria-label="Remove from wishlist">
<i class="bi bi-x-lg"></i>
</button>
</div>
`;
},
attachCartEventListeners() {
document.querySelectorAll(".cart-item").forEach((item) => {
const productId = item.dataset.productId;
// Quantity controls
const decreaseBtn = item.querySelector('[data-action="decrease"]');
const increaseBtn = item.querySelector('[data-action="increase"]');
const quantityInput = item.querySelector(".quantity-input");
if (decreaseBtn) {
decreaseBtn.addEventListener("click", () => {
const currentQty = parseInt(quantityInput.value) || 1;
if (currentQty > 1) {
quantityInput.value = currentQty - 1;
this.updateCartQuantity(productId, currentQty - 1);
}
});
}
if (increaseBtn) {
increaseBtn.addEventListener("click", () => {
const currentQty = parseInt(quantityInput.value) || 1;
if (currentQty < 99) {
quantityInput.value = currentQty + 1;
this.updateCartQuantity(productId, currentQty + 1);
}
});
}
if (quantityInput) {
quantityInput.addEventListener("change", (e) => {
const newQty = parseInt(e.target.value) || 1;
this.updateCartQuantity(productId, newQty);
});
}
// Remove button
const removeBtn = item.querySelector('[data-action="remove"]');
if (removeBtn) {
removeBtn.addEventListener("click", () => {
this.removeFromCart(productId);
});
}
});
},
attachWishlistEventListeners() {
document.querySelectorAll(".wishlist-item").forEach((item) => {
const productId = item.dataset.productId;
// Add to cart button
const addBtn = item.querySelector(".btn-add-to-cart");
if (addBtn) {
addBtn.addEventListener("click", () => {
const product = this.wishlist.find((p) => p.id === productId);
if (product) {
this.addToCart(product);
}
});
}
// Remove button
const removeBtn = item.querySelector('[data-action="remove"]');
if (removeBtn) {
removeBtn.addEventListener("click", () => {
this.removeFromWishlist(productId);
});
}
});
},
// ========================================
// NOTIFICATIONS
// ========================================
showNotification(message, type = "info") {
if (!message) return;
let container = document.querySelector(".notification-container");
if (!container) {
container = document.createElement("div");
container.className = "notification-container";
document.body.appendChild(container);
}
const notification = document.createElement("div");
notification.className = `notification ${type}`;
const icon =
type === "success"
? "check-circle-fill"
: type === "error"
? "exclamation-circle-fill"
: "info-circle-fill";
notification.innerHTML = `
<i class="bi bi-${icon}"></i>
<span class="notification-message">${this.sanitizeHTML(message)}</span>
`;
container.appendChild(notification);
setTimeout(() => {
notification.style.animation = "slideOut 0.3s ease forwards";
setTimeout(() => notification.remove(), 300);
}, 3000);
},
// ========================================
// API INTEGRATION
// ========================================
async fetchProducts() {
try {
const response = await fetch("/api/products");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (data.success && Array.isArray(data.products)) {
this.products = data.products;
window.dispatchEvent(
new CustomEvent("products-loaded", { detail: this.products })
);
return this.products;
}
throw new Error("Invalid API response");
} catch (error) {
logger.error("Error fetching products:", error);
this.showNotification("Error loading products", "error");
return [];
}
},
async fetchSettings() {
try {
const response = await fetch("/api/settings");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (data.success && data.settings) {
this.settings = data.settings;
window.dispatchEvent(
new CustomEvent("settings-loaded", { detail: this.settings })
);
return this.settings;
}
throw new Error("Invalid API response");
} catch (error) {
logger.error("Error fetching settings:", error);
return null;
}
},
// ========================================
// UTILITY METHODS
// ========================================
getProductImage(product) {
if (!product) return "/assets/img/placeholder.jpg";
// Check various image properties
if (product.image_url) return product.image_url;
if (product.imageUrl) return product.imageUrl;
if (
product.images &&
Array.isArray(product.images) &&
product.images.length > 0
) {
return (
product.images[0].image_url ||
product.images[0].url ||
"/assets/img/placeholder.jpg"
);
}
if (product.thumbnail) return product.thumbnail;
return "/assets/img/placeholder.jpg";
},
sanitizeHTML(str) {
if (!str) return "";
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
},
formatPrice(price) {
const num = parseFloat(price) || 0;
return `$${num.toFixed(2)}`;
},
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
};
// ========================================
// DROPDOWN MANAGEMENT
// ========================================
window.DropdownManager = {
activeDropdown: null,
init() {
this.attachEventListeners();
logger.info("[DropdownManager] Initialized");
},
attachEventListeners() {
// Cart toggle
const cartBtn = document.querySelector("#cart-btn");
if (cartBtn) {
cartBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.toggle("cart");
});
}
// Wishlist toggle
const wishlistBtn = document.querySelector("#wishlist-btn");
if (wishlistBtn) {
wishlistBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.toggle("wishlist");
});
}
// Close on outside click
document.addEventListener("click", (e) => {
if (!e.target.closest(".dropdown") && !e.target.closest(".icon-btn")) {
this.closeAll();
}
});
// Close on escape key
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
this.closeAll();
}
});
},
toggle(type) {
const dropdown = document.querySelector(`#${type}-dropdown`);
if (!dropdown) return;
if (this.activeDropdown === dropdown) {
this.close(dropdown);
} else {
this.closeAll();
this.open(dropdown, type);
}
},
open(dropdown, type) {
dropdown.style.display = "flex";
this.activeDropdown = dropdown;
// Update content
if (type === "cart") {
window.AppState.updateCartDropdown();
} else if (type === "wishlist") {
window.AppState.updateWishlistDropdown();
}
},
close(dropdown) {
if (dropdown) {
dropdown.style.display = "none";
}
this.activeDropdown = null;
},
closeAll() {
document.querySelectorAll(".dropdown").forEach((dropdown) => {
dropdown.style.display = "none";
});
this.activeDropdown = null;
},
};
// ========================================
// MOBILE MENU
// ========================================
window.MobileMenu = {
menu: null,
overlay: null,
isOpen: false,
init() {
this.menu = document.querySelector(".mobile-menu");
this.overlay = document.querySelector(".mobile-menu-overlay");
if (!this.menu || !this.overlay) {
logger.warn("[MobileMenu] Elements not found");
return;
}
this.attachEventListeners();
logger.info("[MobileMenu] Initialized");
},
attachEventListeners() {
// Toggle button
const toggleBtn = document.querySelector(".mobile-menu-toggle");
if (toggleBtn) {
toggleBtn.addEventListener("click", () => this.toggle());
}
// Close button
const closeBtn = document.querySelector(".mobile-menu-close");
if (closeBtn) {
closeBtn.addEventListener("click", () => this.close());
}
// Overlay click
if (this.overlay) {
this.overlay.addEventListener("click", () => this.close());
}
// Menu links
this.menu.querySelectorAll("a").forEach((link) => {
link.addEventListener("click", () => {
setTimeout(() => this.close(), 100);
});
});
// Escape key
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && this.isOpen) {
this.close();
}
});
},
toggle() {
this.isOpen ? this.close() : this.open();
},
open() {
this.menu.classList.add("active");
this.overlay.classList.add("active");
this.isOpen = true;
document.body.style.overflow = "hidden";
},
close() {
this.menu.classList.remove("active");
this.overlay.classList.remove("active");
this.isOpen = false;
document.body.style.overflow = "";
},
};
// ========================================
// INITIALIZATION
// ========================================
function initialize() {
logger.info("[App] Initializing...");
// Initialize state
if (window.AppState) {
window.AppState.init();
}
// Initialize dropdown manager
if (window.DropdownManager) {
window.DropdownManager.init();
}
// Initialize mobile menu
if (window.MobileMenu) {
window.MobileMenu.init();
}
// Fetch initial data
if (window.AppState.fetchSettings) {
window.AppState.fetchSettings();
}
logger.info("[App] Initialization complete");
}
// Run on DOM ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initialize);
} else {
initialize();
}
// Export for debugging in development
if (isDevelopment) {
window.DEBUG = {
AppState: window.AppState,
DropdownManager: window.DropdownManager,
MobileMenu: window.MobileMenu,
logger,
};
}
})();