Files

871 lines
25 KiB
JavaScript
Raw Permalink Normal View History

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");
}
});
}
// 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;