307 lines
9.1 KiB
JavaScript
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.ShopSystem?.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.ShopSystem.addToCart(product, 1);
|
|
}
|
|
});
|
|
});
|
|
|
|
// 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.ShopSystem.isInWishlist(id)) {
|
|
window.ShopSystem.removeFromWishlist(id);
|
|
} else {
|
|
window.ShopSystem.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();
|
|
}
|
|
}
|
|
})();
|