2026-01-18 02:22:05 -06:00
|
|
|
/* ============================================
|
|
|
|
|
SKY ART SHOP - MODERN THEME JAVASCRIPT
|
|
|
|
|
Complete Frontend Functionality
|
|
|
|
|
============================================ */
|
|
|
|
|
|
|
|
|
|
// Global State
|
|
|
|
|
const SkyArtShop = {
|
|
|
|
|
cart: JSON.parse(localStorage.getItem("skyart_cart") || "[]"),
|
|
|
|
|
wishlist: JSON.parse(localStorage.getItem("skyart_wishlist") || "[]"),
|
|
|
|
|
|
|
|
|
|
// Initialize
|
|
|
|
|
init() {
|
|
|
|
|
this.initNavbar();
|
|
|
|
|
// Delay slider init slightly to ensure DOM is ready
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
this.initSlider();
|
|
|
|
|
});
|
|
|
|
|
this.initCart();
|
|
|
|
|
this.initWishlist();
|
|
|
|
|
this.initWishlistDrawer();
|
|
|
|
|
this.initProducts();
|
|
|
|
|
this.initAnimations();
|
|
|
|
|
this.updateCartCount();
|
|
|
|
|
this.updateWishlistCount();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Navbar Functionality
|
|
|
|
|
initNavbar() {
|
|
|
|
|
const navbar = document.querySelector(".nav-wrapper");
|
|
|
|
|
const mobileToggle = document.querySelector(".nav-mobile-toggle");
|
|
|
|
|
const navMenu = document.querySelector(".nav-menu");
|
|
|
|
|
|
|
|
|
|
// Scroll effect
|
|
|
|
|
if (navbar) {
|
|
|
|
|
window.addEventListener("scroll", () => {
|
|
|
|
|
if (window.scrollY > 50) {
|
|
|
|
|
navbar.classList.add("scrolled");
|
|
|
|
|
} else {
|
|
|
|
|
navbar.classList.remove("scrolled");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 20:29:33 -06:00
|
|
|
// Mobile/Tablet menu toggle
|
2026-01-18 02:22:05 -06:00
|
|
|
if (mobileToggle && navMenu) {
|
2026-01-20 20:29:33 -06:00
|
|
|
// Create overlay element for background dimming
|
|
|
|
|
let overlay = document.querySelector(".nav-menu-overlay");
|
|
|
|
|
if (!overlay) {
|
|
|
|
|
overlay = document.createElement("div");
|
|
|
|
|
overlay.className = "nav-menu-overlay";
|
|
|
|
|
document.body.appendChild(overlay);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create close button inside menu
|
|
|
|
|
let closeBtn = navMenu.querySelector(".nav-menu-close");
|
|
|
|
|
if (!closeBtn) {
|
|
|
|
|
closeBtn = document.createElement("button");
|
|
|
|
|
closeBtn.className = "nav-menu-close";
|
|
|
|
|
closeBtn.innerHTML = '<i class="bi bi-x-lg"></i>';
|
|
|
|
|
closeBtn.setAttribute("aria-label", "Close menu");
|
|
|
|
|
navMenu.insertBefore(closeBtn, navMenu.firstChild);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Function to open menu
|
|
|
|
|
const openMenu = () => {
|
|
|
|
|
navMenu.classList.add("open");
|
|
|
|
|
mobileToggle.classList.add("active");
|
|
|
|
|
overlay.classList.add("active");
|
|
|
|
|
document.body.classList.add("nav-menu-open");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Function to close menu
|
|
|
|
|
const closeMenu = () => {
|
|
|
|
|
navMenu.classList.remove("open");
|
|
|
|
|
mobileToggle.classList.remove("active");
|
|
|
|
|
overlay.classList.remove("active");
|
|
|
|
|
document.body.classList.remove("nav-menu-open");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Toggle button click
|
2026-01-18 02:22:05 -06:00
|
|
|
mobileToggle.addEventListener("click", (e) => {
|
|
|
|
|
e.stopPropagation();
|
2026-01-20 20:29:33 -06:00
|
|
|
if (navMenu.classList.contains("open")) {
|
|
|
|
|
closeMenu();
|
|
|
|
|
} else {
|
|
|
|
|
openMenu();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Close button click
|
|
|
|
|
closeBtn.addEventListener("click", (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
closeMenu();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Overlay click to close
|
|
|
|
|
overlay.addEventListener("click", () => {
|
|
|
|
|
closeMenu();
|
2026-01-18 02:22:05 -06:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Close menu when clicking a link
|
|
|
|
|
navMenu.querySelectorAll(".nav-link").forEach((link) => {
|
|
|
|
|
link.addEventListener("click", () => {
|
2026-01-20 20:29:33 -06:00
|
|
|
closeMenu();
|
2026-01-18 02:22:05 -06:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Close menu when clicking outside
|
|
|
|
|
document.addEventListener("click", (e) => {
|
|
|
|
|
if (
|
|
|
|
|
navMenu.classList.contains("open") &&
|
|
|
|
|
!navMenu.contains(e.target) &&
|
|
|
|
|
!mobileToggle.contains(e.target)
|
|
|
|
|
) {
|
2026-01-20 20:29:33 -06:00
|
|
|
closeMenu();
|
2026-01-18 02:22:05 -06:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-20 20:29:33 -06:00
|
|
|
// Close menu when touching outside (for mobile/tablet)
|
2026-01-18 02:22:05 -06:00
|
|
|
document.addEventListener(
|
|
|
|
|
"touchstart",
|
|
|
|
|
(e) => {
|
|
|
|
|
if (
|
|
|
|
|
navMenu.classList.contains("open") &&
|
|
|
|
|
!navMenu.contains(e.target) &&
|
|
|
|
|
!mobileToggle.contains(e.target)
|
|
|
|
|
) {
|
2026-01-20 20:29:33 -06:00
|
|
|
closeMenu();
|
2026-01-18 02:22:05 -06:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ passive: true },
|
|
|
|
|
);
|
2026-01-20 20:29:33 -06:00
|
|
|
|
|
|
|
|
// Close menu on escape key
|
|
|
|
|
document.addEventListener("keydown", (e) => {
|
|
|
|
|
if (e.key === "Escape" && navMenu.classList.contains("open")) {
|
|
|
|
|
closeMenu();
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-01-18 02:22:05 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set active nav link
|
|
|
|
|
const currentPath = window.location.pathname;
|
|
|
|
|
document.querySelectorAll(".nav-link").forEach((link) => {
|
|
|
|
|
if (link.getAttribute("href") === currentPath) {
|
|
|
|
|
link.classList.add("active");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Hero Slider
|
|
|
|
|
sliderInitialized: false,
|
|
|
|
|
sliderInterval: null,
|
|
|
|
|
|
|
|
|
|
initSlider() {
|
|
|
|
|
const slider = document.querySelector(".hero-slider");
|
|
|
|
|
if (!slider) return;
|
|
|
|
|
|
|
|
|
|
// Prevent multiple initializations
|
|
|
|
|
if (this.sliderInitialized || slider.dataset.initialized === "true") {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.sliderInitialized = true;
|
|
|
|
|
slider.dataset.initialized = "true";
|
|
|
|
|
|
|
|
|
|
const slides = slider.querySelectorAll(".slide");
|
|
|
|
|
const dots = slider.querySelectorAll(".slider-dot");
|
|
|
|
|
const prevBtn = slider.querySelector(".slider-arrow.prev");
|
|
|
|
|
const nextBtn = slider.querySelector(".slider-arrow.next");
|
|
|
|
|
|
|
|
|
|
// Need at least 2 slides for auto-play to make sense
|
|
|
|
|
if (slides.length < 2) return;
|
|
|
|
|
|
|
|
|
|
let currentSlide = 0;
|
|
|
|
|
let isAnimating = false;
|
|
|
|
|
const self = this;
|
|
|
|
|
|
|
|
|
|
// Clear any existing interval
|
|
|
|
|
if (this.sliderInterval) {
|
|
|
|
|
clearInterval(this.sliderInterval);
|
|
|
|
|
this.sliderInterval = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize slides - first slide active, others positioned off-screen right
|
|
|
|
|
slides.forEach((slide, i) => {
|
|
|
|
|
slide.classList.remove("active", "outgoing");
|
|
|
|
|
slide.style.transition = "none";
|
|
|
|
|
if (i === 0) {
|
|
|
|
|
slide.classList.add("active");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// Force reflow then re-enable transitions
|
|
|
|
|
void slider.offsetWidth;
|
|
|
|
|
slides.forEach((slide) => (slide.style.transition = ""));
|
|
|
|
|
if (dots[0]) dots[0].classList.add("active");
|
|
|
|
|
|
|
|
|
|
const showSlide = (index) => {
|
|
|
|
|
if (isAnimating) return;
|
|
|
|
|
|
|
|
|
|
const prevIndex = currentSlide;
|
|
|
|
|
currentSlide = (index + slides.length) % slides.length;
|
|
|
|
|
|
|
|
|
|
if (prevIndex === currentSlide) return;
|
|
|
|
|
|
|
|
|
|
isAnimating = true;
|
|
|
|
|
|
|
|
|
|
const oldSlide = slides[prevIndex];
|
|
|
|
|
const newSlide = slides[currentSlide];
|
|
|
|
|
|
|
|
|
|
// Update dots
|
|
|
|
|
dots.forEach((dot, i) =>
|
|
|
|
|
dot.classList.toggle("active", i === currentSlide),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Position new slide off-screen to the right (no transition)
|
|
|
|
|
newSlide.style.transition = "none";
|
|
|
|
|
newSlide.classList.remove("outgoing");
|
|
|
|
|
newSlide.style.transform = "translateX(100%)";
|
|
|
|
|
|
|
|
|
|
// Force browser to register the position
|
|
|
|
|
void newSlide.offsetWidth;
|
|
|
|
|
|
|
|
|
|
// Re-enable transition and animate
|
|
|
|
|
newSlide.style.transition = "";
|
|
|
|
|
newSlide.style.transform = "";
|
|
|
|
|
newSlide.classList.add("active");
|
|
|
|
|
|
|
|
|
|
// Old slide moves out to the left
|
|
|
|
|
oldSlide.classList.remove("active");
|
|
|
|
|
oldSlide.classList.add("outgoing");
|
|
|
|
|
|
|
|
|
|
// Cleanup after animation (800ms matches CSS)
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
oldSlide.classList.remove("outgoing");
|
|
|
|
|
oldSlide.style.transform = "";
|
|
|
|
|
isAnimating = false;
|
|
|
|
|
}, 850);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const nextSlide = () => showSlide(currentSlide + 1);
|
|
|
|
|
const prevSlide = () => showSlide(currentSlide - 1);
|
|
|
|
|
|
|
|
|
|
// Auto-play with 7 second intervals (7000ms)
|
|
|
|
|
const startAutoPlay = () => {
|
|
|
|
|
// Clear any existing interval first
|
|
|
|
|
if (self.sliderInterval) {
|
|
|
|
|
clearInterval(self.sliderInterval);
|
|
|
|
|
}
|
|
|
|
|
self.sliderInterval = setInterval(nextSlide, 7000);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const stopAutoPlay = () => {
|
|
|
|
|
if (self.sliderInterval) {
|
|
|
|
|
clearInterval(self.sliderInterval);
|
|
|
|
|
self.sliderInterval = null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Event listeners
|
|
|
|
|
if (prevBtn)
|
|
|
|
|
prevBtn.addEventListener("click", () => {
|
|
|
|
|
stopAutoPlay();
|
|
|
|
|
prevSlide();
|
|
|
|
|
startAutoPlay();
|
|
|
|
|
});
|
|
|
|
|
if (nextBtn)
|
|
|
|
|
nextBtn.addEventListener("click", () => {
|
|
|
|
|
stopAutoPlay();
|
|
|
|
|
nextSlide();
|
|
|
|
|
startAutoPlay();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
dots.forEach((dot, i) => {
|
|
|
|
|
dot.addEventListener("click", () => {
|
|
|
|
|
stopAutoPlay();
|
|
|
|
|
showSlide(i);
|
|
|
|
|
startAutoPlay();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Start auto-play immediately (first slide already initialized)
|
|
|
|
|
startAutoPlay();
|
|
|
|
|
|
|
|
|
|
// Pause on hover
|
|
|
|
|
slider.addEventListener("mouseenter", stopAutoPlay);
|
|
|
|
|
slider.addEventListener("mouseleave", startAutoPlay);
|
|
|
|
|
|
|
|
|
|
// Pause when tab is not visible, resume when visible
|
|
|
|
|
document.addEventListener("visibilitychange", () => {
|
|
|
|
|
if (document.hidden) {
|
|
|
|
|
stopAutoPlay();
|
|
|
|
|
} else {
|
|
|
|
|
startAutoPlay();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Cart Functionality
|
|
|
|
|
initCart() {
|
|
|
|
|
const cartBtn = document.querySelector(".cart-btn");
|
|
|
|
|
const cartDrawer = document.querySelector(".cart-drawer");
|
|
|
|
|
const cartOverlay = document.querySelector(".cart-overlay");
|
|
|
|
|
const cartClose = document.querySelector(".cart-close");
|
|
|
|
|
|
|
|
|
|
if (cartBtn && cartDrawer) {
|
|
|
|
|
cartBtn.addEventListener("click", () => this.openCart());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cartClose) cartClose.addEventListener("click", () => this.closeCart());
|
|
|
|
|
if (cartOverlay)
|
|
|
|
|
cartOverlay.addEventListener("click", () => this.closeCart());
|
|
|
|
|
|
|
|
|
|
// Close on escape
|
|
|
|
|
document.addEventListener("keydown", (e) => {
|
|
|
|
|
if (e.key === "Escape") this.closeCart();
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
openCart() {
|
|
|
|
|
const cartDrawer = document.querySelector(".cart-drawer");
|
|
|
|
|
const cartOverlay = document.querySelector(".cart-overlay");
|
|
|
|
|
if (cartDrawer) cartDrawer.classList.add("open");
|
|
|
|
|
if (cartOverlay) cartOverlay.classList.add("open");
|
|
|
|
|
document.body.style.overflow = "hidden";
|
|
|
|
|
this.renderCart();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
closeCart() {
|
|
|
|
|
const cartDrawer = document.querySelector(".cart-drawer");
|
|
|
|
|
const cartOverlay = document.querySelector(".cart-overlay");
|
|
|
|
|
if (cartDrawer) cartDrawer.classList.remove("open");
|
|
|
|
|
if (cartOverlay) cartOverlay.classList.remove("open");
|
|
|
|
|
document.body.style.overflow = "";
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
addToCart(product) {
|
|
|
|
|
const existingItem = this.cart.find((item) => item.id === product.id);
|
|
|
|
|
|
|
|
|
|
if (existingItem) {
|
|
|
|
|
existingItem.quantity += 1;
|
|
|
|
|
} else {
|
|
|
|
|
this.cart.push({ ...product, quantity: 1 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.saveCart();
|
|
|
|
|
this.updateCartCount();
|
|
|
|
|
this.showNotification(`${product.name} added to cart!`);
|
|
|
|
|
this.openCart();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
removeFromCart(productId) {
|
|
|
|
|
this.cart = this.cart.filter((item) => item.id !== productId);
|
|
|
|
|
this.saveCart();
|
|
|
|
|
this.updateCartCount();
|
|
|
|
|
this.renderCart();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
updateCartQty(productId, change) {
|
|
|
|
|
const item = this.cart.find((item) => item.id === productId);
|
|
|
|
|
if (item) {
|
|
|
|
|
item.quantity += change;
|
|
|
|
|
if (item.quantity <= 0) {
|
|
|
|
|
this.removeFromCart(productId);
|
|
|
|
|
} else {
|
|
|
|
|
this.saveCart();
|
|
|
|
|
this.renderCart();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
saveCart() {
|
|
|
|
|
localStorage.setItem("skyart_cart", JSON.stringify(this.cart));
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
updateCartCount() {
|
|
|
|
|
const count = this.cart.reduce((sum, item) => sum + item.quantity, 0);
|
|
|
|
|
document.querySelectorAll(".cart-count").forEach((el) => {
|
|
|
|
|
el.textContent = count;
|
|
|
|
|
el.style.display = count > 0 ? "flex" : "none";
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
getCartTotal() {
|
|
|
|
|
return this.cart.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.price) * item.quantity,
|
|
|
|
|
0,
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
renderCart() {
|
|
|
|
|
const cartItems = document.querySelector(".cart-items");
|
|
|
|
|
const cartTotal = document.querySelector(".cart-total-amount");
|
|
|
|
|
|
|
|
|
|
if (!cartItems) return;
|
|
|
|
|
|
|
|
|
|
if (this.cart.length === 0) {
|
|
|
|
|
cartItems.innerHTML = `
|
|
|
|
|
<div class="cart-empty">
|
|
|
|
|
<i class="bi bi-cart-x" style="font-size: 3rem; color: var(--text-light); margin-bottom: 16px;"></i>
|
|
|
|
|
<p>Your cart is empty</p>
|
|
|
|
|
<a href="/shop" class="btn btn-primary" style="margin-top: 16px; min-width: 200px;">Continue Shopping</a>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
} else {
|
|
|
|
|
cartItems.innerHTML = this.cart
|
|
|
|
|
.map(
|
|
|
|
|
(item) => `
|
|
|
|
|
<div class="cart-item">
|
|
|
|
|
<div class="cart-item-image">
|
|
|
|
|
<img src="${item.image || "/assets/images/placeholder.jpg"}" alt="${
|
|
|
|
|
item.name
|
|
|
|
|
}">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="cart-item-info">
|
|
|
|
|
<div class="cart-item-name">${item.name}</div>
|
|
|
|
|
${
|
|
|
|
|
item.color
|
|
|
|
|
? `<div class="cart-item-color" style="font-size: 0.85rem; color: #666;">Color: ${item.color}</div>`
|
|
|
|
|
: ""
|
|
|
|
|
}
|
|
|
|
|
<div class="cart-item-price">$${parseFloat(item.price).toFixed(
|
|
|
|
|
2,
|
|
|
|
|
)}</div>
|
|
|
|
|
<div class="cart-item-qty">
|
|
|
|
|
<button class="qty-btn" onclick="SkyArtShop.updateCartQty('${
|
|
|
|
|
item.id
|
|
|
|
|
}', -1)">-</button>
|
|
|
|
|
<span>${item.quantity}</span>
|
|
|
|
|
<button class="qty-btn" onclick="SkyArtShop.updateCartQty('${
|
|
|
|
|
item.id
|
|
|
|
|
}', 1)">+</button>
|
|
|
|
|
<button class="qty-btn" onclick="SkyArtShop.removeFromCart('${
|
|
|
|
|
item.id
|
|
|
|
|
}')" style="margin-left: auto; color: #e74c3c;">
|
|
|
|
|
<i class="bi bi-trash"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.join("");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cartTotal) {
|
|
|
|
|
cartTotal.textContent = `$${this.getCartTotal().toFixed(2)}`;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Wishlist Functionality
|
|
|
|
|
initWishlist() {
|
|
|
|
|
this.updateWishlistCount();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
toggleWishlist(product) {
|
|
|
|
|
const index = this.wishlist.findIndex((item) => item.id === product.id);
|
|
|
|
|
|
|
|
|
|
if (index > -1) {
|
|
|
|
|
this.wishlist.splice(index, 1);
|
|
|
|
|
this.showNotification(`${product.name} removed from wishlist`);
|
|
|
|
|
} else {
|
|
|
|
|
this.wishlist.push(product);
|
|
|
|
|
this.showNotification(`${product.name} added to wishlist!`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.saveWishlist();
|
|
|
|
|
this.updateWishlistCount();
|
|
|
|
|
this.updateWishlistButtons();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
isInWishlist(productId) {
|
|
|
|
|
return this.wishlist.some((item) => item.id === productId);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
saveWishlist() {
|
|
|
|
|
localStorage.setItem("skyart_wishlist", JSON.stringify(this.wishlist));
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
updateWishlistCount() {
|
|
|
|
|
const count = this.wishlist.length;
|
|
|
|
|
document.querySelectorAll(".wishlist-count").forEach((el) => {
|
|
|
|
|
el.textContent = count;
|
|
|
|
|
el.style.display = count > 0 ? "flex" : "none";
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
updateWishlistButtons() {
|
|
|
|
|
document.querySelectorAll(".wishlist-btn").forEach((btn) => {
|
|
|
|
|
const productId = btn.dataset.productId;
|
|
|
|
|
if (this.isInWishlist(productId)) {
|
|
|
|
|
btn.classList.add("active");
|
|
|
|
|
btn.innerHTML = '<i class="bi bi-heart-fill"></i>';
|
|
|
|
|
} else {
|
|
|
|
|
btn.classList.remove("active");
|
|
|
|
|
btn.innerHTML = '<i class="bi bi-heart"></i>';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Wishlist Drawer
|
|
|
|
|
initWishlistDrawer() {
|
|
|
|
|
const wishlistBtn = document.querySelector(".wishlist-btn-nav");
|
|
|
|
|
const wishlistDrawer = document.querySelector(".wishlist-drawer");
|
|
|
|
|
const wishlistOverlay = document.querySelector(".wishlist-overlay");
|
|
|
|
|
const wishlistClose = document.querySelector(".wishlist-close");
|
|
|
|
|
|
|
|
|
|
if (wishlistBtn && wishlistDrawer) {
|
|
|
|
|
wishlistBtn.addEventListener("click", (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.openWishlist();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (wishlistClose)
|
|
|
|
|
wishlistClose.addEventListener("click", () => this.closeWishlist());
|
|
|
|
|
if (wishlistOverlay)
|
|
|
|
|
wishlistOverlay.addEventListener("click", () => this.closeWishlist());
|
|
|
|
|
|
|
|
|
|
// Close on escape
|
|
|
|
|
document.addEventListener("keydown", (e) => {
|
|
|
|
|
if (e.key === "Escape") this.closeWishlist();
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
openWishlist() {
|
|
|
|
|
const wishlistDrawer = document.querySelector(".wishlist-drawer");
|
|
|
|
|
const wishlistOverlay = document.querySelector(".wishlist-overlay");
|
|
|
|
|
if (wishlistDrawer) wishlistDrawer.classList.add("open");
|
|
|
|
|
if (wishlistOverlay) wishlistOverlay.classList.add("open");
|
|
|
|
|
document.body.style.overflow = "hidden";
|
|
|
|
|
this.renderWishlist();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
closeWishlist() {
|
|
|
|
|
const wishlistDrawer = document.querySelector(".wishlist-drawer");
|
|
|
|
|
const wishlistOverlay = document.querySelector(".wishlist-overlay");
|
|
|
|
|
if (wishlistDrawer) wishlistDrawer.classList.remove("open");
|
|
|
|
|
if (wishlistOverlay) wishlistOverlay.classList.remove("open");
|
|
|
|
|
document.body.style.overflow = "";
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
renderWishlist() {
|
|
|
|
|
const wishlistItems = document.querySelector(".wishlist-items");
|
|
|
|
|
if (!wishlistItems) return;
|
|
|
|
|
|
|
|
|
|
if (this.wishlist.length === 0) {
|
|
|
|
|
wishlistItems.innerHTML = `
|
|
|
|
|
<div class="wishlist-empty">
|
|
|
|
|
<i class="bi bi-heart"></i>
|
|
|
|
|
<p>Your wishlist is empty</p>
|
|
|
|
|
<p style="font-size: 0.9rem;">Browse our products and add items you love!</p>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wishlistItems.innerHTML = this.wishlist
|
|
|
|
|
.map(
|
|
|
|
|
(item) => `
|
|
|
|
|
<div class="wishlist-item" data-id="${item.id}">
|
|
|
|
|
<div class="wishlist-item-image">
|
|
|
|
|
<img src="${item.image || "/uploads/default-product.png"}" alt="${
|
|
|
|
|
item.name
|
|
|
|
|
}">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="wishlist-item-info">
|
|
|
|
|
<div class="wishlist-item-name">${item.name}</div>
|
|
|
|
|
${
|
|
|
|
|
item.color
|
|
|
|
|
? `<div class="wishlist-item-color" style="font-size: 0.85rem; color: #666;">Color: ${item.color}</div>`
|
|
|
|
|
: ""
|
|
|
|
|
}
|
|
|
|
|
<div class="wishlist-item-price">$${parseFloat(item.price).toFixed(
|
|
|
|
|
2,
|
|
|
|
|
)}</div>
|
|
|
|
|
<div class="wishlist-item-actions">
|
|
|
|
|
<button class="wishlist-add-to-cart" onclick="SkyArtShop.moveToCart('${
|
|
|
|
|
item.id
|
|
|
|
|
}')">
|
|
|
|
|
<i class="bi bi-cart-plus"></i> Add to Cart
|
|
|
|
|
</button>
|
|
|
|
|
<button class="wishlist-remove" onclick="SkyArtShop.removeFromWishlistById('${
|
|
|
|
|
item.id
|
|
|
|
|
}')">
|
|
|
|
|
<i class="bi bi-trash"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.join("");
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
moveToCart(productId) {
|
|
|
|
|
const item = this.wishlist.find((item) => item.id === productId);
|
|
|
|
|
if (item) {
|
|
|
|
|
// Pass the full item including color and image
|
|
|
|
|
this.addToCart({
|
|
|
|
|
id: item.id,
|
|
|
|
|
productId: item.productId || item.id,
|
|
|
|
|
name: item.name,
|
|
|
|
|
price: item.price,
|
|
|
|
|
image: item.image,
|
|
|
|
|
color: item.color || null,
|
|
|
|
|
});
|
|
|
|
|
this.removeFromWishlistById(productId);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
removeFromWishlistById(productId) {
|
|
|
|
|
const index = this.wishlist.findIndex((item) => item.id === productId);
|
|
|
|
|
if (index > -1) {
|
|
|
|
|
const item = this.wishlist[index];
|
|
|
|
|
this.wishlist.splice(index, 1);
|
|
|
|
|
this.saveWishlist();
|
|
|
|
|
this.updateWishlistCount();
|
|
|
|
|
this.updateWishlistButtons();
|
|
|
|
|
this.renderWishlist();
|
|
|
|
|
this.showNotification(`${item.name} removed from wishlist`);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Products
|
|
|
|
|
initProducts() {
|
|
|
|
|
// Attach event listeners to product cards
|
|
|
|
|
document.querySelectorAll(".add-to-cart-btn").forEach((btn) => {
|
|
|
|
|
btn.addEventListener("click", (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const card = btn.closest(".product-card");
|
|
|
|
|
const product = this.getProductFromCard(card);
|
|
|
|
|
this.addToCart(product);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll(".wishlist-btn").forEach((btn) => {
|
|
|
|
|
btn.addEventListener("click", (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const card = btn.closest(".product-card");
|
|
|
|
|
const product = this.getProductFromCard(card);
|
|
|
|
|
this.toggleWishlist(product);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.updateWishlistButtons();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
getProductFromCard(card) {
|
|
|
|
|
return {
|
|
|
|
|
id: card.dataset.productId,
|
|
|
|
|
name:
|
|
|
|
|
card.querySelector(".product-name a")?.textContent ||
|
|
|
|
|
card.querySelector(".product-name")?.textContent ||
|
|
|
|
|
"Product",
|
|
|
|
|
price:
|
|
|
|
|
card.dataset.productPrice ||
|
|
|
|
|
card.querySelector(".price-current")?.textContent?.replace("$", "") ||
|
|
|
|
|
"0",
|
|
|
|
|
image: card.querySelector(".product-image img")?.src || "",
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Animations
|
|
|
|
|
initAnimations() {
|
|
|
|
|
// Intersection Observer for scroll animations
|
|
|
|
|
const observer = new IntersectionObserver(
|
|
|
|
|
(entries) => {
|
|
|
|
|
entries.forEach((entry) => {
|
|
|
|
|
if (entry.isIntersecting) {
|
|
|
|
|
entry.target.classList.add("fade-in");
|
|
|
|
|
observer.unobserve(entry.target);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
{ threshold: 0.1 },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
document
|
|
|
|
|
.querySelectorAll(".section, .product-card, .blog-card, .portfolio-card")
|
|
|
|
|
.forEach((el) => {
|
|
|
|
|
observer.observe(el);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Notifications
|
|
|
|
|
showNotification(message, type = "success") {
|
|
|
|
|
// Remove existing notifications
|
|
|
|
|
document.querySelectorAll(".notification").forEach((n) => n.remove());
|
|
|
|
|
|
|
|
|
|
const notification = document.createElement("div");
|
|
|
|
|
notification.className = `notification notification-${type}`;
|
|
|
|
|
notification.innerHTML = `
|
|
|
|
|
<i class="bi bi-${
|
|
|
|
|
type === "success" ? "check-circle" : "exclamation-circle"
|
|
|
|
|
}"></i>
|
|
|
|
|
<span>${message}</span>
|
|
|
|
|
`;
|
|
|
|
|
notification.style.cssText = `
|
|
|
|
|
position: fixed;
|
|
|
|
|
bottom: 20px;
|
|
|
|
|
right: 20px;
|
|
|
|
|
background: ${type === "success" ? "#202023" : "#e74c3c"};
|
|
|
|
|
color: white;
|
|
|
|
|
padding: 16px 24px;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
|
|
|
|
|
z-index: 9999;
|
|
|
|
|
animation: slideIn 0.3s ease;
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
notification.style.animation = "slideOut 0.3s ease forwards";
|
|
|
|
|
setTimeout(() => notification.remove(), 300);
|
|
|
|
|
}, 3000);
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Add notification animations
|
|
|
|
|
const style = document.createElement("style");
|
|
|
|
|
style.textContent = `
|
|
|
|
|
@keyframes slideIn {
|
|
|
|
|
from { transform: translateX(100%); opacity: 0; }
|
|
|
|
|
to { transform: translateX(0); opacity: 1; }
|
|
|
|
|
}
|
|
|
|
|
@keyframes slideOut {
|
|
|
|
|
from { transform: translateX(0); opacity: 1; }
|
|
|
|
|
to { transform: translateX(100%); opacity: 0; }
|
|
|
|
|
}
|
|
|
|
|
.cart-empty {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
height: 300px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
color: var(--text-light);
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
document.head.appendChild(style);
|
|
|
|
|
|
|
|
|
|
// Initialize on DOM ready
|
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
|
|
|
SkyArtShop.init();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// API Functions
|
|
|
|
|
const API = {
|
|
|
|
|
baseUrl: "/api",
|
|
|
|
|
|
|
|
|
|
async get(endpoint, noCache = false) {
|
|
|
|
|
try {
|
|
|
|
|
const url = noCache
|
|
|
|
|
? `${this.baseUrl}${endpoint}${
|
|
|
|
|
endpoint.includes("?") ? "&" : "?"
|
|
|
|
|
}_t=${Date.now()}`
|
|
|
|
|
: `${this.baseUrl}${endpoint}`;
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
return data.success ? data : null;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("API Error:", error);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async loadFeaturedProducts() {
|
|
|
|
|
const data = await this.get("/products/featured?limit=4");
|
|
|
|
|
return data?.products || [];
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async loadAllProducts() {
|
|
|
|
|
const data = await this.get("/products");
|
|
|
|
|
return data?.products || [];
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async loadProduct(slug) {
|
|
|
|
|
// Always fetch fresh product data to get latest color variants
|
|
|
|
|
const data = await this.get(`/products/${slug}`, true);
|
|
|
|
|
return data?.product || null;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async loadHomepageSections() {
|
|
|
|
|
const data = await this.get("/homepage/sections");
|
|
|
|
|
return data?.sections || [];
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async loadBlogPosts() {
|
|
|
|
|
const data = await this.get("/blog/posts");
|
|
|
|
|
return data?.posts || [];
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async loadPortfolioProjects() {
|
|
|
|
|
const data = await this.get("/portfolio/projects", true);
|
|
|
|
|
return data?.projects || [];
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async loadTeamMembers() {
|
|
|
|
|
const data = await this.get("/team-members");
|
|
|
|
|
return data?.teamMembers || [];
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async loadCategories() {
|
|
|
|
|
const data = await this.get("/categories");
|
|
|
|
|
return data?.categories || [];
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Product Renderer
|
|
|
|
|
const ProductRenderer = {
|
|
|
|
|
renderCard(product) {
|
|
|
|
|
const primaryImage =
|
|
|
|
|
product.images?.find((img) => img.is_primary) || product.images?.[0];
|
|
|
|
|
const imageUrl =
|
|
|
|
|
primaryImage?.image_url ||
|
|
|
|
|
product.imageurl ||
|
|
|
|
|
"/assets/images/placeholder.jpg";
|
|
|
|
|
const inWishlist = SkyArtShop.isInWishlist(product.id);
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
<div class="product-card" data-product-id="${
|
|
|
|
|
product.id
|
|
|
|
|
}" data-product-slug="${
|
|
|
|
|
product.slug || product.id
|
|
|
|
|
}" data-product-price="${product.price}" style="cursor: pointer;">
|
|
|
|
|
<div class="product-image">
|
|
|
|
|
<img src="${imageUrl}" alt="${product.name}" loading="lazy">
|
|
|
|
|
${
|
|
|
|
|
product.isfeatured
|
|
|
|
|
? '<div class="product-badges"><span class="product-badge new">Featured</span></div>'
|
|
|
|
|
: ""
|
|
|
|
|
}
|
|
|
|
|
<div class="product-actions">
|
|
|
|
|
<button class="product-action-btn wishlist-btn ${
|
|
|
|
|
inWishlist ? "active" : ""
|
|
|
|
|
}" data-product-id="${product.id}" title="Add to Wishlist">
|
|
|
|
|
<i class="bi bi-heart${inWishlist ? "-fill" : ""}"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="product-action-btn quick-view-btn" title="Quick View">
|
|
|
|
|
<i class="bi bi-eye"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="product-info">
|
|
|
|
|
<div class="product-category">${product.category || "General"}</div>
|
|
|
|
|
<h3 class="product-name">${product.name}</h3>
|
|
|
|
|
<div class="product-price">
|
|
|
|
|
<span class="price-current">$${parseFloat(product.price).toFixed(
|
|
|
|
|
2,
|
|
|
|
|
)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="product-footer">
|
|
|
|
|
<span class="product-stock ${
|
|
|
|
|
product.stockquantity > 0
|
|
|
|
|
? product.stockquantity < 10
|
|
|
|
|
? "low-stock"
|
|
|
|
|
: "in-stock"
|
|
|
|
|
: ""
|
|
|
|
|
}">
|
|
|
|
|
${
|
|
|
|
|
product.stockquantity > 0
|
|
|
|
|
? product.stockquantity < 10
|
|
|
|
|
? `Only ${product.stockquantity} left`
|
|
|
|
|
: "In Stock"
|
|
|
|
|
: "Out of Stock"
|
|
|
|
|
}
|
|
|
|
|
</span>
|
|
|
|
|
<button class="add-to-cart-btn" ${
|
|
|
|
|
product.stockquantity <= 0 ? "disabled" : ""
|
|
|
|
|
}>
|
|
|
|
|
<i class="bi bi-cart-plus"></i> Add
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async renderProducts(container, products) {
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
if (products.length === 0) {
|
|
|
|
|
container.innerHTML = '<p class="text-center">No products found.</p>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
container.innerHTML = products.map((p) => this.renderCard(p)).join("");
|
|
|
|
|
SkyArtShop.initProducts();
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Blog Renderer
|
|
|
|
|
const BlogRenderer = {
|
|
|
|
|
renderCard(post) {
|
|
|
|
|
const date = new Date(post.createdat).toLocaleDateString("en-US", {
|
|
|
|
|
year: "numeric",
|
|
|
|
|
month: "short",
|
|
|
|
|
day: "numeric",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
<article class="blog-card">
|
|
|
|
|
<div class="blog-image">
|
|
|
|
|
<a href="/blog/${post.slug}">
|
|
|
|
|
<img src="${
|
|
|
|
|
post.imageurl || "/assets/images/blog-placeholder.jpg"
|
|
|
|
|
}" alt="${post.title}" loading="lazy">
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="blog-content">
|
|
|
|
|
<div class="blog-meta">
|
|
|
|
|
<span><i class="bi bi-calendar3"></i> ${date}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<h3 class="blog-title">
|
|
|
|
|
<a href="/blog/${post.slug}">${post.title}</a>
|
|
|
|
|
</h3>
|
|
|
|
|
<p class="blog-excerpt">${post.excerpt || ""}</p>
|
|
|
|
|
<a href="/blog/${post.slug}" class="blog-read-more">
|
|
|
|
|
Read More <i class="bi bi-arrow-right"></i>
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
</article>
|
|
|
|
|
`;
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Portfolio Renderer
|
|
|
|
|
const PortfolioRenderer = {
|
|
|
|
|
renderCard(project) {
|
|
|
|
|
return `
|
|
|
|
|
<div class="portfolio-card" data-project-id="${project.id}">
|
|
|
|
|
<img src="${
|
|
|
|
|
project.featuredimage || "/assets/images/portfolio-placeholder.jpg"
|
|
|
|
|
}" alt="${project.title}" loading="lazy">
|
|
|
|
|
<div class="portfolio-overlay">
|
|
|
|
|
<h3>${project.title}</h3>
|
|
|
|
|
<p>${project.description || ""}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
// AUTO-REFRESH SYSTEM
|
|
|
|
|
// Automatically refresh frontend when admin makes changes
|
|
|
|
|
// ============================================
|
|
|
|
|
const AutoRefresh = {
|
|
|
|
|
lastChecked: Date.now(),
|
|
|
|
|
checkInterval: 5000, // Check every 5 seconds
|
|
|
|
|
|
|
|
|
|
init() {
|
|
|
|
|
// Listen for BroadcastChannel messages from admin
|
|
|
|
|
try {
|
|
|
|
|
const channel = new BroadcastChannel("skyartshop_updates");
|
|
|
|
|
channel.onmessage = (event) => {
|
|
|
|
|
console.log("[AutoRefresh] Received update:", event.data);
|
|
|
|
|
this.handleUpdate(event.data.type);
|
|
|
|
|
};
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// Fallback to localStorage polling if BroadcastChannel not supported
|
|
|
|
|
this.startPolling();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Also start polling as backup
|
|
|
|
|
this.startPolling();
|
|
|
|
|
|
|
|
|
|
// Check on visibility change (when user returns to tab)
|
|
|
|
|
document.addEventListener("visibilitychange", () => {
|
|
|
|
|
if (!document.hidden) {
|
|
|
|
|
this.checkForUpdates();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
startPolling() {
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
if (!document.hidden) {
|
|
|
|
|
this.checkForUpdates();
|
|
|
|
|
}
|
|
|
|
|
}, this.checkInterval);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
checkForUpdates() {
|
|
|
|
|
const lastChange = localStorage.getItem("skyartshop_last_change");
|
|
|
|
|
if (lastChange && parseInt(lastChange) > this.lastChecked) {
|
|
|
|
|
this.lastChecked = parseInt(lastChange);
|
|
|
|
|
|
|
|
|
|
// Determine what changed
|
|
|
|
|
const productsChanged = localStorage.getItem(
|
|
|
|
|
"skyartshop_change_products",
|
|
|
|
|
);
|
|
|
|
|
const settingsChanged = localStorage.getItem(
|
|
|
|
|
"skyartshop_change_settings",
|
|
|
|
|
);
|
|
|
|
|
const pagesChanged = localStorage.getItem("skyartshop_change_pages");
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
productsChanged &&
|
|
|
|
|
parseInt(productsChanged) > this.lastChecked - this.checkInterval
|
|
|
|
|
) {
|
|
|
|
|
this.handleUpdate("products");
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
settingsChanged &&
|
|
|
|
|
parseInt(settingsChanged) > this.lastChecked - this.checkInterval
|
|
|
|
|
) {
|
|
|
|
|
this.handleUpdate("settings");
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
pagesChanged &&
|
|
|
|
|
parseInt(pagesChanged) > this.lastChecked - this.checkInterval
|
|
|
|
|
) {
|
|
|
|
|
this.handleUpdate("pages");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleUpdate(type) {
|
|
|
|
|
console.log(`[AutoRefresh] Handling ${type} update`);
|
|
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
case "products":
|
|
|
|
|
this.refreshProducts();
|
|
|
|
|
break;
|
|
|
|
|
case "settings":
|
|
|
|
|
this.refreshSettings();
|
|
|
|
|
break;
|
|
|
|
|
case "pages":
|
|
|
|
|
// Soft reload for page content changes
|
|
|
|
|
location.reload();
|
|
|
|
|
break;
|
|
|
|
|
case "all":
|
|
|
|
|
location.reload();
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
// For unknown types, refresh products as default
|
|
|
|
|
this.refreshProducts();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async refreshProducts() {
|
|
|
|
|
// Refresh featured products on homepage
|
|
|
|
|
const featuredGrid = document.querySelector(".featured-products-grid");
|
|
|
|
|
if (featuredGrid) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`/api/products/featured?_t=${Date.now()}`);
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
if (data.success && data.products) {
|
|
|
|
|
featuredGrid.innerHTML = data.products
|
|
|
|
|
.map((p) => ProductRenderer.renderCard(p))
|
|
|
|
|
.join("");
|
|
|
|
|
console.log("[AutoRefresh] Featured products refreshed");
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("[AutoRefresh] Failed to refresh products:", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Refresh products page if we're on it
|
|
|
|
|
const productsGrid = document.querySelector(".products-grid");
|
|
|
|
|
if (productsGrid && window.loadProducts) {
|
|
|
|
|
window.loadProducts();
|
|
|
|
|
console.log("[AutoRefresh] Products page refreshed");
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async refreshSettings() {
|
|
|
|
|
// Settings changes might affect header/footer, just reload
|
|
|
|
|
location.reload();
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Initialize auto-refresh on page load
|
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
|
|
|
AutoRefresh.init();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Export for global use
|
|
|
|
|
window.SkyArtShop = SkyArtShop;
|
|
|
|
|
window.API = API;
|
|
|
|
|
window.ProductRenderer = ProductRenderer;
|
|
|
|
|
window.BlogRenderer = BlogRenderer;
|
|
|
|
|
window.PortfolioRenderer = PortfolioRenderer;
|
|
|
|
|
window.AutoRefresh = AutoRefresh;
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
// MOBILE TOUCH OPTIMIZATION
|
|
|
|
|
// Prevent double-tap behavior on mobile devices
|
|
|
|
|
// ============================================
|
|
|
|
|
(function () {
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
// Only run on touch devices
|
|
|
|
|
if (!("ontouchstart" in window)) return;
|
|
|
|
|
|
|
|
|
|
// Optimize touch interaction on mobile devices
|
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
|
|
|
// Add mobile-optimized class to body for CSS targeting
|
|
|
|
|
document.body.classList.add("mobile-optimized");
|
|
|
|
|
|
|
|
|
|
// Override hover states on touch devices
|
|
|
|
|
if (window.matchMedia("(hover: none)").matches) {
|
|
|
|
|
const style = document.createElement("style");
|
|
|
|
|
style.id = "mobile-touch-override";
|
|
|
|
|
style.textContent = `
|
|
|
|
|
/* Remove hover delays on touch devices */
|
|
|
|
|
.product-card, .portfolio-card, .blog-card, .btn, .nav-link,
|
|
|
|
|
.filter-btn, .add-to-cart-btn, .product-action-btn, .view-project,
|
|
|
|
|
.nav-icon-btn, .social-link, .footer-links a, .inspiration-card,
|
|
|
|
|
.featured-post, .faq-question, .user-btn, .back-to-top {
|
|
|
|
|
-webkit-tap-highlight-color: transparent !important;
|
|
|
|
|
touch-action: manipulation !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Immediate visual feedback on touch */
|
|
|
|
|
.product-card:active, .portfolio-card:active, .blog-card:active,
|
|
|
|
|
.btn:active, .nav-link:active, .filter-btn:active,
|
|
|
|
|
.add-to-cart-btn:active, .product-action-btn:active {
|
|
|
|
|
transform: scale(0.98) !important;
|
|
|
|
|
opacity: 0.85 !important;
|
|
|
|
|
transition: transform 0.1s ease, opacity 0.1s ease !important;
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
document.head.appendChild(style);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fast click handling for immediate response
|
|
|
|
|
let touchStartTime = 0;
|
|
|
|
|
let touchStartX = 0;
|
|
|
|
|
let touchStartY = 0;
|
|
|
|
|
|
|
|
|
|
document.addEventListener(
|
|
|
|
|
"touchstart",
|
|
|
|
|
function (e) {
|
|
|
|
|
touchStartTime = Date.now();
|
|
|
|
|
touchStartX = e.touches[0].clientX;
|
|
|
|
|
touchStartY = e.touches[0].clientY;
|
|
|
|
|
},
|
|
|
|
|
{ passive: true },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
document.addEventListener(
|
|
|
|
|
"touchend",
|
|
|
|
|
function (e) {
|
|
|
|
|
const touchEndTime = Date.now();
|
|
|
|
|
const touchDuration = touchEndTime - touchStartTime;
|
|
|
|
|
|
|
|
|
|
// Only process quick taps (not long presses)
|
|
|
|
|
if (touchDuration < 300) {
|
|
|
|
|
const touchEndX = e.changedTouches[0].clientX;
|
|
|
|
|
const touchEndY = e.changedTouches[0].clientY;
|
|
|
|
|
|
|
|
|
|
// Check if it's a tap (not a swipe)
|
|
|
|
|
const deltaX = Math.abs(touchEndX - touchStartX);
|
|
|
|
|
const deltaY = Math.abs(touchEndY - touchStartY);
|
|
|
|
|
|
|
|
|
|
if (deltaX < 10 && deltaY < 10) {
|
|
|
|
|
const target = e.target.closest(
|
|
|
|
|
"a, button, [data-bs-toggle], .product-card, .portfolio-card, .blog-card, .nav-link, .btn, .filter-btn, .add-to-cart-btn",
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (target && !target.disabled) {
|
|
|
|
|
// Add immediate visual feedback
|
|
|
|
|
target.style.transform = "scale(0.95)";
|
|
|
|
|
target.style.transition = "transform 0.1s ease";
|
|
|
|
|
|
|
|
|
|
// Reset after short delay
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
target.style.transform = "";
|
|
|
|
|
target.style.transition = "";
|
|
|
|
|
}, 100);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ passive: false },
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
})();
|
2026-01-19 01:17:43 -06:00
|
|
|
|
|
|
|
|
// Load and display social media links in footer
|
|
|
|
|
(function loadFooterSocialLinks() {
|
|
|
|
|
document.addEventListener("DOMContentLoaded", async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch("/api/settings");
|
|
|
|
|
if (!response.ok) return;
|
|
|
|
|
|
|
|
|
|
const settings = await response.json();
|
|
|
|
|
|
|
|
|
|
// Map of social platform to element ID and URL format
|
|
|
|
|
const socialMap = {
|
|
|
|
|
socialFacebook: { id: "footerFacebook", url: (v) => v },
|
|
|
|
|
socialInstagram: { id: "footerInstagram", url: (v) => v },
|
|
|
|
|
socialTwitter: { id: "footerTwitter", url: (v) => v },
|
|
|
|
|
socialYoutube: { id: "footerYoutube", url: (v) => v },
|
|
|
|
|
socialPinterest: { id: "footerPinterest", url: (v) => v },
|
|
|
|
|
socialTiktok: { id: "footerTiktok", url: (v) => v },
|
|
|
|
|
socialWhatsapp: {
|
|
|
|
|
id: "footerWhatsapp",
|
|
|
|
|
url: (v) =>
|
|
|
|
|
v.startsWith("http")
|
|
|
|
|
? v
|
|
|
|
|
: `https://wa.me/${v.replace(/[^0-9]/g, "")}`,
|
|
|
|
|
},
|
|
|
|
|
socialLinkedin: { id: "footerLinkedin", url: (v) => v },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (const [key, config] of Object.entries(socialMap)) {
|
|
|
|
|
const value = settings[key];
|
|
|
|
|
const el = document.getElementById(config.id);
|
|
|
|
|
|
|
|
|
|
if (el && value && value.trim()) {
|
|
|
|
|
el.href = config.url(value.trim());
|
|
|
|
|
el.target = "_blank";
|
|
|
|
|
el.rel = "noopener noreferrer";
|
|
|
|
|
el.style.display = "";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log("Could not load social links:", error);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
})();
|