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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,65 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
<!-- Gradient definitions -->
<defs>
<linearGradient id="catGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#FF6B9D;stop-opacity:1" />
<stop offset="100%" style="stop-color:#C239B3;stop-opacity:1" />
</linearGradient>
<linearGradient id="accentGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#FEC6DF;stop-opacity:1" />
<stop offset="100%" style="stop-color:#FF6B9D;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="100" cy="100" r="95" fill="url(#accentGradient)" opacity="0.2"/>
<!-- Cat body -->
<ellipse cx="100" cy="130" rx="45" ry="50" fill="url(#catGradient)"/>
<!-- Cat head -->
<circle cx="100" cy="80" r="35" fill="url(#catGradient)"/>
<!-- Left ear -->
<path d="M 75 55 L 65 30 L 85 50 Z" fill="url(#catGradient)"/>
<path d="M 75 55 L 70 35 L 82 52 Z" fill="#FEC6DF"/>
<!-- Right ear -->
<path d="M 125 55 L 135 30 L 115 50 Z" fill="url(#catGradient)"/>
<path d="M 125 55 L 130 35 L 118 52 Z" fill="#FEC6DF"/>
<!-- Left eye -->
<ellipse cx="88" cy="75" rx="6" ry="10" fill="#2D3436"/>
<ellipse cx="89" cy="73" rx="2" ry="3" fill="white"/>
<!-- Right eye -->
<ellipse cx="112" cy="75" rx="6" ry="10" fill="#2D3436"/>
<ellipse cx="113" cy="73" rx="2" ry="3" fill="white"/>
<!-- Nose -->
<path d="M 100 85 L 97 90 L 103 90 Z" fill="#FF6B9D"/>
<!-- Mouth -->
<path d="M 100 90 Q 95 93 92 91" stroke="#2D3436" stroke-width="1.5" fill="none" stroke-linecap="round"/>
<path d="M 100 90 Q 105 93 108 91" stroke="#2D3436" stroke-width="1.5" fill="none" stroke-linecap="round"/>
<!-- Whiskers left -->
<line x1="70" y1="80" x2="50" y2="78" stroke="#2D3436" stroke-width="1.5" stroke-linecap="round"/>
<line x1="70" y1="85" x2="50" y2="85" stroke="#2D3436" stroke-width="1.5" stroke-linecap="round"/>
<line x1="70" y1="90" x2="50" y2="92" stroke="#2D3436" stroke-width="1.5" stroke-linecap="round"/>
<!-- Whiskers right -->
<line x1="130" y1="80" x2="150" y2="78" stroke="#2D3436" stroke-width="1.5" stroke-linecap="round"/>
<line x1="130" y1="85" x2="150" y2="85" stroke="#2D3436" stroke-width="1.5" stroke-linecap="round"/>
<line x1="130" y1="90" x2="150" y2="92" stroke="#2D3436" stroke-width="1.5" stroke-linecap="round"/>
<!-- Paws -->
<ellipse cx="80" cy="170" rx="12" ry="8" fill="#FF6B9D"/>
<ellipse cx="120" cy="170" rx="12" ry="8" fill="#FF6B9D"/>
<!-- Tail -->
<path d="M 140 140 Q 160 130 165 110 Q 168 90 160 75" stroke="url(#catGradient)" stroke-width="12" fill="none" stroke-linecap="round"/>
<!-- Heart on chest (art/creativity symbol) -->
<path d="M 100 120 L 95 115 Q 93 112 93 109 Q 93 106 95 104 Q 97 102 100 104 Q 103 102 105 104 Q 107 106 107 109 Q 107 112 105 115 Z" fill="#FEC6DF"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

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;
}