webupdate

This commit is contained in:
Local Server
2026-01-18 02:22:05 -06:00
parent 6fc159051a
commit 2a2a3d99e5
135 changed files with 54897 additions and 9825 deletions

View File

@@ -0,0 +1,616 @@
/**
* Accessibility Enhancements
* Adds ARIA labels, focus states, keyboard navigation, and screen reader support
*/
(function () {
"use strict";
console.log("[Accessibility] Loading...");
const A11y = {
init() {
this.addARIALabels();
this.enhanceKeyboardNavigation();
this.addSkipLinks();
this.enhanceFocusStates();
this.announceLiveRegions();
this.fixMobileAccessibility();
console.log("[Accessibility] Initialized");
},
// Add ARIA labels to interactive elements
addARIALabels() {
// Navbar
const navbar = document.querySelector(".navbar");
if (navbar) {
navbar.setAttribute("role", "navigation");
navbar.setAttribute("aria-label", "Main navigation");
}
// Nav menu
const navMenu = document.querySelector(".nav-menu");
if (navMenu) {
navMenu.setAttribute("role", "menubar");
navMenu.setAttribute("aria-label", "Primary menu");
navMenu.querySelectorAll(".nav-link").forEach((link, index) => {
link.setAttribute("role", "menuitem");
link.setAttribute("tabindex", "0");
});
}
// Nav actions buttons
const navActions = document.querySelector(".nav-actions");
if (navActions) {
navActions.setAttribute("role", "group");
navActions.setAttribute("aria-label", "Account and cart actions");
}
// Cart button
const cartBtn = document.querySelector(".cart-btn");
if (cartBtn) {
cartBtn.setAttribute("aria-label", "Shopping cart");
cartBtn.setAttribute("aria-haspopup", "dialog");
const cartCount = cartBtn.querySelector(".cart-count");
if (cartCount) {
cartBtn.setAttribute("aria-describedby", "cart-count-desc");
// Create hidden description for screen readers
if (!document.getElementById("cart-count-desc")) {
const desc = document.createElement("span");
desc.id = "cart-count-desc";
desc.className = "sr-only";
desc.textContent = "items in cart";
cartBtn.appendChild(desc);
}
}
}
// Wishlist button
const wishlistBtn = document.querySelector(".wishlist-btn-nav");
if (wishlistBtn) {
wishlistBtn.setAttribute("aria-label", "Wishlist");
wishlistBtn.setAttribute("aria-haspopup", "dialog");
}
// Sign in link
const signinLink = document.querySelector('a[href="/signin"]');
if (signinLink) {
signinLink.setAttribute("aria-label", "Sign in to your account");
}
// Mobile menu toggle
const mobileToggle = document.querySelector(".nav-mobile-toggle");
if (mobileToggle) {
mobileToggle.setAttribute("aria-expanded", "false");
mobileToggle.setAttribute("aria-controls", "nav-menu");
mobileToggle.setAttribute("aria-label", "Toggle navigation menu");
}
// Product cards
document.querySelectorAll(".product-card").forEach((card, index) => {
card.setAttribute("role", "article");
const title = card.querySelector(".product-title, .product-name, h3");
if (title) {
card.setAttribute("aria-label", title.textContent.trim());
}
// Quick view button
const quickView = card.querySelector(
'.quick-view-btn, [data-action="quick-view"]'
);
if (quickView) {
quickView.setAttribute(
"aria-label",
`Quick view ${title ? title.textContent.trim() : "product"}`
);
}
// Add to cart button
const addCart = card.querySelector(
'.add-to-cart-btn, [data-action="add-to-cart"]'
);
if (addCart) {
addCart.setAttribute(
"aria-label",
`Add ${title ? title.textContent.trim() : "product"} to cart`
);
}
// Wishlist button
const wishlist = card.querySelector(
'.wishlist-btn, [data-action="wishlist"]'
);
if (wishlist) {
wishlist.setAttribute(
"aria-label",
`Add ${title ? title.textContent.trim() : "product"} to wishlist`
);
wishlist.setAttribute("aria-pressed", "false");
}
});
// Slider controls
const sliderPrev = document.querySelector(".slider-arrow.prev");
const sliderNext = document.querySelector(".slider-arrow.next");
if (sliderPrev) sliderPrev.setAttribute("aria-label", "Previous slide");
if (sliderNext) sliderNext.setAttribute("aria-label", "Next slide");
// Slider nav dots
document
.querySelectorAll(".slider-nav .dot, .slider-dot")
.forEach((dot, index) => {
dot.setAttribute("role", "tab");
dot.setAttribute("aria-label", `Go to slide ${index + 1}`);
dot.setAttribute(
"aria-selected",
dot.classList.contains("active") ? "true" : "false"
);
});
// Cart drawer
const cartDrawer = document.querySelector(".cart-drawer");
if (cartDrawer) {
cartDrawer.setAttribute("role", "dialog");
cartDrawer.setAttribute("aria-modal", "true");
cartDrawer.setAttribute("aria-label", "Shopping cart");
const closeBtn = cartDrawer.querySelector(".cart-close, .close-cart");
if (closeBtn) {
closeBtn.setAttribute("aria-label", "Close cart");
}
}
// Wishlist drawer
const wishlistDrawer = document.querySelector(".wishlist-drawer");
if (wishlistDrawer) {
wishlistDrawer.setAttribute("role", "dialog");
wishlistDrawer.setAttribute("aria-modal", "true");
wishlistDrawer.setAttribute("aria-label", "Wishlist");
const closeBtn = wishlistDrawer.querySelector(
".wishlist-close, .close-wishlist"
);
if (closeBtn) {
closeBtn.setAttribute("aria-label", "Close wishlist");
}
}
// Form inputs
document
.querySelectorAll("input:not([aria-label]):not([aria-labelledby])")
.forEach((input) => {
const label =
input.closest("label") ||
document.querySelector(`label[for="${input.id}"]`);
if (label) {
if (!input.id) {
input.id = `input-${Math.random().toString(36).substr(2, 9)}`;
label.setAttribute("for", input.id);
}
} else {
const placeholder = input.getAttribute("placeholder");
if (placeholder) {
input.setAttribute("aria-label", placeholder);
}
}
});
// Images without alt text
document.querySelectorAll("img:not([alt])").forEach((img) => {
img.setAttribute("alt", "");
img.setAttribute("role", "presentation");
});
// Footer
const footer = document.querySelector("footer, .footer");
if (footer) {
footer.setAttribute("role", "contentinfo");
}
// Main content
const main = document.querySelector("main, .page-content");
if (main) {
main.setAttribute("role", "main");
main.id = main.id || "main-content";
}
// Sections
document.querySelectorAll("section").forEach((section) => {
const heading = section.querySelector("h1, h2, h3");
if (heading) {
section.setAttribute(
"aria-labelledby",
heading.id ||
(heading.id = `heading-${Math.random()
.toString(36)
.substr(2, 9)}`)
);
}
});
},
// Enhance keyboard navigation
enhanceKeyboardNavigation() {
// ESC key to close modals/drawers
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
// Close cart drawer
const cartDrawer = document.querySelector(
".cart-drawer.open, .cart-drawer.active"
);
if (cartDrawer) {
const closeBtn = cartDrawer.querySelector(
".cart-close, .close-cart"
);
if (closeBtn) closeBtn.click();
document.querySelector(".cart-btn")?.focus();
}
// Close wishlist drawer
const wishlistDrawer = document.querySelector(
".wishlist-drawer.open, .wishlist-drawer.active"
);
if (wishlistDrawer) {
const closeBtn = wishlistDrawer.querySelector(
".wishlist-close, .close-wishlist"
);
if (closeBtn) closeBtn.click();
document.querySelector(".wishlist-btn-nav")?.focus();
}
// Close mobile menu
const navMenu = document.querySelector(
".nav-menu.open, .nav-menu.active"
);
if (navMenu) {
document.querySelector(".nav-mobile-toggle")?.click();
}
// Close modals
const modal = document.querySelector(".modal.show, .modal.open");
if (modal) {
const closeBtn = modal.querySelector(
'.modal-close, .close-modal, [data-dismiss="modal"]'
);
if (closeBtn) closeBtn.click();
}
// Close user dropdown
const userDropdown = document.querySelector(
".user-dropdown-menu.show"
);
if (userDropdown) {
userDropdown.classList.remove("show");
}
}
});
// Arrow key navigation for nav menu
const navLinks = document.querySelectorAll(".nav-menu .nav-link");
navLinks.forEach((link, index) => {
link.addEventListener("keydown", (e) => {
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
e.preventDefault();
const next = navLinks[index + 1] || navLinks[0];
next.focus();
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
e.preventDefault();
const prev = navLinks[index - 1] || navLinks[navLinks.length - 1];
prev.focus();
}
});
});
// Enter/Space for clickable elements
document
.querySelectorAll('[role="button"], [role="tab"], .product-card')
.forEach((el) => {
if (!el.getAttribute("tabindex")) {
el.setAttribute("tabindex", "0");
}
el.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
el.click();
}
});
});
// Focus trap for modals/drawers
this.setupFocusTrap(".cart-drawer");
this.setupFocusTrap(".wishlist-drawer");
this.setupFocusTrap(".modal");
},
// Setup focus trap for modal-like elements
setupFocusTrap(selector) {
const container = document.querySelector(selector);
if (!container) return;
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === "class") {
const isOpen =
container.classList.contains("open") ||
container.classList.contains("active") ||
container.classList.contains("show");
if (isOpen) {
this.trapFocus(container);
}
}
});
});
observer.observe(container, { attributes: true });
},
// Trap focus within container
trapFocus(container) {
const focusableElements = container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements.length === 0) return;
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
firstElement.focus();
container.addEventListener("keydown", function trapHandler(e) {
if (e.key !== "Tab") return;
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
});
},
// Add skip to main content link
addSkipLinks() {
if (document.querySelector(".skip-link")) return;
const main = document.querySelector("main, .page-content, #main-content");
if (!main) return;
main.id = main.id || "main-content";
const skipLink = document.createElement("a");
skipLink.href = "#" + main.id;
skipLink.className = "skip-link";
skipLink.textContent = "Skip to main content";
skipLink.setAttribute("tabindex", "0");
document.body.insertBefore(skipLink, document.body.firstChild);
// Add CSS for skip link
if (!document.getElementById("skip-link-styles")) {
const style = document.createElement("style");
style.id = "skip-link-styles";
style.textContent = `
.skip-link {
position: absolute;
top: -100px;
left: 50%;
transform: translateX(-50%);
background: var(--primary-pink, #f8c8dc);
color: var(--text-primary, #333);
padding: 12px 24px;
border-radius: 0 0 8px 8px;
font-weight: 600;
text-decoration: none;
z-index: 10000;
transition: top 0.3s ease;
}
.skip-link:focus {
top: 0;
outline: 3px solid var(--accent-pink, #ff69b4);
outline-offset: 2px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
`;
document.head.appendChild(style);
}
},
// Enhance focus states for better visibility
enhanceFocusStates() {
if (document.getElementById("focus-state-styles")) return;
const style = document.createElement("style");
style.id = "focus-state-styles";
style.textContent = `
/* Enhanced focus states for accessibility */
:focus {
outline: 2px solid var(--accent-pink, #ff69b4);
outline-offset: 2px;
}
:focus:not(:focus-visible) {
outline: none;
}
:focus-visible {
outline: 3px solid var(--accent-pink, #ff69b4);
outline-offset: 2px;
}
/* Button focus */
.btn:focus-visible,
button:focus-visible {
outline: 3px solid var(--accent-pink, #ff69b4);
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(255, 105, 180, 0.25);
}
/* Link focus */
a:focus-visible {
outline: 2px solid var(--accent-pink, #ff69b4);
outline-offset: 2px;
border-radius: 2px;
}
/* Input focus */
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: 2px solid var(--primary-pink, #f8c8dc);
outline-offset: 0;
border-color: var(--accent-pink, #ff69b4);
box-shadow: 0 0 0 3px rgba(248, 200, 220, 0.3);
}
/* Card focus */
.product-card:focus-visible,
.blog-card:focus-visible {
outline: 3px solid var(--accent-pink, #ff69b4);
outline-offset: 4px;
}
/* Nav link focus */
.nav-link:focus-visible {
background: rgba(248, 200, 220, 0.3);
border-radius: var(--radius-sm, 4px);
}
/* Icon button focus */
.nav-icon-btn:focus-visible {
outline: 2px solid var(--accent-pink, #ff69b4);
outline-offset: 2px;
background: rgba(248, 200, 220, 0.3);
}
`;
document.head.appendChild(style);
},
// Setup live regions for dynamic content announcements
announceLiveRegions() {
// Create live region for announcements
if (!document.getElementById("a11y-live-region")) {
const liveRegion = document.createElement("div");
liveRegion.id = "a11y-live-region";
liveRegion.setAttribute("aria-live", "polite");
liveRegion.setAttribute("aria-atomic", "true");
liveRegion.className = "sr-only";
document.body.appendChild(liveRegion);
}
// Announce cart updates
const originalAddToCart = window.ShopState?.addToCart;
if (originalAddToCart) {
window.ShopState.addToCart = function (...args) {
const result = originalAddToCart.apply(this, args);
A11y.announce("Item added to cart");
return result;
};
}
// Announce wishlist updates
const originalAddToWishlist = window.ShopState?.addToWishlist;
if (originalAddToWishlist) {
window.ShopState.addToWishlist = function (...args) {
const result = originalAddToWishlist.apply(this, args);
A11y.announce("Item added to wishlist");
return result;
};
}
},
// Announce message to screen readers
announce(message) {
const liveRegion = document.getElementById("a11y-live-region");
if (liveRegion) {
liveRegion.textContent = message;
setTimeout(() => {
liveRegion.textContent = "";
}, 1000);
}
},
// Mobile accessibility enhancements
fixMobileAccessibility() {
// Ensure touch targets are at least 44x44px
const style = document.createElement("style");
style.id = "mobile-a11y-styles";
style.textContent = `
@media (max-width: 768px) {
/* Minimum touch target size */
button,
.btn,
.nav-icon-btn,
.nav-link,
input[type="button"],
input[type="submit"],
a {
min-height: 44px;
min-width: 44px;
}
/* Ensure adequate spacing for touch */
.nav-actions {
gap: 8px;
}
/* Larger tap targets for mobile menu */
.nav-menu .nav-link {
padding: 16px 20px;
}
}
`;
if (!document.getElementById("mobile-a11y-styles")) {
document.head.appendChild(style);
}
// Update mobile toggle aria-expanded on click
const mobileToggle = document.querySelector(".nav-mobile-toggle");
if (mobileToggle) {
const observer = new MutationObserver(() => {
const navMenu = document.querySelector(".nav-menu");
const isOpen =
navMenu?.classList.contains("open") ||
navMenu?.classList.contains("active") ||
document.body.classList.contains("nav-open");
mobileToggle.setAttribute("aria-expanded", isOpen ? "true" : "false");
});
observer.observe(document.body, {
attributes: true,
subtree: true,
attributeFilter: ["class"],
});
}
},
};
// Initialize on DOM ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => A11y.init());
} else {
A11y.init();
}
// Expose for external use
window.A11y = A11y;
})();

View File

@@ -0,0 +1,421 @@
/**
* 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.image ||
item.imageurl ||
item.imageUrl ||
item.image_url ||
"/assets/images/placeholder.svg";
const title = window.Utils.escapeHtml(
item.title || item.name || "Product"
);
const color = item.color ? window.Utils.escapeHtml(item.color) : null;
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>
${
color
? `<p class="cart-item-color" style="font-size: 0.85rem; color: #666; margin: 2px 0;">Color: ${color}</p>`
: ""
}
<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();
}
})();

View File

@@ -0,0 +1,540 @@
/**
* Shared HTML Components
* Eliminates duplication across HTML files
*/
// Component templates
const Components = {
/**
* Render cart drawer HTML
* @returns {string} Cart drawer HTML
*/
cartDrawer: () => `
<div id="cartDrawer" class="cart-drawer">
<div class="cart-drawer-header">
<h3>Shopping Cart</h3>
<button id="closeCart" class="close-cart" aria-label="Close cart">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div id="cartItems" class="cart-items"></div>
<div class="cart-footer">
<div class="cart-total">
<span>Total:</span>
<span id="cartTotal">$0.00</span>
</div>
<button id="checkoutBtn" class="btn btn-primary checkout-btn">
Proceed to Checkout
</button>
</div>
</div>
`,
/**
* Render wishlist drawer HTML
* @returns {string} Wishlist drawer HTML
*/
wishlistDrawer: () => `
<div id="wishlistDrawer" class="wishlist-drawer">
<div class="wishlist-drawer-header">
<h3>Wishlist</h3>
<button id="closeWishlist" class="close-wishlist" aria-label="Close wishlist">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div id="wishlistItems" class="wishlist-items"></div>
</div>
`,
/**
* Render navbar HTML
* @param {Object} options - Navbar options
* @param {string} options.activePage - Current active page
* @returns {string} Navbar HTML
*/
navbar: ({ activePage = "" } = {}) => {
const isActive = (page) => (page === activePage ? "active" : "");
return `
<nav class="navbar">
<div class="nav-container">
<a href="/" class="nav-logo">
<img src="/assets/images/logo.png" alt="SkyArtShop Logo" class="logo-img" />
<span>SkyArtShop</span>
</a>
<button class="mobile-menu-toggle" id="mobileMenuToggle" aria-label="Toggle menu">
<span></span>
<span></span>
<span></span>
</button>
<div class="nav-menu" id="navMenu">
<a href="/" class="nav-link ${isActive("home")}">Home</a>
<a href="/shop" class="nav-link ${isActive("shop")}">Shop</a>
<a href="/portfolio" class="nav-link ${isActive(
"portfolio"
)}">Portfolio</a>
<a href="/blog" class="nav-link ${isActive("blog")}">Blog</a>
<a href="/about" class="nav-link ${isActive("about")}">About</a>
<a href="/contact" class="nav-link ${isActive(
"contact"
)}">Contact</a>
<div class="nav-actions">
<button id="wishlistBtn" class="icon-btn" aria-label="Wishlist">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
</svg>
<span id="wishlistBadge" class="badge">0</span>
</button>
<button id="cartBtn" class="icon-btn" aria-label="Shopping cart">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="9" cy="21" r="1"></circle>
<circle cx="20" cy="21" r="1"></circle>
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
</svg>
<span id="cartBadge" class="badge">0</span>
</button>
</div>
</div>
</div>
</nav>
`;
},
/**
* Render footer HTML
* @returns {string} Footer HTML
*/
footer: () => `
<footer class="footer">
<div class="footer-container">
<div class="footer-section">
<h3>About SkyArtShop</h3>
<p>Your premier destination for unique art pieces and custom designs.</p>
</div>
<div class="footer-section">
<h3>Quick Links</h3>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/shop">Shop</a></li>
<li><a href="/portfolio">Portfolio</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
<div class="footer-section">
<h3>Customer Service</h3>
<ul>
<li><a href="/faq">FAQ</a></li>
<li><a href="/shipping-info">Shipping Info</a></li>
<li><a href="/returns">Returns</a></li>
<li><a href="/privacy">Privacy Policy</a></li>
</ul>
</div>
<div class="footer-section">
<h3>Connect With Us</h3>
<div class="social-links">
<a href="#" aria-label="Facebook">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path>
</svg>
</a>
<a href="#" aria-label="Instagram">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
</svg>
</a>
<a href="#" aria-label="Twitter">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path>
</svg>
</a>
</div>
</div>
</div>
<div class="footer-bottom">
<p>&copy; ${new Date().getFullYear()} SkyArtShop. All rights reserved.</p>
</div>
</footer>
`,
/**
* Render notification container
* @returns {string} Notification container HTML
*/
notificationContainer: () => `
<div id="notificationContainer" class="notification-container"></div>
`,
};
/**
* Initialize components in the DOM
* @param {Object} options - Component options
* @param {boolean} options.navbar - Include navbar
* @param {boolean} options.footer - Include footer
* @param {boolean} options.cart - Include cart drawer
* @param {boolean} options.wishlist - Include wishlist drawer
* @param {boolean} options.notifications - Include notifications
* @param {string} options.activePage - Active page for navbar
*/
function initializeComponents({
navbar = true,
footer = true,
cart = true,
wishlist = true,
notifications = true,
activePage = "",
} = {}) {
// Inject components into DOM
if (navbar) {
const navPlaceholder = document.getElementById("navbar-placeholder");
if (navPlaceholder) {
navPlaceholder.innerHTML = Components.navbar({ activePage });
}
}
if (footer) {
const footerPlaceholder = document.getElementById("footer-placeholder");
if (footerPlaceholder) {
footerPlaceholder.innerHTML = Components.footer();
}
}
if (cart) {
const cartPlaceholder = document.getElementById("cart-drawer-placeholder");
if (cartPlaceholder) {
cartPlaceholder.innerHTML = Components.cartDrawer();
}
}
if (wishlist) {
const wishlistPlaceholder = document.getElementById(
"wishlist-drawer-placeholder"
);
if (wishlistPlaceholder) {
wishlistPlaceholder.innerHTML = Components.wishlistDrawer();
}
}
if (notifications) {
const notificationPlaceholder = document.getElementById(
"notification-placeholder"
);
if (notificationPlaceholder) {
notificationPlaceholder.innerHTML = Components.notificationContainer();
}
}
// Initialize mobile menu toggle
initMobileMenu();
// Initialize cart and wishlist interactions
if (cart) initCartDrawer();
if (wishlist) initWishlistDrawer();
}
/**
* Initialize mobile menu functionality
*/
function initMobileMenu() {
const menuToggle = document.getElementById("mobileMenuToggle");
const navMenu = document.getElementById("navMenu");
if (menuToggle && navMenu) {
menuToggle.addEventListener("click", () => {
navMenu.classList.toggle("active");
menuToggle.classList.toggle("active");
});
// Close menu when clicking outside
document.addEventListener("click", (e) => {
if (
!menuToggle.contains(e.target) &&
!navMenu.contains(e.target) &&
navMenu.classList.contains("active")
) {
navMenu.classList.remove("active");
menuToggle.classList.remove("active");
}
});
}
}
/**
* Initialize cart drawer functionality
*/
function initCartDrawer() {
const cartBtn = document.getElementById("cartBtn");
const cartDrawer = document.getElementById("cartDrawer");
const closeCart = document.getElementById("closeCart");
if (cartBtn && cartDrawer) {
cartBtn.addEventListener("click", () => {
cartDrawer.classList.add("active");
document.body.style.overflow = "hidden";
updateCartUI();
});
if (closeCart) {
closeCart.addEventListener("click", () => {
cartDrawer.classList.remove("active");
document.body.style.overflow = "";
});
}
// Close on overlay click
cartDrawer.addEventListener("click", (e) => {
if (e.target === cartDrawer) {
cartDrawer.classList.remove("active");
document.body.style.overflow = "";
}
});
}
// Initialize checkout button
const checkoutBtn = document.getElementById("checkoutBtn");
if (checkoutBtn) {
checkoutBtn.addEventListener("click", () => {
window.location.href = "/checkout";
});
}
}
/**
* Initialize wishlist drawer functionality
*/
function initWishlistDrawer() {
const wishlistBtn = document.getElementById("wishlistBtn");
const wishlistDrawer = document.getElementById("wishlistDrawer");
const closeWishlist = document.getElementById("closeWishlist");
if (wishlistBtn && wishlistDrawer) {
wishlistBtn.addEventListener("click", () => {
wishlistDrawer.classList.add("active");
document.body.style.overflow = "hidden";
updateWishlistUI();
});
if (closeWishlist) {
closeWishlist.addEventListener("click", () => {
wishlistDrawer.classList.remove("active");
document.body.style.overflow = "";
});
}
// Close on overlay click
wishlistDrawer.addEventListener("click", (e) => {
if (e.target === wishlistDrawer) {
wishlistDrawer.classList.remove("active");
document.body.style.overflow = "";
}
});
}
}
/**
* Update cart UI with current items
*/
function updateCartUI() {
if (typeof CartUtils === "undefined") return;
const cart = CartUtils.getCart();
const cartItems = document.getElementById("cartItems");
const cartTotal = document.getElementById("cartTotal");
const cartBadge = document.getElementById("cartBadge");
if (cartBadge) {
cartBadge.textContent = cart.length;
cartBadge.style.display = cart.length > 0 ? "flex" : "none";
}
if (!cartItems) return;
if (cart.length === 0) {
cartItems.innerHTML = '<p class="empty-cart">Your cart is empty</p>';
if (cartTotal) cartTotal.textContent = "$0.00";
return;
}
cartItems.innerHTML = cart
.map(
(item) => `
<div class="cart-item" data-id="${item.id}">
<img src="${item.image}" alt="${item.name}" class="cart-item-image" />
<div class="cart-item-details">
<h4>${item.name}</h4>
<p class="cart-item-price">${formatPrice(item.price)}</p>
<div class="quantity-controls">
<button class="qty-btn" onclick="decreaseQuantity('${
item.id
}')">-</button>
<span class="quantity">${item.quantity}</span>
<button class="qty-btn" onclick="increaseQuantity('${
item.id
}')">+</button>
</div>
</div>
<button class="remove-item" onclick="removeFromCart('${
item.id
}')" aria-label="Remove item">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
`
)
.join("");
if (cartTotal) {
cartTotal.textContent = formatPrice(CartUtils.getCartTotal());
}
}
/**
* Update wishlist UI with current items
*/
function updateWishlistUI() {
if (typeof WishlistUtils === "undefined") return;
const wishlist = WishlistUtils.getWishlist();
const wishlistItems = document.getElementById("wishlistItems");
const wishlistBadge = document.getElementById("wishlistBadge");
if (wishlistBadge) {
wishlistBadge.textContent = wishlist.length;
wishlistBadge.style.display = wishlist.length > 0 ? "flex" : "none";
}
if (!wishlistItems) return;
if (wishlist.length === 0) {
wishlistItems.innerHTML =
'<p class="empty-wishlist">Your wishlist is empty</p>';
return;
}
wishlistItems.innerHTML = wishlist
.map(
(item) => `
<div class="wishlist-item" data-id="${item.id}">
<img src="${item.image}" alt="${
item.name
}" class="wishlist-item-image" />
<div class="wishlist-item-details">
<h4>${item.name}</h4>
<p class="wishlist-item-price">${formatPrice(item.price)}</p>
<button class="btn-sm btn-primary" onclick="moveToCart('${item.id}')">
Add to Cart
</button>
</div>
<button class="remove-item" onclick="removeFromWishlist('${
item.id
}')" aria-label="Remove from wishlist">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
`
)
.join("");
}
// Global functions for inline event handlers
window.increaseQuantity = (productId) => {
if (typeof CartUtils !== "undefined") {
const cart = CartUtils.getCart();
const item = cart.find((item) => item.id === productId);
if (item) {
CartUtils.updateQuantity(productId, item.quantity + 1);
updateCartUI();
}
}
};
window.decreaseQuantity = (productId) => {
if (typeof CartUtils !== "undefined") {
const cart = CartUtils.getCart();
const item = cart.find((item) => item.id === productId);
if (item && item.quantity > 1) {
CartUtils.updateQuantity(productId, item.quantity - 1);
updateCartUI();
}
}
};
window.removeFromCart = (productId) => {
if (typeof CartUtils !== "undefined") {
CartUtils.removeFromCart(productId);
updateCartUI();
showNotification("Item removed from cart", "info");
}
};
window.removeFromWishlist = (productId) => {
if (typeof WishlistUtils !== "undefined") {
WishlistUtils.removeFromWishlist(productId);
updateWishlistUI();
showNotification("Item removed from wishlist", "info");
}
};
window.moveToCart = (productId) => {
if (
typeof WishlistUtils !== "undefined" &&
typeof CartUtils !== "undefined"
) {
const wishlist = WishlistUtils.getWishlist();
const item = wishlist.find((item) => item.id === productId);
if (item) {
CartUtils.addToCart(item);
WishlistUtils.removeFromWishlist(productId);
updateWishlistUI();
updateCartUI();
showNotification("Item moved to cart", "success");
}
}
};
// Initialize on DOM load
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
// Auto-initialize if data attribute is set
const autoInit = document.body.dataset.autoInitComponents;
if (autoInit !== "false") {
initializeComponents({
activePage: document.body.dataset.activePage || "",
});
}
});
} else {
// Already loaded
const autoInit = document.body.dataset.autoInitComponents;
if (autoInit !== "false") {
initializeComponents({
activePage: document.body.dataset.activePage || "",
});
}
}
// Export for manual initialization
window.Components = Components;
window.initializeComponents = initializeComponents;

View File

@@ -0,0 +1,538 @@
/**
* Customer Authentication State Manager
* Handles checking login status and updating UI accordingly
*/
(function () {
"use strict";
console.log("[customer-auth.js] Loading...");
// Customer authentication state
window.CustomerAuth = {
user: null,
isLoggedIn: false,
isLoading: true,
// Initialize - check session on page load
async init() {
console.log("[CustomerAuth] Initializing...");
await this.checkSession();
this.updateNavbar();
this.setupEventListeners();
},
// Check if user is logged in
async checkSession() {
try {
const response = await fetch("/api/customers/session", {
credentials: "include",
});
if (response.ok) {
const data = await response.json();
if (data.success && data.loggedIn) {
this.user = data.customer;
this.isLoggedIn = true;
console.log("[CustomerAuth] User logged in:", this.user.firstName);
// Sync local cart/wishlist with server
await this.syncCartWithServer();
await this.syncWishlistWithServer();
} else {
this.user = null;
this.isLoggedIn = false;
}
} else {
this.user = null;
this.isLoggedIn = false;
}
} catch (error) {
console.error("[CustomerAuth] Session check failed:", error);
this.user = null;
this.isLoggedIn = false;
}
this.isLoading = false;
},
// Update navbar based on auth state
updateNavbar() {
// Find the sign in link/button in nav-actions
const navActions = document.querySelector(".nav-actions");
if (!navActions) {
console.warn("[CustomerAuth] nav-actions not found");
return;
}
// Find the existing sign in link
const signinLink = navActions.querySelector(
'a[href="/signin"], a[href="/account"]',
);
if (this.isLoggedIn && this.user) {
// User is logged in - update UI
if (signinLink) {
// Replace with user dropdown
const userDropdown = document.createElement("div");
userDropdown.className = "user-dropdown";
userDropdown.innerHTML = `
<button class="nav-icon-btn user-btn" title="Account" id="userDropdownBtn">
<i class="bi bi-person-check-fill"></i>
<span class="user-name-short">${this.escapeHtml(
this.user.firstName,
)}</span>
</button>
<div class="user-dropdown-menu" id="userDropdownMenu">
<div class="user-dropdown-header">
<i class="bi bi-person-circle"></i>
<div class="user-info">
<span class="welcome-text">Welcome back,</span>
<span class="user-name">${this.escapeHtml(
this.user.firstName,
)} ${this.escapeHtml(this.user.lastName || "")}</span>
</div>
</div>
<div class="user-dropdown-divider"></div>
<a href="/account" class="user-dropdown-item">
<i class="bi bi-person"></i> My Account
</a>
<a href="/account#orders" class="user-dropdown-item">
<i class="bi bi-bag-check"></i> My Orders
</a>
<a href="/account#wishlist" class="user-dropdown-item">
<i class="bi bi-heart"></i> My Wishlist
</a>
<div class="user-dropdown-divider"></div>
<button class="user-dropdown-item logout-btn" id="logoutBtn">
<i class="bi bi-box-arrow-right"></i> Sign Out
</button>
</div>
`;
signinLink.replaceWith(userDropdown);
// Add dropdown toggle
const dropdownBtn = document.getElementById("userDropdownBtn");
const dropdownMenu = document.getElementById("userDropdownMenu");
const logoutBtn = document.getElementById("logoutBtn");
if (dropdownBtn && dropdownMenu) {
dropdownBtn.addEventListener("click", (e) => {
e.stopPropagation();
dropdownMenu.classList.toggle("show");
});
// Close dropdown when clicking outside
document.addEventListener("click", () => {
dropdownMenu.classList.remove("show");
});
}
if (logoutBtn) {
logoutBtn.addEventListener("click", () => this.logout());
}
}
} else {
// User is not logged in - ensure sign in link is present
if (signinLink) {
// Update to show sign in icon
signinLink.innerHTML = '<i class="bi bi-person"></i>';
signinLink.href = "/signin";
signinLink.title = "Sign In";
}
}
// Add user dropdown styles if not present
this.addStyles();
},
// Sync local cart with server when logged in
async syncCartWithServer() {
if (!this.isLoggedIn) return;
try {
// Get server cart
const response = await fetch("/api/customers/cart", {
credentials: "include",
});
if (response.ok) {
const data = await response.json();
if (data.success && data.items) {
// Merge with local cart
const localCart = JSON.parse(
localStorage.getItem("skyart_cart") || "[]",
);
// If local cart has items, push them to server
for (const item of localCart) {
const exists = data.items.find((i) => i.productId === item.id);
if (!exists) {
await this.addToServerCart(item);
}
}
// Update local cart with server cart
const mergedCart = data.items.map((item) => ({
id: item.productId,
name: item.name,
price: item.price,
image: item.image,
quantity: item.quantity,
variantColor: item.variantColor,
variantSize: item.variantSize,
}));
localStorage.setItem("skyart_cart", JSON.stringify(mergedCart));
if (window.AppState) {
window.AppState.cart = mergedCart;
window.AppState.updateUI();
}
}
}
} catch (error) {
console.error("[CustomerAuth] Cart sync failed:", error);
}
},
// Sync local wishlist with server
async syncWishlistWithServer() {
if (!this.isLoggedIn) return;
try {
const response = await fetch("/api/customers/wishlist", {
credentials: "include",
});
if (response.ok) {
const data = await response.json();
if (data.success && data.items) {
// Merge with local wishlist
const localWishlist = JSON.parse(
localStorage.getItem("wishlist") || "[]",
);
// Push local items to server
for (const item of localWishlist) {
const exists = data.items.find((i) => i.productId === item.id);
if (!exists) {
await this.addToServerWishlist(item);
}
}
// Update local wishlist with server data
const mergedWishlist = data.items.map((item) => ({
id: item.productId,
name: item.name,
price: item.price,
image: item.image,
}));
localStorage.setItem("wishlist", JSON.stringify(mergedWishlist));
if (window.AppState) {
window.AppState.wishlist = mergedWishlist;
window.AppState.updateUI();
}
}
}
} catch (error) {
console.error("[CustomerAuth] Wishlist sync failed:", error);
}
},
// Add item to server cart
async addToServerCart(item) {
try {
await fetch("/api/customers/cart", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
productId: item.id,
quantity: item.quantity || 1,
variantColor: item.variantColor,
variantSize: item.variantSize,
}),
});
} catch (error) {
console.error("[CustomerAuth] Add to server cart failed:", error);
}
},
// Add item to server wishlist
async addToServerWishlist(item) {
try {
await fetch("/api/customers/wishlist", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ productId: item.id }),
});
} catch (error) {
console.error("[CustomerAuth] Add to server wishlist failed:", error);
}
},
// Setup event listeners for cart/wishlist changes
setupEventListeners() {
// Listen for cart changes to sync with server
window.addEventListener("cart-updated", async (e) => {
if (this.isLoggedIn && e.detail) {
// Debounce sync
clearTimeout(this._syncCartTimeout);
this._syncCartTimeout = setTimeout(() => {
this.syncCartChanges(e.detail);
}, 500);
}
});
// Listen for wishlist changes
window.addEventListener("wishlist-updated", async (e) => {
if (this.isLoggedIn && e.detail) {
clearTimeout(this._syncWishlistTimeout);
this._syncWishlistTimeout = setTimeout(() => {
this.syncWishlistChanges(e.detail);
}, 500);
}
});
},
// Sync cart changes with server
async syncCartChanges(cart) {
if (!this.isLoggedIn) return;
// For now, just ensure items are added/removed
// More sophisticated sync could be implemented
},
// Sync wishlist changes with server
async syncWishlistChanges(wishlist) {
if (!this.isLoggedIn) return;
// Sync wishlist changes
},
// Logout
async logout() {
try {
const response = await fetch("/api/customers/logout", {
method: "POST",
credentials: "include",
});
if (response.ok) {
this.user = null;
this.isLoggedIn = false;
// Show notification
if (window.AppState && window.AppState.showNotification) {
window.AppState.showNotification(
"You have been signed out",
"info",
);
}
// Redirect to home or refresh
window.location.href = "/home";
}
} catch (error) {
console.error("[CustomerAuth] Logout failed:", error);
}
},
// Helper: Escape HTML
escapeHtml(text) {
if (!text) return "";
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
},
// Add styles for user dropdown
addStyles() {
if (document.getElementById("customer-auth-styles")) return;
const style = document.createElement("style");
style.id = "customer-auth-styles";
style.textContent = `
.user-dropdown {
position: relative;
}
.user-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px !important;
border-radius: 20px;
background: var(--primary-pink-light, #fff5f7);
transition: all 0.2s ease;
}
.user-btn:hover {
background: var(--primary-pink, #ff85a2);
color: white;
}
.user-btn i {
font-size: 1.1rem;
color: var(--primary-pink, #ff85a2);
}
.user-btn:hover i {
color: white;
}
.user-name-short {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-primary, #333);
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-btn:hover .user-name-short {
color: white;
}
.user-dropdown-menu {
position: absolute;
top: calc(100% + 10px);
right: 0;
min-width: 220px;
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.15);
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.2s ease;
z-index: 1000;
overflow: hidden;
}
.user-dropdown-menu.show {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.user-dropdown-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: var(--primary-pink-light, #fff5f7);
}
.user-dropdown-header > i {
font-size: 2rem;
color: var(--primary-pink, #ff85a2);
}
.user-dropdown-header .user-info {
display: flex;
flex-direction: column;
}
.user-dropdown-header .welcome-text {
font-size: 0.75rem;
color: var(--text-light, #999);
}
.user-dropdown-header .user-name {
font-size: 0.9rem;
font-weight: 600;
color: var(--text-primary, #333);
}
.user-dropdown-divider {
height: 1px;
background: var(--border-light, #eee);
margin: 4px 0;
}
.user-dropdown-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
color: var(--text-primary, #333);
text-decoration: none;
font-size: 0.9rem;
transition: all 0.2s ease;
cursor: pointer;
border: none;
background: none;
width: 100%;
text-align: left;
}
.user-dropdown-item:hover {
background: var(--primary-pink-light, #fff5f7);
color: var(--primary-pink, #ff85a2);
}
.user-dropdown-item i {
font-size: 1rem;
width: 20px;
text-align: center;
}
.logout-btn {
color: #e74c3c;
}
.logout-btn:hover {
background: #fdf2f2;
color: #c0392b;
}
@media (max-width: 768px) {
.user-name-short {
display: none;
}
.user-btn {
padding: 8px !important;
border-radius: 50%;
}
.user-dropdown {
-webkit-transform: translateZ(0);
transform: translateZ(0);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}
.user-dropdown-menu {
right: -10px;
min-width: 200px;
position: fixed;
top: 70px;
right: 10px;
-webkit-transform: translateZ(0);
transform: translateZ(0);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
will-change: opacity, visibility;
}
.user-dropdown-menu.show {
transform: translateZ(0);
}
}
`;
document.head.appendChild(style);
},
};
// Initialize on DOM ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
window.CustomerAuth.init();
});
} else {
window.CustomerAuth.init();
}
})();

View File

@@ -0,0 +1,228 @@
/**
* Dynamic Page Content Loader
* Loads page content from the API and renders it with proper formatting
*/
// Convert Quill Delta to HTML - accurate conversion matching backend format
function convertDeltaToHtml(delta) {
if (!delta || !delta.ops) return "";
let html = "";
let currentLine = "";
let inListType = null; // 'bullet' or 'ordered'
const ops = delta.ops;
for (let i = 0; i < ops.length; i++) {
const op = ops[i];
const nextOp = ops[i + 1];
if (typeof op.insert === "string") {
const text = op.insert;
const inlineAttrs = op.attributes || {};
// Check if this is a standalone newline with block attributes
if (text === "\n") {
const blockAttrs = inlineAttrs;
// Handle list transitions
if (blockAttrs.list) {
const newListType = blockAttrs.list;
if (inListType !== newListType) {
if (inListType) {
html += inListType === "ordered" ? "</ol>" : "</ul>";
}
html += newListType === "ordered" ? "<ol>" : "<ul>";
inListType = newListType;
}
html += `<li>${currentLine}</li>`;
} else {
// Close any open list
if (inListType) {
html += inListType === "ordered" ? "</ol>" : "</ul>";
inListType = null;
}
// Apply block formatting
if (blockAttrs.header) {
html += `<h${blockAttrs.header}>${currentLine}</h${blockAttrs.header}>`;
} else if (blockAttrs.blockquote) {
html += `<blockquote>${currentLine}</blockquote>`;
} else if (blockAttrs["code-block"]) {
html += `<pre><code>${currentLine}</code></pre>`;
} else if (currentLine) {
html += `<p>${currentLine}</p>`;
}
}
currentLine = "";
} else {
// Regular text - may contain embedded newlines
const parts = text.split("\n");
for (let j = 0; j < parts.length; j++) {
const part = parts[j];
// Format the text part
if (part.length > 0) {
let formatted = escapeHtml(part);
// Apply inline formatting
if (inlineAttrs.bold) formatted = `<strong>${formatted}</strong>`;
if (inlineAttrs.italic) formatted = `<em>${formatted}</em>`;
if (inlineAttrs.underline) formatted = `<u>${formatted}</u>`;
if (inlineAttrs.strike) formatted = `<s>${formatted}</s>`;
if (inlineAttrs.code) formatted = `<code>${formatted}</code>`;
if (inlineAttrs.link)
formatted = `<a href="${inlineAttrs.link}" target="_blank" rel="noopener">${formatted}</a>`;
currentLine += formatted;
}
// Handle embedded newlines (not the last part)
if (j < parts.length - 1) {
// Close any open list for embedded newlines
if (inListType) {
html += inListType === "ordered" ? "</ol>" : "</ul>";
inListType = null;
}
if (currentLine) {
html += `<p>${currentLine}</p>`;
}
currentLine = "";
}
}
}
} else if (op.insert && op.insert.image) {
// Flush pending content
if (currentLine) {
if (inListType) {
html += `<li>${currentLine}</li>`;
html += inListType === "ordered" ? "</ol>" : "</ul>";
inListType = null;
} else {
html += `<p>${currentLine}</p>`;
}
currentLine = "";
}
html += `<img src="${op.insert.image}" class="content-image" alt="Content image">`;
}
}
// Flush remaining content
if (inListType) {
if (currentLine) html += `<li>${currentLine}</li>`;
html += inListType === "ordered" ? "</ol>" : "</ul>";
} else if (currentLine) {
html += `<p>${currentLine}</p>`;
}
return html;
}
function escapeHtml(text) {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
// Parse and render page content (handles both Delta JSON and raw HTML)
function parsePageContent(content) {
if (!content) return "<p>Content coming soon...</p>";
try {
const delta = JSON.parse(content);
if (delta.ops) {
return convertDeltaToHtml(delta);
}
return content;
} catch (e) {
// Not JSON, return as-is (probably HTML)
return content;
}
}
// Load page content from API
async function loadPageContent(slug, options = {}) {
const {
titleSelector = "#pageTitle",
contentSelector = "#dynamicContent",
staticSelector = "#staticContent",
showLoading = true,
} = options;
const dynamicContent = document.querySelector(contentSelector);
const staticContent = document.querySelector(staticSelector);
const titleElement = document.querySelector(titleSelector);
if (!dynamicContent) {
console.warn("Dynamic content container not found:", contentSelector);
return null;
}
if (showLoading) {
dynamicContent.innerHTML = `
<div style="text-align: center; padding: 40px;">
<div class="loading-spinner" style="width: 40px; height: 40px; border: 3px solid rgba(252,177,216,0.2); border-top-color: #FCB1D8; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto;"></div>
<p style="margin-top: 16px; color: #888;">Loading content...</p>
</div>
`;
}
try {
const response = await fetch(`/api/pages/${slug}`);
const data = await response.json();
if (data.success && data.page) {
const page = data.page;
// Update page title if provided
if (page.title && titleElement) {
titleElement.textContent = page.title;
}
// Parse and render content
const htmlContent = parsePageContent(page.content);
dynamicContent.innerHTML = htmlContent;
// Hide static fallback if exists
if (staticContent) {
staticContent.style.display = "none";
}
return page;
} else {
// Show static fallback
dynamicContent.style.display = "none";
if (staticContent) {
staticContent.style.display = "block";
}
return null;
}
} catch (error) {
console.error("Failed to load page content:", error);
dynamicContent.style.display = "none";
if (staticContent) {
staticContent.style.display = "block";
}
return null;
}
}
// Auto-initialize on DOMContentLoaded if data-page-slug is set
document.addEventListener("DOMContentLoaded", () => {
const pageContainer = document.querySelector("[data-page-slug]");
if (pageContainer) {
const slug = pageContainer.dataset.pageSlug;
loadPageContent(slug);
}
});
// Export for use in other scripts
window.DynamicPage = {
loadPageContent,
parsePageContent,
convertDeltaToHtml,
};

View File

@@ -0,0 +1,114 @@
/**
* Mobile Touch Optimization
* Eliminates double-tap behavior and ensures immediate response on mobile devices
*/
(function () {
"use strict";
// Only apply on touch devices
if (!("ontouchstart" in window)) return;
// Disable 300ms click delay on mobile
let touchStartX, touchStartY;
// Add fastclick functionality for immediate response
document.addEventListener(
"touchstart",
function (e) {
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
},
{ passive: true },
);
document.addEventListener(
"touchend",
function (e) {
if (!touchStartX || !touchStartY) return;
const touchEndX = e.changedTouches[0].clientX;
const touchEndY = e.changedTouches[0].clientY;
// Check if it's a tap (not a swipe)
const diffX = Math.abs(touchEndX - touchStartX);
const diffY = Math.abs(touchEndY - touchStartY);
if (diffX < 10 && diffY < 10) {
// This is a tap, ensure immediate response
const target = e.target.closest(
"a, button, [data-bs-toggle], .product-card, .portfolio-card, .blog-card, .nav-link, .btn",
);
if (target && !target.disabled) {
// Add visual feedback
target.style.opacity = "0.7";
setTimeout(() => {
target.style.opacity = "";
}, 100);
}
}
touchStartX = null;
touchStartY = null;
},
{ passive: false },
);
// Remove hover states that cause double-tap on mobile
if (window.matchMedia("(hover: none)").matches) {
const style = document.createElement("style");
style.textContent = `
/* Override hover states for immediate touch response */
.product-card, .portfolio-card, .blog-card, .btn, .nav-link {
transition: transform 0.1s ease, opacity 0.1s ease !important;
}
`;
document.head.appendChild(style);
}
// Optimize click handlers for mobile
function optimizeClickHandler(selector) {
const elements = document.querySelectorAll(selector);
elements.forEach((el) => {
if (el.dataset.mobileOptimized) return;
el.dataset.mobileOptimized = "true";
// Remove any existing event listeners that might cause delays
el.style.pointerEvents = "auto";
el.style.touchAction = "manipulation";
// Ensure proper z-index for touch
const computedStyle = window.getComputedStyle(el);
if (computedStyle.position === "static") {
el.style.position = "relative";
}
});
}
// Apply optimizations when DOM is ready
document.addEventListener("DOMContentLoaded", function () {
optimizeClickHandler(
"a, button, .product-card, .portfolio-card, .blog-card, .btn, .nav-link, .filter-btn, .add-to-cart-btn",
);
});
// Apply optimizations to dynamically added content
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.type === "childList") {
mutation.addedNodes.forEach(function (node) {
if (node.nodeType === Node.ELEMENT_NODE) {
optimizeClickHandler(
"a, button, .product-card, .portfolio-card, .blog-card, .btn, .nav-link, .filter-btn, .add-to-cart-btn",
);
}
});
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
})();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,332 @@
/**
* Shared Cart and Wishlist Utilities
* Extracted from duplicated code across multiple pages
*/
const CartUtils = {
/**
* Get cart from localStorage
* @returns {Array} Cart items
*/
getCart() {
try {
return JSON.parse(localStorage.getItem("skyart_cart") || "[]");
} catch (error) {
console.error("Error loading cart:", error);
return [];
}
},
/**
* Save cart to localStorage
* @param {Array} cart - Cart items
*/
saveCart(cart) {
try {
localStorage.setItem("skyart_cart", JSON.stringify(cart));
this.updateCartBadge();
} catch (error) {
console.error("Error saving cart:", error);
}
},
/**
* Add item to cart
* @param {Object} product - Product to add
* @param {number} quantity - Quantity to add
* @param {string} variant - Selected variant
*/
addToCart(product, quantity = 1, variant = null) {
const cart = this.getCart();
const cartKey = variant ? `${product.id}-${variant}` : product.id;
const existingIndex = cart.findIndex((item) => {
const itemKey = item.variant
? `${item.productId}-${item.variant}`
: item.productId;
return itemKey === cartKey;
});
if (existingIndex >= 0) {
cart[existingIndex].quantity += quantity;
} else {
cart.push({
productId: product.id,
name: product.name,
price: product.price,
image: product.images?.[0]?.image_url || product.imageurl,
quantity,
variant,
slug: product.slug,
});
}
this.saveCart(cart);
return cart;
},
/**
* Remove item from cart
* @param {string} productId - Product ID to remove
* @param {string} variant - Variant to remove
*/
removeFromCart(productId, variant = null) {
const cart = this.getCart();
const cartKey = variant ? `${productId}-${variant}` : productId;
const filtered = cart.filter((item) => {
const itemKey = item.variant
? `${item.productId}-${item.variant}`
: item.productId;
return itemKey !== cartKey;
});
this.saveCart(filtered);
return filtered;
},
/**
* Update cart item quantity
* @param {string} productId - Product ID
* @param {number} quantity - New quantity
* @param {string} variant - Variant
*/
updateQuantity(productId, quantity, variant = null) {
const cart = this.getCart();
const cartKey = variant ? `${productId}-${variant}` : productId;
const index = cart.findIndex((item) => {
const itemKey = item.variant
? `${item.productId}-${item.variant}`
: item.productId;
return itemKey === cartKey;
});
if (index >= 0) {
if (quantity <= 0) {
cart.splice(index, 1);
} else {
cart[index].quantity = quantity;
}
this.saveCart(cart);
}
return cart;
},
/**
* Get cart total
* @returns {number} Total price
*/
getCartTotal() {
const cart = this.getCart();
return cart.reduce((total, item) => {
return total + parseFloat(item.price) * item.quantity;
}, 0);
},
/**
* Update cart badge count
*/
updateCartBadge() {
const cart = this.getCart();
const count = cart.reduce((sum, item) => sum + item.quantity, 0);
const badge = document.querySelector(".cart-badge");
if (badge) {
badge.textContent = count;
badge.style.display = count > 0 ? "flex" : "none";
}
},
/**
* Clear cart
*/
clearCart() {
localStorage.removeItem("skyart_cart");
this.updateCartBadge();
},
};
const WishlistUtils = {
/**
* Get wishlist from localStorage
* @returns {Array} Wishlist items
*/
getWishlist() {
try {
return JSON.parse(localStorage.getItem("wishlist") || "[]");
} catch (error) {
console.error("Error loading wishlist:", error);
return [];
}
},
/**
* Save wishlist to localStorage
* @param {Array} wishlist - Wishlist items
*/
saveWishlist(wishlist) {
try {
localStorage.setItem("wishlist", JSON.stringify(wishlist));
this.updateWishlistBadge();
} catch (error) {
console.error("Error saving wishlist:", error);
}
},
/**
* Add item to wishlist
* @param {Object} product - Product to add
*/
addToWishlist(product) {
const wishlist = this.getWishlist();
if (!wishlist.find((item) => item.id === product.id)) {
wishlist.push({
id: product.id,
name: product.name,
price: product.price,
image: product.images?.[0]?.image_url || product.imageurl,
slug: product.slug,
});
this.saveWishlist(wishlist);
}
return wishlist;
},
/**
* Remove item from wishlist
* @param {string} productId - Product ID to remove
*/
removeFromWishlist(productId) {
const wishlist = this.getWishlist();
const filtered = wishlist.filter((item) => item.id !== productId);
this.saveWishlist(filtered);
return filtered;
},
/**
* Check if product is in wishlist
* @param {string} productId - Product ID
* @returns {boolean} True if in wishlist
*/
isInWishlist(productId) {
const wishlist = this.getWishlist();
return wishlist.some((item) => item.id === productId);
},
/**
* Update wishlist badge count
*/
updateWishlistBadge() {
const wishlist = this.getWishlist();
const badge = document.querySelector(".wishlist-badge");
if (badge) {
badge.textContent = wishlist.length;
badge.style.display = wishlist.length > 0 ? "flex" : "none";
}
},
/**
* Clear wishlist
*/
clearWishlist() {
localStorage.removeItem("wishlist");
this.updateWishlistBadge();
},
};
/**
* Format price for display
* @param {number} price - Price to format
* @returns {string} Formatted price
*/
const formatPrice = (price) => {
return `$${parseFloat(price).toFixed(2)}`;
};
/**
* Format date for display
* @param {string} dateString - ISO date string
* @returns {string} Formatted date
*/
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
};
/**
* Debounce function
* @param {Function} func - Function to debounce
* @param {number} wait - Wait time in ms
* @returns {Function} Debounced function
*/
const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
/**
* Show notification toast
* @param {string} message - Message to display
* @param {string} type - Type: success, error, info
*/
const showNotification = (message, type = "success") => {
// Check if a notification container exists
let container = document.querySelector(".notification-container");
if (!container) {
container = document.createElement("div");
container.className = "notification-container";
container.style.cssText = `
position: fixed;
top: 100px;
right: 20px;
z-index: 10000;
`;
document.body.appendChild(container);
}
const notification = document.createElement("div");
notification.className = `notification notification-${type}`;
notification.style.cssText = `
background: ${
type === "success" ? "#10b981" : type === "error" ? "#ef4444" : "#3b82f6"
};
color: white;
padding: 12px 20px;
border-radius: 8px;
margin-bottom: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
animation: slideIn 0.3s ease-out;
`;
notification.textContent = message;
container.appendChild(notification);
setTimeout(() => {
notification.style.animation = "slideOut 0.3s ease-out";
setTimeout(() => notification.remove(), 300);
}, 3000);
};
// Export for use in other scripts
if (typeof window !== "undefined") {
window.CartUtils = CartUtils;
window.WishlistUtils = WishlistUtils;
window.formatPrice = formatPrice;
window.formatDate = formatDate;
window.debounce = debounce;
window.showNotification = showNotification;
}