webupdatev1

This commit is contained in:
Local Server
2026-01-04 17:52:37 -06:00
parent 1919f6f8bb
commit c1da8eff42
81 changed files with 16728 additions and 475 deletions

View File

@@ -0,0 +1,220 @@
/**
* Accessibility Enhancements
* WCAG 2.1 AA Compliance Utilities
*/
(function () {
"use strict";
class AccessibilityManager {
constructor() {
this.init();
}
init() {
this.addSkipLinks();
this.enhanceFocusManagement();
this.addARIALabels();
this.setupKeyboardNavigation();
this.announceChanges();
}
// Add skip link to main content
addSkipLinks() {
if (document.querySelector(".skip-link")) return;
const skipLink = document.createElement("a");
skipLink.href = "#main-content";
skipLink.className = "skip-link";
skipLink.textContent = "Skip to main content";
skipLink.addEventListener("click", (e) => {
e.preventDefault();
const main =
document.getElementById("main-content") ||
document.querySelector("main");
if (main) {
main.setAttribute("tabindex", "-1");
main.focus();
main.removeAttribute("tabindex");
}
});
document.body.insertBefore(skipLink, document.body.firstChild);
}
// Enhance focus management
enhanceFocusManagement() {
// Track focus for modal/dropdown management
let lastFocusedElement = null;
document.addEventListener("focusin", (e) => {
const dropdown = e.target.closest(
'[role="dialog"], .cart-dropdown, .wishlist-dropdown'
);
if (dropdown && dropdown.classList.contains("active")) {
if (!lastFocusedElement) {
lastFocusedElement = document.activeElement;
}
}
});
// Return focus when dropdowns close
const observeDropdowns = () => {
const dropdowns = document.querySelectorAll(
".cart-dropdown, .wishlist-dropdown"
);
dropdowns.forEach((dropdown) => {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === "class") {
if (
!dropdown.classList.contains("active") &&
lastFocusedElement
) {
lastFocusedElement.focus();
lastFocusedElement = null;
}
}
});
});
observer.observe(dropdown, { attributes: true });
});
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", observeDropdowns);
} else {
observeDropdowns();
}
}
// Add missing ARIA labels
addARIALabels() {
// Add labels to buttons without text
document
.querySelectorAll("button:not([aria-label]):not([aria-labelledby])")
.forEach((button) => {
if (button.textContent.trim() === "") {
const icon = button.querySelector('i[class*="bi-"]');
if (icon) {
const iconClass = Array.from(icon.classList).find((c) =>
c.startsWith("bi-")
);
if (iconClass) {
const label = iconClass.replace("bi-", "").replace(/-/g, " ");
button.setAttribute("aria-label", label);
}
}
}
});
// Ensure all images have alt text
document.querySelectorAll("img:not([alt])").forEach((img) => {
img.setAttribute("alt", "");
});
// Add role to navigation landmarks
document.querySelectorAll(".navbar, .modern-navbar").forEach((nav) => {
if (!nav.getAttribute("role")) {
nav.setAttribute("role", "navigation");
}
});
}
// Setup keyboard navigation
setupKeyboardNavigation() {
// Escape key closes dropdowns
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
const activeDropdown = document.querySelector(
".cart-dropdown.active, .wishlist-dropdown.active"
);
if (activeDropdown) {
const closeBtn = activeDropdown.querySelector(
".close-btn, [data-close]"
);
if (closeBtn) closeBtn.click();
}
}
});
// Tab trap in modal/dropdowns
document
.querySelectorAll(".cart-dropdown, .wishlist-dropdown")
.forEach((dropdown) => {
dropdown.addEventListener("keydown", (e) => {
if (e.key === "Tab" && dropdown.classList.contains("active")) {
const focusableElements = dropdown.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement =
focusableElements[focusableElements.length - 1];
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (
!e.shiftKey &&
document.activeElement === lastElement
) {
e.preventDefault();
firstElement.focus();
}
}
});
});
}
// Announce dynamic changes to screen readers
announceChanges() {
// Create live region if it doesn't exist
let liveRegion = document.getElementById("aria-live-region");
if (!liveRegion) {
liveRegion = document.createElement("div");
liveRegion.id = "aria-live-region";
liveRegion.setAttribute("role", "status");
liveRegion.setAttribute("aria-live", "polite");
liveRegion.setAttribute("aria-atomic", "true");
liveRegion.className = "sr-only";
document.body.appendChild(liveRegion);
}
// Listen for cart/wishlist updates
window.addEventListener("cart-updated", () => {
const count = window.AppState?.getCartCount?.() || 0;
this.announce(
`Cart updated. ${count} item${count !== 1 ? "s" : ""} in cart.`
);
});
window.addEventListener("wishlist-updated", () => {
const count = window.AppState?.wishlist?.length || 0;
this.announce(
`Wishlist updated. ${count} item${
count !== 1 ? "s" : ""
} in wishlist.`
);
});
}
// Announce message to screen readers
announce(message) {
const liveRegion = document.getElementById("aria-live-region");
if (liveRegion) {
liveRegion.textContent = "";
setTimeout(() => {
liveRegion.textContent = message;
}, 100);
}
}
}
// Initialize accessibility manager
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
window.A11y = new AccessibilityManager();
});
} else {
window.A11y = new AccessibilityManager();
}
})();

View File

@@ -0,0 +1,160 @@
/**
* API Integration Improvements
* Enhanced API client with retry logic and better error handling
*/
(function () {
"use strict";
class EnhancedAPIClient {
constructor(baseURL = "") {
this.baseURL = baseURL;
this.retryAttempts = 3;
this.retryDelay = 1000;
this.cache = new Map();
this.cacheTimeout = 5 * 60 * 1000; // 5 minutes
}
async requestWithRetry(endpoint, options = {}, attempt = 1) {
try {
const response = await fetch(this.baseURL + endpoint, {
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const contentType = response.headers.get("content-type");
if (contentType?.includes("application/json")) {
return await response.json();
}
return await response.text();
} catch (error) {
// Retry logic for network errors
if (attempt < this.retryAttempts && this.isRetryableError(error)) {
await this.delay(this.retryDelay * attempt);
return this.requestWithRetry(endpoint, options, attempt + 1);
}
throw error;
}
}
isRetryableError(error) {
// Retry on network errors or 5xx server errors
return (
error.message.includes("Failed to fetch") ||
error.message.includes("NetworkError") ||
error.message.includes("500") ||
error.message.includes("502") ||
error.message.includes("503")
);
}
delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
getCacheKey(endpoint, params) {
return endpoint + JSON.stringify(params || {});
}
getFromCache(key) {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
return cached.data;
}
this.cache.delete(key);
return null;
}
setCache(key, data) {
this.cache.set(key, {
data,
timestamp: Date.now(),
});
}
clearCache() {
this.cache.clear();
}
async get(endpoint, params = {}, useCache = true) {
const cacheKey = this.getCacheKey(endpoint, params);
if (useCache) {
const cached = this.getFromCache(cacheKey);
if (cached) return cached;
}
const queryString = new URLSearchParams(params).toString();
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
const data = await this.requestWithRetry(url, { method: "GET" });
if (useCache) {
this.setCache(cacheKey, data);
}
return data;
}
async post(endpoint, body = {}) {
return this.requestWithRetry(endpoint, {
method: "POST",
body: JSON.stringify(body),
});
}
async put(endpoint, body = {}) {
return this.requestWithRetry(endpoint, {
method: "PUT",
body: JSON.stringify(body),
});
}
async delete(endpoint) {
return this.requestWithRetry(endpoint, { method: "DELETE" });
}
// Product methods with caching
async getProducts(params = {}) {
return this.get("/api/products", params, true);
}
async getProduct(id) {
return this.get(`/api/products/${id}`, {}, true);
}
async getFeaturedProducts(limit = 4) {
return this.get("/api/products/featured", { limit }, true);
}
// Pages methods
async getPages() {
return this.get("/api/pages", {}, true);
}
async getPage(slug) {
return this.get(`/api/pages/${slug}`, {}, true);
}
// Menu methods
async getMenu() {
return this.get("/api/menu", {}, true);
}
}
// Replace global API client if it exists
if (window.API) {
const oldAPI = window.API;
window.API = new EnhancedAPIClient(oldAPI.baseURL || "");
} else {
window.API = new EnhancedAPIClient();
}
})();

View File

@@ -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();
}
})();

View File

@@ -0,0 +1,63 @@
/**
* Frontend Error Handler
* Centralized error handling and logging
*/
(function () {
"use strict";
class ErrorHandler {
constructor() {
this.errors = [];
this.maxErrors = 100;
this.productionMode = window.location.hostname !== "localhost";
}
log(context, error, level = "error") {
const errorEntry = {
timestamp: new Date().toISOString(),
context,
message: error?.message || error,
level,
stack: error?.stack,
};
this.errors.push(errorEntry);
if (this.errors.length > this.maxErrors) {
this.errors.shift();
}
// Only log to console in development
if (!this.productionMode && level === "error") {
console.error(`[${context}]`, error);
} else if (!this.productionMode && level === "warn") {
console.warn(`[${context}]`, error);
}
}
getErrors() {
return [...this.errors];
}
clearErrors() {
this.errors = [];
}
}
// Global error handler
window.ErrorHandler = window.ErrorHandler || new ErrorHandler();
// Global error event handler
window.addEventListener("error", (event) => {
window.ErrorHandler.log(
"GlobalError",
event.error || event.message,
"error"
);
});
// Unhandled promise rejection handler
window.addEventListener("unhandledrejection", (event) => {
window.ErrorHandler.log("UnhandledRejection", event.reason, "error");
});
})();

View File

@@ -0,0 +1,234 @@
/**
* Performance-Optimized Application Initializer
* Loads critical resources first, then defers non-critical scripts
*/
(function () {
"use strict";
// ============================================================
// CRITICAL PATH - Load immediately
// ============================================================
// Performance monitoring
const perfMarks = {
scriptStart: performance.now(),
domReady: 0,
imagesLoaded: 0,
appInitialized: 0,
};
// Optimized lazy image loader initialization
let lazyLoader = null;
function initLazyLoading() {
if (
window.PerformanceUtils &&
window.PerformanceUtils.OptimizedLazyLoader
) {
lazyLoader = new window.PerformanceUtils.OptimizedLazyLoader({
rootMargin: "100px",
threshold: 0.01,
});
console.log("[Performance] Lazy loading initialized");
}
}
// Resource hints for external resources
function addResourceHints() {
if (window.PerformanceUtils && window.PerformanceUtils.ResourceHints) {
// Preconnect to CDNs (if used)
window.PerformanceUtils.ResourceHints.addPreconnect([
"https://cdn.jsdelivr.net",
"https://fonts.googleapis.com",
"https://fonts.gstatic.com",
]);
console.log("[Performance] Resource hints added");
}
}
// Debounced scroll handler
let scrollHandler = null;
function initOptimizedScrollHandlers() {
if (window.PerformanceUtils && window.PerformanceUtils.rafThrottle) {
// Use RAF throttle for smooth 60fps scrolling
scrollHandler = window.PerformanceUtils.rafThrottle(() => {
// Any scroll-based updates here
const scrollTop =
window.pageYOffset || document.documentElement.scrollTop;
document.body.classList.toggle("scrolled", scrollTop > 50);
});
window.addEventListener("scroll", scrollHandler, { passive: true });
console.log("[Performance] Optimized scroll handler attached");
}
}
// Event delegation for better memory management
let eventDelegator = null;
function initEventDelegation() {
if (window.PerformanceUtils && window.PerformanceUtils.EventDelegator) {
eventDelegator = new window.PerformanceUtils.EventDelegator();
// Delegate all button clicks
eventDelegator.on("click", "button[data-action]", function (e) {
const action = this.dataset.action;
const event = new CustomEvent("app:action", {
detail: { action, element: this },
bubbles: true,
});
document.dispatchEvent(event);
});
console.log("[Performance] Event delegation initialized");
}
}
// DOM Batcher for efficient updates
let domBatcher = null;
function initDOMBatcher() {
if (window.PerformanceUtils && window.PerformanceUtils.DOMBatcher) {
domBatcher = new window.PerformanceUtils.DOMBatcher();
window.AppState = window.AppState || {};
window.AppState.domBatcher = domBatcher;
console.log("[Performance] DOM batcher initialized");
}
}
// ============================================================
// INITIALIZATION SEQUENCE
// ============================================================
function onDOMReady() {
perfMarks.domReady = performance.now();
console.log(
`[Performance] DOM ready in ${(
perfMarks.domReady - perfMarks.scriptStart
).toFixed(2)}ms`
);
// Add resource hints immediately
addResourceHints();
// Initialize performance utilities
initLazyLoading();
initOptimizedScrollHandlers();
initEventDelegation();
initDOMBatcher();
// Initialize main app (if loaded)
if (window.AppState && typeof window.AppState.init === "function") {
window.AppState.init();
perfMarks.appInitialized = performance.now();
console.log(
`[Performance] App initialized in ${(
perfMarks.appInitialized - perfMarks.domReady
).toFixed(2)}ms`
);
}
// Monitor when all images are loaded
if (document.images.length > 0) {
Promise.all(
Array.from(document.images)
.filter((img) => !img.complete)
.map(
(img) =>
new Promise((resolve) => {
img.addEventListener("load", resolve);
img.addEventListener("error", resolve);
})
)
).then(() => {
perfMarks.imagesLoaded = performance.now();
console.log(
`[Performance] All images loaded in ${(
perfMarks.imagesLoaded - perfMarks.domReady
).toFixed(2)}ms`
);
reportPerformanceMetrics();
});
} else {
perfMarks.imagesLoaded = performance.now();
reportPerformanceMetrics();
}
}
function onWindowLoad() {
console.log(
`[Performance] Window fully loaded in ${(
performance.now() - perfMarks.scriptStart
).toFixed(2)}ms`
);
}
// Report performance metrics
function reportPerformanceMetrics() {
if (!window.performance || !window.performance.timing) return;
const timing = performance.timing;
const metrics = {
// Page load metrics
domContentLoaded:
timing.domContentLoadedEventEnd - timing.navigationStart,
windowLoad: timing.loadEventEnd - timing.navigationStart,
// Network metrics
dns: timing.domainLookupEnd - timing.domainLookupStart,
tcp: timing.connectEnd - timing.connectStart,
request: timing.responseStart - timing.requestStart,
response: timing.responseEnd - timing.responseStart,
// Rendering metrics
domProcessing: timing.domComplete - timing.domLoading,
// Script metrics
scriptExecution: perfMarks.appInitialized - perfMarks.scriptStart,
imageLoading: perfMarks.imagesLoaded - perfMarks.domReady,
// Paint metrics (if available)
firstPaint: null,
firstContentfulPaint: null,
};
// Get paint metrics
if (window.performance && window.performance.getEntriesByType) {
const paintEntries = performance.getEntriesByType("paint");
paintEntries.forEach((entry) => {
if (entry.name === "first-paint") {
metrics.firstPaint = entry.startTime;
} else if (entry.name === "first-contentful-paint") {
metrics.firstContentfulPaint = entry.startTime;
}
});
}
console.table(metrics);
// Store for analytics (if needed)
window.performanceMetrics = metrics;
}
// ============================================================
// EVENT LISTENERS
// ============================================================
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", onDOMReady);
} else {
// DOM already loaded
setTimeout(onDOMReady, 0);
}
window.addEventListener("load", onWindowLoad);
// Cleanup on page unload
window.addEventListener("beforeunload", () => {
if (lazyLoader) lazyLoader.destroy();
if (scrollHandler) window.removeEventListener("scroll", scrollHandler);
});
// Export performance marks for debugging
window.perfMarks = perfMarks;
console.log("[Performance] Optimized initializer loaded");
})();

View File

@@ -0,0 +1,210 @@
/**
* Optimized Lazy Loading for Images
* Improves page load time by deferring offscreen images
* Uses Intersection Observer API for efficient monitoring
*/
(function () {
"use strict";
// Configuration
const CONFIG = {
rootMargin: "50px", // Start loading 50px before entering viewport
threshold: 0.01,
loadingClass: "lazy-loading",
loadedClass: "lazy-loaded",
errorClass: "lazy-error",
};
// Image cache to prevent duplicate loads
const imageCache = new Set();
/**
* Preload image and return promise
*/
function preloadImage(src) {
if (imageCache.has(src)) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
imageCache.add(src);
resolve();
};
img.onerror = reject;
img.src = src;
});
}
/**
* Load image with fade-in effect
*/
async function loadImage(img) {
const src = img.dataset.src;
const srcset = img.dataset.srcset;
if (!src) return;
img.classList.add(CONFIG.loadingClass);
try {
// Preload the image
await preloadImage(src);
// Set the actual src
img.src = src;
if (srcset) {
img.srcset = srcset;
}
// Remove data attributes to free memory
delete img.dataset.src;
delete img.dataset.srcset;
// Add loaded class for fade-in animation
img.classList.remove(CONFIG.loadingClass);
img.classList.add(CONFIG.loadedClass);
} catch (error) {
console.error("Failed to load image:", src, error);
img.classList.remove(CONFIG.loadingClass);
img.classList.add(CONFIG.errorClass);
// Set fallback/placeholder if available
if (img.dataset.fallback) {
img.src = img.dataset.fallback;
}
}
}
/**
* Initialize lazy loading with Intersection Observer
*/
function initLazyLoad() {
// Check for browser support
if (!("IntersectionObserver" in window)) {
// Fallback: load all images immediately
console.warn("IntersectionObserver not supported, loading all images");
const images = document.querySelectorAll("img[data-src]");
images.forEach(loadImage);
return;
}
// Create observer
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
loadImage(img);
observer.unobserve(img); // Stop observing once loaded
}
});
},
{
rootMargin: CONFIG.rootMargin,
threshold: CONFIG.threshold,
}
);
// Observe all lazy images
const lazyImages = document.querySelectorAll("img[data-src]");
lazyImages.forEach((img) => observer.observe(img));
// Also observe images added dynamically
const mutationObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeName === "IMG" && node.dataset && node.dataset.src) {
observer.observe(node);
}
// Check child images
if (node.querySelectorAll) {
const childImages = node.querySelectorAll("img[data-src]");
childImages.forEach((img) => observer.observe(img));
}
});
});
});
mutationObserver.observe(document.body, {
childList: true,
subtree: true,
});
// Store observers globally for cleanup
window._lazyLoadObservers = { observer, mutationObserver };
}
/**
* Cleanup observers (call on page unload if needed)
*/
function cleanup() {
if (window._lazyLoadObservers) {
const { observer, mutationObserver } = window._lazyLoadObservers;
observer.disconnect();
mutationObserver.disconnect();
}
}
// Add CSS for loading states
function injectStyles() {
if (document.getElementById("lazy-load-styles")) return;
const style = document.createElement("style");
style.id = "lazy-load-styles";
style.textContent = `
img[data-src] {
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
img.lazy-loading {
opacity: 0.5;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
img.lazy-loaded {
opacity: 1;
}
img.lazy-error {
opacity: 0.3;
border: 2px dashed #ccc;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Prevent layout shift */
img[data-src] {
min-height: 200px;
background-color: #f5f5f5;
}
`;
document.head.appendChild(style);
}
// Initialize on DOM ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
injectStyles();
initLazyLoad();
});
} else {
injectStyles();
initLazyLoad();
}
// Export for manual usage
window.LazyLoad = {
init: initLazyLoad,
load: loadImage,
cleanup: cleanup,
};
})();

View File

@@ -6,6 +6,8 @@
(function () {
"use strict";
console.log('[main.js] Loading...');
// Global state management
window.AppState = {
cart: [],
@@ -13,12 +15,17 @@
products: [],
settings: null,
user: null,
_saveCartTimeout: null,
_saveWishlistTimeout: null,
// Initialize state from localStorage
init() {
console.log('[AppState] Initializing...');
console.log('[AppState] window.AppState exists:', !!window.AppState);
this.loadCart();
this.loadWishlist();
this.updateUI();
console.log('[AppState] Initialized - Cart:', this.cart.length, 'items, Wishlist:', this.wishlist.length, 'items');
},
// Cart management
@@ -33,23 +40,33 @@
},
saveCart() {
try {
localStorage.setItem("cart", JSON.stringify(this.cart));
this.updateUI();
} catch (error) {
console.error("Error saving cart:", error);
// Debounce saves to reduce localStorage writes
if (this._saveCartTimeout) {
clearTimeout(this._saveCartTimeout);
}
this._saveCartTimeout = setTimeout(() => {
try {
localStorage.setItem("cart", JSON.stringify(this.cart));
this.updateUI();
} catch (error) {
console.error("Error saving cart:", error);
}
}, 100);
},
addToCart(product, quantity = 1) {
console.log('[AppState] addToCart called:', product, 'quantity:', quantity);
const existing = this.cart.find((item) => item.id === product.id);
if (existing) {
console.log('[AppState] Product exists in cart, updating quantity');
existing.quantity += quantity;
} else {
console.log('[AppState] Adding new product to cart');
this.cart.push({ ...product, quantity });
}
console.log('[AppState] Cart after add:', this.cart);
this.saveCart();
this.showNotification("Added to cart", "success");
this.showNotification(`${product.name || product.title || 'Item'} added to cart`, "success");
},
removeFromCart(productId) {
@@ -60,6 +77,9 @@
updateCartQuantity(productId, quantity) {
const item = this.cart.find((item) => item.id === productId);
// Dispatch custom event for cart dropdown
window.dispatchEvent(new CustomEvent('cart-updated', { detail: this.cart }));
if (item) {
item.quantity = Math.max(1, quantity);
this.saveCart();
@@ -98,9 +118,17 @@
},
addToWishlist(product) {
if (!this.wishlist.find((item) => item.id === product.id)) {
if (!this.wishlist.find(`${product.name || product.title || 'Item'} added to wishlist`, "success");
// Dispatch custom event for wishlist dropdown
window.dispatchEvent(new CustomEvent('wishlist-updated', { detail: this.wishlist }));
} else {
this.showNotification("Already in wishlist", "info.id)) {
this.wishlist.push(product);
this.saveWishlist();
// Dispatch custom event for wishlist dropdown
window.dispatchEvent(new CustomEvent('wishlist-updated', { detail: this.wishlist }));
this.showNotification("Added to wishlist", "success");
}
},
@@ -123,20 +151,32 @@
updateCartUI() {
const count = this.getCartCount();
console.log('[AppState] Updating cart UI, count:', count);
const badge = document.getElementById("cartCount");
if (badge) {
badge.textContent = count;
badge.style.display = count > 0 ? "flex" : "none";
console.log('[AppState] Cart badge updated');
} else {
console.warn('[AppState] Cart badge element not found');
}
// Also trigger cart dropdown update
window.dispatchEvent(new CustomEvent('cart-updated', { detail: this.cart }));
},
updateWishlistUI() {
const count = this.wishlist.length;
console.log('[AppState] Updating wishlist UI, count:', count);
const badge = document.getElementById("wishlistCount");
if (badge) {
badge.textContent = count;
badge.style.display = count > 0 ? "flex" : "none";
console.log('[AppState] Wishlist badge updated');
} else {
console.warn('[AppState] Wishlist badge element not found');
}
// Also trigger wishlist dropdown update
window.dispatchEvent(new CustomEvent('wishlist-updated', { detail: this.wishlist }));
},
// Notifications
@@ -301,11 +341,14 @@
};
// Initialize on DOM ready
console.log('[main.js] Script loaded, document.readyState:', document.readyState);
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
console.log('[main.js] DOMContentLoaded fired');
window.AppState.init();
});
} else {
console.log('[main.js] DOM already loaded, initializing immediately');
window.AppState.init();
}

View File

@@ -0,0 +1,302 @@
/**
* Frontend Performance Optimization Utilities
* Provides utilities for optimizing frontend load time and performance
*/
/**
* Lazy load images with IntersectionObserver
* More efficient than scroll-based lazy loading
*/
class OptimizedLazyLoader {
constructor(options = {}) {
this.options = {
rootMargin: options.rootMargin || "50px",
threshold: options.threshold || 0.01,
loadingClass: options.loadingClass || "lazy-loading",
loadedClass: options.loadedClass || "lazy-loaded",
errorClass: options.errorClass || "lazy-error",
};
this.observer = null;
this.init();
}
init() {
if ("IntersectionObserver" in window) {
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
{
rootMargin: this.options.rootMargin,
threshold: this.options.threshold,
}
);
this.observeImages();
} else {
// Fallback for older browsers
this.loadAllImages();
}
}
handleIntersection(entries) {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
this.observer.unobserve(entry.target);
}
});
}
loadImage(img) {
const src = img.dataset.src;
const srcset = img.dataset.srcset;
if (!src) return;
img.classList.add(this.options.loadingClass);
// Preload the image
const tempImg = new Image();
tempImg.onload = () => {
img.src = src;
if (srcset) img.srcset = srcset;
img.classList.remove(this.options.loadingClass);
img.classList.add(this.options.loadedClass);
img.removeAttribute("data-src");
img.removeAttribute("data-srcset");
};
tempImg.onerror = () => {
img.classList.remove(this.options.loadingClass);
img.classList.add(this.options.errorClass);
console.warn("Failed to load image:", src);
};
tempImg.src = src;
if (srcset) tempImg.srcset = srcset;
}
observeImages() {
const images = document.querySelectorAll("img[data-src]");
images.forEach((img) => {
this.observer.observe(img);
});
}
loadAllImages() {
const images = document.querySelectorAll("img[data-src]");
images.forEach((img) => this.loadImage(img));
}
destroy() {
if (this.observer) {
this.observer.disconnect();
}
}
}
/**
* Resource Hints Manager
* Adds DNS prefetch, preconnect, and preload hints
*/
class ResourceHints {
static addDNSPrefetch(domains) {
domains.forEach((domain) => {
if (
!document.querySelector(`link[rel="dns-prefetch"][href="${domain}"]`)
) {
const link = document.createElement("link");
link.rel = "dns-prefetch";
link.href = domain;
document.head.appendChild(link);
}
});
}
static addPreconnect(urls) {
urls.forEach((url) => {
if (!document.querySelector(`link[rel="preconnect"][href="${url}"]`)) {
const link = document.createElement("link");
link.rel = "preconnect";
link.href = url;
link.crossOrigin = "anonymous";
document.head.appendChild(link);
}
});
}
static preloadImage(src, as = "image") {
if (!document.querySelector(`link[rel="preload"][href="${src}"]`)) {
const link = document.createElement("link");
link.rel = "preload";
link.as = as;
link.href = src;
document.head.appendChild(link);
}
}
}
/**
* Debounce with leading edge option
* Better for performance-critical events
*/
function optimizedDebounce(func, wait, options = {}) {
let timeout;
let lastCallTime = 0;
const { leading = false, maxWait = 0 } = options;
return function debounced(...args) {
const now = Date.now();
const timeSinceLastCall = now - lastCallTime;
const callNow = leading && !timeout;
const shouldInvokeFromMaxWait = maxWait > 0 && timeSinceLastCall >= maxWait;
clearTimeout(timeout);
if (callNow) {
lastCallTime = now;
return func.apply(this, args);
}
if (shouldInvokeFromMaxWait) {
lastCallTime = now;
return func.apply(this, args);
}
timeout = setTimeout(() => {
lastCallTime = Date.now();
func.apply(this, args);
}, wait);
};
}
/**
* Request Animation Frame Throttle
* Ensures maximum one execution per frame
*/
function rafThrottle(func) {
let rafId = null;
let lastArgs = null;
return function throttled(...args) {
lastArgs = args;
if (rafId === null) {
rafId = requestAnimationFrame(() => {
func.apply(this, lastArgs);
rafId = null;
lastArgs = null;
});
}
};
}
/**
* Memory-efficient event delegation
*/
class EventDelegator {
constructor(rootElement = document) {
this.root = rootElement;
this.handlers = new Map();
}
on(eventType, selector, handler) {
if (!this.handlers.has(eventType)) {
this.handlers.set(eventType, []);
this.root.addEventListener(
eventType,
this.handleEvent.bind(this, eventType)
);
}
this.handlers.get(eventType).push({ selector, handler });
}
handleEvent(eventType, event) {
const handlers = this.handlers.get(eventType);
if (!handlers) return;
const target = event.target;
handlers.forEach(({ selector, handler }) => {
const element = target.closest(selector);
if (element) {
handler.call(element, event);
}
});
}
off(eventType, selector) {
const handlers = this.handlers.get(eventType);
if (!handlers) return;
const filtered = handlers.filter((h) => h.selector !== selector);
if (filtered.length === 0) {
this.root.removeEventListener(eventType, this.handleEvent);
this.handlers.delete(eventType);
} else {
this.handlers.set(eventType, filtered);
}
}
}
/**
* Batch DOM updates to minimize reflows
*/
class DOMBatcher {
constructor() {
this.reads = [];
this.writes = [];
this.scheduled = false;
}
read(fn) {
this.reads.push(fn);
this.schedule();
}
write(fn) {
this.writes.push(fn);
this.schedule();
}
schedule() {
if (this.scheduled) return;
this.scheduled = true;
requestAnimationFrame(() => {
// Execute all reads first
this.reads.forEach((fn) => fn());
this.reads = [];
// Then execute all writes
this.writes.forEach((fn) => fn());
this.writes = [];
this.scheduled = false;
});
}
}
// Export for use
if (typeof module !== "undefined" && module.exports) {
module.exports = {
OptimizedLazyLoader,
ResourceHints,
optimizedDebounce,
rafThrottle,
EventDelegator,
DOMBatcher,
};
} else if (typeof window !== "undefined") {
window.PerformanceUtils = {
OptimizedLazyLoader,
ResourceHints,
optimizedDebounce,
rafThrottle,
EventDelegator,
DOMBatcher,
};
}

View File

@@ -0,0 +1,248 @@
/**
* Resource Preloading and Optimization Manager
* Manages critical resource loading and performance optimization
*/
(function () {
"use strict";
const ResourceOptimizer = {
// Preload critical resources
preloadCritical() {
const criticalResources = [
{ href: "/assets/css/main.css", as: "style" },
{ href: "/assets/css/navbar.css", as: "style" },
{ href: "/assets/js/main.js", as: "script" },
];
criticalResources.forEach((resource) => {
const link = document.createElement("link");
link.rel = "preload";
link.href = resource.href;
link.as = resource.as;
if (resource.as === "style") {
link.onload = function () {
this.rel = "stylesheet";
};
}
document.head.appendChild(link);
});
},
// Prefetch resources for likely navigation
prefetchRoutes() {
const routes = [
"/shop.html",
"/product.html",
"/about.html",
"/contact.html",
];
// Prefetch on idle
if ("requestIdleCallback" in window) {
requestIdleCallback(() => {
routes.forEach((route) => {
const link = document.createElement("link");
link.rel = "prefetch";
link.href = route;
document.head.appendChild(link);
});
});
}
},
// Defer non-critical scripts
deferNonCritical() {
const scripts = document.querySelectorAll("script[data-defer]");
const loadScript = (script) => {
const newScript = document.createElement("script");
newScript.src = script.dataset.defer;
newScript.async = true;
document.body.appendChild(newScript);
};
if ("requestIdleCallback" in window) {
scripts.forEach((script) => {
requestIdleCallback(() => loadScript(script));
});
} else {
setTimeout(() => {
scripts.forEach(loadScript);
}, 2000);
}
},
// Optimize font loading
optimizeFonts() {
// Use font-display: swap for all fonts
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(() => {
document.body.classList.add("fonts-loaded");
});
}
},
// Reduce main thread work with requestIdleCallback
scheduleIdleWork(callback) {
if ("requestIdleCallback" in window) {
requestIdleCallback(callback, { timeout: 2000 });
} else {
setTimeout(callback, 1);
}
},
// Batch DOM reads and writes
batchDOMOperations(operations) {
requestAnimationFrame(() => {
operations.forEach((op) => op());
});
},
// Monitor performance
monitorPerformance() {
if ("PerformanceObserver" in window) {
// Monitor Long Tasks
try {
const longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.warn("Long task detected:", {
duration: entry.duration,
startTime: entry.startTime,
});
}
}
});
longTaskObserver.observe({ entryTypes: ["longtask"] });
} catch (e) {
// Long task API not supported
}
// Monitor Largest Contentful Paint
try {
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log("LCP:", lastEntry.renderTime || lastEntry.loadTime);
});
lcpObserver.observe({ entryTypes: ["largest-contentful-paint"] });
} catch (e) {
// LCP API not supported
}
}
},
// Get performance metrics
getMetrics() {
if (!window.performance || !window.performance.timing) {
return null;
}
const timing = window.performance.timing;
const navigation = window.performance.navigation;
return {
// Page load metrics
domContentLoaded:
timing.domContentLoadedEventEnd - timing.navigationStart,
loadComplete: timing.loadEventEnd - timing.navigationStart,
// Network metrics
dns: timing.domainLookupEnd - timing.domainLookupStart,
tcp: timing.connectEnd - timing.connectStart,
request: timing.responseStart - timing.requestStart,
response: timing.responseEnd - timing.responseStart,
// Rendering metrics
domProcessing: timing.domComplete - timing.domLoading,
domInteractive: timing.domInteractive - timing.navigationStart,
// Navigation type
navigationType: navigation.type,
redirectCount: navigation.redirectCount,
};
},
// Log metrics to console (development only)
logMetrics() {
window.addEventListener("load", () => {
setTimeout(() => {
const metrics = this.getMetrics();
if (metrics) {
console.table(metrics);
}
}, 0);
});
},
// Optimize images
optimizeImages() {
const images = document.querySelectorAll("img:not([loading])");
images.forEach((img) => {
// Add native lazy loading
if ("loading" in HTMLImageElement.prototype) {
img.loading = "lazy";
}
// Add decoding hint
img.decoding = "async";
});
},
// Preconnect to external domains
preconnectDomains() {
const domains = [
"https://fonts.googleapis.com",
"https://fonts.gstatic.com",
"https://cdn.jsdelivr.net",
];
domains.forEach((domain) => {
const link = document.createElement("link");
link.rel = "preconnect";
link.href = domain;
link.crossOrigin = "anonymous";
document.head.appendChild(link);
});
},
// Initialize all optimizations
init() {
// Preconnect to external domains early
this.preconnectDomains();
// Optimize fonts
this.optimizeFonts();
// Optimize existing images
this.optimizeImages();
// Monitor performance in development
if (
window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1"
) {
this.monitorPerformance();
this.logMetrics();
}
// Defer non-critical resources
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
this.deferNonCritical();
this.prefetchRoutes();
});
} else {
this.deferNonCritical();
this.prefetchRoutes();
}
},
};
// Auto-initialize
ResourceOptimizer.init();
// Export globally
window.ResourceOptimizer = ResourceOptimizer;
})();

View File

@@ -0,0 +1,729 @@
/**
* 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;
cartBadge.style.display = count > 0 ? "flex" : "none";
}
// Update wishlist badge
const wishlistBadge = document.getElementById("wishlistCount");
if (wishlistBadge) {
const count = this.wishlist.length;
wishlistBadge.textContent = count;
wishlistBadge.style.display = count > 0 ? "flex" : "none";
}
}
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) {
footer.innerHTML =
'<a href="/shop" class="btn-outline">Continue Shopping</a>';
} else {
footer.innerHTML = `
<div class="cart-total">
<span>Total:</span>
<strong>$${total.toFixed(2)}</strong>
</div>
<a href="/shop" class="btn-text">Continue Shopping</a>
<button class="btn-primary-full" onclick="alert('Checkout coming soon!')">
Proceed to Checkout
</button>
`;
}
}
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!");
})();

View File

@@ -182,7 +182,7 @@
? product.description.substring(0, 100) + "..."
: "");
const isInWishlist = window.AppState?.isInWishlist(id) || false;
const isInWishlist = window.ShopSystem?.isInWishlist(id) || false;
return `
<article class="product-card" data-id="${id}">
@@ -228,7 +228,7 @@
const id = parseInt(e.currentTarget.dataset.id);
const product = this.products.find((p) => p.id === id);
if (product) {
window.AppState.addToCart(product);
window.ShopSystem.addToCart(product, 1);
}
});
});
@@ -242,10 +242,10 @@
const id = parseInt(e.currentTarget.dataset.id);
const product = this.products.find((p) => p.id === id);
if (product) {
if (window.AppState.isInWishlist(id)) {
window.AppState.removeFromWishlist(id);
if (window.ShopSystem.isInWishlist(id)) {
window.ShopSystem.removeFromWishlist(id);
} else {
window.AppState.addToWishlist(product);
window.ShopSystem.addToWishlist(product);
}
this.renderProducts(this.products);
}

View File

@@ -61,7 +61,7 @@
// Cart methods
addToCart(product, quantity = 1) {
const existing = this.state.cart.find((item) => item.id === product.id);
const existing = this._findById(this.state.cart, product.id);
if (existing) {
existing.quantity += quantity;
@@ -73,27 +73,26 @@
});
}
this.saveToStorage();
this.emit("cartUpdated", this.state.cart);
this._updateState("cart");
return this.state.cart;
}
removeFromCart(productId) {
this.state.cart = this.state.cart.filter((item) => item.id !== productId);
this.saveToStorage();
this.emit("cartUpdated", this.state.cart);
this.state.cart = this.state.cart.filter(
(item) => String(item.id) !== String(productId)
);
this._updateState("cart");
return this.state.cart;
}
updateCartQuantity(productId, quantity) {
const item = this.state.cart.find((item) => item.id === productId);
const item = this._findById(this.state.cart, productId);
if (item) {
item.quantity = Math.max(0, quantity);
if (item.quantity === 0) {
return this.removeFromCart(productId);
}
this.saveToStorage();
this.emit("cartUpdated", this.state.cart);
this._updateState("cart");
}
return this.state.cart;
}
@@ -103,20 +102,16 @@
}
getCartTotal() {
return this.state.cart.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return this._calculateTotal(this.state.cart);
}
getCartCount() {
return this.state.cart.reduce((sum, item) => sum + item.quantity, 0);
return this._calculateCount(this.state.cart);
}
clearCart() {
this.state.cart = [];
this.saveToStorage();
this.emit("cartUpdated", this.state.cart);
this._updateState("cart");
}
// Wishlist methods
@@ -179,6 +174,31 @@
});
}
}
// Helper methods
_findById(collection, id) {
return collection.find((item) => String(item.id) === String(id));
}
_updateState(type) {
this.saveToStorage();
this.emit(`${type}Updated`, this.state[type]);
}
_calculateTotal(items) {
return items.reduce((sum, item) => {
const price = parseFloat(item.price) || 0;
const quantity = parseInt(item.quantity) || 0;
return sum + price * quantity;
}, 0);
}
_calculateCount(items) {
return items.reduce((sum, item) => {
const quantity = parseInt(item.quantity) || 0;
return sum + quantity;
}, 0);
}
}
// Create global instance