Phase 1: Remove obsolete files and standardize all pages
- Standardize script loading on faq, privacy, returns, shipping-info pages - Archive 14 unused JS files (cart-functions, shopping, cart.js, enhanced versions, etc.) - Archive 2 unused CSS files (responsive-enhanced, responsive-fixes) - All pages now use consistent script loading order - Eliminated shopping.js dependency (not needed after standardization)
This commit is contained in:
287
website/public/assets/js/archive/accessibility-enhanced.js
Normal file
287
website/public/assets/js/archive/accessibility-enhanced.js
Normal file
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* Accessibility Enhancements
|
||||
* WCAG 2.1 AA Compliant
|
||||
*/
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const A11y = {
|
||||
init() {
|
||||
this.addSkipLink();
|
||||
this.enhanceFocusManagement();
|
||||
this.addARIALabels();
|
||||
this.improveKeyboardNav();
|
||||
this.addLiveRegions();
|
||||
this.enhanceFormAccessibility();
|
||||
console.log("[A11y] Accessibility enhancements loaded");
|
||||
},
|
||||
|
||||
// Add skip to main content link
|
||||
addSkipLink() {
|
||||
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.querySelector("#main-content, main");
|
||||
if (main) {
|
||||
main.setAttribute("tabindex", "-1");
|
||||
main.focus();
|
||||
}
|
||||
});
|
||||
|
||||
document.body.insertBefore(skipLink, document.body.firstChild);
|
||||
},
|
||||
|
||||
// Enhance focus management
|
||||
enhanceFocusManagement() {
|
||||
// Trap focus in modals
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key !== "Tab") return;
|
||||
|
||||
const modal = document.querySelector(
|
||||
'.modal.active, .dropdown[style*="display: flex"]'
|
||||
);
|
||||
if (!modal) return;
|
||||
|
||||
const focusable = modal.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
|
||||
if (focusable.length === 0) return;
|
||||
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
|
||||
if (e.shiftKey && document.activeElement === first) {
|
||||
e.preventDefault();
|
||||
last.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === last) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Focus visible styles
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
*:focus-visible {
|
||||
outline: 3px solid #667eea !important;
|
||||
outline-offset: 2px !important;
|
||||
}
|
||||
|
||||
button:focus-visible,
|
||||
a:focus-visible,
|
||||
input:focus-visible,
|
||||
select:focus-visible,
|
||||
textarea:focus-visible {
|
||||
outline: 3px solid #667eea !important;
|
||||
outline-offset: 2px !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
},
|
||||
|
||||
// Add ARIA labels to interactive elements
|
||||
addARIALabels() {
|
||||
// Cart button
|
||||
const cartBtn = document.querySelector("#cart-btn");
|
||||
if (cartBtn && !cartBtn.hasAttribute("aria-label")) {
|
||||
cartBtn.setAttribute("aria-label", "Shopping cart");
|
||||
cartBtn.setAttribute("aria-haspopup", "true");
|
||||
}
|
||||
|
||||
// Wishlist button
|
||||
const wishlistBtn = document.querySelector("#wishlist-btn");
|
||||
if (wishlistBtn && !wishlistBtn.hasAttribute("aria-label")) {
|
||||
wishlistBtn.setAttribute("aria-label", "Wishlist");
|
||||
wishlistBtn.setAttribute("aria-haspopup", "true");
|
||||
}
|
||||
|
||||
// Mobile menu toggle
|
||||
const menuToggle = document.querySelector(".mobile-menu-toggle");
|
||||
if (menuToggle && !menuToggle.hasAttribute("aria-label")) {
|
||||
menuToggle.setAttribute("aria-label", "Open navigation menu");
|
||||
menuToggle.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
|
||||
// Add ARIA labels to product cards
|
||||
document.querySelectorAll(".product-card").forEach((card, index) => {
|
||||
if (!card.hasAttribute("role")) {
|
||||
card.setAttribute("role", "article");
|
||||
}
|
||||
|
||||
const title = card.querySelector("h3, .product-title");
|
||||
if (title && !title.id) {
|
||||
title.id = `product-title-${index}`;
|
||||
card.setAttribute("aria-labelledby", title.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Add labels to icon-only buttons
|
||||
document.querySelectorAll("button:not([aria-label])").forEach((btn) => {
|
||||
const icon = btn.querySelector('i[class*="bi-"]');
|
||||
if (icon && !btn.textContent.trim()) {
|
||||
const iconClass = icon.className;
|
||||
let label = "Button";
|
||||
|
||||
if (iconClass.includes("cart")) label = "Add to cart";
|
||||
else if (iconClass.includes("heart")) label = "Add to wishlist";
|
||||
else if (iconClass.includes("trash")) label = "Remove";
|
||||
else if (iconClass.includes("plus")) label = "Increase";
|
||||
else if (iconClass.includes("minus") || iconClass.includes("dash"))
|
||||
label = "Decrease";
|
||||
else if (iconClass.includes("close") || iconClass.includes("x"))
|
||||
label = "Close";
|
||||
|
||||
btn.setAttribute("aria-label", label);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Improve keyboard navigation
|
||||
improveKeyboardNav() {
|
||||
// Dropdown keyboard support
|
||||
document.querySelectorAll("[data-dropdown-toggle]").forEach((toggle) => {
|
||||
toggle.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
toggle.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Product card keyboard navigation
|
||||
document.querySelectorAll(".product-card").forEach((card) => {
|
||||
const link = card.querySelector("a");
|
||||
if (link) {
|
||||
card.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" && e.target === card) {
|
||||
link.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Quantity input keyboard support
|
||||
document.querySelectorAll(".quantity-input").forEach((input) => {
|
||||
input.addEventListener("keydown", (e) => {
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
const newValue = parseInt(input.value || 1) + 1;
|
||||
if (newValue <= 99) {
|
||||
input.value = newValue;
|
||||
input.dispatchEvent(new Event("change"));
|
||||
}
|
||||
} else if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
const newValue = parseInt(input.value || 1) - 1;
|
||||
if (newValue >= 1) {
|
||||
input.value = newValue;
|
||||
input.dispatchEvent(new Event("change"));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// Add live regions for dynamic content
|
||||
addLiveRegions() {
|
||||
// Create announcement region
|
||||
if (!document.querySelector("#a11y-announcements")) {
|
||||
const announcer = document.createElement("div");
|
||||
announcer.id = "a11y-announcements";
|
||||
announcer.setAttribute("role", "status");
|
||||
announcer.setAttribute("aria-live", "polite");
|
||||
announcer.setAttribute("aria-atomic", "true");
|
||||
announcer.className = "sr-only";
|
||||
document.body.appendChild(announcer);
|
||||
}
|
||||
|
||||
// Announce cart/wishlist updates
|
||||
window.addEventListener("cart-updated", (e) => {
|
||||
this.announce(`Cart updated. ${e.detail.length} items in cart.`);
|
||||
});
|
||||
|
||||
window.addEventListener("wishlist-updated", (e) => {
|
||||
this.announce(
|
||||
`Wishlist updated. ${e.detail.length} items in wishlist.`
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
announce(message) {
|
||||
const announcer = document.querySelector("#a11y-announcements");
|
||||
if (announcer) {
|
||||
announcer.textContent = "";
|
||||
setTimeout(() => {
|
||||
announcer.textContent = message;
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
|
||||
// Enhance form accessibility
|
||||
enhanceFormAccessibility() {
|
||||
// Add required indicators
|
||||
document
|
||||
.querySelectorAll(
|
||||
"input[required], select[required], textarea[required]"
|
||||
)
|
||||
.forEach((field) => {
|
||||
const label = document.querySelector(`label[for="${field.id}"]`);
|
||||
if (label && !label.querySelector(".required-indicator")) {
|
||||
const indicator = document.createElement("span");
|
||||
indicator.className = "required-indicator";
|
||||
indicator.textContent = " *";
|
||||
indicator.setAttribute("aria-label", "required");
|
||||
label.appendChild(indicator);
|
||||
}
|
||||
});
|
||||
|
||||
// Add error message associations
|
||||
document.querySelectorAll(".error-message").forEach((error, index) => {
|
||||
if (!error.id) {
|
||||
error.id = `error-${index}`;
|
||||
}
|
||||
|
||||
const field = error.previousElementSibling;
|
||||
if (
|
||||
field &&
|
||||
(field.tagName === "INPUT" ||
|
||||
field.tagName === "SELECT" ||
|
||||
field.tagName === "TEXTAREA")
|
||||
) {
|
||||
field.setAttribute("aria-describedby", error.id);
|
||||
field.setAttribute("aria-invalid", "true");
|
||||
}
|
||||
});
|
||||
|
||||
// Add autocomplete attributes
|
||||
document.querySelectorAll('input[type="email"]').forEach((field) => {
|
||||
if (!field.hasAttribute("autocomplete")) {
|
||||
field.setAttribute("autocomplete", "email");
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('input[type="tel"]').forEach((field) => {
|
||||
if (!field.hasAttribute("autocomplete")) {
|
||||
field.setAttribute("autocomplete", "tel");
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// Initialize on DOM ready
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => A11y.init());
|
||||
} else {
|
||||
A11y.init();
|
||||
}
|
||||
|
||||
// Export for external use
|
||||
window.A11y = A11y;
|
||||
})();
|
||||
220
website/public/assets/js/archive/accessibility.js
Normal file
220
website/public/assets/js/archive/accessibility.js
Normal 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();
|
||||
}
|
||||
})();
|
||||
160
website/public/assets/js/archive/api-enhanced.js
Normal file
160
website/public/assets/js/archive/api-enhanced.js
Normal 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();
|
||||
}
|
||||
})();
|
||||
155
website/public/assets/js/archive/cart-functions.js
Normal file
155
website/public/assets/js/archive/cart-functions.js
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Shared Cart and Wishlist Functions
|
||||
* Simple localStorage-based implementation that works on all pages
|
||||
*/
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// Cart Functions
|
||||
window.addToCart = function (productId, name, price, imageurl) {
|
||||
try {
|
||||
const cart = JSON.parse(localStorage.getItem("cart") || "[]");
|
||||
const existingItem = cart.find((item) => item.id === productId);
|
||||
|
||||
if (existingItem) {
|
||||
existingItem.quantity = (existingItem.quantity || 1) + 1;
|
||||
} else {
|
||||
cart.push({
|
||||
id: productId,
|
||||
name,
|
||||
price: parseFloat(price),
|
||||
imageurl,
|
||||
quantity: 1,
|
||||
});
|
||||
}
|
||||
|
||||
localStorage.setItem("cart", JSON.stringify(cart));
|
||||
updateCartBadge();
|
||||
showNotification(`${name} added to cart!`, "success");
|
||||
} catch (e) {
|
||||
console.error("Cart error:", e);
|
||||
showNotification("Added to cart!", "success");
|
||||
}
|
||||
};
|
||||
|
||||
// Wishlist Functions
|
||||
window.addToWishlist = function (productId, name, price, imageurl) {
|
||||
try {
|
||||
const wishlist = JSON.parse(localStorage.getItem("wishlist") || "[]");
|
||||
const exists = wishlist.find((item) => item.id === productId);
|
||||
|
||||
if (!exists) {
|
||||
wishlist.push({
|
||||
id: productId,
|
||||
name,
|
||||
price: parseFloat(price),
|
||||
imageurl,
|
||||
});
|
||||
localStorage.setItem("wishlist", JSON.stringify(wishlist));
|
||||
updateWishlistBadge();
|
||||
showNotification(`${name} added to wishlist!`, "success");
|
||||
} else {
|
||||
showNotification("Already in wishlist!", "info");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Wishlist error:", e);
|
||||
showNotification("Added to wishlist!", "success");
|
||||
}
|
||||
};
|
||||
|
||||
// Update Badge Functions
|
||||
function updateCartBadge() {
|
||||
try {
|
||||
const cart = JSON.parse(localStorage.getItem("cart") || "[]");
|
||||
const badge = document.querySelector(".cart-badge");
|
||||
if (badge) {
|
||||
const total = cart.reduce((sum, item) => sum + (item.quantity || 1), 0);
|
||||
badge.textContent = total;
|
||||
badge.style.display = total > 0 ? "flex" : "none";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Badge update error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function updateWishlistBadge() {
|
||||
try {
|
||||
const wishlist = JSON.parse(localStorage.getItem("wishlist") || "[]");
|
||||
const badge = document.querySelector(".wishlist-badge");
|
||||
if (badge) {
|
||||
badge.textContent = wishlist.length;
|
||||
badge.style.display = wishlist.length > 0 ? "flex" : "none";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Badge update error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Notification Function
|
||||
function showNotification(message, type = "info") {
|
||||
// Remove existing notifications
|
||||
document.querySelectorAll(".cart-notification").forEach((n) => n.remove());
|
||||
|
||||
const notification = document.createElement("div");
|
||||
notification.className = `cart-notification notification-${type}`;
|
||||
notification.textContent = message;
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
right: 20px;
|
||||
background: ${
|
||||
type === "success"
|
||||
? "#10b981"
|
||||
: type === "error"
|
||||
? "#ef4444"
|
||||
: "#3b82f6"
|
||||
};
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 10000;
|
||||
animation: slideInFromRight 0.3s ease;
|
||||
`;
|
||||
|
||||
// Add animation styles if not already present
|
||||
if (!document.getElementById("notification-animations")) {
|
||||
const style = document.createElement("style");
|
||||
style.id = "notification-animations";
|
||||
style.textContent = `
|
||||
@keyframes slideInFromRight {
|
||||
from { transform: translateX(400px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@keyframes slideOutToRight {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(400px); opacity: 0; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.animation = "slideOutToRight 0.3s ease";
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Initialize badges on page load
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
updateCartBadge();
|
||||
updateWishlistBadge();
|
||||
});
|
||||
} else {
|
||||
updateCartBadge();
|
||||
updateWishlistBadge();
|
||||
}
|
||||
|
||||
// Expose update functions globally
|
||||
window.updateCartBadge = updateCartBadge;
|
||||
window.updateWishlistBadge = updateWishlistBadge;
|
||||
})();
|
||||
406
website/public/assets/js/archive/cart.js
Normal file
406
website/public/assets/js/archive/cart.js
Normal file
@@ -0,0 +1,406 @@
|
||||
/**
|
||||
* Shopping Cart Component
|
||||
* Handles cart dropdown, updates, and interactions
|
||||
*/
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// Base Dropdown Component
|
||||
class BaseDropdown {
|
||||
constructor(config) {
|
||||
this.toggleBtn = document.getElementById(config.toggleId);
|
||||
this.panel = document.getElementById(config.panelId);
|
||||
this.content = document.getElementById(config.contentId);
|
||||
this.closeBtn = document.getElementById(config.closeId);
|
||||
this.wrapperClass = config.wrapperClass;
|
||||
this.eventName = config.eventName;
|
||||
this.emptyMessage = config.emptyMessage;
|
||||
this.isOpen = false;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.render();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
if (this.toggleBtn) {
|
||||
this.toggleBtn.addEventListener("click", () => this.toggle());
|
||||
}
|
||||
|
||||
if (this.closeBtn) {
|
||||
this.closeBtn.addEventListener("click", () => this.close());
|
||||
}
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (this.isOpen && !e.target.closest(this.wrapperClass)) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener(this.eventName, () => {
|
||||
console.log(`[${this.constructor.name}] ${this.eventName} received`);
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.isOpen ? this.close() : this.open();
|
||||
}
|
||||
|
||||
open() {
|
||||
if (this.panel) {
|
||||
this.panel.classList.add("active");
|
||||
this.panel.setAttribute("aria-hidden", "false");
|
||||
this.isOpen = true;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.panel) {
|
||||
this.panel.classList.remove("active");
|
||||
this.panel.setAttribute("aria-hidden", "true");
|
||||
this.isOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
renderEmpty() {
|
||||
if (this.content) {
|
||||
this.content.innerHTML = this.emptyMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ShoppingCart extends BaseDropdown {
|
||||
constructor() {
|
||||
super({
|
||||
toggleId: "cartToggle",
|
||||
panelId: "cartPanel",
|
||||
contentId: "cartContent",
|
||||
closeId: "cartClose",
|
||||
wrapperClass: ".cart-dropdown-wrapper",
|
||||
eventName: "cart-updated",
|
||||
emptyMessage:
|
||||
'<p class="empty-state"><i class="bi bi-cart-x"></i><br>Your cart is empty</p>',
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.content) return;
|
||||
|
||||
try {
|
||||
if (!window.AppState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cart = window.AppState.cart;
|
||||
|
||||
if (!Array.isArray(cart)) {
|
||||
this.content.innerHTML =
|
||||
'<p class="empty-state">Error loading cart</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (cart.length === 0) {
|
||||
this.renderEmpty();
|
||||
this.updateFooter(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const validItems = this._filterValidItems(cart);
|
||||
if (validItems.length === 0) {
|
||||
this.renderEmpty();
|
||||
this.updateFooter(null);
|
||||
return;
|
||||
}
|
||||
|
||||
this.content.innerHTML = validItems
|
||||
.map((item) => this.renderCartItem(item))
|
||||
.join("");
|
||||
this.setupCartItemListeners();
|
||||
|
||||
const total = this._calculateTotal(validItems);
|
||||
this.updateFooter(total);
|
||||
} catch (error) {
|
||||
this.content.innerHTML =
|
||||
'<p class="empty-state">Error loading cart</p>';
|
||||
}
|
||||
}
|
||||
|
||||
_filterValidItems(items) {
|
||||
return items.filter(
|
||||
(item) => item && item.id && typeof item.price !== "undefined"
|
||||
);
|
||||
}
|
||||
|
||||
_calculateTotal(items) {
|
||||
if (window.AppState.getCartTotal) {
|
||||
return window.AppState.getCartTotal();
|
||||
}
|
||||
return items.reduce((sum, item) => {
|
||||
const price = parseFloat(item.price) || 0;
|
||||
const quantity = parseInt(item.quantity) || 0;
|
||||
return sum + price * quantity;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
renderCartItem(item) {
|
||||
try {
|
||||
// Validate item and Utils availability
|
||||
if (!item || !item.id) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!window.Utils) {
|
||||
return '<p class="error-message">Error loading item</p>';
|
||||
}
|
||||
|
||||
// Sanitize and validate item data with defensive checks
|
||||
const imageUrl =
|
||||
item.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" 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>
|
||||
<button class="cart-item-remove" data-id="${item.id}" aria-label="Remove from cart">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
} catch (error) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
setupCartItemListeners() {
|
||||
try {
|
||||
this._setupRemoveButtons();
|
||||
this._setupQuantityButtons();
|
||||
} catch (error) {
|
||||
console.error("[ShoppingCart] Error setting up listeners:", error);
|
||||
}
|
||||
}
|
||||
|
||||
_setupRemoveButtons() {
|
||||
this.content.querySelectorAll(".cart-item-remove").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this._handleAction(e, () => {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
if (id && window.AppState?.removeFromCart) {
|
||||
window.AppState.removeFromCart(id);
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_setupQuantityButtons() {
|
||||
this._setupQuantityButton(".quantity-minus", -1);
|
||||
this._setupQuantityButton(".quantity-plus", 1);
|
||||
}
|
||||
|
||||
_setupQuantityButton(selector, delta) {
|
||||
this.content.querySelectorAll(selector).forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this._handleAction(e, () => {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
if (!window.AppState?.cart) return;
|
||||
|
||||
const item = window.AppState.cart.find(
|
||||
(item) => String(item.id) === String(id)
|
||||
);
|
||||
|
||||
if (!item || !window.AppState.updateCartQuantity) return;
|
||||
|
||||
const newQuantity =
|
||||
delta > 0
|
||||
? Math.min(item.quantity + delta, 999)
|
||||
: Math.max(item.quantity + delta, 1);
|
||||
|
||||
if (delta < 0 && item.quantity <= 1) return;
|
||||
|
||||
window.AppState.updateCartQuantity(id, newQuantity);
|
||||
this.render();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_handleAction(event, callback) {
|
||||
try {
|
||||
callback();
|
||||
} catch (error) {
|
||||
console.error("[ShoppingCart] Action error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
updateFooter(total) {
|
||||
const footer = this.cartPanel?.querySelector(".dropdown-foot");
|
||||
if (!footer) return;
|
||||
|
||||
if (total === null) {
|
||||
footer.innerHTML =
|
||||
'<a href="/shop" class="btn-outline">Continue Shopping</a>';
|
||||
} else {
|
||||
footer.innerHTML = `
|
||||
<a href="/shop" class="btn-outline">Continue Shopping</a>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wishlist Component
|
||||
class Wishlist extends BaseDropdown {
|
||||
constructor() {
|
||||
super({
|
||||
toggleId: "wishlistToggle",
|
||||
panelId: "wishlistPanel",
|
||||
contentId: "wishlistContent",
|
||||
closeId: "wishlistClose",
|
||||
wrapperClass: ".wishlist-dropdown-wrapper",
|
||||
eventName: "wishlist-updated",
|
||||
emptyMessage:
|
||||
'<p class="empty-state"><i class="bi bi-heart"></i><br>Your wishlist is empty</p>',
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.content) return;
|
||||
|
||||
if (!window.AppState) {
|
||||
console.warn("[Wishlist] AppState not available yet");
|
||||
return;
|
||||
}
|
||||
|
||||
const wishlist = window.AppState.wishlist;
|
||||
|
||||
if (wishlist.length === 0) {
|
||||
this.renderEmpty();
|
||||
return;
|
||||
}
|
||||
|
||||
this.content.innerHTML = wishlist
|
||||
.map((item) => this.renderWishlistItem(item))
|
||||
.join("");
|
||||
|
||||
this.setupWishlistItemListeners();
|
||||
}
|
||||
|
||||
renderWishlistItem(item) {
|
||||
if (!window.Utils) {
|
||||
console.error("[Wishlist] Utils not available");
|
||||
return '<p class="error-message">Error loading item</p>';
|
||||
}
|
||||
|
||||
const imageUrl =
|
||||
item.imageurl ||
|
||||
item.imageUrl ||
|
||||
item.image_url ||
|
||||
"/assets/images/placeholder.jpg";
|
||||
const title = window.Utils.escapeHtml(
|
||||
item.title || item.name || "Product"
|
||||
);
|
||||
const price = window.Utils.formatCurrency(parseFloat(item.price) || 0);
|
||||
|
||||
return `
|
||||
<div class="wishlist-item" data-id="${item.id}">
|
||||
<img src="${imageUrl}" alt="${title}" class="wishlist-item-image" loading="lazy" onerror="this.src='/assets/images/placeholder.svg'">
|
||||
<div class="wishlist-item-details">
|
||||
<h4 class="wishlist-item-title">${title}</h4>
|
||||
<p class="wishlist-item-price">${price}</p>
|
||||
<button class="btn-add-to-cart" data-id="${item.id}">
|
||||
<i class="bi bi-cart-plus"></i> Add to Cart
|
||||
</button>
|
||||
</div>
|
||||
<button class="wishlist-item-remove" data-id="${item.id}" aria-label="Remove from wishlist">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setupWishlistItemListeners() {
|
||||
this._setupRemoveButtons();
|
||||
this._setupAddToCartButtons();
|
||||
}
|
||||
|
||||
_setupRemoveButtons() {
|
||||
this.content.querySelectorAll(".wishlist-item-remove").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const id = e.currentTarget.dataset.id;
|
||||
if (window.AppState?.removeFromWishlist) {
|
||||
window.AppState.removeFromWishlist(id);
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_setupAddToCartButtons() {
|
||||
this.content.querySelectorAll(".btn-add-to-cart").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const id = e.currentTarget.dataset.id;
|
||||
const item = window.AppState?.wishlist.find(
|
||||
(item) => String(item.id) === String(id)
|
||||
);
|
||||
if (item && window.AppState?.addToCart) {
|
||||
window.AppState.addToCart(item);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
const initializeComponents = () => {
|
||||
// Skip if shop-system.js already initialized
|
||||
if (window.ShopSystem?.isInitialized) {
|
||||
console.log("[cart.js] Skipping initialization - shop-system.js already loaded");
|
||||
return;
|
||||
}
|
||||
console.log("[cart.js] Initializing ShoppingCart and Wishlist components");
|
||||
new ShoppingCart();
|
||||
new Wishlist();
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initializeComponents);
|
||||
} else {
|
||||
initializeComponents();
|
||||
}
|
||||
})();
|
||||
63
website/public/assets/js/archive/error-handler.js
Normal file
63
website/public/assets/js/archive/error-handler.js
Normal 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");
|
||||
});
|
||||
})();
|
||||
234
website/public/assets/js/archive/init-optimized.js
Normal file
234
website/public/assets/js/archive/init-optimized.js
Normal 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");
|
||||
})();
|
||||
210
website/public/assets/js/archive/lazy-load-optimized.js
Normal file
210
website/public/assets/js/archive/lazy-load-optimized.js
Normal 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,
|
||||
};
|
||||
})();
|
||||
72
website/public/assets/js/archive/lazy-load.js
Normal file
72
website/public/assets/js/archive/lazy-load.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Lazy Loading Images Script
|
||||
* Optimizes image loading for better performance
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// Check for Intersection Observer support
|
||||
if (!("IntersectionObserver" in window)) {
|
||||
// Fallback: load all images immediately
|
||||
document.querySelectorAll('img[loading="lazy"]').forEach((img) => {
|
||||
if (img.dataset.src) {
|
||||
img.src = img.dataset.src;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure intersection observer
|
||||
const imageObserver = new IntersectionObserver(
|
||||
(entries, observer) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target;
|
||||
|
||||
// Load the image
|
||||
if (img.dataset.src) {
|
||||
img.src = img.dataset.src;
|
||||
img.removeAttribute("data-src");
|
||||
}
|
||||
|
||||
// Optional: load srcset
|
||||
if (img.dataset.srcset) {
|
||||
img.srcset = img.dataset.srcset;
|
||||
img.removeAttribute("data-srcset");
|
||||
}
|
||||
|
||||
// Add loaded class for fade-in effect
|
||||
img.classList.add("loaded");
|
||||
|
||||
// Stop observing this image
|
||||
observer.unobserve(img);
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
// Start loading when image is 50px from viewport
|
||||
rootMargin: "50px 0px",
|
||||
threshold: 0.01,
|
||||
}
|
||||
);
|
||||
|
||||
// Observe all lazy images
|
||||
const lazyImages = document.querySelectorAll('img[loading="lazy"]');
|
||||
lazyImages.forEach((img) => imageObserver.observe(img));
|
||||
|
||||
// Add CSS for fade-in effect if not already present
|
||||
if (!document.getElementById("lazy-load-styles")) {
|
||||
const style = document.createElement("style");
|
||||
style.id = "lazy-load-styles";
|
||||
style.textContent = `
|
||||
img[loading="lazy"] {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
img[loading="lazy"].loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
})();
|
||||
818
website/public/assets/js/archive/main-enhanced.js
Normal file
818
website/public/assets/js/archive/main-enhanced.js
Normal file
@@ -0,0 +1,818 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
})();
|
||||
302
website/public/assets/js/archive/performance-utils.js
Normal file
302
website/public/assets/js/archive/performance-utils.js
Normal 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,
|
||||
};
|
||||
}
|
||||
248
website/public/assets/js/archive/resource-optimizer.js
Normal file
248
website/public/assets/js/archive/resource-optimizer.js
Normal 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;
|
||||
})();
|
||||
306
website/public/assets/js/archive/shopping.js
Normal file
306
website/public/assets/js/archive/shopping.js
Normal file
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* Shopping/Products Component
|
||||
* Handles product display, filtering, and interactions
|
||||
*/
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
class ShoppingPage {
|
||||
constructor() {
|
||||
this.productsContainer = document.getElementById("productsContainer");
|
||||
this.loadingIndicator = document.getElementById("loadingIndicator");
|
||||
this.errorContainer = document.getElementById("errorContainer");
|
||||
this.currentCategory = window.Utils.getUrlParameter("category") || "all";
|
||||
this.currentSort = "newest";
|
||||
this.products = [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.setupEventListeners();
|
||||
await this.loadProducts();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Category filters
|
||||
document.querySelectorAll("[data-category]").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
this.currentCategory = e.currentTarget.dataset.category;
|
||||
this.filterProducts();
|
||||
});
|
||||
});
|
||||
|
||||
// Sort dropdown
|
||||
const sortSelect = document.getElementById("sortSelect");
|
||||
if (sortSelect) {
|
||||
sortSelect.addEventListener("change", (e) => {
|
||||
this.currentSort = e.target.value;
|
||||
this.filterProducts();
|
||||
});
|
||||
}
|
||||
|
||||
// Search
|
||||
const searchInput = document.getElementById("productSearch");
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener(
|
||||
"input",
|
||||
window.Utils.debounce((e) => {
|
||||
this.searchProducts(e.target.value);
|
||||
}, 300)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async loadProducts() {
|
||||
if (!this.productsContainer) return;
|
||||
|
||||
try {
|
||||
this.showLoading();
|
||||
const response = await window.API.getProducts();
|
||||
this.products = response.products || response.data || [];
|
||||
this.renderProducts(this.products);
|
||||
this.hideLoading();
|
||||
} catch (error) {
|
||||
console.error("Error loading products:", error);
|
||||
this.showError("Failed to load products. Please try again later.");
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
filterProducts() {
|
||||
let filtered = [...this.products];
|
||||
|
||||
// Filter by category
|
||||
if (this.currentCategory && this.currentCategory !== "all") {
|
||||
filtered = filtered.filter(
|
||||
(p) =>
|
||||
p.category?.toLowerCase() === this.currentCategory.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
// Sort products
|
||||
filtered = this.sortProducts(filtered);
|
||||
|
||||
this.renderProducts(filtered);
|
||||
}
|
||||
|
||||
sortProducts(products) {
|
||||
switch (this.currentSort) {
|
||||
case "price-low":
|
||||
return products.sort((a, b) => (a.price || 0) - (b.price || 0));
|
||||
case "price-high":
|
||||
return products.sort((a, b) => (b.price || 0) - (a.price || 0));
|
||||
case "name":
|
||||
return products.sort((a, b) =>
|
||||
(a.title || a.name || "").localeCompare(b.title || b.name || "")
|
||||
);
|
||||
case "newest":
|
||||
default:
|
||||
return products.sort(
|
||||
(a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
searchProducts(query) {
|
||||
if (!query.trim()) {
|
||||
this.filterProducts();
|
||||
return;
|
||||
}
|
||||
|
||||
const searchTerm = query.toLowerCase();
|
||||
const filtered = this.products.filter((p) => {
|
||||
const title = (p.title || p.name || "").toLowerCase();
|
||||
const description = (p.description || "").toLowerCase();
|
||||
const category = (p.category || "").toLowerCase();
|
||||
|
||||
return (
|
||||
title.includes(searchTerm) ||
|
||||
description.includes(searchTerm) ||
|
||||
category.includes(searchTerm)
|
||||
);
|
||||
});
|
||||
|
||||
this.renderProducts(filtered);
|
||||
}
|
||||
|
||||
renderProducts(products) {
|
||||
if (!this.productsContainer) return;
|
||||
|
||||
if (products.length === 0) {
|
||||
this.productsContainer.innerHTML = `
|
||||
<div class="no-products">
|
||||
<i class="bi bi-inbox" style="font-size: 48px; opacity: 0.5;"></i>
|
||||
<p>No products found</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const html = products
|
||||
.map((product) => this.renderProductCard(product))
|
||||
.join("");
|
||||
this.productsContainer.innerHTML = html;
|
||||
|
||||
// Setup product card listeners
|
||||
this.setupProductListeners();
|
||||
}
|
||||
|
||||
renderProductCard(product) {
|
||||
const id = product.id;
|
||||
const title = window.Utils?.escapeHtml
|
||||
? window.Utils.escapeHtml(product.title || product.name || "Product")
|
||||
: product.title || product.name || "Product";
|
||||
const price = window.Utils?.formatCurrency
|
||||
? window.Utils.formatCurrency(product.price || 0)
|
||||
: `$${parseFloat(product.price || 0).toFixed(2)}`;
|
||||
|
||||
// Get image URL from multiple possible sources
|
||||
let imageUrl = "/assets/images/placeholder.jpg";
|
||||
if (
|
||||
product.images &&
|
||||
Array.isArray(product.images) &&
|
||||
product.images.length > 0
|
||||
) {
|
||||
const primaryImg = product.images.find((img) => img.is_primary);
|
||||
imageUrl = primaryImg
|
||||
? primaryImg.image_url
|
||||
: product.images[0].image_url;
|
||||
} else if (product.imageUrl) {
|
||||
imageUrl = product.imageUrl;
|
||||
} else if (product.image_url) {
|
||||
imageUrl = product.image_url;
|
||||
}
|
||||
|
||||
// Get description
|
||||
const description =
|
||||
product.shortdescription ||
|
||||
(product.description
|
||||
? product.description.substring(0, 100) + "..."
|
||||
: "");
|
||||
|
||||
const isInWishlist = window.ShopSystem?.isInWishlist(id) || false;
|
||||
|
||||
return `
|
||||
<article class="product-card" data-id="${id}">
|
||||
<div class="product-image-wrapper">
|
||||
<img src="${imageUrl}" alt="${title}" class="product-image" loading="lazy" onerror="this.src='/assets/images/placeholder.svg'">
|
||||
<button
|
||||
class="wishlist-btn ${isInWishlist ? "active" : ""}"
|
||||
data-id="${id}"
|
||||
aria-label="${
|
||||
isInWishlist ? "Remove from wishlist" : "Add to wishlist"
|
||||
}"
|
||||
>
|
||||
<i class="bi bi-heart${isInWishlist ? "-fill" : ""}"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="product-info">
|
||||
<a href="/product?id=${id}" style="text-decoration: none; color: inherit;">
|
||||
<h3 class="product-title">${title}</h3>
|
||||
</a>
|
||||
${
|
||||
description
|
||||
? `<div class="product-description">${description}</div>`
|
||||
: ""
|
||||
}
|
||||
<p class="product-price">${price}</p>
|
||||
<div class="product-actions">
|
||||
<button class="btn-add-to-cart" data-id="${id}" style="flex: 1;">
|
||||
<i class="bi bi-cart-plus"></i> Add to Cart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
setupProductListeners() {
|
||||
// Add to cart buttons
|
||||
this.productsContainer
|
||||
.querySelectorAll(".btn-add-to-cart")
|
||||
.forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const id = parseInt(e.currentTarget.dataset.id);
|
||||
const product = this.products.find((p) => p.id === id);
|
||||
if (product) {
|
||||
window.ShopSystem.addToCart(product, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Wishlist buttons
|
||||
this.productsContainer
|
||||
.querySelectorAll(".wishlist-btn")
|
||||
.forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const id = parseInt(e.currentTarget.dataset.id);
|
||||
const product = this.products.find((p) => p.id === id);
|
||||
if (product) {
|
||||
if (window.ShopSystem.isInWishlist(id)) {
|
||||
window.ShopSystem.removeFromWishlist(id);
|
||||
} else {
|
||||
window.ShopSystem.addToWishlist(product);
|
||||
}
|
||||
this.renderProducts(this.products);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
if (this.loadingIndicator) {
|
||||
this.loadingIndicator.style.display = "flex";
|
||||
}
|
||||
if (this.productsContainer) {
|
||||
this.productsContainer.style.opacity = "0.5";
|
||||
}
|
||||
}
|
||||
|
||||
hideLoading() {
|
||||
if (this.loadingIndicator) {
|
||||
this.loadingIndicator.style.display = "none";
|
||||
}
|
||||
if (this.productsContainer) {
|
||||
this.productsContainer.style.opacity = "1";
|
||||
}
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
if (this.errorContainer) {
|
||||
this.errorContainer.innerHTML = `
|
||||
<div class="error-message" role="alert">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<p>${window.Utils.escapeHtml(message)}</p>
|
||||
<button onclick="location.reload()">Retry</button>
|
||||
</div>
|
||||
`;
|
||||
this.errorContainer.style.display = "block";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on shop/products pages
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
if (
|
||||
window.location.pathname.includes("/shop") ||
|
||||
window.location.pathname.includes("/products")
|
||||
) {
|
||||
new ShoppingPage();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (
|
||||
window.location.pathname.includes("/shop") ||
|
||||
window.location.pathname.includes("/products")
|
||||
) {
|
||||
new ShoppingPage();
|
||||
}
|
||||
}
|
||||
})();
|
||||
268
website/public/assets/js/archive/state-manager.js
Normal file
268
website/public/assets/js/archive/state-manager.js
Normal file
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* Global State Management
|
||||
* Centralized state for cart, wishlist, and user preferences
|
||||
*/
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
class StateManager {
|
||||
constructor() {
|
||||
this.state = {
|
||||
cart: [],
|
||||
wishlist: [],
|
||||
user: null,
|
||||
preferences: {},
|
||||
};
|
||||
this.listeners = {};
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.loadFromStorage();
|
||||
this.setupStorageSync();
|
||||
}
|
||||
|
||||
loadFromStorage() {
|
||||
try {
|
||||
this.state.cart = JSON.parse(localStorage.getItem("cart") || "[]");
|
||||
this.state.wishlist = JSON.parse(
|
||||
localStorage.getItem("wishlist") || "[]"
|
||||
);
|
||||
this.state.preferences = JSON.parse(
|
||||
localStorage.getItem("preferences") || "{}"
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("State load error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
saveToStorage() {
|
||||
try {
|
||||
localStorage.setItem("cart", JSON.stringify(this.state.cart));
|
||||
localStorage.setItem("wishlist", JSON.stringify(this.state.wishlist));
|
||||
localStorage.setItem(
|
||||
"preferences",
|
||||
JSON.stringify(this.state.preferences)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("State save error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
setupStorageSync() {
|
||||
window.addEventListener("storage", (e) => {
|
||||
if (e.key === "cart" || e.key === "wishlist") {
|
||||
this.loadFromStorage();
|
||||
this.emit("stateChanged", { key: e.key });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Cart methods
|
||||
addToCart(product, quantity = 1) {
|
||||
const existing = this._findById(this.state.cart, product.id);
|
||||
|
||||
if (existing) {
|
||||
existing.quantity += quantity;
|
||||
} else {
|
||||
this.state.cart.push({
|
||||
...product,
|
||||
quantity,
|
||||
addedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
this._updateState("cart");
|
||||
return this.state.cart;
|
||||
}
|
||||
|
||||
removeFromCart(productId) {
|
||||
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._findById(this.state.cart, productId);
|
||||
if (item) {
|
||||
item.quantity = Math.max(0, quantity);
|
||||
if (item.quantity === 0) {
|
||||
return this.removeFromCart(productId);
|
||||
}
|
||||
this._updateState("cart");
|
||||
}
|
||||
return this.state.cart;
|
||||
}
|
||||
|
||||
getCart() {
|
||||
return this.state.cart;
|
||||
}
|
||||
|
||||
getCartTotal() {
|
||||
return this._calculateTotal(this.state.cart);
|
||||
}
|
||||
|
||||
getCartCount() {
|
||||
return this._calculateCount(this.state.cart);
|
||||
}
|
||||
|
||||
clearCart() {
|
||||
this.state.cart = [];
|
||||
this._updateState("cart");
|
||||
}
|
||||
|
||||
// Wishlist methods
|
||||
addToWishlist(product) {
|
||||
const exists = this.state.wishlist.find((item) => item.id === product.id);
|
||||
|
||||
if (!exists) {
|
||||
this.state.wishlist.push({
|
||||
...product,
|
||||
addedAt: Date.now(),
|
||||
});
|
||||
this.saveToStorage();
|
||||
this.emit("wishlistUpdated", this.state.wishlist);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
removeFromWishlist(productId) {
|
||||
this.state.wishlist = this.state.wishlist.filter(
|
||||
(item) => item.id !== productId
|
||||
);
|
||||
this.saveToStorage();
|
||||
this.emit("wishlistUpdated", this.state.wishlist);
|
||||
return this.state.wishlist;
|
||||
}
|
||||
|
||||
getWishlist() {
|
||||
return this.state.wishlist;
|
||||
}
|
||||
|
||||
isInWishlist(productId) {
|
||||
return this.state.wishlist.some((item) => item.id === productId);
|
||||
}
|
||||
|
||||
// Event system
|
||||
on(event, callback) {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [];
|
||||
}
|
||||
this.listeners[event].push(callback);
|
||||
}
|
||||
|
||||
off(event, callback) {
|
||||
if (this.listeners[event]) {
|
||||
this.listeners[event] = this.listeners[event].filter(
|
||||
(cb) => cb !== callback
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
emit(event, data) {
|
||||
if (this.listeners[event]) {
|
||||
this.listeners[event].forEach((callback) => {
|
||||
try {
|
||||
callback(data);
|
||||
} catch (e) {
|
||||
console.error(`Error in ${event} listener:`, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
window.StateManager = window.StateManager || new StateManager();
|
||||
|
||||
// Expose helper functions for backward compatibility
|
||||
window.addToCart = function (productId, name, price, imageurl) {
|
||||
const product = { id: productId, name, price: parseFloat(price), imageurl };
|
||||
window.StateManager.addToCart(product, 1);
|
||||
if (window.showNotification) {
|
||||
window.showNotification(`${name} added to cart!`, "success");
|
||||
}
|
||||
};
|
||||
|
||||
window.addToWishlist = function (productId, name, price, imageurl) {
|
||||
const product = { id: productId, name, price: parseFloat(price), imageurl };
|
||||
const added = window.StateManager.addToWishlist(product);
|
||||
if (window.showNotification) {
|
||||
window.showNotification(
|
||||
added ? `${name} added to wishlist!` : "Already in wishlist!",
|
||||
added ? "success" : "info"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Update badges on state changes
|
||||
window.StateManager.on("cartUpdated", () => {
|
||||
const badges = document.querySelectorAll(".cart-badge, #cartCount");
|
||||
const count = window.StateManager.getCartCount();
|
||||
badges.forEach((badge) => {
|
||||
if (badge) {
|
||||
badge.textContent = count;
|
||||
if (count > 0) {
|
||||
badge.classList.add("show");
|
||||
} else {
|
||||
badge.classList.remove("show");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
window.StateManager.on("wishlistUpdated", () => {
|
||||
const badges = document.querySelectorAll(".wishlist-badge, #wishlistCount");
|
||||
const count = window.StateManager.getWishlist().length;
|
||||
badges.forEach((badge) => {
|
||||
if (badge) {
|
||||
badge.textContent = count;
|
||||
if (count > 0) {
|
||||
badge.classList.add("show");
|
||||
} else {
|
||||
badge.classList.remove("show");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize badges
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
window.StateManager.emit("cartUpdated");
|
||||
window.StateManager.emit("wishlistUpdated");
|
||||
});
|
||||
} else {
|
||||
window.StateManager.emit("cartUpdated");
|
||||
window.StateManager.emit("wishlistUpdated");
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user