mirror of
https://git.kh3group.com/georgebiri/khy_website.git
synced 2026-07-02 06:13:30 +00:00
All checks were successful
continuous-integration/drone/push Build is passing
- Added CSS styles for lazy loading images, improving initial load times and user experience. - Updated ProductManager class to initialize lazy loading functionality, utilizing Intersection Observer for efficient image loading as they come into view. - Modified image elements in product catalog to support lazy loading with appropriate data attributes.
581 lines
18 KiB
JavaScript
581 lines
18 KiB
JavaScript
// Product Management System
|
||
class ProductManager {
|
||
constructor() {
|
||
this.products = [];
|
||
this.categories = [];
|
||
this.pagination = {};
|
||
this.currentPage = 1;
|
||
this.itemsPerPage = 16;
|
||
this.filteredProducts = [];
|
||
this.selectedCategories = new Set();
|
||
}
|
||
|
||
// Load products from JSON file
|
||
async loadProducts() {
|
||
try {
|
||
const response = await fetch("/data/products.json");
|
||
const data = await response.json();
|
||
|
||
this.products = data.products;
|
||
this.categories = data.categories;
|
||
this.pagination = data.pagination;
|
||
this.filteredProducts = [...this.products];
|
||
|
||
this.renderProducts();
|
||
this.updatePagination();
|
||
this.updateResultsCount();
|
||
this.setupEventListeners();
|
||
this.renderCategoryFilters();
|
||
|
||
// Check for URL parameters and pre-select category
|
||
this.handleUrlParameters();
|
||
} catch (error) {
|
||
console.error("Error loading products:", error);
|
||
}
|
||
}
|
||
|
||
// Handle URL parameters for pre-selecting category filters
|
||
handleUrlParameters() {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const category = urlParams.get("category");
|
||
|
||
if (category) {
|
||
// Pre-select the category in the filter
|
||
this.selectedCategories = new Set([category]);
|
||
this.applyFilters();
|
||
this.currentPage = 1;
|
||
this.renderProducts();
|
||
this.updatePagination();
|
||
this.updateResultsCount();
|
||
|
||
// Update the UI to show the filter is active
|
||
this.updateFilterUI(category);
|
||
}
|
||
}
|
||
|
||
// Update filter UI to show selected category
|
||
updateFilterUI(categoryId) {
|
||
// Find the category object to get the display name
|
||
const category = this.categories.find((c) => c.id === categoryId);
|
||
const displayName = category ? category.name : categoryId;
|
||
|
||
// Update filter button text to show active filter
|
||
const filterToggle = document.getElementById("filter-toggle");
|
||
if (filterToggle) {
|
||
const filterText = filterToggle.querySelector("span:last-child");
|
||
if (filterText) {
|
||
filterText.textContent = `Filter: ${displayName}`;
|
||
}
|
||
}
|
||
|
||
// Check the corresponding checkbox in the dropdown
|
||
setTimeout(() => {
|
||
const checkboxes = document.querySelectorAll(".category-checkbox");
|
||
checkboxes.forEach((checkbox) => {
|
||
if (checkbox.value === categoryId) {
|
||
checkbox.checked = true;
|
||
}
|
||
});
|
||
}, 100);
|
||
}
|
||
|
||
// Update filter button text based on selected categories
|
||
updateFilterButtonText() {
|
||
const filterToggle = document.getElementById("filter-toggle");
|
||
if (!filterToggle) return;
|
||
|
||
const filterText = filterToggle.querySelector("span:last-child");
|
||
if (!filterText) return;
|
||
|
||
// If no categories selected or "all" is selected, show default text
|
||
if (
|
||
this.selectedCategories.size === 0 ||
|
||
this.selectedCategories.has("all")
|
||
) {
|
||
filterText.textContent = "Filter";
|
||
return;
|
||
}
|
||
|
||
// Get the first selected category and find its display name
|
||
const firstSelectedCategory = Array.from(this.selectedCategories)[0];
|
||
const category = this.categories.find(
|
||
(c) => c.id === firstSelectedCategory
|
||
);
|
||
|
||
if (category) {
|
||
filterText.textContent = `Filter: ${category.name}`;
|
||
} else {
|
||
filterText.textContent = "Filter";
|
||
}
|
||
}
|
||
|
||
// Render products in the grid
|
||
renderProducts() {
|
||
const productGrid = document.getElementById("product-grid");
|
||
if (!productGrid) return;
|
||
|
||
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
|
||
const endIndex = startIndex + this.itemsPerPage;
|
||
const productsToShow = this.filteredProducts.slice(startIndex, endIndex);
|
||
|
||
productGrid.innerHTML = productsToShow
|
||
.map((product) => this.createProductCard(product))
|
||
.join("");
|
||
this.updateResultsCount();
|
||
this.updatePagination();
|
||
|
||
// Re-add image enlargement listeners and lazy loading after products are rendered
|
||
setTimeout(() => {
|
||
addImageEnlargementListeners();
|
||
this.initLazyLoading();
|
||
}, 50);
|
||
}
|
||
|
||
// Create individual product card HTML
|
||
createProductCard(product) {
|
||
// Check if we're in comparison mode
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const returnTo = urlParams.get("returnTo");
|
||
const isComparisonMode = returnTo === "comparison";
|
||
|
||
return `
|
||
<div class="group relative bg-light-bg rounded-lg overflow-hidden hover:shadow-lg transition-shadow product-card" data-product-id="${
|
||
product.id
|
||
}">
|
||
<div class="relative h-80 overflow-hidden">
|
||
<img
|
||
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='320' viewBox='0 0 400 320'%3E%3Crect width='400' height='320' fill='%23f3f4f6'/%3E%3C/svg%3E"
|
||
data-src="${product.image}"
|
||
alt="${product.alt}"
|
||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300 cursor-pointer lazy-load"
|
||
data-enlarge-src="${product.image}"
|
||
loading="lazy"
|
||
/>
|
||
<!-- Hover Overlay -->
|
||
<div class="absolute inset-0 bg-dark-charcoal bg-opacity-70 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
|
||
<div class="text-center">
|
||
<button
|
||
class="bg-white text-uc-gold font-poppins font-semibold px-8 py-3 rounded-md hover:bg-uc-gold hover:text-white transition-colors ${
|
||
isComparisonMode ? "cursor-pointer" : ""
|
||
}"
|
||
onclick="productManager.viewProduct(${product.id})"
|
||
>
|
||
${isComparisonMode ? "Add to Comparison" : "View"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="p-6">
|
||
<h3 class="font-poppins font-semibold text-2xl text-dark-charcoal mb-2">
|
||
${product.name}
|
||
</h3>
|
||
<p class="font-poppins font-medium text-base text-quick-silver">
|
||
${product.description}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Filter products by category
|
||
filterByCategory(category) {
|
||
// Single category helper (not used directly by UI)
|
||
this.selectedCategories = new Set([category]);
|
||
this.applyFilters();
|
||
this.currentPage = 1;
|
||
this.renderProducts();
|
||
this.updatePagination();
|
||
}
|
||
|
||
// Search products
|
||
searchProducts(query) {
|
||
if (!query.trim()) {
|
||
this.filteredProducts = [...this.products];
|
||
} else {
|
||
this.filteredProducts = this.products.filter(
|
||
(product) =>
|
||
product.name.toLowerCase().includes(query.toLowerCase()) ||
|
||
product.description.toLowerCase().includes(query.toLowerCase())
|
||
);
|
||
}
|
||
this.currentPage = 1;
|
||
this.renderProducts();
|
||
this.updatePagination();
|
||
}
|
||
|
||
// Apply selected category filters
|
||
applyFilters() {
|
||
if (
|
||
this.selectedCategories.size === 0 ||
|
||
this.selectedCategories.has("all")
|
||
) {
|
||
this.filteredProducts = [...this.products];
|
||
return;
|
||
}
|
||
this.filteredProducts = this.products.filter((product) =>
|
||
this.selectedCategories.has(product.category)
|
||
);
|
||
}
|
||
|
||
// Sort products
|
||
sortProducts(sortBy) {
|
||
switch (sortBy) {
|
||
case "name-asc":
|
||
this.filteredProducts.sort((a, b) => a.name.localeCompare(b.name));
|
||
break;
|
||
case "name-desc":
|
||
this.filteredProducts.sort((a, b) => b.name.localeCompare(a.name));
|
||
break;
|
||
default:
|
||
// Default sorting by ID
|
||
this.filteredProducts.sort((a, b) => a.id - b.id);
|
||
}
|
||
this.renderProducts();
|
||
}
|
||
|
||
// Change page
|
||
changePage(page) {
|
||
this.currentPage = page;
|
||
this.renderProducts();
|
||
this.updatePagination();
|
||
}
|
||
|
||
// Update pagination controls
|
||
updatePagination() {
|
||
const totalPages = Math.ceil(
|
||
this.filteredProducts.length / this.itemsPerPage
|
||
);
|
||
const paginationContainer = document.getElementById("pagination");
|
||
|
||
if (!paginationContainer) return;
|
||
|
||
let paginationHTML = "";
|
||
|
||
for (let i = 1; i <= totalPages; i++) {
|
||
const isActive = i === this.currentPage;
|
||
paginationHTML += `
|
||
<button
|
||
class="w-20 h-15 ${
|
||
isActive
|
||
? "bg-uc-gold text-white"
|
||
: "bg-floral-white text-black hover:bg-uc-gold hover:text-white"
|
||
} font-poppins font-normal text-xl rounded-lg flex items-center justify-center transition-colors"
|
||
onclick="productManager.changePage(${i})"
|
||
>
|
||
${i}
|
||
</button>
|
||
`;
|
||
}
|
||
|
||
if (totalPages > 1 && this.currentPage < totalPages) {
|
||
paginationHTML += `
|
||
<button
|
||
class="w-28 h-15 bg-floral-white text-black font-poppins font-light text-xl rounded-lg flex items-center justify-center hover:bg-uc-gold hover:text-white transition-colors"
|
||
onclick="productManager.changePage(${this.currentPage + 1})"
|
||
>
|
||
Next
|
||
</button>
|
||
`;
|
||
}
|
||
|
||
paginationContainer.innerHTML = paginationHTML;
|
||
}
|
||
|
||
// Build category multi-select dropdown
|
||
renderCategoryFilters() {
|
||
const container = document.getElementById("filter-categories");
|
||
if (!container) return;
|
||
|
||
const categoryOptions = this.categories
|
||
.map(
|
||
(c) => `
|
||
<label class="flex items-center gap-2 cursor-pointer">
|
||
<input type="checkbox" value="${c.id}" class="category-checkbox category-specific">
|
||
<span class="font-poppins text-sm text-black">${c.name}</span>
|
||
</label>
|
||
`
|
||
)
|
||
.join("");
|
||
|
||
const allOption = `
|
||
<label class="flex items-center gap-2 cursor-pointer">
|
||
<input type="checkbox" value="all" class="category-checkbox category-all">
|
||
<span class="font-poppins text-sm text-black">All</span>
|
||
</label>
|
||
`;
|
||
|
||
container.innerHTML = allOption + categoryOptions;
|
||
|
||
// Add event listeners for "All" checkbox behavior
|
||
const allCheckbox = container.querySelector(".category-all");
|
||
const specificCheckboxes = container.querySelectorAll(".category-specific");
|
||
|
||
if (allCheckbox) {
|
||
allCheckbox.addEventListener("change", (e) => {
|
||
const isChecked = e.target.checked;
|
||
specificCheckboxes.forEach((checkbox) => {
|
||
checkbox.checked = isChecked;
|
||
});
|
||
});
|
||
}
|
||
|
||
// Update "All" checkbox when specific categories change
|
||
specificCheckboxes.forEach((checkbox) => {
|
||
checkbox.addEventListener("change", () => {
|
||
const allChecked = Array.from(specificCheckboxes).every(
|
||
(c) => c.checked
|
||
);
|
||
const anyChecked = Array.from(specificCheckboxes).some(
|
||
(c) => c.checked
|
||
);
|
||
|
||
if (allChecked) {
|
||
allCheckbox.checked = true;
|
||
} else if (!anyChecked) {
|
||
allCheckbox.checked = false;
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// View product details
|
||
viewProduct(productId) {
|
||
const product = this.products.find((p) => p.id === productId);
|
||
if (product) {
|
||
// Check if we're in comparison mode
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const returnTo = urlParams.get("returnTo");
|
||
const slot = urlParams.get("slot");
|
||
const product1Id = urlParams.get("product1");
|
||
const product2Id = urlParams.get("product2");
|
||
|
||
if (returnTo === "comparison" && slot) {
|
||
// Navigate back to comparison page with the selected product
|
||
let comparisonUrl = "product-comparison.html?";
|
||
|
||
if (slot === "1") {
|
||
// Replace product 1
|
||
comparisonUrl += `product1=${productId}`;
|
||
if (product2Id) {
|
||
comparisonUrl += `&product2=${product2Id}`;
|
||
}
|
||
} else {
|
||
// Replace product 2
|
||
if (product1Id) {
|
||
comparisonUrl += `product1=${product1Id}&`;
|
||
}
|
||
comparisonUrl += `product2=${productId}`;
|
||
}
|
||
|
||
console.log("Navigating to comparison page:", comparisonUrl);
|
||
window.location.href = comparisonUrl;
|
||
} else {
|
||
// Normal mode - navigate to product detail page
|
||
window.location.href = `product-detail.html?id=${productId}`;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Setup event listeners
|
||
setupEventListeners() {
|
||
// Filter dropdown toggle and outside click
|
||
const filterToggle = document.getElementById("filter-toggle");
|
||
const filterDropdown = document.getElementById("filter-dropdown");
|
||
if (filterToggle && filterDropdown) {
|
||
filterToggle.addEventListener("click", () => {
|
||
filterDropdown.classList.toggle("hidden");
|
||
});
|
||
document.addEventListener("click", (e) => {
|
||
if (
|
||
!filterDropdown.contains(e.target) &&
|
||
!filterToggle.contains(e.target)
|
||
) {
|
||
filterDropdown.classList.add("hidden");
|
||
}
|
||
});
|
||
}
|
||
|
||
// Sort dropdown
|
||
const sortSelect = document.querySelector("select");
|
||
if (sortSelect) {
|
||
sortSelect.addEventListener("change", (e) => {
|
||
this.sortProducts(e.target.value);
|
||
});
|
||
}
|
||
|
||
// Apply/clear category filters
|
||
const applyBtn = document.getElementById("filter-apply");
|
||
const clearBtn = document.getElementById("filter-clear");
|
||
if (applyBtn) {
|
||
applyBtn.addEventListener("click", () => {
|
||
const checks = Array.from(
|
||
document.querySelectorAll(".category-checkbox")
|
||
);
|
||
this.selectedCategories = new Set(
|
||
checks.filter((c) => c.checked).map((c) => c.value)
|
||
);
|
||
this.currentPage = 1;
|
||
this.applyFilters();
|
||
this.renderProducts();
|
||
this.updatePagination();
|
||
this.updateResultsCount();
|
||
|
||
// Update filter button text to show selected category
|
||
this.updateFilterButtonText();
|
||
|
||
const dropdown = document.getElementById("filter-dropdown");
|
||
if (dropdown) dropdown.classList.add("hidden");
|
||
});
|
||
}
|
||
if (clearBtn) {
|
||
clearBtn.addEventListener("click", () => {
|
||
const checks = Array.from(
|
||
document.querySelectorAll(".category-checkbox")
|
||
);
|
||
checks.forEach((c) => (c.checked = false));
|
||
this.selectedCategories.clear();
|
||
this.currentPage = 1;
|
||
this.applyFilters();
|
||
this.renderProducts();
|
||
this.updatePagination();
|
||
this.updateResultsCount();
|
||
|
||
// Update filter button text to show default
|
||
this.updateFilterButtonText();
|
||
});
|
||
}
|
||
|
||
// Initialize lazy loading
|
||
this.initLazyLoading();
|
||
}
|
||
|
||
// Initialize lazy loading for images
|
||
initLazyLoading() {
|
||
// Use Intersection Observer for better performance
|
||
if ("IntersectionObserver" in window) {
|
||
const imageObserver = new IntersectionObserver(
|
||
(entries, observer) => {
|
||
entries.forEach((entry) => {
|
||
if (entry.isIntersecting) {
|
||
const img = entry.target;
|
||
if (img.dataset.src) {
|
||
img.src = img.dataset.src;
|
||
img.classList.remove("lazy-load");
|
||
img.classList.add("loaded");
|
||
observer.unobserve(img);
|
||
}
|
||
}
|
||
});
|
||
},
|
||
{
|
||
rootMargin: "50px 0px", // Start loading 50px before image comes into view
|
||
threshold: 0.01,
|
||
}
|
||
);
|
||
|
||
// Observe all lazy-loaded images
|
||
const lazyImages = document.querySelectorAll(".lazy-load");
|
||
lazyImages.forEach((img) => imageObserver.observe(img));
|
||
} else {
|
||
// Fallback for older browsers - load all images immediately
|
||
const lazyImages = document.querySelectorAll(".lazy-load");
|
||
lazyImages.forEach((img) => {
|
||
if (img.dataset.src) {
|
||
img.src = img.dataset.src;
|
||
img.classList.remove("lazy-load");
|
||
img.classList.add("loaded");
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Update results count
|
||
updateResultsCount() {
|
||
const resultsElement = document.querySelector(".text-quick-silver");
|
||
if (resultsElement) {
|
||
const startIndex = (this.currentPage - 1) * this.itemsPerPage + 1;
|
||
const endIndex = Math.min(
|
||
startIndex + this.itemsPerPage - 1,
|
||
this.filteredProducts.length
|
||
);
|
||
resultsElement.textContent = `Showing ${startIndex}–${endIndex} of ${this.filteredProducts.length} results`;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Initialize product manager
|
||
const productManager = new ProductManager();
|
||
|
||
// Image enlargement modal functionality
|
||
function addImageEnlargementListeners() {
|
||
const productImages = document.querySelectorAll(
|
||
"#product-grid img[data-enlarge-src]"
|
||
);
|
||
const modal = document.getElementById("image-modal");
|
||
const modalImage = document.getElementById("modal-image");
|
||
const modalCloseBtn = document.getElementById("modal-close-btn");
|
||
|
||
if (!modal || !modalImage || !modalCloseBtn) return;
|
||
|
||
productImages.forEach((img) => {
|
||
img.addEventListener("click", function (e) {
|
||
e.preventDefault(); // Prevent any default behavior
|
||
e.stopPropagation(); // Stop event bubbling to parent elements
|
||
|
||
const imageSrc = this.getAttribute("data-enlarge-src");
|
||
const imageAlt = this.getAttribute("alt");
|
||
|
||
modalImage.src = imageSrc;
|
||
modalImage.alt = imageAlt;
|
||
|
||
// Show modal with animation
|
||
modal.classList.remove("hidden");
|
||
document.body.style.overflow = "hidden"; // Prevent background scrolling
|
||
|
||
// Trigger animation after a brief delay
|
||
setTimeout(() => {
|
||
modalImage.classList.remove("scale-95");
|
||
modalImage.classList.add("scale-100");
|
||
}, 10);
|
||
});
|
||
});
|
||
|
||
// Close modal functionality
|
||
function closeModal() {
|
||
// Animate out
|
||
modalImage.classList.remove("scale-100");
|
||
modalImage.classList.add("scale-95");
|
||
|
||
// Hide modal after animation
|
||
setTimeout(() => {
|
||
modal.classList.add("hidden");
|
||
document.body.style.overflow = ""; // Restore scrolling
|
||
}, 300);
|
||
}
|
||
|
||
modalCloseBtn.addEventListener("click", closeModal);
|
||
|
||
// Close modal when clicking outside the image
|
||
modal.addEventListener("click", function (e) {
|
||
if (e.target === modal) {
|
||
closeModal();
|
||
}
|
||
});
|
||
|
||
// Close modal with Escape key
|
||
document.addEventListener("keydown", function (e) {
|
||
if (e.key === "Escape" && !modal.classList.contains("hidden")) {
|
||
closeModal();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Load products when DOM is ready
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
productManager.loadProducts();
|
||
|
||
// Add image enlargement listeners after products are loaded
|
||
setTimeout(() => {
|
||
addImageEnlargementListeners();
|
||
}, 100);
|
||
});
|