webupdate
This commit is contained in:
870
website/assets/js/modern-theme.js
Normal file
870
website/assets/js/modern-theme.js
Normal file
@@ -0,0 +1,870 @@
|
||||
/* ============================================
|
||||
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");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Mobile menu toggle
|
||||
if (mobileToggle && navMenu) {
|
||||
mobileToggle.addEventListener("click", () => {
|
||||
navMenu.classList.toggle("open");
|
||||
mobileToggle.classList.toggle("active");
|
||||
});
|
||||
|
||||
// Close menu when clicking a link
|
||||
navMenu.querySelectorAll(".nav-link").forEach((link) => {
|
||||
link.addEventListener("click", () => {
|
||||
navMenu.classList.remove("open");
|
||||
mobileToggle.classList.remove("active");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 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>
|
||||
`;
|
||||
},
|
||||
};
|
||||
|
||||
// Export for global use
|
||||
window.SkyArtShop = SkyArtShop;
|
||||
window.API = API;
|
||||
window.ProductRenderer = ProductRenderer;
|
||||
window.BlogRenderer = BlogRenderer;
|
||||
window.PortfolioRenderer = PortfolioRenderer;
|
||||
Reference in New Issue
Block a user