mirror of
https://git.kh3group.com/georgebiri/khy_website.git
synced 2026-07-02 07:03:33 +00:00
feat: add product catalog and product detail pages
This commit is contained in:
parent
9a3a106fca
commit
0d5fd84762
13 changed files with 4256 additions and 555 deletions
513
scripts/main.js
513
scripts/main.js
|
|
@ -1,5 +1,5 @@
|
|||
// Update year in footer and handle smooth scrolling
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
function initSite() {
|
||||
// Update footer year
|
||||
const footer = document.querySelector("footer p");
|
||||
if (footer) {
|
||||
|
|
@ -9,81 +9,148 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
|
||||
// Smooth scrolling for navigation links
|
||||
const navLinks = document.querySelectorAll('.nav-link[href^="#"]');
|
||||
|
||||
navLinks.forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const targetId = this.getAttribute("href").substring(1);
|
||||
const targetElement = document.getElementById(targetId);
|
||||
|
||||
if (targetElement) {
|
||||
// Calculate offset for header height (112px = 28 * 4)
|
||||
const headerHeight = 112;
|
||||
const targetPosition = targetElement.offsetTop - headerHeight;
|
||||
|
||||
window.scrollTo({
|
||||
top: targetPosition,
|
||||
behavior: "smooth",
|
||||
});
|
||||
window.scrollTo({ top: targetPosition, behavior: "smooth" });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle cross-page navigation links (links that point to other pages with anchors)
|
||||
const crossPageLinks = document.querySelectorAll('.nav-link[href*=".html#"]');
|
||||
|
||||
crossPageLinks.forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
// Don't prevent default - let the browser handle the navigation
|
||||
// The smooth scrolling will work on the target page
|
||||
link.addEventListener("click", function () {
|
||||
// allow default navigation
|
||||
});
|
||||
});
|
||||
|
||||
// Highlight current page link
|
||||
const currentPage = window.location.pathname.split("/").pop() || "index.html";
|
||||
const currentPageLink = document.querySelector(
|
||||
`.nav-link[href="${currentPage}"]`
|
||||
);
|
||||
if (currentPageLink) {
|
||||
currentPageLink.classList.add("active");
|
||||
}
|
||||
// Remove any lingering .active classes and avoid adding new ones
|
||||
const allNavLinks = document.querySelectorAll(".nav-link");
|
||||
allNavLinks.forEach((a) => a.classList.remove("active"));
|
||||
|
||||
// Active link highlighting on scroll
|
||||
const sections = document.querySelectorAll("section[id]");
|
||||
const navItems = document.querySelectorAll('.nav-link[href^="#"]');
|
||||
// Do not add page-specific active state anymore
|
||||
// const currentPage = window.location.pathname.split("/").pop() || "index.html";
|
||||
// const currentPageLink = document.querySelector(`.nav-link[href="${currentPage}"]`);
|
||||
// if (currentPageLink) currentPageLink.classList.add("active");
|
||||
|
||||
// Disable scroll-based active highlighting
|
||||
// (Keeping function for potential future use, but it does nothing now.)
|
||||
function updateActiveLink() {
|
||||
const scrollPosition = window.scrollY + 150; // Offset for better detection
|
||||
// no-op: active highlighting disabled globally
|
||||
}
|
||||
window.addEventListener("scroll", updateActiveLink);
|
||||
updateActiveLink();
|
||||
|
||||
sections.forEach((section) => {
|
||||
const sectionTop = section.offsetTop;
|
||||
const sectionHeight = section.offsetHeight;
|
||||
const sectionId = section.getAttribute("id");
|
||||
// Quantity controls on product detail page
|
||||
(function initQuantityControls() {
|
||||
const decr = document.getElementById("qty-decr");
|
||||
const incr = document.getElementById("qty-incr");
|
||||
const valueEl = document.getElementById("qty-value");
|
||||
if (!decr || !incr || !valueEl) return; // Not on product detail page
|
||||
|
||||
if (
|
||||
scrollPosition >= sectionTop &&
|
||||
scrollPosition < sectionTop + sectionHeight
|
||||
) {
|
||||
// Remove active class from all nav links
|
||||
navItems.forEach((item) => item.classList.remove("active"));
|
||||
function parseQty() {
|
||||
const n = parseInt(valueEl.textContent || "1", 10);
|
||||
return Number.isFinite(n) && n > 0 ? n : 1;
|
||||
}
|
||||
decr.addEventListener("click", () => {
|
||||
const next = Math.max(1, parseQty() - 1);
|
||||
valueEl.textContent = String(next);
|
||||
});
|
||||
incr.addEventListener("click", () => {
|
||||
const next = parseQty() + 1;
|
||||
valueEl.textContent = String(next);
|
||||
});
|
||||
})();
|
||||
|
||||
// Add active class to corresponding nav link
|
||||
const activeLink = document.querySelector(
|
||||
`.nav-link[href="#${sectionId}"]`
|
||||
);
|
||||
if (activeLink) {
|
||||
activeLink.classList.add("active");
|
||||
// Related products (dynamic)
|
||||
(async function initRelated() {
|
||||
const grid = document.getElementById("related-grid");
|
||||
if (!grid) return;
|
||||
|
||||
// Don't run this function on product detail pages - let the product detail function handle it
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const productId = urlParams.get("id");
|
||||
if (productId) return;
|
||||
|
||||
try {
|
||||
const res = await fetch("data/products.json", { cache: "no-store" });
|
||||
const data = await res.json();
|
||||
const products = Array.isArray(data.products) ? data.products : [];
|
||||
|
||||
// Determine current product's category from the title text (fallback seating)
|
||||
const title = document.querySelector("h1");
|
||||
const currentName = (title?.textContent || "").trim();
|
||||
const current = products.find(
|
||||
(p) => (p.name || "").trim() === currentName
|
||||
);
|
||||
const category = current?.category || "seating";
|
||||
|
||||
// Filter related: same category but not the current product
|
||||
const related = products.filter(
|
||||
(p) => p.category === category && p.name !== currentName
|
||||
);
|
||||
|
||||
let page = 1;
|
||||
const pageSize = 4;
|
||||
|
||||
function render() {
|
||||
grid.innerHTML = "";
|
||||
const start = 0;
|
||||
const end = page * pageSize; // cumulative show-more
|
||||
related.slice(start, end).forEach((p) => {
|
||||
const card = document.createElement("article");
|
||||
card.className = "rounded-lg overflow-hidden bg-white shadow-sm";
|
||||
|
||||
// Distinct image background for Asgaard sofa to match others
|
||||
const imageBgClass =
|
||||
(p.name || "").toLowerCase() === "asgaard sofa"
|
||||
? "bg-linen"
|
||||
: "bg-white";
|
||||
const panelBgClass = "bg-light-bg";
|
||||
|
||||
card.innerHTML = `
|
||||
<div class=\"h-72 ${imageBgClass} flex items-center justify-center\">
|
||||
<img src=\"${p.image}\" alt=\"${
|
||||
p.alt || p.name
|
||||
}\" class=\"w-full h-full object-cover\" />
|
||||
</div>
|
||||
<div class=\"${panelBgClass} p-6\">
|
||||
<h3 class=\"font-poppins font-semibold text-2xl text-[#3A3A3A] mb-0\">${
|
||||
p.name
|
||||
}</h3>
|
||||
</div>`;
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
// Toggle show-more visibility
|
||||
const btn = document.getElementById("related-show-more");
|
||||
if (btn) {
|
||||
const hasMore = related.length > page * pageSize;
|
||||
btn.style.display = hasMore ? "inline-flex" : "none";
|
||||
// Ensure centered label
|
||||
btn.classList.add("inline-flex", "items-center", "justify-center");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for scroll events
|
||||
window.addEventListener("scroll", updateActiveLink);
|
||||
const btn = document.getElementById("related-show-more");
|
||||
if (btn) {
|
||||
btn.addEventListener("click", () => {
|
||||
page += 1;
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
// Initial call to set active link on page load
|
||||
updateActiveLink();
|
||||
render();
|
||||
} catch (e) {
|
||||
console.error("Failed to load related products:", e);
|
||||
}
|
||||
})();
|
||||
|
||||
// Blog search functionality (only runs on blog page)
|
||||
const blogSearchInput = document.getElementById("blog-search-input");
|
||||
|
|
@ -93,10 +160,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
"#blog-pagination .blog-page-btn"
|
||||
);
|
||||
const nextButton = document.getElementById("blog-next-btn");
|
||||
const tagButtons = document.querySelectorAll(".tag-filter");
|
||||
let activeTag = ""; // empty = no tag filter
|
||||
const blogTagButtons = document.querySelectorAll(".tag-filter");
|
||||
let activeTag = "";
|
||||
let currentPage = 1;
|
||||
const pageSize = 4; // number of posts per page
|
||||
const pageSize = 4;
|
||||
|
||||
function normalize(text) {
|
||||
return (text || "")
|
||||
|
|
@ -104,16 +171,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
.replace(/[^a-z0-9]+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function getFilteredArticles() {
|
||||
const articles = Array.from(
|
||||
document.querySelectorAll("#main-blog-content article")
|
||||
);
|
||||
// Sort by date descending if data-date is present (YYYY-MM-DD)
|
||||
articles.sort((a, b) => {
|
||||
const ad = a.getAttribute("data-date") || "";
|
||||
const bd = b.getAttribute("data-date") || "";
|
||||
// Fallback: keep original order if dates missing
|
||||
if (!ad && !bd) return 0;
|
||||
return bd.localeCompare(ad);
|
||||
});
|
||||
|
|
@ -130,26 +194,20 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
return matchesQuery && matchesTag;
|
||||
});
|
||||
}
|
||||
|
||||
function renderPage() {
|
||||
const filtered = getFilteredArticles();
|
||||
const total = filtered.length;
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
currentPage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
|
||||
// Show only items for current page
|
||||
const start = (currentPage - 1) * pageSize;
|
||||
const end = start + pageSize;
|
||||
const toShow = new Set(filtered.slice(start, end));
|
||||
document.querySelectorAll("#main-blog-content article").forEach((a) => {
|
||||
a.style.display = toShow.has(a) ? "" : "none";
|
||||
});
|
||||
|
||||
// Update pagination visibility and active state
|
||||
if (pagination) {
|
||||
const hasQuery = blogSearchInput.value.trim().length > 0;
|
||||
pagination.style.display = hasQuery ? "none" : "flex";
|
||||
|
||||
pageButtons.forEach((btn) => {
|
||||
const p = Number(btn.getAttribute("data-page"));
|
||||
btn.classList.toggle("bg-uc-gold", p === currentPage);
|
||||
|
|
@ -158,31 +216,22 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
btn.classList.toggle("text-black", p !== currentPage);
|
||||
btn.style.display = p <= totalPages ? "inline-flex" : "none";
|
||||
});
|
||||
|
||||
if (nextButton) {
|
||||
nextButton.style.display = totalPages > 1 ? "inline-flex" : "none";
|
||||
nextButton.disabled = currentPage >= totalPages;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function filterPosts() {
|
||||
// Reset to first page when filters change
|
||||
currentPage = 1;
|
||||
renderPage();
|
||||
}
|
||||
|
||||
blogSearchInput.addEventListener("input", filterPosts);
|
||||
|
||||
// Tag filtering
|
||||
tagButtons.forEach((btn) => {
|
||||
blogTagButtons.forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const clickedTag = normalize(btn.getAttribute("data-tag"));
|
||||
// Toggle logic: clicking active tag clears it
|
||||
activeTag = activeTag === clickedTag ? "" : clickedTag;
|
||||
|
||||
// Visual active state
|
||||
tagButtons.forEach((b) => {
|
||||
blogTagButtons.forEach((b) => {
|
||||
b.classList.remove("border-uc-gold", "bg-gray-50", "text-uc-gold");
|
||||
b.setAttribute("aria-pressed", "false");
|
||||
});
|
||||
|
|
@ -190,12 +239,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
btn.classList.add("border-uc-gold", "bg-gray-50", "text-uc-gold");
|
||||
btn.setAttribute("aria-pressed", "true");
|
||||
}
|
||||
|
||||
filterPosts();
|
||||
});
|
||||
});
|
||||
|
||||
// Pagination handlers
|
||||
pageButtons.forEach((btn) =>
|
||||
btn.addEventListener("click", () => {
|
||||
currentPage = Number(btn.getAttribute("data-page")) || 1;
|
||||
|
|
@ -208,7 +254,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
renderPage();
|
||||
});
|
||||
}
|
||||
// Ensure correct initial state on load
|
||||
renderPage();
|
||||
}
|
||||
|
||||
|
|
@ -285,11 +330,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
subscribeButton = emailInput.parentElement?.querySelector("button");
|
||||
}
|
||||
if (!subscribeButton) return;
|
||||
|
||||
// Ensure non-submitting behavior
|
||||
subscribeButton.type = "button";
|
||||
|
||||
// Create or reuse feedback element right after the row
|
||||
let feedback = inputRow ? inputRow.nextElementSibling : null;
|
||||
if (!(feedback instanceof HTMLElement) || !feedback.dataset.feedback) {
|
||||
feedback = document.createElement("div");
|
||||
|
|
@ -300,7 +341,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
}
|
||||
feedback.className =
|
||||
"hidden mt-3 rounded-lg px-4 py-3 font-playfair text-base";
|
||||
|
||||
function showMessage(text, type) {
|
||||
feedback.textContent = text;
|
||||
feedback.className =
|
||||
|
|
@ -323,22 +363,17 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
feedback.classList.remove("hidden");
|
||||
setTimeout(() => feedback.classList.add("hidden"), 3000);
|
||||
}
|
||||
|
||||
subscribeButton.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const value = (emailInput.value || "").trim();
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
// Reset error styles
|
||||
emailInput.classList.remove("border-red-500");
|
||||
|
||||
if (!emailRegex.test(value)) {
|
||||
emailInput.classList.add("border-red-500");
|
||||
showMessage("Please enter a valid email address.", "error");
|
||||
emailInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
showMessage("Thank you for subscribing!", "success");
|
||||
emailInput.value = "";
|
||||
subscribeButton.disabled = true;
|
||||
|
|
@ -363,4 +398,310 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Product detail page functionality
|
||||
(async function initProductDetail() {
|
||||
console.log("Product detail script running...");
|
||||
|
||||
// Check if we're on the product detail page
|
||||
const productTitle = document.querySelector("h1");
|
||||
if (!productTitle) {
|
||||
console.log("No h1 found, not on product detail page");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get product ID from URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const productId = parseInt(urlParams.get("id"));
|
||||
console.log("Product ID from URL:", productId);
|
||||
|
||||
if (!productId) {
|
||||
console.log("No product ID found in URL");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Load product data
|
||||
const response = await fetch("data/products.json");
|
||||
const data = await response.json();
|
||||
const product = data.products.find((p) => p.id === productId);
|
||||
|
||||
if (!product) {
|
||||
console.error("Product not found:", productId);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Loading product:", product);
|
||||
|
||||
// Update page title
|
||||
document.title = `${product.name} - KHY`;
|
||||
|
||||
// Update product title (the main product title, not the breadcrumb)
|
||||
const allH1s = document.querySelectorAll("h1");
|
||||
console.log("All H1 elements:", allH1s);
|
||||
if (allH1s.length > 1) {
|
||||
const mainProductTitle = allH1s[1]; // The second h1 should be the product title
|
||||
console.log("Main product title element:", mainProductTitle);
|
||||
if (mainProductTitle) {
|
||||
mainProductTitle.textContent = product.name;
|
||||
}
|
||||
}
|
||||
|
||||
// Update product description - find by text content
|
||||
const allParagraphs = document.querySelectorAll("p");
|
||||
console.log("All paragraphs:", allParagraphs);
|
||||
const descriptionEl = Array.from(allParagraphs).find(
|
||||
(p) =>
|
||||
p.textContent &&
|
||||
p.textContent.includes(
|
||||
"Setting the bar as one of the loudest speakers"
|
||||
)
|
||||
);
|
||||
console.log("Description element:", descriptionEl);
|
||||
if (descriptionEl) {
|
||||
descriptionEl.textContent = product.description;
|
||||
}
|
||||
|
||||
// Update main product image - find by alt text
|
||||
const allImages = document.querySelectorAll("img");
|
||||
console.log("All images:", allImages);
|
||||
const mainImage = Array.from(allImages).find(
|
||||
(img) => img.alt && img.alt.includes("Asgaard sofa")
|
||||
);
|
||||
console.log("Main image element:", mainImage);
|
||||
if (mainImage) {
|
||||
mainImage.src = product.image;
|
||||
mainImage.alt = product.alt || product.name;
|
||||
}
|
||||
|
||||
// Update thumbnail images - find by alt text
|
||||
if (product.images && product.images.length > 0) {
|
||||
const allImages = document.querySelectorAll("img");
|
||||
const thumbnails = Array.from(allImages).filter(
|
||||
(img) =>
|
||||
img.alt &&
|
||||
(img.alt.includes("Outdoor sofa set") ||
|
||||
img.alt.includes("Stuart sofa") ||
|
||||
img.alt.includes("Maya sofa"))
|
||||
);
|
||||
console.log("Thumbnail images found:", thumbnails);
|
||||
product.images.forEach((imageSrc, index) => {
|
||||
if (thumbnails[index]) {
|
||||
thumbnails[index].src = imageSrc;
|
||||
thumbnails[index].alt = product.alt || product.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update size options - use the specific size-button class
|
||||
if (product.sizes) {
|
||||
const sizeButtons = document.querySelectorAll(".size-button");
|
||||
console.log("Size buttons found:", sizeButtons);
|
||||
|
||||
// Hide all size buttons first
|
||||
sizeButtons.forEach((button) => {
|
||||
button.style.display = "none";
|
||||
});
|
||||
|
||||
// Show and update only the buttons we need
|
||||
product.sizes.forEach((size, index) => {
|
||||
if (sizeButtons[index]) {
|
||||
sizeButtons[index].style.display = "flex";
|
||||
sizeButtons[index].textContent = size;
|
||||
// Set selected size
|
||||
if (size === product.selectedSize) {
|
||||
sizeButtons[index].classList.remove(
|
||||
"bg-floral-white",
|
||||
"text-black"
|
||||
);
|
||||
sizeButtons[index].classList.add("bg-uc-gold", "text-white");
|
||||
} else {
|
||||
sizeButtons[index].classList.remove("bg-uc-gold", "text-white");
|
||||
sizeButtons[index].classList.add("bg-floral-white", "text-black");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update color options - find only the rounded color buttons, not size buttons
|
||||
if (product.colors) {
|
||||
const allButtons = document.querySelectorAll("button");
|
||||
const colorButtons = Array.from(allButtons).filter(
|
||||
(button) =>
|
||||
button.classList.contains("w-8") &&
|
||||
button.classList.contains("h-8") &&
|
||||
button.classList.contains("rounded-full") &&
|
||||
!button.textContent // Color buttons should not have text content
|
||||
);
|
||||
console.log("Color buttons found:", colorButtons);
|
||||
|
||||
product.colors.forEach((color, index) => {
|
||||
if (colorButtons[index]) {
|
||||
colorButtons[index].style.backgroundColor = color.value;
|
||||
// Set selected color
|
||||
if (color.selected) {
|
||||
colorButtons[index].classList.add("border-2", "border-black");
|
||||
} else {
|
||||
colorButtons[index].classList.remove("border-2", "border-black");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update product metadata - find by text content
|
||||
const allSpans = document.querySelectorAll("span");
|
||||
|
||||
// Find and update Model No.
|
||||
const modelNoLabel = Array.from(allSpans).find(
|
||||
(span) => span.textContent === "Model No."
|
||||
);
|
||||
if (modelNoLabel && product.modelNo) {
|
||||
const modelNoValue =
|
||||
modelNoLabel.parentElement.querySelector("span:last-child");
|
||||
if (modelNoValue) {
|
||||
modelNoValue.textContent = product.modelNo;
|
||||
}
|
||||
}
|
||||
|
||||
// Find and update Category
|
||||
const categoryLabel = Array.from(allSpans).find(
|
||||
(span) => span.textContent === "Category"
|
||||
);
|
||||
if (categoryLabel && product.category) {
|
||||
const categoryValue =
|
||||
categoryLabel.parentElement.querySelector("span:last-child");
|
||||
if (categoryValue) {
|
||||
categoryValue.textContent =
|
||||
product.category.charAt(0).toUpperCase() +
|
||||
product.category.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Find and update Tags
|
||||
const tagsLabel = Array.from(allSpans).find(
|
||||
(span) => span.textContent === "Tags"
|
||||
);
|
||||
if (tagsLabel && product.tags) {
|
||||
const tagsValue =
|
||||
tagsLabel.parentElement.querySelector("span:last-child");
|
||||
if (tagsValue) {
|
||||
tagsValue.textContent = product.tags.join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
// Find and update Dimensions
|
||||
const dimensionsLabel = Array.from(allSpans).find(
|
||||
(span) => span.textContent === "Dimension"
|
||||
);
|
||||
console.log("Dimensions label found:", dimensionsLabel);
|
||||
console.log("Product dimensions:", product.dimensions);
|
||||
|
||||
if (dimensionsLabel && product.dimensions) {
|
||||
const dimensionsValue =
|
||||
dimensionsLabel.parentElement.querySelector("span:last-child");
|
||||
console.log("Dimensions value element:", dimensionsValue);
|
||||
if (dimensionsValue) {
|
||||
dimensionsValue.textContent = product.dimensions;
|
||||
console.log("Updated dimensions to:", product.dimensions);
|
||||
}
|
||||
}
|
||||
|
||||
// Update description content
|
||||
if (product.descriptionLong) {
|
||||
const descriptionContainer = document.querySelector(
|
||||
".max-w-5xl.mx-auto.space-y-6"
|
||||
);
|
||||
if (descriptionContainer) {
|
||||
const descriptionParagraphs =
|
||||
descriptionContainer.querySelectorAll("p");
|
||||
console.log("Description paragraphs found:", descriptionParagraphs);
|
||||
|
||||
// Update each paragraph with the product's description content
|
||||
product.descriptionLong.forEach((paragraph, index) => {
|
||||
if (descriptionParagraphs[index]) {
|
||||
descriptionParagraphs[index].textContent = paragraph;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update gallery images
|
||||
if (product.galleryPairs) {
|
||||
// Find gallery images by alt text
|
||||
const allImages = document.querySelectorAll("img");
|
||||
const galleryImages = Array.from(allImages).filter(
|
||||
(img) =>
|
||||
img.alt &&
|
||||
(img.alt.includes("Sofa variant left") ||
|
||||
img.alt.includes("Sofa variant right"))
|
||||
);
|
||||
console.log("Gallery images found:", galleryImages);
|
||||
|
||||
product.galleryPairs.forEach((pair, index) => {
|
||||
if (galleryImages[index * 2]) {
|
||||
galleryImages[index * 2].src = pair.left;
|
||||
galleryImages[index * 2].alt = product.alt || product.name;
|
||||
}
|
||||
if (galleryImages[index * 2 + 1]) {
|
||||
galleryImages[index * 2 + 1].src = pair.right;
|
||||
galleryImages[index * 2 + 1].alt = product.alt || product.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update related products section
|
||||
const relatedGrid = document.getElementById("related-grid");
|
||||
if (relatedGrid && product.category) {
|
||||
console.log("Current product category:", product.category);
|
||||
console.log(
|
||||
"All products:",
|
||||
data.products.map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
category: p.category,
|
||||
}))
|
||||
);
|
||||
|
||||
// Filter related products by category (excluding current product)
|
||||
const relatedProducts = data.products
|
||||
.filter((p) => p.category === product.category && p.id !== product.id)
|
||||
.slice(0, 4);
|
||||
|
||||
console.log("Related products found:", relatedProducts);
|
||||
|
||||
relatedGrid.innerHTML = relatedProducts
|
||||
.map(
|
||||
(p) => `
|
||||
<a href="product-detail.html?id=${p.id}" class="block">
|
||||
<div class="rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-lg transition-shadow">
|
||||
<div class="h-72 ${
|
||||
p.name.toLowerCase().includes("asgaard")
|
||||
? "bg-linen"
|
||||
: "bg-white"
|
||||
} flex items-center justify-center">
|
||||
<img src="${p.image}" alt="${
|
||||
p.alt || p.name
|
||||
}" class="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div class="bg-light-bg p-6">
|
||||
<h3 class="font-poppins font-semibold text-2xl text-[#3A3A3A] mb-0">${
|
||||
p.name
|
||||
}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading product data:", error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initSite);
|
||||
} else {
|
||||
initSite();
|
||||
}
|
||||
|
|
|
|||
381
scripts/products.js
Normal file
381
scripts/products.js
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
// 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(category) {
|
||||
// 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: ${category}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Check the corresponding checkbox in the dropdown
|
||||
setTimeout(() => {
|
||||
const checkboxes = document.querySelectorAll(".category-checkbox");
|
||||
checkboxes.forEach((checkbox) => {
|
||||
if (checkbox.value === category) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// Create individual product card HTML
|
||||
createProductCard(product) {
|
||||
return `
|
||||
<div class="group relative bg-light-bg rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
|
||||
<div class="relative h-80 overflow-hidden">
|
||||
<img
|
||||
src="${product.image}"
|
||||
alt="${product.alt}"
|
||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
<!-- 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"
|
||||
onclick="productManager.viewProduct(${product.id})"
|
||||
>
|
||||
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) {
|
||||
// Navigate to product detail page with product ID
|
||||
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();
|
||||
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 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();
|
||||
|
||||
// Load products when DOM is ready
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
productManager.loadProducts();
|
||||
});
|
||||
91
scripts/update_products_tabs.js
Normal file
91
scripts/update_products_tabs.js
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const productsPath = path.join(__dirname, "../data/products.json");
|
||||
const db = JSON.parse(fs.readFileSync(productsPath, "utf8"));
|
||||
|
||||
// Default content for tabs (copy matches current UI text style)
|
||||
const defaultLong = [
|
||||
"Embodying the raw, wayward spirit of rock ‘n’ roll, the Kilburn portable active stereo speaker takes the unmistakable look and sound of Marshall, unplugs the chords, and takes the show on the road.",
|
||||
"Weighing in under 7 pounds, the Kilburn is a lightweight piece of vintage styled engineering. Setting the bar as one of the loudest speakers in its class, the Kilburn is a compact, stout-hearted hero with a well-balanced audio which boasts a clear midrange and extended highs for a sound that is both articulate and pronounced. The analogue knobs allow you to fine tune the controls to your personal preferences while the guitar-influenced leather strap enables easy and stylish travel.",
|
||||
];
|
||||
|
||||
const defaultsByCategory = {
|
||||
seating: {
|
||||
additionalInformation: {
|
||||
Material: "Fabric, engineered wood base",
|
||||
Upholstery: "Performance fabric",
|
||||
Dimensions: "See size options",
|
||||
Warranty: "2 years",
|
||||
},
|
||||
},
|
||||
tables: {
|
||||
additionalInformation: {
|
||||
Material: "Engineered wood, steel frame",
|
||||
Finish: "Matte laminate",
|
||||
Dimensions: "Small/Medium/Large",
|
||||
Warranty: "2 years",
|
||||
},
|
||||
},
|
||||
storage: {
|
||||
additionalInformation: {
|
||||
Material: "Powder-coated steel",
|
||||
Capacity: "Modular shelves",
|
||||
Dimensions: "Standard/Large/XL",
|
||||
Warranty: "2 years",
|
||||
},
|
||||
},
|
||||
workspace: {
|
||||
additionalInformation: {
|
||||
Material: "Acoustic panels, aluminum frame",
|
||||
Power: "Integrated power module",
|
||||
Dimensions: "Standard/Large",
|
||||
Warranty: "2 years",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function ensureFields(p) {
|
||||
const cat =
|
||||
p.category && defaultsByCategory[p.category] ? p.category : "seating";
|
||||
const d = defaultsByCategory[cat];
|
||||
|
||||
if (!Array.isArray(p.descriptionLong) || p.descriptionLong.length === 0) {
|
||||
p.descriptionLong = defaultLong;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof p.additionalInformation !== "object" ||
|
||||
p.additionalInformation === null
|
||||
) {
|
||||
p.additionalInformation = { ...d.additionalInformation };
|
||||
} else {
|
||||
// Fill any missing keys with defaults without overwriting existing
|
||||
for (const [k, v] of Object.entries(d.additionalInformation)) {
|
||||
if (
|
||||
p.additionalInformation[k] == null ||
|
||||
p.additionalInformation[k] === ""
|
||||
) {
|
||||
p.additionalInformation[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure reviewsCount mirrors numeric reviews
|
||||
const reviewsNum = Number(p.reviews || 0);
|
||||
p.reviews = Number.isFinite(reviewsNum) ? reviewsNum : 0;
|
||||
if (p.reviewsCount == null) p.reviewsCount = p.reviews;
|
||||
|
||||
// Ensure galleryPairs (two wide images for tabs). Use product image as fallback.
|
||||
if (!Array.isArray(p.galleryPairs) || p.galleryPairs.length === 0) {
|
||||
const img = p.image || "assets/images/asgaard_sofa.png";
|
||||
p.galleryPairs = [{ left: img, right: img }];
|
||||
}
|
||||
}
|
||||
|
||||
(db.products || []).forEach(ensureFields);
|
||||
|
||||
fs.writeFileSync(productsPath, JSON.stringify(db, null, 2));
|
||||
console.log(
|
||||
"Ensured tabs data for all products: descriptionLong, additionalInformation, reviewsCount, galleryPairs"
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue