feat: add product catalog and product detail pages

This commit is contained in:
George Birikorang 2025-08-22 07:40:59 -04:00
parent 9a3a106fca
commit 0d5fd84762
13 changed files with 4256 additions and 555 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -28,7 +28,9 @@
</head>
<body class="bg-white font-sans text-gray-800">
<!-- Header -->
<header class="absolute w-full h-28 top-0 left-0 bg-white shadow-lg z-50">
<header
class="absolute w-full h-28 top-0 left-0 bg-white shadow-[0_8px_24px_rgba(0,0,0,0.06)] border-b border-black/10 z-50"
>
<nav class="h-full">
<div
class="max-w-7xl mx-auto h-full flex items-center justify-between px-5"

View file

@ -28,7 +28,9 @@
</head>
<body class="bg-white font-sans text-gray-800">
<!-- Header -->
<header class="absolute w-full h-28 top-0 left-0 bg-white shadow-lg z-50">
<header
class="absolute w-full h-28 top-0 left-0 bg-white shadow-[0_8px_24px_rgba(0,0,0,0.06)] border-b border-black/10 z-50"
>
<nav class="h-full">
<div
class="max-w-7xl mx-auto h-full flex items-center justify-between px-5"

1678
data/products.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -109,7 +109,9 @@
</head>
<body class="bg-gray-50 font-sans text-gray-800">
<!-- Header -->
<header class="absolute w-full h-28 top-0 left-0 bg-white shadow-lg z-50">
<header
class="absolute w-full h-28 top-0 left-0 bg-white shadow-[0_8px_24px_rgba(0,0,0,0.06)] border-b border-black/10 z-50"
>
<nav class="h-full">
<div
class="max-w-7xl mx-auto h-full flex items-center justify-between px-5"
@ -431,213 +433,231 @@
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-12"
>
<!-- Product Card 1: Lounge Chairs -->
<div
class="product-card bg-light-bg rounded-lg overflow-hidden h-[446px]"
>
<div class="product-card-image h-[301px] bg-gray-200">
<img
src="assets/images/lounge_chair.jpg"
alt="Lounge Chairs"
class="w-full h-full object-cover"
/>
<a href="product-catalog.html?category=seating" class="block">
<div
class="product-card bg-light-bg rounded-lg overflow-hidden h-[446px]"
>
<div class="product-card-image h-[301px] bg-gray-200">
<img
src="assets/images/lounge_chair.jpg"
alt="Lounge Chairs"
class="w-full h-full object-cover"
/>
</div>
<div class="p-4 h-[145px] flex flex-col justify-center">
<h3
class="product-card-title font-poppins font-semibold text-xl text-gray-800 mb-2"
>
Lounge Chairs
</h3>
<p
class="product-card-description font-poppins font-medium text-sm text-gray-500"
>
Stylish cafe chair
</p>
</div>
</div>
<div class="p-4 h-[145px] flex flex-col justify-center">
<h3
class="product-card-title font-poppins font-semibold text-xl text-gray-800 mb-2"
>
Lounge Chairs
</h3>
<p
class="product-card-description font-poppins font-medium text-sm text-gray-500"
>
Stylish cafe chair
</p>
</div>
</div>
</a>
<!-- Product Card 2: Office Chairs -->
<div
class="product-card bg-light-bg rounded-lg overflow-hidden h-[446px]"
>
<div class="product-card-image h-[301px] bg-gray-200">
<img
src="assets/images/office_chair.jpg"
alt="Office Chairs"
class="w-full h-full object-cover"
/>
<a href="product-catalog.html?category=seating" class="block">
<div
class="product-card bg-light-bg rounded-lg overflow-hidden h-[446px]"
>
<div class="product-card-image h-[301px] bg-gray-200">
<img
src="assets/images/office_chair.jpg"
alt="Office Chairs"
class="w-full h-full object-cover"
/>
</div>
<div class="p-4 h-[145px] flex flex-col justify-center">
<h3
class="product-card-title font-poppins font-semibold text-xl text-gray-800 mb-2"
>
Office Chairs
</h3>
<p
class="product-card-description font-poppins font-medium text-sm text-gray-500"
>
Luxury big sofa
</p>
</div>
</div>
<div class="p-4 h-[145px] flex flex-col justify-center">
<h3
class="product-card-title font-poppins font-semibold text-xl text-gray-800 mb-2"
>
Office Chairs
</h3>
<p
class="product-card-description font-poppins font-medium text-sm text-gray-500"
>
Luxury big sofa
</p>
</div>
</div>
</a>
<!-- Product Card 3: Conference Room -->
<div
class="product-card bg-light-bg rounded-lg overflow-hidden h-[446px]"
>
<div class="product-card-image h-[301px] bg-gray-200">
<img
src="assets/images/conference_rooms.jpg"
alt="Conference Room"
class="w-full h-full object-cover"
/>
<a href="product-catalog.html?category=tables" class="block">
<div
class="product-card bg-light-bg rounded-lg overflow-hidden h-[446px]"
>
<div class="product-card-image h-[301px] bg-gray-200">
<img
src="assets/images/conference_rooms.jpg"
alt="Conference Room"
class="w-full h-full object-cover"
/>
</div>
<div class="p-4 h-[145px] flex flex-col justify-center">
<h3
class="product-card-title font-poppins font-semibold text-xl text-gray-800 mb-2"
>
Conference Room
</h3>
<p
class="product-card-description font-poppins font-medium text-sm text-gray-500"
>
Tables
</p>
</div>
</div>
<div class="p-4 h-[145px] flex flex-col justify-center">
<h3
class="product-card-title font-poppins font-semibold text-xl text-gray-800 mb-2"
>
Conference Room
</h3>
<p
class="product-card-description font-poppins font-medium text-sm text-gray-500"
>
Tables
</p>
</div>
</div>
</a>
<!-- Product Card 4: Storage -->
<div
class="product-card bg-light-bg rounded-lg overflow-hidden h-[446px]"
>
<div class="product-card-image h-[301px] bg-gray-200">
<img
src="assets/images/storage.jpg"
alt="Storage"
class="w-full h-full object-cover"
/>
<a href="product-catalog.html?category=storage" class="block">
<div
class="product-card bg-light-bg rounded-lg overflow-hidden h-[446px]"
>
<div class="product-card-image h-[301px] bg-gray-200">
<img
src="assets/images/storage.jpg"
alt="Storage"
class="w-full h-full object-cover"
/>
</div>
<div class="p-4 h-[145px] flex flex-col justify-center">
<h3
class="product-card-title font-poppins font-semibold text-xl text-gray-800 mb-2"
>
Storage
</h3>
<p
class="product-card-description font-poppins font-medium text-sm text-gray-500"
>
Outdoor bar table and stool
</p>
</div>
</div>
<div class="p-4 h-[145px] flex flex-col justify-center">
<h3
class="product-card-title font-poppins font-semibold text-xl text-gray-800 mb-2"
>
Storage
</h3>
<p
class="product-card-description font-poppins font-medium text-sm text-gray-500"
>
Outdoor bar table and stool
</p>
</div>
</div>
</a>
<!-- Product Card 5: Kitchen -->
<div
class="product-card bg-light-bg rounded-lg overflow-hidden h-[446px]"
>
<div class="product-card-image h-[301px] bg-gray-200">
<img
src="assets/images/kitchen.jpg"
alt="Kitchen"
class="w-full h-full object-cover"
/>
<a href="product-catalog.html?category=workspace" class="block">
<div
class="product-card bg-light-bg rounded-lg overflow-hidden h-[446px]"
>
<div class="product-card-image h-[301px] bg-gray-200">
<img
src="assets/images/kitchen.jpg"
alt="Kitchen"
class="w-full h-full object-cover"
/>
</div>
<div class="p-4 h-[145px] flex flex-col justify-center">
<h3
class="product-card-title font-poppins font-semibold text-xl text-gray-800 mb-2"
>
Kitchen
</h3>
<p
class="product-card-description font-poppins font-medium text-sm text-gray-500"
>
Night lamp
</p>
</div>
</div>
<div class="p-4 h-[145px] flex flex-col justify-center">
<h3
class="product-card-title font-poppins font-semibold text-xl text-gray-800 mb-2"
>
Kitchen
</h3>
<p
class="product-card-description font-poppins font-medium text-sm text-gray-500"
>
Night lamp
</p>
</div>
</div>
</a>
<!-- Product Card 6: Chairs -->
<div
class="product-card bg-light-bg rounded-lg overflow-hidden h-[446px]"
>
<div class="product-card-image h-[301px] bg-gray-200">
<img
src="assets/images/chairs.jpg"
alt="Chairs"
class="w-full h-full object-cover"
/>
<a href="product-catalog.html?category=seating" class="block">
<div
class="product-card bg-light-bg rounded-lg overflow-hidden h-[446px]"
>
<div class="product-card-image h-[301px] bg-gray-200">
<img
src="assets/images/chairs.jpg"
alt="Chairs"
class="w-full h-full object-cover"
/>
</div>
<div class="p-4 h-[145px] flex flex-col justify-center">
<h3
class="product-card-title font-poppins font-semibold text-xl text-gray-800 mb-2"
>
Chairs
</h3>
<p
class="product-card-description font-poppins font-medium text-sm text-gray-500"
>
Small mug
</p>
</div>
</div>
<div class="p-4 h-[145px] flex flex-col justify-center">
<h3
class="product-card-title font-poppins font-semibold text-xl text-gray-800 mb-2"
>
Chairs
</h3>
<p
class="product-card-description font-poppins font-medium text-sm text-gray-500"
>
Small mug
</p>
</div>
</div>
</a>
<!-- Product Card 7: Pods -->
<div
class="product-card bg-light-bg rounded-lg overflow-hidden h-[446px]"
>
<div class="product-card-image h-[301px] bg-gray-200">
<img
src="assets/images/pods.jpg"
alt="Pods"
class="w-full h-full object-cover"
/>
<a href="product-catalog.html?category=seating" class="block">
<div
class="product-card bg-light-bg rounded-lg overflow-hidden h-[446px]"
>
<div class="product-card-image h-[301px] bg-gray-200">
<img
src="assets/images/pods.jpg"
alt="Pods"
class="w-full h-full object-cover"
/>
</div>
<div class="p-4 h-[145px] flex flex-col justify-center">
<h3
class="product-card-title font-poppins font-semibold text-xl text-gray-800 mb-2"
>
Pods
</h3>
<p
class="product-card-description font-poppins font-medium text-sm text-gray-500"
>
Cute bed set
</p>
</div>
</div>
<div class="p-4 h-[145px] flex flex-col justify-center">
<h3
class="product-card-title font-poppins font-semibold text-xl text-gray-800 mb-2"
>
Pods
</h3>
<p
class="product-card-description font-poppins font-medium text-sm text-gray-500"
>
Cute bed set
</p>
</div>
</div>
</a>
<!-- Product Card 8: Potty -->
<div
class="product-card bg-light-bg rounded-lg overflow-hidden h-[446px]"
>
<div class="product-card-image h-[301px] bg-gray-200">
<img
src="assets/images/potty.jpg"
alt="Potty"
class="w-full h-full object-cover"
/>
<a href="product-catalog.html?category=workspace" class="block">
<div
class="product-card bg-light-bg rounded-lg overflow-hidden h-[446px]"
>
<div class="product-card-image h-[301px] bg-gray-200">
<img
src="assets/images/potty.jpg"
alt="Potty"
class="w-full h-full object-cover"
/>
</div>
<div class="p-4 h-[145px] flex flex-col justify-center">
<h3
class="product-card-title font-poppins font-semibold text-xl text-gray-800 mb-2"
>
Potty
</h3>
<p
class="product-card-description font-poppins font-medium text-sm text-gray-500"
>
Minimalist flower pot
</p>
</div>
</div>
<div class="p-4 h-[145px] flex flex-col justify-center">
<h3
class="product-card-title font-poppins font-semibold text-xl text-gray-800 mb-2"
>
Potty
</h3>
<p
class="product-card-description font-poppins font-medium text-sm text-gray-500"
>
Minimalist flower pot
</p>
</div>
</div>
</a>
</div>
<!-- Call to Action Button -->
<div class="text-center">
<button
class="bg-white border border-uc-gold text-uc-gold font-playfair font-semibold text-base px-8 py-3 rounded-lg hover:bg-uc-gold hover:text-white transition-all"
>
Browse product catalog
</button>
<a href="product-catalog.html">
<button
class="bg-white border border-uc-gold text-uc-gold font-playfair font-semibold text-base px-8 py-3 rounded-lg hover:bg-uc-gold hover:text-white transition-all"
>
Browse product catalog
</button>
</a>
</div>
</div>
</section>

425
product-catalog.html Normal file
View file

@ -0,0 +1,425 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Product Catalog - KHY</title>
<link rel="stylesheet" href="styles/main.css" />
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&family=Playfair+Display:wght@100;200;300;400;500;600;700&family=Poppins:wght@400;500;600&display=swap"
rel="stylesheet"
/>
</head>
<body class="bg-white font-sans text-gray-800">
<!-- Header -->
<header class="absolute w-full h-28 top-0 left-0 bg-white shadow-[0_8px_24px_rgba(0,0,0,0.06)] border-b border-black/10 z-50">
<nav class="h-full">
<div
class="max-w-7xl mx-auto h-full flex items-center justify-between px-5"
>
<!-- Logo Section -->
<div class="flex items-center">
<img
src="assets/images/khy_logo.png"
alt="KHY Logo"
class="h-16 w-auto drop-shadow-sm"
/>
</div>
<!-- Navigation -->
<ul class="flex space-x-10">
<li>
<a
href="index.html"
class="nav-link text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors text-shadow-default"
>Home</a
>
</li>
<li>
<a
href="index.html#products"
class="nav-link text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors text-shadow-default"
>Products</a
>
</li>
<li>
<a
href="index.html#projects-button"
class="nav-link text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors text-shadow-default"
>Projects</a
>
</li>
<li>
<a
href="index.html#clients"
class="nav-link text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors text-shadow-default"
>Clients</a
>
</li>
<li>
<a
href="index.html#about"
class="nav-link text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors text-shadow-default"
>About</a
>
</li>
<li>
<a
href="contact.html"
class="nav-link text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors text-shadow-default"
>Contact</a
>
</li>
<li>
<a
href="blog.html"
class="nav-link text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors text-shadow-default"
>Blog</a
>
</li>
<li>
<a
href="quote.html"
class="nav-link text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors text-shadow-default"
>Quote</a
>
</li>
</ul>
</nav>
</header>
<main>
<!-- Hero Section -->
<section class="relative h-80 mt-28">
<!-- Background Image -->
<div class="absolute inset-0 w-full h-full">
<img
src="assets/images/potty.jpg"
alt="Product catalog background"
class="w-full h-full object-cover object-center"
style="filter: blur(3px)"
/>
<!-- White Overlay -->
<div class="absolute inset-0 bg-white bg-opacity-60"></div>
<!-- Overlay Content -->
<div
class="absolute z-10 inset-0 flex items-center justify-center text-center text-black"
>
<h1
class="font-playfair font-medium text-3xl md:text-4xl lg:text-5xl leading-tight"
>
Product catalog
</h1>
</section>
<!-- Product Controls Section -->
<section class="relative w-full h-25 bg-floral-white py-6">
<div class="max-w-7xl mx-auto px-5">
<div class="flex justify-between items-center">
<!-- Left side: Filter and View Controls -->
<div class="flex items-center gap-4 relative">
<!-- Filter Button -->
<button
id="filter-toggle"
class="flex items-center gap-2 px-4 py-2 border border-light-silver rounded-md bg-white hover:bg-light-bg transition-colors"
>
<span class="text-lg"></span>
<span class="font-poppins text-lg text-black">Filter</span>
</button>
<!-- Categories Dropdown -->
<div
id="filter-dropdown"
class="hidden absolute top-12 left-0 w-56 bg-white border border-light-silver rounded-md shadow-lg p-3 space-y-2 z-20"
>
<!-- Categories will be injected dynamically -->
<div id="filter-categories" class="space-y-2"></div>
<div class="pt-2 flex justify-end gap-2">
<button id="filter-clear" class="text-quick-silver text-sm hover:text-black">Clear</button>
<button id="filter-apply" class="text-uc-gold text-sm hover:underline">Apply</button>
</div>
</div>
<!-- View Toggle Buttons (removed) -->
<!-- Results Count -->
<span class="font-poppins text-base text-quick-silver"
>Showing 116 of 32 results</span
>
</div>
<!-- Right side: Sort Dropdown -->
<div class="flex items-center gap-4">
<!-- Sort Dropdown -->
<div class="flex items-center gap-2">
<span class="font-poppins text-lg text-black">Sort by:</span>
<select
class="border border-light-silver rounded px-2 py-1 bg-white"
>
<option value="default">Default</option>
<option value="name-asc">Name: A to Z</option>
<option value="name-desc">Name: Z to A</option>
</select>
</div>
</div>
</div>
</div>
</section>
<!-- Product Grid Section -->
<section class="relative w-full bg-white py-12">
<div class="max-w-7xl mx-auto px-5">
<!-- Product Grid -->
<div
id="product-grid"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-12"
>
<!-- Products will be loaded dynamically -->
</div>
<!-- Pagination -->
<div id="pagination" class="flex justify-center items-center space-x-10">
<!-- Pagination will be loaded dynamically -->
</div>
</section>
<!-- Features Section -->
<section class="relative bg-linen py-16">
<div class="max-w-7xl mx-auto px-5">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<!-- High Quality -->
<div class="flex items-center space-x-4">
<div class="w-12 h-12">
<img
src="assets/icons/trophy.png"
alt="Trophy"
class="w-full h-full object-contain"
/>
</div>
<div>
<h3
class="font-playfair font-semibold text-2xl text-black mb-1"
>
High Quality
</h3>
<p class="font-playfair font-normal text-lg text-gray-500">
crafted from top materials
</p>
</div>
</div>
<!-- Warranty Protection -->
<div class="flex items-center space-x-4">
<div class="w-12 h-12">
<img
src="assets/icons/warranty.png"
alt="Guarantee"
class="w-full h-full object-contain"
/>
</div>
<div>
<h3
class="font-playfair font-semibold text-2xl text-black mb-1"
>
Warranty Protection
</h3>
<p class="font-playfair font-normal text-lg text-gray-500">
Over 2 years
</p>
</div>
</div>
<!-- Free Shipping -->
<div class="flex items-center space-x-4">
<div class="w-12 h-12">
<img
src="assets/icons/shipping.png"
alt="Shipping"
class="w-full h-full object-contain"
/>
</div>
<div>
<h3
class="font-playfair font-semibold text-2xl text-black mb-1"
>
Free Shipping
</h3>
<p class="font-playfair font-normal text-lg text-gray-500">
Order over 150 $
</p>
</div>
</div>
<!-- 24/7 Support -->
<div class="flex items-center space-x-4">
<div class="w-12 h-12">
<img
src="assets/icons/support.png"
alt="Support"
class="w-full h-full object-contain"
/>
</div>
<div>
<h3
class="font-playfair font-semibold text-2xl text-black mb-1"
>
24 / 7 Support
</h3>
<p class="font-playfair font-normal text-lg text-gray-500">
Dedicated support
</p>
</div>
</div>
</div>
</div>
</section>
</main>
<!-- Footer -->
<footer class="bg-white">
<!-- Main Footer Content -->
<div class="max-w-7xl mx-auto px-5 py-16">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<!-- Company Information -->
<div class="space-y-2">
<!-- Logo -->
<div class="w-16 h-20">
<img
src="assets/images/khy_logo.png"
alt="khy"
class="w-full h-full object-contain"
/>
</div>
<!-- Address -->
<p
class="font-playfair font-normal text-base leading-relaxed text-gray-600"
>
5 Labone Crescent, Greater Accra, Ghana
</p>
<!-- Contact Info -->
<div class="space-y-1">
<!-- Phone -->
<div class="flex items-center space-x-3">
<img
src="assets/images/phone.png"
alt="Phone"
class="w-4 h-4"
/>
<span class="font-playfair font-normal text-base text-gray-800">
+233 (555) 76677
</span>
</div>
<!-- Email -->
<div class="flex items-center space-x-3">
<img src="assets/images/mail.png" alt="Email" class="w-4 h-4" />
<span class="font-playfair font-normal text-base text-gray-800">
design@khyltd.com
</span>
</div>
</div>
</div>
<!-- Quick Links -->
<div class="space-y-6">
<h3
class="font-playfair font-normal text-md leading-relaxed tracking-wider text-eerie-black uppercase"
>
Quick Links
</h3>
<div class="space-y-4">
<a
href="#"
class="block font-playfair font-normal text-base leading-relaxed tracking-wider text-gray-800 hover:text-black transition-colors"
>
Home
</a>
<a
href="#"
class="block font-playfair font-normal text-base leading-relaxed tracking-wider text-gray-800 hover:text-black transition-colors"
>
Products
</a>
<a
href="#"
class="block font-playfair font-normal text-base leading-relaxed tracking-wider text-gray-800 hover:text-black transition-colors"
>
About
</a>
</div>
</div>
<!-- Help -->
<div class="space-y-6">
<h3
class="font-playfair font-normal text-md leading-relaxed tracking-wider text-eerie-black uppercase"
>
Help
</h3>
<div class="space-y-4">
<a
href="#"
class="block font-playfair font-normal text-base leading-relaxed tracking-wider text-gray-800 hover:text-black transition-colors"
>
Contact Us
</a>
<a
href="#"
class="block font-playfair font-normal text-base leading-relaxed tracking-wider text-gray-800 hover:text-black transition-colors"
>
Privacy Policies
</a>
<a
href="#"
class="block font-playfair font-normal text-base leading-relaxed tracking-wider text-gray-800 hover:text-black transition-colors"
>
Terms and Conditions
</a>
</div>
</div>
<!-- Newsletter -->
<div class="space-y-6">
<h3
class="font-playfair font-normal text-md leading-relaxed tracking-wider text-eerie-black uppercase"
>
Newsletter
</h3>
<div class="space-y-4">
<div class="flex items-center space-x-4">
<input
type="email"
placeholder="Enter Your Email Address"
class="flex-1 font-playfair font-normal text-sm leading-relaxed tracking-wider text-taupe-gray bg-transparent border-b border-black focus:outline-none focus:border-black"
/>
<button
class="font-playfair font-normal text-sm leading-relaxed tracking-wider text-gray-800 border-b border-black hover:text-black transition-colors"
>
Subscribe
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Copyright Section -->
<div class="border-t border-light-silver">
<div class="max-w-7xl mx-auto px-5 py-4">
<p
class="font-playfair font-normal text-xs leading-relaxed text-davys-grey"
>
© 2025 khy. All rights reserved.
</p>
</div>
</div>
</footer>
<script src="scripts/main.js"></script>
<script src="scripts/products.js"></script>
</body>
</html>

592
product-detail.html Normal file
View file

@ -0,0 +1,592 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Product Detail - KHY</title>
<link rel="stylesheet" href="styles/main.css" />
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&family=Playfair+Display:wght@100;200;300;400;500;600;700&family=Poppins:wght@400;500;600&display=swap"
rel="stylesheet"
/>
</head>
<body class="bg-white font-sans text-gray-800">
<!-- Header -->
<header
class="absolute w-full h-28 top-0 left-0 bg-white shadow-[0_8px_24px_rgba(0,0,0,0.06)] border-b border-black/10 z-50"
>
<nav class="h-full">
<div
class="max-w-7xl mx-auto h-full flex items-center justify-between px-5"
>
<!-- Logo Section -->
<div class="flex items-center">
<img
src="assets/images/khy_logo.png"
alt="KHY Logo"
class="h-16 w-auto drop-shadow-sm"
/>
</div>
<!-- Navigation -->
<ul class="flex space-x-10">
<li>
<a
href="index.html"
class="nav-link text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors text-shadow-default"
>Home</a
>
</li>
<li>
<a
href="index.html#products"
class="nav-link text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors text-shadow-default"
>Products</a
>
</li>
<li>
<a
href="index.html#projects-button"
class="nav-link text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors text-shadow-default"
>Projects</a
>
</li>
<li>
<a
href="index.html#clients"
class="nav-link text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors text-shadow-default"
>Clients</a
>
</li>
<li>
<a
href="index.html#about"
class="nav-link text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors text-shadow-default"
>About</a
>
</li>
<li>
<a
href="contact.html"
class="nav-link text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors text-shadow-default"
>Contact</a
>
</li>
<li>
<a
href="blog.html"
class="nav-link text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors text-shadow-default"
>Blog</a
>
</li>
<li>
<a
href="quote.html"
class="nav-link text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors text-shadow-default"
>Quote</a
>
</li>
</ul>
</div>
</nav>
</header>
<main class="pt-28">
<!-- Product Details Section -->
<section class="relative w-full bg-white py-6">
<div class="max-w-7xl mx-auto px-5">
<div class="flex items-center">
<h1 class="font-playfair font-normal text-base text-quick-silver">
Product Details
</h1>
</div>
</div>
</section>
<!-- Main Product Section -->
<section class="relative w-full bg-white py-8">
<div class="max-w-7xl mx-auto px-5">
<div class="flex gap-8">
<!-- Left Section - Product Images -->
<div class="flex gap-4">
<!-- Thumbnails -->
<div class="flex flex-col gap-4 w-32">
<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>
</div>
<!-- Main Product Image -->
<div
class="w-[500px] 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"
/>
</div>
</div>
<!-- Right Section - Product Details -->
<div class="w-[500px]">
<!-- Product Title -->
<h1 class="font-playfair font-normal text-4xl text-black mb-8">
Asgaard sofa
</h1>
<!-- Product Description -->
<p
class="font-playfair font-normal text-sm text-black mb-12 max-w-md"
>
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.
</p>
<!-- Size Options -->
<div class="mb-8">
<h3
class="font-playfair font-normal text-sm text-quick-silver mb-4"
>
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-poppins 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-poppins 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-poppins 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-poppins 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-poppins 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-poppins font-normal text-sm rounded flex items-center justify-center size-button"
>
2XL
</button>
</div>
</div>
<!-- Color Options -->
<div class="mb-8">
<h3
class="font-playfair font-normal text-sm text-quick-silver mb-4"
>
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>
</div>
<!-- Quantity and Action Buttons -->
<div class="flex gap-6 mb-8">
<!-- Quantity Selector -->
<div
class="inline-flex items-center justify-between w-[180px] h-[64px] min-h-[64px] bg-white border border-quick-silver rounded-[15px] px-4 box-border shadow-sm hover:shadow-md transition-all duration-200"
>
<button
id="qty-decr"
aria-label="Decrease quantity"
class="font-playfair font-light text-[20px] leading-none text-black w-8 h-8 flex items-center justify-center rounded-lg hover:bg-light-bg transition-colors cursor-pointer"
>
-
</button>
<span
id="qty-value"
class="font-playfair font-light text-[20px] leading-none text-black"
>1</span
>
<button
id="qty-incr"
aria-label="Increase quantity"
class="font-playfair font-light text-[20px] leading-none text-black w-8 h-8 flex items-center justify-center rounded-lg hover:bg-light-bg transition-colors cursor-pointer"
>
+
</button>
</div>
<!-- Action Buttons -->
<button
class="inline-flex items-center justify-center w-[380px] h-[64px] min-h-[64px] bg-white text-black font-playfair font-light text-[20px] leading-none rounded-[15px] border border-quick-silver hover:bg-uc-gold hover:text-white hover:border-uc-gold hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-200 box-border whitespace-nowrap"
>
Add To Quote
</button>
<button
class="inline-flex items-center justify-center w-[440px] h-[64px] min-h-[64px] bg-white text-black font-playfair font-light text-[20px] leading-none rounded-[15px] border border-quick-silver hover:bg-black hover:text-white hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-200 box-border whitespace-nowrap"
>
Compare Products
</button>
</div>
<!-- Divider -->
<div class="w-full h-px bg-light-silver mb-8"></div>
<!-- Product Metadata -->
<div class="space-y-4">
<div class="flex items-center">
<span
class="font-poppins font-normal text-base text-quick-silver w-20"
>Model No.</span
>
<span
class="font-poppins font-medium text-base text-quick-silver mx-2"
>:</span
>
<span
class="font-poppins font-normal text-base text-quick-silver"
>SS001</span
>
</div>
<div class="flex items-center">
<span
class="font-poppins font-normal text-base text-quick-silver w-20"
>Category</span
>
<span
class="font-poppins font-medium text-base text-quick-silver mx-2"
>:</span
>
<span
class="font-poppins font-normal text-base text-quick-silver"
>Sofas</span
>
</div>
<div class="flex items-center">
<span
class="font-poppins font-normal text-base text-quick-silver w-20"
>Tags</span
>
<span
class="font-poppins font-medium text-base text-quick-silver mx-2"
>:</span
>
<span
class="font-poppins font-normal text-base text-quick-silver"
>Sofa, Chair, Home, Shop</span
>
</div>
<div class="flex items-center">
<span
class="font-poppins font-normal text-base text-quick-silver w-20"
>Dimension</span
>
<span
class="font-poppins font-medium text-base text-quick-silver mx-2"
>:</span
>
<span
class="font-poppins font-normal text-base text-quick-silver"
>180cm x 85cm x 75cm</span
>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Product Detail Tabs Section -->
<section id="product-tabs" class="w-full bg-white">
<div class="border-t border-light-silver"></div>
<div class="max-w-7xl mx-auto px-5 py-10">
<!-- Tabs -->
<div class="flex justify-center gap-12 mb-8">
<button class="font-playfair text-2xl text-black">
Description
</button>
<button class="font-playfair text-2xl text-quick-silver">
Additional Information
</button>
<button class="font-playfair text-2xl text-quick-silver">
Reviews [5]
</button>
</div>
<!-- Copy -->
<div class="max-w-5xl mx-auto space-y-6">
<p
class="font-playfair text-base leading-6 text-[#333333] text-justify"
>
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>
</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>
</div>
</div>
</section>
<!-- full-width divider before related products -->
<div class="w-full border-t border-light-silver"></div>
<!-- Related Products Section -->
<section id="related-products" class="w-full bg-white py-14">
<div class="max-w-7xl mx-auto px-5">
<h2
class="font-playfair text-4xl font-medium text-center text-black mb-10"
>
Related Products
</h2>
<div
id="related-grid"
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8"
>
<!-- Related items will be injected here -->
</div>
<div class="mt-10 flex justify-center">
<button
id="related-show-more"
class="w-[245px] h-12 border border-uc-gold rounded-md bg-white font-playfair font-semibold text-base text-uc-gold hover:bg-uc-gold hover:text-white transition-colors"
>
Show More
</button>
</div>
</div>
</section>
<!-- full-width divider after related products -->
<div class="w-full border-t border-light-silver"></div>
</main>
<!-- Footer -->
<footer class="bg-white border-t border-black border-opacity-20">
<!-- Main Footer Content -->
<div class="max-w-7xl mx-auto px-5 py-16">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<!-- Company Information -->
<div class="space-y-1">
<!-- Logo -->
<div class="w-16 h-20 -mt-6">
<a
href="index.html"
aria-label="Go to KHY home"
title="KHY Home"
class="inline-block w-full h-full group focus:outline-none focus-visible:ring-2 focus-visible:ring-uc-gold rounded-md transition"
>
<img
src="assets/images/khy_logo.png"
alt="KHY logo"
loading="lazy"
class="w-full h-full object-contain transition-transform duration-300 group-hover:scale-105"
/>
</a>
</div>
<!-- Address -->
<p
class="font-playfair font-normal text-base leading-relaxed text-gray-600 -mt-4"
>
5 Labone Crescent, Greater Accra, Ghana
</p>
<!-- Contact Info -->
<div class="space-y-1 -mt-2">
<!-- Phone -->
<div class="flex items-center space-x-3 -mt-1">
<img
src="assets/images/phone.png"
alt="Phone"
class="w-4 h-4"
/>
<span class="font-playfair font-normal text-base text-gray-800">
+233 (555) 76677
</span>
</div>
<!-- Email -->
<div class="flex items-center space-x-3 -mt-1">
<img src="assets/images/mail.png" alt="Email" class="w-4 h-4" />
<span class="font-playfair font-normal text-base text-gray-800">
design@khyltd.com
</span>
</div>
</div>
</div>
<!-- Quick Links -->
<div class="space-y-6">
<h3
class="font-playfair font-normal text-md leading-relaxed tracking-wider text-eerie-black uppercase"
>
Quick Links
</h3>
<div class="space-y-4">
<a
href="#"
class="block font-playfair font-normal text-base leading-relaxed tracking-wider text-gray-800 hover:text-black transition-colors"
>
Home
</a>
<a
href="#"
class="block font-playfair font-normal text-base leading-relaxed tracking-wider text-gray-800 hover:text-black transition-colors"
>
Products
</a>
<a
href="#"
class="block font-playfair font-normal text-base leading-relaxed tracking-wider text-gray-800 hover:text-black transition-colors"
>
About
</a>
</div>
</div>
<!-- Help -->
<div class="space-y-6">
<h3
class="font-playfair font-normal text-md leading-relaxed tracking-wider text-eerie-black uppercase"
>
Help
</h3>
<div class="space-y-4">
<a
href="#"
class="block font-playfair font-normal text-base leading-relaxed tracking-wider text-gray-800 hover:text-black transition-colors"
>
Contact Us
</a>
<a
href="#"
class="block font-playfair font-normal text-base leading-relaxed tracking-wider text-gray-800 hover:text-black transition-colors"
>
Privacy Policies
</a>
<a
href="#"
class="block font-playfair font-normal text-base leading-relaxed tracking-wider text-gray-800 hover:text-black transition-colors"
>
Terms and Conditions
</a>
</div>
</div>
<!-- Newsletter -->
<div class="space-y-6">
<h3
class="font-playfair font-normal text-md leading-relaxed tracking-wider text-eerie-black uppercase"
>
Newsletter
</h3>
<div class="space-y-4">
<div class="flex items-center space-x-4">
<input
type="email"
placeholder="Enter Your Email Address"
class="flex-1 font-playfair font-normal text-sm leading-relaxed tracking-wider text-taupe-gray bg-transparent border-b border-black focus:outline-none focus:border-black"
/>
<button
class="font-playfair font-normal text-sm leading-relaxed tracking-wider text-gray-800 border-b border-black hover:text-black transition-colors"
>
Subscribe
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Copyright Section -->
<div class="border-t border-light-silver">
<div class="max-w-7xl mx-auto px-5 py-4">
<p
class="font-playfair font-normal text-xs leading-relaxed text-davys-grey"
>
© 2025 khy. All rights reserved.
</p>
</div>
</div>
</footer>
<script src="scripts/main.js"></script>
</body>
</html>

View file

@ -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
View 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();
});

View 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"
);

File diff suppressed because it is too large Load diff

View file

@ -35,6 +35,9 @@ module.exports = {
150: "150px",
100: "100px",
120: "120px",
60: "240px",
180: "720px",
360: "1440px",
},
textShadow: {
default: "0 2px 4px rgba(0,0,0,0.1)",