Files
SkyArtShop/website/assets/js/shopping.js
Local Server 1919f6f8bb updateweb
2026-01-01 22:24:30 -06:00

307 lines
9.1 KiB
JavaScript

/**
* Shopping/Products Component
* Handles product display, filtering, and interactions
*/
(function () {
"use strict";
class ShoppingPage {
constructor() {
this.productsContainer = document.getElementById("productsContainer");
this.loadingIndicator = document.getElementById("loadingIndicator");
this.errorContainer = document.getElementById("errorContainer");
this.currentCategory = window.Utils.getUrlParameter("category") || "all";
this.currentSort = "newest";
this.products = [];
this.init();
}
async init() {
this.setupEventListeners();
await this.loadProducts();
}
setupEventListeners() {
// Category filters
document.querySelectorAll("[data-category]").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
this.currentCategory = e.currentTarget.dataset.category;
this.filterProducts();
});
});
// Sort dropdown
const sortSelect = document.getElementById("sortSelect");
if (sortSelect) {
sortSelect.addEventListener("change", (e) => {
this.currentSort = e.target.value;
this.filterProducts();
});
}
// Search
const searchInput = document.getElementById("productSearch");
if (searchInput) {
searchInput.addEventListener(
"input",
window.Utils.debounce((e) => {
this.searchProducts(e.target.value);
}, 300)
);
}
}
async loadProducts() {
if (!this.productsContainer) return;
try {
this.showLoading();
const response = await window.API.getProducts();
this.products = response.products || response.data || [];
this.renderProducts(this.products);
this.hideLoading();
} catch (error) {
console.error("Error loading products:", error);
this.showError("Failed to load products. Please try again later.");
this.hideLoading();
}
}
filterProducts() {
let filtered = [...this.products];
// Filter by category
if (this.currentCategory && this.currentCategory !== "all") {
filtered = filtered.filter(
(p) =>
p.category?.toLowerCase() === this.currentCategory.toLowerCase()
);
}
// Sort products
filtered = this.sortProducts(filtered);
this.renderProducts(filtered);
}
sortProducts(products) {
switch (this.currentSort) {
case "price-low":
return products.sort((a, b) => (a.price || 0) - (b.price || 0));
case "price-high":
return products.sort((a, b) => (b.price || 0) - (a.price || 0));
case "name":
return products.sort((a, b) =>
(a.title || a.name || "").localeCompare(b.title || b.name || "")
);
case "newest":
default:
return products.sort(
(a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0)
);
}
}
searchProducts(query) {
if (!query.trim()) {
this.filterProducts();
return;
}
const searchTerm = query.toLowerCase();
const filtered = this.products.filter((p) => {
const title = (p.title || p.name || "").toLowerCase();
const description = (p.description || "").toLowerCase();
const category = (p.category || "").toLowerCase();
return (
title.includes(searchTerm) ||
description.includes(searchTerm) ||
category.includes(searchTerm)
);
});
this.renderProducts(filtered);
}
renderProducts(products) {
if (!this.productsContainer) return;
if (products.length === 0) {
this.productsContainer.innerHTML = `
<div class="no-products">
<i class="bi bi-inbox" style="font-size: 48px; opacity: 0.5;"></i>
<p>No products found</p>
</div>
`;
return;
}
const html = products
.map((product) => this.renderProductCard(product))
.join("");
this.productsContainer.innerHTML = html;
// Setup product card listeners
this.setupProductListeners();
}
renderProductCard(product) {
const id = product.id;
const title = window.Utils?.escapeHtml
? window.Utils.escapeHtml(product.title || product.name || "Product")
: product.title || product.name || "Product";
const price = window.Utils?.formatCurrency
? window.Utils.formatCurrency(product.price || 0)
: `$${parseFloat(product.price || 0).toFixed(2)}`;
// Get image URL from multiple possible sources
let imageUrl = "/assets/images/placeholder.jpg";
if (
product.images &&
Array.isArray(product.images) &&
product.images.length > 0
) {
const primaryImg = product.images.find((img) => img.is_primary);
imageUrl = primaryImg
? primaryImg.image_url
: product.images[0].image_url;
} else if (product.imageUrl) {
imageUrl = product.imageUrl;
} else if (product.image_url) {
imageUrl = product.image_url;
}
// Get description
const description =
product.shortdescription ||
(product.description
? product.description.substring(0, 100) + "..."
: "");
const isInWishlist = window.AppState?.isInWishlist(id) || false;
return `
<article class="product-card" data-id="${id}">
<div class="product-image-wrapper">
<img src="${imageUrl}" alt="${title}" class="product-image" loading="lazy" onerror="this.src='/assets/images/placeholder.svg'">
<button
class="wishlist-btn ${isInWishlist ? "active" : ""}"
data-id="${id}"
aria-label="${
isInWishlist ? "Remove from wishlist" : "Add to wishlist"
}"
>
<i class="bi bi-heart${isInWishlist ? "-fill" : ""}"></i>
</button>
</div>
<div class="product-info">
<a href="/product?id=${id}" style="text-decoration: none; color: inherit;">
<h3 class="product-title">${title}</h3>
</a>
${
description
? `<div class="product-description">${description}</div>`
: ""
}
<p class="product-price">${price}</p>
<div class="product-actions">
<button class="btn-add-to-cart" data-id="${id}" style="flex: 1;">
<i class="bi bi-cart-plus"></i> Add to Cart
</button>
</div>
</div>
</article>
`;
}
setupProductListeners() {
// Add to cart buttons
this.productsContainer
.querySelectorAll(".btn-add-to-cart")
.forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
const id = parseInt(e.currentTarget.dataset.id);
const product = this.products.find((p) => p.id === id);
if (product) {
window.AppState.addToCart(product);
}
});
});
// Wishlist buttons
this.productsContainer
.querySelectorAll(".wishlist-btn")
.forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
const id = parseInt(e.currentTarget.dataset.id);
const product = this.products.find((p) => p.id === id);
if (product) {
if (window.AppState.isInWishlist(id)) {
window.AppState.removeFromWishlist(id);
} else {
window.AppState.addToWishlist(product);
}
this.renderProducts(this.products);
}
});
});
}
showLoading() {
if (this.loadingIndicator) {
this.loadingIndicator.style.display = "flex";
}
if (this.productsContainer) {
this.productsContainer.style.opacity = "0.5";
}
}
hideLoading() {
if (this.loadingIndicator) {
this.loadingIndicator.style.display = "none";
}
if (this.productsContainer) {
this.productsContainer.style.opacity = "1";
}
}
showError(message) {
if (this.errorContainer) {
this.errorContainer.innerHTML = `
<div class="error-message" role="alert">
<i class="bi bi-exclamation-triangle"></i>
<p>${window.Utils.escapeHtml(message)}</p>
<button onclick="location.reload()">Retry</button>
</div>
`;
this.errorContainer.style.display = "block";
}
}
}
// Initialize on shop/products pages
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
if (
window.location.pathname.includes("/shop") ||
window.location.pathname.includes("/products")
) {
new ShoppingPage();
}
});
} else {
if (
window.location.pathname.includes("/shop") ||
window.location.pathname.includes("/products")
) {
new ShoppingPage();
}
}
})();