refactor: enhance product detail page with dynamic content loading and improved gallery functionality

- Updated product-detail.html to replace static content with dynamic placeholders for images, title, description, sizes, and colors.
- Modified main.js to implement dynamic loading of product details, including title, description, images, sizes, and colors.
- Improved gallery functionality to support a carousel view for product images, including navigation buttons.
- Adjusted CSS to add new transition durations and easing functions for smoother animations.
This commit is contained in:
George Birikorang 2025-09-17 19:40:13 -07:00
parent 195286e6c8
commit 7295bcba7e
4 changed files with 533 additions and 785 deletions

File diff suppressed because it is too large Load diff

View file

@ -194,71 +194,33 @@
<div
class="flex flex-row md:flex-col gap-3 md:gap-4 w-full md:w-32 overflow-x-auto md:overflow-visible"
>
<div
class="w-32 h-32 bg-floral-white rounded-lg overflow-hidden cursor-pointer"
>
<img
src="assets/images/outdoor_sofa_set.png"
alt="Outdoor sofa set"
class="w-full h-full object-cover"
/>
</div>
<div
class="w-32 h-32 bg-floral-white rounded-lg overflow-hidden cursor-pointer"
>
<img
src="assets/images/outdoor_sofa_set.png"
alt="Outdoor sofa set 2"
class="w-full h-full object-cover"
/>
</div>
<div
class="w-32 h-32 bg-floral-white rounded-lg overflow-hidden cursor-pointer"
>
<img
src="assets/images/outdoor_sofa_set.png"
alt="Stuart sofa"
class="w-full h-full object-cover"
/>
</div>
<div
class="w-32 h-32 bg-floral-white rounded-lg overflow-hidden cursor-pointer"
>
<img
src="assets/images/outdoor_sofa_set.png"
alt="Maya sofa three seater"
class="w-full h-full object-cover"
/>
</div>
<!-- Thumbnail images will be populated dynamically -->
</div>
<!-- Main Product Image -->
<div
class="w-full h-80 md:w-[500px] md:h-[500px] bg-floral-white rounded-lg overflow-hidden"
>
<img
src="assets/images/asgaard_sofa.png"
alt="Asgaard sofa"
class="w-full h-full object-cover"
/>
<!-- Main product image will be populated dynamically -->
</div>
</div>
<!-- Right Section - Product Details -->
<div class="w-full md:w-[500px]">
<!-- Product Title -->
<h1 class="font-playfair font-normal text-4xl text-black mb-8">
Asgaard sofa
<h1
class="font-playfair font-normal text-4xl text-black mb-8"
id="product-title"
>
<!-- Product title will be populated dynamically -->
</h1>
<!-- Product Description -->
<p
class="font-playfair font-normal text-sm text-black mb-12 max-w-md"
id="product-description"
>
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.
<!-- Product description will be populated dynamically -->
</p>
<!-- Size Options -->
@ -269,36 +231,7 @@
Size
</h3>
<div class="grid grid-cols-6 gap-2" id="size-buttons-container">
<button
class="w-8 h-8 bg-uc-gold text-white font-playfair font-normal text-sm rounded flex items-center justify-center size-button"
>
L
</button>
<button
class="w-8 h-8 bg-floral-white text-black font-playfair font-normal text-sm rounded flex items-center justify-center size-button"
>
XL
</button>
<button
class="w-8 h-8 bg-floral-white text-black font-playfair font-normal text-sm rounded flex items-center justify-center size-button"
>
XS
</button>
<button
class="w-8 h-8 bg-floral-white text-black font-playfair font-normal text-sm rounded flex items-center justify-center size-button"
>
S
</button>
<button
class="w-8 h-8 bg-floral-white text-black font-playfair font-normal text-sm rounded flex items-center justify-center size-button"
>
M
</button>
<button
class="w-8 h-8 bg-floral-white text-black font-playfair font-normal text-sm rounded flex items-center justify-center size-button"
>
2XL
</button>
<!-- Size buttons will be populated dynamically -->
</div>
</div>
@ -309,12 +242,8 @@
>
Color
</h3>
<div class="flex gap-4">
<button
class="w-8 h-8 bg-purple-500 rounded-full border-2 border-black"
></button>
<button class="w-8 h-8 bg-black rounded-full"></button>
<button class="w-8 h-8 bg-uc-gold rounded-full"></button>
<div class="flex gap-4" id="color-buttons-container">
<!-- Color buttons will be populated dynamically -->
</div>
</div>
@ -365,62 +294,7 @@
<!-- Product Metadata -->
<div class="space-y-4">
<div class="flex items-center">
<span
class="font-playfair font-normal text-base text-quick-silver w-20"
>Model No.</span
>
<span
class="font-playfair font-medium text-base text-quick-silver mx-2"
>:</span
>
<span
class="font-playfair font-normal text-base text-quick-silver"
>SS001</span
>
</div>
<div class="flex items-center">
<span
class="font-playfair font-normal text-base text-quick-silver w-20"
>Category</span
>
<span
class="font-playfair font-medium text-base text-quick-silver mx-2"
>:</span
>
<span
class="font-playfair font-normal text-base text-quick-silver"
>Sofas</span
>
</div>
<div class="flex items-center">
<span
class="font-playfair font-normal text-base text-quick-silver w-20"
>Tags</span
>
<span
class="font-playfair font-medium text-base text-quick-silver mx-2"
>:</span
>
<span
class="font-playfair font-normal text-base text-quick-silver"
>Sofa, Chair, Home, Shop</span
>
</div>
<div class="flex items-center">
<span
class="font-playfair font-normal text-base text-quick-silver w-20"
>Dimension</span
>
<span
class="font-playfair font-medium text-base text-quick-silver mx-2"
>:</span
>
<span
class="font-playfair font-normal text-base text-quick-silver"
>180cm x 85cm x 75cm</span
>
</div>
<!-- Product metadata will be populated dynamically -->
</div>
</div>
</div>
@ -439,45 +313,61 @@
</div>
<!-- Copy -->
<div class="max-w-5xl mx-auto space-y-6">
<p
class="font-playfair text-base leading-6 text-[#333333] text-justify"
<div
class="max-w-5xl mx-auto space-y-6"
id="product-description-content"
>
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.
</p>
<p
class="font-playfair text-base leading-6 text-[#333333] text-justify"
>
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.
</p>
<!-- Product description content will be populated dynamically -->
</div>
<!-- Images -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mt-10">
<div class="rounded-[10px] bg-floral-white p-6">
<img
src="assets/images/asgaard_sofa.png"
alt="Sofa variant left"
class="w-full h-80 object-contain mx-auto"
/>
</div>
<div class="rounded-[10px] bg-floral-white p-6">
<img
src="assets/images/asgaard_sofa.png"
alt="Sofa variant right"
class="w-full h-80 object-contain mx-auto"
/>
<div class="relative mt-10">
<div
class="grid grid-cols-1 md:grid-cols-2 gap-8"
id="product-gallery-images"
>
<!-- Product gallery images will be populated dynamically -->
</div>
<!-- Back Arrow -->
<button
id="gallery-back-btn"
class="absolute top-1/2 left-0 transform -translate-y-1/2 bg-white border border-gray-300 rounded-full p-3 shadow-lg hover:shadow-xl transition-all duration-200 opacity-0 pointer-events-none"
style="display: none"
>
<svg
class="w-6 h-6 text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 19l-7-7 7-7"
></path>
</svg>
</button>
<!-- Forward Arrow -->
<button
id="gallery-next-btn"
class="absolute top-1/2 right-0 transform -translate-y-1/2 bg-white border border-gray-300 rounded-full p-3 shadow-lg hover:shadow-xl transition-all duration-200 opacity-0 pointer-events-none"
style="display: none"
>
<svg
class="w-6 h-6 text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
></path>
</svg>
</button>
</div>
</div>
</section>

View file

@ -222,10 +222,12 @@ function initSite() {
});
});
}
}
// Product detail page functionality
(async function initProductDetail() {
async function initProductDetail() {
console.log("Product detail script running...");
console.log("Current URL:", window.location.href);
// Check if we're on the product detail page
const productTitle = document.querySelector("h1");
@ -233,6 +235,7 @@ function initSite() {
console.log("No h1 found, not on product detail page");
return;
}
console.log("Found h1 element:", productTitle);
// Get product ID from URL
const urlParams = new URLSearchParams(window.location.search);
@ -269,161 +272,103 @@ function initSite() {
}
// 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;
}
const productTitle = document.getElementById("product-title");
console.log("Product title element:", productTitle);
if (productTitle) {
productTitle.textContent = product.name;
console.log("Updated product title to:", 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"
// Update product description
const productDescription = document.getElementById("product-description");
console.log("Product description element:", productDescription);
if (productDescription) {
productDescription.textContent = product.description;
console.log("Updated product description to:", product.description);
}
// Update main product image
const mainImageContainer = document.querySelector(
".w-full.h-80.md\\:w-\\[500px\\].md\\:h-\\[500px\\]"
);
console.log("Main image container:", mainImageContainer);
if (mainImageContainer) {
mainImageContainer.innerHTML = `
<img
src="${product.image}"
alt="${product.alt || product.name}"
class="w-full h-full object-cover"
/>
`;
console.log("Updated main image to:", product.image);
}
// Update thumbnail images
const thumbnailContainer = document.querySelector(
".flex.flex-row.md\\:flex-col.gap-3.md\\:gap-4"
);
console.log("Thumbnail container:", thumbnailContainer);
if (thumbnailContainer && product.images) {
thumbnailContainer.innerHTML = product.images
.slice(0, 4)
.map(
(img, index) => `
<div class="w-32 h-32 bg-floral-white rounded-lg overflow-hidden cursor-pointer">
<img
src="${img}"
alt="${product.name} ${index + 1}"
class="w-full h-full object-cover"
/>
</div>
`
)
);
console.log("Description element:", descriptionEl);
if (descriptionEl) {
descriptionEl.textContent = product.description;
.join("");
console.log("Updated thumbnails with", product.images.length, "images");
}
// 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 size options
const sizeContainer = document.getElementById("size-buttons-container");
console.log("Size container:", sizeContainer);
if (sizeContainer && product.sizes) {
sizeContainer.innerHTML = product.sizes
.map(
(size, index) => `
<button
class="w-8 h-8 ${
index === 0
? "bg-uc-gold text-white"
: "bg-floral-white text-black"
} font-playfair font-normal text-sm rounded flex items-center justify-center size-button"
>
${size}
</button>
`
)
.join("");
console.log("Updated size options with", product.sizes.length, "sizes");
}
// 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"))
// Update color options
const colorContainer = document.getElementById("color-buttons-container");
console.log("Color container:", colorContainer);
if (colorContainer && product.colors) {
colorContainer.innerHTML = product.colors
.map(
(color, index) => `
<button
class="w-8 h-8 rounded-full ${
index === 0 ? "border-2 border-black" : ""
}"
style="background-color: ${color}"
></button>
`
)
.join("");
console.log(
"Updated color options with",
product.colors.length,
"colors"
);
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;
sizeButtons[index].setAttribute("data-size", 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");
sizeButtons[index].classList.add("selected");
} else {
sizeButtons[index].classList.remove(
"bg-uc-gold",
"text-white",
"selected"
);
sizeButtons[index].classList.add("bg-floral-white", "text-black");
}
}
});
// Add click event listeners for size buttons
sizeButtons.forEach((button) => {
button.addEventListener("click", function () {
// Remove selected state from all size buttons
sizeButtons.forEach((btn) => {
btn.classList.remove("bg-uc-gold", "text-white", "selected");
btn.classList.add("bg-floral-white", "text-black");
});
// Add selected state to clicked button
this.classList.remove("bg-floral-white", "text-black");
this.classList.add("bg-uc-gold", "text-white", "selected");
});
});
}
// 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;
colorButtons[index].setAttribute(
"data-color",
color.name || color.value
);
// Set selected color
if (color.selected) {
colorButtons[index].classList.add(
"border-2",
"border-black",
"selected"
);
} else {
colorButtons[index].classList.remove(
"border-2",
"border-black",
"selected"
);
}
}
});
// Add click event listeners for color buttons
colorButtons.forEach((button) => {
button.addEventListener("click", function () {
// Remove selected state from all color buttons
colorButtons.forEach((btn) => {
btn.classList.remove("border-2", "border-black", "selected");
});
// Add selected state to clicked button
this.classList.add("border-2", "border-black", "selected");
});
});
}
// Update product metadata - find by text content
@ -450,8 +395,7 @@ function initSite() {
categoryLabel.parentElement.querySelector("span:last-child");
if (categoryValue) {
categoryValue.textContent =
product.category.charAt(0).toUpperCase() +
product.category.slice(1);
product.category.charAt(0).toUpperCase() + product.category.slice(1);
}
}
@ -485,45 +429,133 @@ function initSite() {
}
// Update description content
if (product.descriptionLong) {
const descriptionContainer = document.querySelector(
".max-w-5xl.mx-auto.space-y-6"
const descriptionContainer = document.getElementById(
"product-description-content"
);
console.log("Description content container:", descriptionContainer);
if (descriptionContainer && product.descriptionLong) {
descriptionContainer.innerHTML = product.descriptionLong
.map(
(paragraph) => `
<p class="font-playfair text-base leading-6 text-[#333333] text-justify">
${paragraph}
</p>
`
)
.join("");
console.log(
"Updated description content with",
product.descriptionLong.length,
"paragraphs"
);
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 with carousel
const galleryContainer = document.getElementById("product-gallery-images");
const galleryBackBtn = document.getElementById("gallery-back-btn");
const galleryNextBtn = document.getElementById("gallery-next-btn");
console.log("Gallery container:", galleryContainer);
if (galleryContainer && product.galleryPairs) {
// galleryPairs is now a simple array of image paths
const allGalleryImages = product.galleryPairs;
// Store gallery images for carousel
window.currentGalleryImages = allGalleryImages;
window.currentGalleryIndex = 0;
// Render initial gallery view
renderGalleryCarousel();
// Show navigation arrows for looped carousel (always show if more than 2 images)
if (allGalleryImages.length > 2) {
galleryBackBtn.style.display = "block";
galleryNextBtn.style.display = "block";
} else {
galleryBackBtn.style.display = "none";
galleryNextBtn.style.display = "none";
}
console.log(
"Gallery carousel:",
allGalleryImages.length,
"gallery images total (sliding)"
);
}
function renderGalleryCarousel() {
if (!window.currentGalleryImages || !galleryContainer) return;
// Handle single image vs multiple images
if (window.currentGalleryImages.length === 1) {
// Single image - show only one image, hide arrows
const image1 = window.currentGalleryImages[0];
galleryContainer.innerHTML = `
<div class="rounded-[10px] bg-floral-white p-6 transition-all duration-1500 ease-out" id="gallery-image-1">
<img
src="${image1}"
alt="${product.name} gallery"
class="w-full h-80 object-contain mx-auto"
/>
</div>
`;
// Hide arrows for single image
galleryBackBtn.style.display = "none";
galleryNextBtn.style.display = "none";
} else {
// Multiple images - show 2 images with carousel
const image1 = window.currentGalleryImages[window.currentGalleryIndex];
const image2Index =
(window.currentGalleryIndex + 1) % window.currentGalleryImages.length;
const image2 = window.currentGalleryImages[image2Index];
galleryContainer.innerHTML = `
<div class="rounded-[10px] bg-floral-white p-6 transition-all duration-1500 ease-out" id="gallery-image-1">
<img
src="${image1}"
alt="${product.name} gallery"
class="w-full h-80 object-contain mx-auto"
/>
</div>
<div class="rounded-[10px] bg-floral-white p-6 transition-all duration-1500 ease-out" id="gallery-image-2">
<img
src="${image2}"
alt="${product.name} gallery"
class="w-full h-80 object-contain mx-auto"
/>
</div>
`;
// Show arrows for multiple images
galleryBackBtn.style.display = "block";
galleryBackBtn.style.opacity = "1";
galleryBackBtn.style.pointerEvents = "auto";
galleryNextBtn.style.display = "block";
galleryNextBtn.style.opacity = "1";
galleryNextBtn.style.pointerEvents = "auto";
}
}
// Add click handlers for looped gallery carousel navigation
if (galleryBackBtn) {
galleryBackBtn.addEventListener("click", function () {
// Left arrow moves forward: if at last position, go to 0
window.currentGalleryIndex =
(window.currentGalleryIndex + 1) % window.currentGalleryImages.length;
renderGalleryCarousel();
});
}
}
// 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;
}
if (galleryNextBtn) {
galleryNextBtn.addEventListener("click", function () {
// Right arrow moves backward: if at 0, go to last position
window.currentGalleryIndex =
window.currentGalleryIndex === 0
? window.currentGalleryImages.length - 1
: window.currentGalleryIndex - 1;
renderGalleryCarousel();
});
}
@ -608,7 +640,6 @@ function initSite() {
} catch (error) {
console.error("Error loading product data:", error);
}
})();
}
// Product Comparison Page Functionality
@ -1001,6 +1032,8 @@ if (window.location.pathname.includes("product-detail.html")) {
"Product detail page detected, initializing compare functionality immediately"
);
initProductDetailCompare();
// Also initialize product detail loading
initProductDetail();
}
// Also try to initialize on DOMContentLoaded
@ -1009,6 +1042,8 @@ document.addEventListener("DOMContentLoaded", function () {
if (window.location.pathname.includes("product-detail.html")) {
console.log("Product detail page detected in DOMContentLoaded");
initProductDetailCompare();
// Also initialize product detail loading
initProductDetail();
}
});

View file

@ -1922,10 +1922,22 @@ video {
transition-duration: 300ms;
}
.duration-500 {
transition-duration: 500ms;
}
.duration-1000 {
transition-duration: 1000ms;
}
.ease-in-out {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.ease-out {
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
.hover\:-translate-y-0\.5:hover {
--tw-translate-y: -0.125rem;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));