webupdate
This commit is contained in:
1819
website/public/assets/css/mobile-fixes.css
Normal file
1819
website/public/assets/css/mobile-fixes.css
Normal file
File diff suppressed because it is too large
Load Diff
2696
website/public/assets/css/modern-theme.css
Normal file
2696
website/public/assets/css/modern-theme.css
Normal file
File diff suppressed because it is too large
Load Diff
65
website/public/assets/images/logo/cat-logo.svg
Normal file
65
website/public/assets/images/logo/cat-logo.svg
Normal 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 |
616
website/public/assets/js/accessibility.js
Normal file
616
website/public/assets/js/accessibility.js
Normal 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;
|
||||
})();
|
||||
421
website/public/assets/js/cart.js
Normal file
421
website/public/assets/js/cart.js
Normal 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();
|
||||
}
|
||||
})();
|
||||
540
website/public/assets/js/components.js
Normal file
540
website/public/assets/js/components.js
Normal 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>© ${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;
|
||||
538
website/public/assets/js/customer-auth.js
Normal file
538
website/public/assets/js/customer-auth.js
Normal 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();
|
||||
}
|
||||
})();
|
||||
228
website/public/assets/js/dynamic-page.js
Normal file
228
website/public/assets/js/dynamic-page.js
Normal 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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
// 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,
|
||||
};
|
||||
114
website/public/assets/js/mobile-touch-fix.js
Normal file
114
website/public/assets/js/mobile-touch-fix.js
Normal 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,
|
||||
});
|
||||
})();
|
||||
1132
website/public/assets/js/modern-theme.js
Normal file
1132
website/public/assets/js/modern-theme.js
Normal file
File diff suppressed because it is too large
Load Diff
332
website/public/assets/js/shared-utils.js
Normal file
332
website/public/assets/js/shared-utils.js
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user