Initial commit: KHY Admin project

This commit is contained in:
George Birikorang 2025-09-10 16:56:52 -07:00
commit d3403cb5e2
65 changed files with 12739 additions and 0 deletions

71
.gitignore vendored Normal file
View file

@ -0,0 +1,71 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
# nyc test coverage
.nyc_output
# Grunt intermediate storage
.grunt
# Bower dependency directory
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons
build/Release
# Dependency directories
jspm_packages/
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
logs
*.log
# Temporary files
tmp/
temp/

150
DEPLOY_SETUP.md Normal file
View file

@ -0,0 +1,150 @@
# Deploy Setup Guide
This guide explains how to configure the admin dashboard to deploy to your main KHY website when they are in separate repositories.
## Repository Structure Examples
### Example 1: Sibling Directories
```
Documents/
├── khy_website/ # Main website repository
│ ├── index.html
│ ├── data/
│ └── assets/
└── khy_admin/ # Admin repository
├── admin.html
├── data/
└── assets/
```
**Setup**: Run `npm run setup` and enter `../khy_website`
### Example 2: Different Parent Directories
```
Documents/
├── projects/
│ └── khy_website/ # Main website repository
└── admin_tools/
└── khy_admin/ # Admin repository
```
**Setup**: Run `npm run setup` and enter `../../projects/khy_website`
### Example 3: Absolute Paths
```
/Users/george/Documents/khy_website/ # Main website
/Users/george/Documents/admin_tools/ # Admin repository
```
**Setup**: Run `npm run setup` and enter `/Users/george/Documents/khy_website`
## Setup Process
1. **Run the setup script**:
```bash
npm run setup
```
2. **Enter the path to your main KHY website**:
- Use relative paths like `../khy_website` or `../../projects/khy_website`
- Use absolute paths like `/full/path/to/khy_website`
3. **Verify the configuration**:
- The script will check if the path exists
- It will verify required files are present
- It will update `deploy-config.js` with your settings
## Manual Configuration
If you prefer to configure manually, edit `deploy-config.js`:
```javascript
module.exports = {
targets: {
main: "../khy_website", // Change this to your path
},
defaultTarget: "main",
// ... rest of config
};
```
## Testing the Setup
After setup, test the configuration:
```bash
# Test with the configured path
npm run deploy
# Test with a specific path
node deploy.js /path/to/your/khy_website
```
## Troubleshooting
### "Target directory does not exist"
- Check the path you entered during setup
- Use absolute paths if relative paths don't work
- Make sure the main website directory exists
### "Missing required files"
- Ensure your main website has `index.html` and `data/products.json`
- The admin will create these files if they don't exist
### "Permission denied"
- Make sure you have write permissions to the main website directory
- On macOS/Linux, you might need to adjust file permissions
## Advanced Usage
### Multiple Targets
You can configure multiple deployment targets:
```javascript
targets: {
staging: "../khy_website_staging",
production: "../khy_website",
backup: "/backup/khy_website"
}
```
Then deploy to specific targets:
```bash
node deploy.js staging
node deploy.js production
```
### Custom File Mappings
Modify `filesToCopy` in `deploy-config.js` to copy different files:
```javascript
filesToCopy: [
{
source: "data/products.json",
target: "data/products.json",
description: "Product catalog data",
},
{
source: "assets/images",
target: "assets/images",
description: "Product images",
isDirectory: true,
},
{
source: "config/settings.json",
target: "config/admin-settings.json",
description: "Admin settings",
},
];
```

146
README.md Normal file
View file

@ -0,0 +1,146 @@
# KHY Admin Dashboard
A **preview/staging environment** for managing KHY Furniture's product catalog with safe testing and one-click deployment to production.
## Features
- ✅ **Add Products**: Create new products with full details
- ✅ **Edit Products**: Modify existing product information
- ✅ **Delete Products**: Remove products with confirmation
- ✅ **Preview Products**: See how products will look before saving
- ✅ **Export JSON**: Download updated products.json file
- ✅ **Live Preview**: See exactly how changes will look on the website
- ✅ **One-Click Deploy**: Deploy changes to production with a single command
- ✅ **Automatic Backups**: Safe deployment with backup system
- ✅ **No Technical Knowledge Required**: Simple form-based interface
## Quick Start
1. **Setup deploy paths** (first time only):
```bash
npm run setup
# Follow the prompts to configure where your main KHY website is located
```
2. **Open the dashboard**:
```bash
# Option 1: Double-click admin.html in your file explorer
# Option 2: Run a local server
npm start
# Then open http://localhost:8080/admin.html
```
3. **Preview the website** (optional):
```bash
# Option A: Auto-start server and open all preview pages
npm run preview
# Option B: Start server manually
npm start
# Then open http://localhost:8080/index.html (homepage)
# Open http://localhost:8080/product-catalog.html (catalog)
# Open http://localhost:8080/product-detail.html (product details)
```
4. **Manage products**:
- Add new products using the form
- Edit existing products by clicking "Edit"
- Delete products by clicking "Delete"
- Preview changes before saving
5. **Deploy to production**:
```bash
# Option A: Automated deploy (recommended)
npm run deploy
# This copies admin/data/products.json → main website data/products.json
# And copies admin/assets/images/ → main website assets/images/
# Option B: Manual deploy
# Click "Download products.json" and replace in main website
```
## Complete Workflow
1. **Make Changes** → Use admin dashboard to add/edit/delete products
2. **Preview Changes** → Open `index.html`, `product-catalog.html`, `product-detail.html` to see how changes look
3. **Confirm Changes** → Review everything thoroughly
4. **Deploy to Production** → Run `npm run deploy` to copy updated files to the main KHY website
> 📖 **Detailed Workflow**: See [WORKFLOW.md](WORKFLOW.md) for complete instructions
## File Structure
```
admin/
├── admin.html # Main admin dashboard
├── index.html # Homepage (reference)
├── product-catalog.html # Product catalog page (reference)
├── product-detail.html # Product detail page (reference)
├── data/
│ └── products.json # Product data file
├── assets/ # Images and resources
├── scripts/ # JavaScript files
├── styles/ # CSS files
├── src/ # Source files
├── tailwind.config.js # Tailwind configuration
├── deploy.js # Deploy script
├── start-server.js # Auto-start server script
├── package.json # Project configuration
├── README.md # This file
└── WORKFLOW.md # Detailed workflow guide
```
## Usage Instructions
### Adding a New Product
1. Fill out the product form with:
- Product name and description
- Category selection
- Price and model number
- Available sizes and colors
- Stock status and rating
2. Click "Preview Product" to see how it looks
3. Click "Add Product" to save
### Editing a Product
1. Click "Edit" on any product in the list
2. The form will auto-fill with existing data
3. Make your changes
4. Click "Preview Product" to see changes
5. Click "Update Product" to save or "Cancel Edit" to abort
### Deleting a Product
1. Click "Delete" on any product
2. Confirm the deletion in the popup
3. Product is removed immediately
### Exporting Changes
1. After making all your changes
2. Click "Download products.json"
3. Replace the existing `data/products.json` file in your main website
4. Your website will show the updated products
## Technical Notes
- **No server required**: Works entirely in the browser
- **No database needed**: Uses JSON file storage
- **Cross-platform**: Works on Windows, Mac, Linux
- **Offline capable**: Works without internet connection
- **Simple deployment**: Just copy files to any web server
## Support
For technical support or questions about the admin dashboard, contact your development team.
## Version History
- **v1.0.0**: Initial release with full CRUD functionality

164
WORKFLOW.md Normal file
View file

@ -0,0 +1,164 @@
# KHY Admin Workflow Guide
## Overview
This admin dashboard provides a **preview/staging environment** where you can make changes, see exactly how they'll look on the website, and then deploy to production with confidence.
## Complete Workflow
### 1. 🎯 **Make Changes**
- Open `admin.html` in your browser
- Add, edit, or delete products using the form interface
- Use "Preview Product" to see how individual products will look
### 2. 👀 **Preview Changes**
#### Option A: Auto-Start Preview (Recommended)
```bash
# One command to start server and open all preview pages
npm run preview
# Server will automatically stop when:
# - All preview tabs are closed
# - Deploy command is run
# - Manual stop: npm run preview:kill
```
#### Option B: Manual Preview
- Click "👀 Preview Website" button in admin dashboard
- Or manually open `index.html`, `product-catalog.html`, `product-detail.html`
- Navigate through the site to ensure everything looks correct
### 3. ✅ **Confirm Changes**
- Review all changes thoroughly
- Test product links and functionality
- Ensure images load correctly
### 4. 🚀 **Deploy to Production**
The deploy process copies updated files from the **admin directory** to the **main KHY website directory**.
#### Option A: Automated Deploy (Recommended)
```bash
# Navigate to admin directory
cd admin
# Copy updated files to main website
npm run deploy
```
**What this does:**
- Copies `admin/data/products.json``../data/products.json` (main website)
- Copies `admin/assets/images/``../assets/images/` (main website)
- Creates backup of current main website files
- Updates the live KHY website immediately
#### Option B: Manual Deploy
1. Click "Download products.json" in the admin dashboard
2. Manually copy the downloaded file to `../data/products.json`
3. Manually copy any new images from `admin/assets/images/` to `../assets/images/`
## File Structure
```
admin/ # Admin/Staging Environment
├── admin.html # Admin dashboard
├── index.html # Homepage preview
├── product-catalog.html # Catalog preview
├── product-detail.html # Product detail preview
├── data/products.json # Staging product data
├── assets/images/ # Staging images
├── deploy.js # Deploy script
└── package.json # NPM scripts
../ # Main Website (Production)
├── index.html # Live homepage
├── product-catalog.html # Live catalog
├── product-detail.html # Live product details
├── data/products.json # Live product data
└── assets/images/ # Live images
```
## Key Benefits
**Safe Testing**: Make changes without affecting the live site
**Visual Preview**: See exactly how changes will look
**Easy Deployment**: One-click deploy to production
**Backup System**: Automatic backups before deployment
**No Technical Knowledge**: Simple interface for non-technical users
## Deployment Process
### What Happens During Deploy:
1. **Stop Preview Server**: Automatically stops the preview server (if running)
2. **Validation**: Checks that main KHY website directory exists and has required files
3. **Backup**: Creates timestamped backup of current main website data
4. **Copy Files**:
- Copies `admin/data/products.json``../data/products.json` (main website)
- Copies `admin/assets/images/``../assets/images/` (main website)
5. **Verification**: Confirms all files were copied successfully
6. **Completion**: Main KHY website is updated immediately with new product data
### Backup System:
- All deployments create automatic backups in `admin/backups/`
- Backups are timestamped for easy identification
- Can restore from backup if needed
## Troubleshooting
### Deploy Fails?
- Check that you're in the admin directory
- Verify the main website directory exists
- Ensure you have write permissions
### Changes Not Showing?
- Clear browser cache (Ctrl+F5 or Cmd+Shift+R)
- Check that the correct `products.json` file was updated
- Verify image paths are correct
### Need to Restore?
- Find the backup in `admin/backups/`
- Copy the backup `products.json` to the main website
- Or run the deploy script again
## Best Practices
1. **Always Preview**: Use the preview pages before deploying
2. **Test Everything**: Check all product links and images
3. **Deploy Regularly**: Don't let changes pile up
4. **Keep Backups**: The system creates them automatically
5. **Document Changes**: Keep notes of what you changed and why
## Quick Commands
```bash
# Start preview server
npm start
# Deploy to production
npm run deploy
# View help
npm run preview
```
## Support
If you encounter any issues:
1. Check the browser console for errors
2. Verify file permissions
3. Ensure all required files exist
4. Contact your development team for assistance

728
admin.html Normal file
View file

@ -0,0 +1,728 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Product Management - KHY Admin</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.admin-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.product-card {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 4px;
font-weight: 600;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 8px;
border: 1px solid #d1d5db;
border-radius: 4px;
}
.btn {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
border: none;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-success {
background: #10b981;
color: white;
}
.color-input {
width: 60px;
height: 40px;
border: none;
border-radius: 4px;
}
.image-preview {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 4px;
}
</style>
</head>
<body class="bg-gray-50">
<div class="admin-container">
<h1 class="text-3xl font-bold mb-8">Product Management</h1>
<!-- Add New Product Form -->
<div class="bg-white p-6 rounded-lg shadow mb-8">
<h2 class="text-xl font-semibold mb-4">Add New Product</h2>
<!-- Preview Section -->
<div
id="previewSection"
class="hidden mb-6 p-4 bg-gray-50 rounded-lg border"
>
<h3 class="font-semibold mb-3">Preview</h3>
<div id="previewContent"></div>
</div>
<form id="productForm">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-group">
<label>Product Name</label>
<input type="text" id="productName" required />
</div>
<div class="form-group">
<label>Category</label>
<select id="productCategory" required>
<option value="seating">Seating</option>
<option value="tables">Tables</option>
<option value="storage">Storage</option>
<option value="workspace">Workspace</option>
</select>
</div>
<div class="form-group">
<label>Price</label>
<input type="number" id="productPrice" step="0.01" required />
</div>
<div class="form-group">
<label>Model Number</label>
<input type="text" id="productModel" />
</div>
<div class="form-group">
<label>Short Description</label>
<textarea id="productDescription" rows="2"></textarea>
</div>
<div class="form-group">
<label>Main Image URL</label>
<input
type="text"
id="productImage"
placeholder="assets/images/product.jpg"
/>
</div>
<div class="form-group">
<label>In Stock</label>
<select id="productInStock">
<option value="true">Yes</option>
<option value="false">No</option>
</select>
</div>
<div class="form-group">
<label>Rating</label>
<input
type="number"
id="productRating"
step="0.1"
min="0"
max="5"
value="4.0"
/>
</div>
</div>
<div class="mt-4">
<h3 class="font-semibold mb-2">Available Sizes</h3>
<div id="sizesContainer">
<div class="flex gap-2 mb-2">
<input
type="text"
placeholder="Size (e.g., S, M, L)"
class="size-input"
/>
<button
type="button"
onclick="removeSize(this)"
class="btn btn-danger"
>
Remove
</button>
</div>
</div>
<button type="button" onclick="addSize()" class="btn btn-primary">
Add Size
</button>
</div>
<div class="mt-4">
<h3 class="font-semibold mb-2">Available Colors</h3>
<div id="colorsContainer">
<div class="flex gap-2 mb-2 items-center">
<input
type="text"
placeholder="Color Name"
class="color-name"
/>
<input type="color" class="color-input" value="#000000" />
<button
type="button"
onclick="removeColor(this)"
class="btn btn-danger"
>
Remove
</button>
</div>
</div>
<button type="button" onclick="addColor()" class="btn btn-primary">
Add Color
</button>
</div>
<div class="mt-6 flex gap-4">
<button type="submit" class="btn btn-success">Add Product</button>
<button
type="button"
onclick="previewProduct()"
class="btn btn-primary"
>
Preview Product
</button>
<button
type="button"
onclick="cancelEdit()"
class="btn btn-danger hidden"
id="cancelEditBtn"
>
Cancel Edit
</button>
</div>
</form>
</div>
<!-- Products List -->
<div class="bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold">Current Products</h2>
<div class="flex gap-2">
<button onclick="previewWebsite()" class="btn btn-primary">
👀 Preview Website
</button>
<button onclick="downloadProductsJSON()" class="btn btn-primary">
Download products.json
</button>
<button onclick="deployToProduction()" class="btn btn-success">
🚀 Copy to Main Website
</button>
</div>
</div>
<div id="productsList"></div>
</div>
</div>
<script>
let products = [];
// Load products on page load
async function loadProducts() {
try {
const response = await fetch("data/products.json");
const data = await response.json();
products = data.products;
displayProducts();
} catch (error) {
console.error("Error loading products:", error);
alert("Error loading products. Please refresh the page.");
}
}
// Display products
function displayProducts() {
const container = document.getElementById("productsList");
container.innerHTML = "";
products.forEach((product, index) => {
const productCard = document.createElement("div");
productCard.className = "product-card";
productCard.innerHTML = `
<div class="flex justify-between items-start">
<div class="flex gap-4">
<img src="${product.image}" alt="${product.name}" class="image-preview" onerror="this.src='assets/images/placeholder.jpg'">
<div>
<h3 class="font-semibold">${product.name}</h3>
<p class="text-gray-600">${product.category} • $${product.price}</p>
<p class="text-sm text-gray-500">${product.description}</p>
</div>
</div>
<div class="flex gap-2">
<button onclick="editProduct(${index})" class="btn btn-primary">Edit</button>
<button onclick="deleteProduct(${index})" class="btn btn-danger">Delete</button>
</div>
</div>
`;
container.appendChild(productCard);
});
}
// Add new product or update existing
document
.getElementById("productForm")
.addEventListener("submit", function (e) {
e.preventDefault();
const sizes = Array.from(document.querySelectorAll(".size-input"))
.map((input) => input.value)
.filter((size) => size.trim());
const colors = Array.from(document.querySelectorAll(".color-name"))
.map((input, index) => {
const colorInput =
input.parentElement.querySelector(".color-input");
return {
name: input.value,
value: colorInput.value,
selected: index === 0,
};
})
.filter((color) => color.name.trim());
const productData = {
name: document.getElementById("productName").value,
description: document.getElementById("productDescription").value,
image: document.getElementById("productImage").value,
alt: document.getElementById("productName").value,
category: document.getElementById("productCategory").value,
modelNo: document.getElementById("productModel").value,
tags: [document.getElementById("productCategory").value],
sizes: sizes,
colors: colors,
selectedSize: sizes[0] || "M",
selectedColor: colors[0]?.name || "Default",
price: parseFloat(document.getElementById("productPrice").value),
originalPrice: parseFloat(
document.getElementById("productPrice").value
),
rating: document.getElementById("productRating").value,
reviews: 0,
inStock: document.getElementById("productInStock").value === "true",
images: [document.getElementById("productImage").value],
descriptionLong: [
document.getElementById("productDescription").value,
],
additionalInformation: {
Material: "Premium materials",
Dimensions: "See size options",
Warranty: "3 years",
},
reviewsCount: 0,
galleryPairs: [
{
left: document.getElementById("productImage").value,
right: document.getElementById("productImage").value,
},
],
dimensions: "See size options",
salesPackage: "1 unit",
configuration: "Standard",
fillingMaterial: "High-density foam",
finishType: "Premium finish",
adjustableHeadrest: "No",
maxLoadCapacity: "150kg",
originOfManufacture: "Ghana",
weight: "25kg",
seatHeight: "45cm",
legHeight: "10cm",
warrantyServiceType: "Standard warranty service",
coveredInWarranty: "Manufacturing defects",
notCoveredInWarranty: "Wear and tear not covered",
};
const form = document.getElementById("productForm");
const editIndex = form.dataset.editIndex;
if (editIndex !== undefined) {
// Update existing product
productData.id = products[editIndex].id;
products[editIndex] = productData;
alert("Product updated successfully!");
// Reset form to add mode
form.dataset.editIndex = "";
form.querySelector("button[type='submit']").textContent =
"Add Product";
} else {
// Add new product
productData.id = Math.max(...products.map((p) => p.id)) + 1;
products.push(productData);
alert("Product added successfully!");
}
displayProducts();
document.getElementById("productForm").reset();
hidePreview();
});
// Helper functions
function addSize() {
const container = document.getElementById("sizesContainer");
const div = document.createElement("div");
div.className = "flex gap-2 mb-2";
div.innerHTML = `
<input type="text" placeholder="Size (e.g., S, M, L)" class="size-input">
<button type="button" onclick="removeSize(this)" class="btn btn-danger">Remove</button>
`;
container.appendChild(div);
}
function removeSize(button) {
button.parentElement.remove();
}
function addColor() {
const container = document.getElementById("colorsContainer");
const div = document.createElement("div");
div.className = "flex gap-2 mb-2 items-center";
div.innerHTML = `
<input type="text" placeholder="Color Name" class="color-name">
<input type="color" class="color-input" value="#000000">
<button type="button" onclick="removeColor(this)" class="btn btn-danger">Remove</button>
`;
container.appendChild(div);
}
function removeColor(button) {
button.parentElement.remove();
}
function editProduct(index) {
const product = products[index];
// Populate form with existing product data
document.getElementById("productName").value = product.name;
document.getElementById("productDescription").value =
product.description;
document.getElementById("productImage").value = product.image;
document.getElementById("productCategory").value = product.category;
document.getElementById("productPrice").value = product.price;
document.getElementById("productModel").value = product.modelNo || "";
document.getElementById("productInStock").value =
product.inStock.toString();
document.getElementById("productRating").value = product.rating;
// Clear existing sizes and colors
document.getElementById("sizesContainer").innerHTML = "";
document.getElementById("colorsContainer").innerHTML = "";
// Add existing sizes
product.sizes.forEach((size) => {
addSize();
const sizeInputs = document.querySelectorAll(".size-input");
sizeInputs[sizeInputs.length - 1].value = size;
});
// Add existing colors
product.colors.forEach((color) => {
addColor();
const colorInputs = document.querySelectorAll(".color-name");
const colorPickers = document.querySelectorAll(".color-input");
colorInputs[colorInputs.length - 1].value = color.name;
colorPickers[colorPickers.length - 1].value = color.value;
});
// Change form to edit mode
const form = document.getElementById("productForm");
form.dataset.editIndex = index;
form.querySelector("button[type='submit']").textContent =
"Update Product";
document.getElementById("cancelEditBtn").classList.remove("hidden");
// Scroll to form
form.scrollIntoView({ behavior: "smooth" });
// Show preview
showPreview(product);
}
function deleteProduct(index) {
const product = products[index];
const confirmMessage = `Are you sure you want to delete "${product.name}"?\n\nThis action cannot be undone.`;
if (confirm(confirmMessage)) {
products.splice(index, 1);
displayProducts();
alert(`"${product.name}" has been deleted successfully!`);
}
}
function previewProduct() {
const sizes = Array.from(document.querySelectorAll(".size-input"))
.map((input) => input.value)
.filter((size) => size.trim());
const colors = Array.from(document.querySelectorAll(".color-name"))
.map((input, index) => {
const colorInput =
input.parentElement.querySelector(".color-input");
return {
name: input.value,
value: colorInput.value,
selected: index === 0,
};
})
.filter((color) => color.name.trim());
const previewProduct = {
name: document.getElementById("productName").value || "Product Name",
description:
document.getElementById("productDescription").value ||
"Product description",
image:
document.getElementById("productImage").value ||
"assets/images/placeholder.jpg",
category:
document.getElementById("productCategory").value || "seating",
price: document.getElementById("productPrice").value || "0",
modelNo: document.getElementById("productModel").value || "",
sizes: sizes,
colors: colors,
inStock: document.getElementById("productInStock").value === "true",
rating: document.getElementById("productRating").value || "4.0",
};
showPreview(previewProduct);
}
function showPreview(product) {
const previewSection = document.getElementById("previewSection");
const previewContent = document.getElementById("previewContent");
previewContent.innerHTML = `
<div class="flex gap-4">
<img src="${product.image}" alt="${
product.name
}" class="w-24 h-24 object-cover rounded" onerror="this.src='assets/images/placeholder.jpg'">
<div class="flex-1">
<h4 class="font-semibold text-lg">${product.name}</h4>
<p class="text-gray-600">${product.category} • $${
product.price
}</p>
<p class="text-sm text-gray-500 mb-2">${product.description}</p>
<div class="text-xs text-gray-400">
<p><strong>Model:</strong> ${product.modelNo || "N/A"}</p>
<p><strong>Sizes:</strong> ${
product.sizes.length > 0 ? product.sizes.join(", ") : "None"
}</p>
<p><strong>Colors:</strong> ${
product.colors.length > 0
? product.colors.map((c) => c.name).join(", ")
: "None"
}</p>
<p><strong>In Stock:</strong> ${
product.inStock ? "Yes" : "No"
}</p>
<p><strong>Rating:</strong> ${product.rating}/5</p>
</div>
</div>
</div>
`;
previewSection.classList.remove("hidden");
previewSection.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
function hidePreview() {
const previewSection = document.getElementById("previewSection");
previewSection.classList.add("hidden");
}
function cancelEdit() {
const form = document.getElementById("productForm");
form.dataset.editIndex = "";
form.querySelector("button[type='submit']").textContent = "Add Product";
document.getElementById("cancelEditBtn").classList.add("hidden");
document.getElementById("productForm").reset();
hidePreview();
}
function downloadProductsJSON() {
const data = {
products: products,
categories: [
{
id: "seating",
name: "Seating",
description:
"Office chairs, lounge chairs, and seating solutions",
},
{
id: "tables",
name: "Tables",
description: "Conference tables, workstations, and dining tables",
},
{
id: "storage",
name: "Storage",
description:
"Storage units, lockers, and organizational solutions",
},
{
id: "workspace",
name: "Workspace",
description: "Pods, phone booths, and collaborative spaces",
},
],
pagination: {
itemsPerPage: 16,
totalItems: products.length,
currentPage: 1,
totalPages: Math.ceil(products.length / 16),
},
};
const blob = new Blob([JSON.stringify(data, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "products.json";
a.click();
URL.revokeObjectURL(url);
alert(`Downloaded products.json with ${products.length} products!`);
}
function previewWebsite() {
const baseUrl =
window.location.origin +
window.location.pathname.replace("admin.html", "");
const previewMessage = `Preview Website\n\nThis will help you preview how your products will look on the website.\n\nChoose your preferred method:\n\nOption 1: Auto-start server (Recommended)\n- Click OK to get instructions for running: npm run preview\n- This will start the server and open all preview pages automatically\n\nOption 2: Open preview pages now\n- Click Cancel to open preview pages in current browser\n- Make sure you have a server running first`;
if (confirm(previewMessage)) {
// Show instructions for auto-start
alert(
`🚀 Auto-Start Preview Server\n\nTo automatically start the server and open preview pages:\n\n1. Open Terminal/Command Prompt\n2. Navigate to the admin folder\n3. Run: npm run preview\n\nThis will:\n- Start the local server\n- Open admin dashboard\n- Open homepage preview\n- Open product catalog preview\n- Open product details preview\n\nAll in one command! 🎉`
);
} else {
// Open preview pages in current browser
const pages = [
{ url: "index.html", name: "Homepage" },
{ url: "product-catalog.html", name: "Product Catalog" },
{ url: "product-detail.html", name: "Product Details" },
];
pages.forEach((page, index) => {
setTimeout(() => {
window.open(page.url, `_blank`);
}, index * 500); // Stagger opening to avoid popup blockers
});
// Show helpful message
setTimeout(() => {
alert(
`👀 Preview Pages Opened\n\nOpened in new tabs:\n- Homepage\n- Product Catalog\n- Product Details\n\nIf tabs didn't open, check your popup blocker settings.\n\n💡 For the best experience, run: npm run preview`
);
}, 2000);
}
}
function deployToProduction() {
const confirmMessage = `Copy to Main Website\n\nThis will copy your updated product data to the main KHY website.\n\nAre you sure you want to proceed?\n\nThis will:\n- Stop the preview server (if running)\n- Copy admin/data/products.json → ../data/products.json\n- Copy admin/assets/images/ → ../assets/images/\n- Create a backup of current main website data\n- Update the main KHY website immediately\n\nMake sure you've previewed your changes first!`;
if (confirm(confirmMessage)) {
// Show deployment instructions
alert(
`Copy Instructions\n\nTo copy your changes to the main KHY website:\n\n1. Open Terminal/Command Prompt\n2. Navigate to the admin folder\n3. Run: npm run deploy\n\nThis will:\n- Automatically stop the preview server\n- Copy admin/data/products.json → main website data/products.json\n- Copy admin/assets/images/ → main website assets/images/\n\nYour main KHY website will be updated immediately!\n\nThe preview server will be automatically stopped during deployment.\n\nNote: If this is your first time, run 'npm run setup' first to configure the deploy paths.`
);
}
}
// Server management
let previewTabs = [];
// Function to kill preview server
function killPreviewServer() {
// This would need to be implemented with a backend endpoint
// For now, we'll show instructions
alert(
`Stop Preview Server\n\nTo stop the preview server:\n\n1. Open Terminal/Command Prompt\n2. Navigate to the admin folder\n3. Run: npm run preview:kill\n\nOr press Ctrl+C in the terminal where the server is running.`
);
}
// Function to track preview tabs
function trackPreviewTab(tab) {
previewTabs.push(tab);
// Check if tab is closed
const checkClosed = setInterval(() => {
if (tab.closed) {
previewTabs = previewTabs.filter((t) => t !== tab);
clearInterval(checkClosed);
// If no preview tabs are open, offer to kill server
if (previewTabs.length === 0) {
setTimeout(() => {
if (
confirm(
`All Preview Tabs Closed\n\nAll preview tabs have been closed.\n\nWould you like to stop the preview server?\n\nThis will free up the port for other uses.`
)
) {
killPreviewServer();
}
}, 1000);
}
}
}, 1000);
}
// Enhanced preview function with tab tracking
function previewWebsiteWithTracking() {
const baseUrl =
window.location.origin +
window.location.pathname.replace("admin.html", "");
const previewMessage = `Preview Website\n\nThis will help you preview how your products will look on the website.\n\nChoose your preferred method:\n\nOption 1: Auto-start server (Recommended)\n- Click OK to get instructions for running: npm run preview\n- This will start the server and open all preview pages automatically\n\nOption 2: Open preview pages now\n- Click Cancel to open preview pages in current browser\n- Make sure you have a server running first`;
if (confirm(previewMessage)) {
// Show instructions for auto-start
alert(
`Auto-Start Preview Server\n\nTo automatically start the server and open preview pages:\n\n1. Open Terminal/Command Prompt\n2. Navigate to the admin folder\n3. Run: npm run preview\n\nThis will:\n- Start the local server\n- Open admin dashboard\n- Open homepage preview\n- Open product catalog preview\n- Open product details preview\n\nAll in one command!\n\nThe server will automatically stop when you close preview tabs or deploy changes.`
);
} else {
// Open preview pages in current browser with tracking
const pages = [
{ url: "index.html", name: "Homepage" },
{ url: "product-catalog.html", name: "Product Catalog" },
{ url: "product-detail.html", name: "Product Details" },
];
pages.forEach((page, index) => {
setTimeout(() => {
const tab = window.open(page.url, `_blank`);
if (tab) {
trackPreviewTab(tab);
}
}, index * 500); // Stagger opening to avoid popup blockers
});
// Show helpful message
setTimeout(() => {
alert(
`Preview Pages Opened\n\nOpened in new tabs:\n- Homepage\n- Product Catalog\n- Product Details\n\nIf tabs didn't open, check your popup blocker settings.\n\nFor the best experience, run: npm run preview\n\nThe server will be automatically stopped when you close all preview tabs.`
);
}, 2000);
}
}
// Override the original preview function
window.previewWebsite = previewWebsiteWithTracking;
// Load products when page loads
loadProducts();
</script>
</body>
</html>

BIN
assets/icons/address.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

BIN
assets/icons/admin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 B

BIN
assets/icons/calendar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

BIN
assets/icons/phone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

BIN
assets/icons/search.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 889 B

BIN
assets/icons/shipping.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
assets/icons/star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/icons/support.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
assets/icons/tag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

BIN
assets/icons/time.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 B

BIN
assets/icons/trophy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
assets/icons/warranty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

BIN
assets/images/bene.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
assets/images/chairs.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 KiB

BIN
assets/images/dbg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

BIN
assets/images/forma_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
assets/images/glico.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
assets/images/grow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
assets/images/khy_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
assets/images/kitchen.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

BIN
assets/images/mail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 B

BIN
assets/images/norfund.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

BIN
assets/images/our_story.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
assets/images/phone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/images/pods.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

BIN
assets/images/potty.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

BIN
assets/images/stanbic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
assets/images/storage.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

104
data/blog.json Normal file
View file

@ -0,0 +1,104 @@
{
"posts": [
{
"id": 1,
"title": "Going all-in with millennial design",
"excerpt": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Mus mauris vitae ultricies leo integer malesuada nunc. In nulla posuere sollicitudin aliquam ultrices. Morbi blandit cursus risus at ultrices mi tempus imperdiet. Libero enim sed faucibus turpis in. Cursus mattis molestie a iaculis at erat. Nibh cras pulvinar mattis nunc sed blandit libero. Pellentesque elit ullamcorper dignissim cras tincidunt. Pharetra et ultrices neque ornare aenean euismod elementum. The millennial generation has redefined what it means to create a home that reflects both personal style and contemporary values. This design philosophy embraces clean lines, sustainable materials, and multifunctional spaces that adapt to the ever-changing needs of modern life. From open-concept layouts that foster social interaction to smart home technology that enhances daily convenience, millennial design prioritizes both aesthetics and functionality.",
"content": [
"Quis hendrerit dolor magna eget est lorem ipsum. Bibendum arcu vitae elementum curabitur vitae nunc sed velit dignissim sodales. Non tellus orci ac auctor augue mauris augue neque gravida in. Nisl condimentum id venenatis a condimentum vitae sapien pellentesque habitant. Sit amet volutpat consequat mauris nunc congue nisi vitae suscipit. Integer enim neque volutpat ac tincidunt vitae semper quis lectus. Nibh tellus molestie nunc non blandit massa enim nec. Amet consectetur adipiscing elit ut aliquam purus sit amet luctus.",
"At tellus at urna condimentum mattis pellentesque id nibh tortor id. Non quam lacus suspendisse faucibus interdum. Interdum velit euismod in pellentesque massa placerat duis ultricies. Platea dictumst quisque sagittis purus sit amet volutpat consequat. Sem viverra aliquet eget sit amet tellus cras adipiscing. Eget lorem dolor sed viverra ipsum nunc aliquet bibendum enim facilisis gravida neque convallis a cras.",
"Mattis rhoncus urna neque viverra justo nec. Aliquet porttitor lacus luctus accumsan tortor posuere ac ut consequat semper viverra nam libero justo laoreet sit amet cursus sit amet dictum sit amet justo. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque elit.",
"The millennial generation has redefined what it means to create a home that reflects both personal style and contemporary values. This design philosophy embraces clean lines, sustainable materials, and multifunctional spaces that adapt to the ever-changing needs of modern life. From open-concept layouts that foster social interaction to smart home technology that enhances daily convenience, millennial design prioritizes both aesthetics and functionality.",
"One of the most significant aspects of millennial design is its emphasis on sustainability and ethical consumption. Young homeowners are increasingly choosing furniture and decor that not only looks good but also aligns with their environmental values. This includes pieces made from responsibly sourced materials, vintage and upcycled items that reduce waste, and locally crafted goods that support small businesses and reduce carbon footprints.",
"The color palette of millennial design often features neutral tones as a foundation, with strategic pops of color that reflect personal taste and current trends. Soft grays, warm whites, and natural wood tones create a calming backdrop, while accent colors like sage green, dusty rose, and deep navy add personality without overwhelming the space. This approach allows for easy updates and seasonal changes without requiring major renovations.",
"Technology integration is another hallmark of millennial design, with smart home features seamlessly incorporated into the aesthetic. From hidden charging stations to voice-controlled lighting systems, technology enhances the living experience without compromising the visual appeal of the space. This balance between high-tech functionality and timeless design creates homes that are both modern and enduring."
],
"author": "Admin",
"date": "2022-10-14",
"tags": ["design"],
"image": "assets/images/blog_post_1.png",
"thumbnail": "assets/images/blog_thumb_1.png",
"alt": "Going all-in with millennial design"
},
{
"id": 2,
"title": "How to create the perfect living room",
"excerpt": "Designing the perfect living room requires careful consideration of space, functionality, and personal style. From choosing the right furniture pieces to creating a cohesive color scheme, every element plays a crucial role in crafting a space that feels both welcoming and reflective of your lifestyle. Let's explore the key principles that make a living room truly exceptional. The foundation of any great living room begins with understanding the space you're working with. Consider the room's dimensions, natural light sources, and traffic flow. These factors will guide your furniture placement and help you create a layout that maximizes both comfort and functionality. When selecting furniture, prioritize pieces that serve multiple purposes and create a harmonious balance between form and function.",
"content": [
"The foundation of any great living room begins with understanding the space you're working with. Consider the room's dimensions, natural light sources, and traffic flow. These factors will guide your furniture placement and help you create a layout that maximizes both comfort and functionality.",
"When selecting furniture, prioritize pieces that serve multiple purposes. A comfortable sofa that can accommodate family gatherings, an accent chair that adds personality, and a coffee table that provides both surface area and storage can transform your living room into a versatile space that adapts to your daily needs.",
"Color and texture play vital roles in creating atmosphere. Choose a color palette that reflects your personal style while maintaining harmony throughout the space. Layer different textures through rugs, throw pillows, and decorative elements to add depth and visual interest."
],
"author": "Admin",
"date": "2022-11-05",
"tags": ["design"],
"image": "assets/images/conference_room.jpg",
"thumbnail": "assets/images/conference_room.jpg",
"alt": "Perfect living room design"
},
{
"id": 3,
"title": "Sustainable furniture choices for modern homes",
"excerpt": "As environmental consciousness grows, more homeowners are seeking sustainable furniture options that align with their values. From responsibly sourced materials to eco-friendly manufacturing processes, sustainable furniture offers both environmental benefits and long-term value. Discover how to make informed choices that benefit both your home and the planet. Sustainable furniture begins with the materials used in its construction. Look for pieces made from responsibly harvested wood, such as FSC-certified timber, or consider alternatives like bamboo, which grows rapidly and requires minimal resources. These materials not only reduce environmental impact but also often provide superior durability and timeless appeal.",
"content": [
"Sustainable furniture begins with the materials used in its construction. Look for pieces made from responsibly harvested wood, such as FSC-certified timber, or consider alternatives like bamboo, which grows rapidly and requires minimal resources. These materials not only reduce environmental impact but also often provide superior durability.",
"Manufacturing processes also play a crucial role in sustainability. Choose furniture from companies that prioritize energy-efficient production methods, minimize waste, and use non-toxic finishes. Many sustainable furniture makers also offer repair services and replacement parts, extending the lifespan of your investment.",
"Consider the full lifecycle of your furniture choices. Opt for timeless designs that won't quickly go out of style, and invest in quality pieces that can be passed down through generations. This approach reduces the need for frequent replacements and creates a more sustainable consumption pattern."
],
"author": "Admin",
"date": "2022-12-20",
"tags": ["sustainability"],
"image": "assets/images/kitchen.JPG",
"thumbnail": "assets/images/kitchen.JPG",
"alt": "Sustainable furniture choices"
},
{
"id": 4,
"title": "The art of mixing vintage and contemporary pieces",
"excerpt": "Creating a home that feels both timeless and current requires mastering the art of mixing vintage and contemporary furniture. This approach allows you to tell a story through your decor while maintaining a cohesive and functional space. Learn how to balance different eras and styles to create a home that's uniquely yours. The key to successfully mixing vintage and contemporary pieces lies in finding common threads that unite them. Look for shared design elements such as similar lines, complementary colors, or matching materials. A vintage wooden table might pair beautifully with modern chairs if they share similar wood tones or geometric shapes.",
"content": [
"The key to successfully mixing vintage and contemporary pieces lies in finding common threads that unite them. Look for shared design elements such as similar lines, complementary colors, or matching materials. A vintage wooden table might pair beautifully with modern chairs if they share similar wood tones or geometric shapes.",
"Scale and proportion are crucial when combining different eras. Ensure that vintage and contemporary pieces work together in terms of size and visual weight. A large vintage armoire might overwhelm a room filled with delicate modern furniture, while smaller vintage accents can add character without dominating the space.",
"Don't be afraid to experiment with unexpected combinations. Sometimes the most interesting interiors come from pairing pieces that seem to have nothing in common. The contrast between a sleek modern sofa and a rustic vintage coffee table can create a dynamic and engaging space that reflects your personal style."
],
"author": "Admin",
"date": "2023-01-15",
"tags": ["vintage"],
"image": "assets/images/lounge_chair.jpg",
"thumbnail": "assets/images/lounge_chair.jpg",
"alt": "Mixing vintage and contemporary furniture"
},
{
"id": 5,
"title": "Maximizing small spaces with smart furniture solutions",
"excerpt": "Living in a smaller space doesn't mean sacrificing style or functionality. With the right furniture choices and design strategies, you can create a home that feels spacious, organized, and beautiful. Discover innovative solutions that make the most of every square foot while maintaining your personal aesthetic. Multi-functional furniture is essential in small spaces. Look for pieces that serve multiple purposes, such as ottomans with hidden storage, sofa beds for guest accommodation, or dining tables that can expand when needed. These versatile pieces help you maximize functionality without cluttering your space.",
"content": [
"Multi-functional furniture is essential in small spaces. Look for pieces that serve multiple purposes, such as ottomans with hidden storage, sofa beds for guest accommodation, or dining tables that can expand when needed. These versatile pieces help you maximize functionality without cluttering your space.",
"Vertical storage solutions can dramatically increase your usable space. Wall-mounted shelves, tall bookcases, and hanging organizers keep your belongings organized while freeing up valuable floor space. Consider custom storage solutions that fit perfectly into awkward corners or underutilized areas.",
"The right color palette and lighting can make a small space feel much larger. Light, neutral colors reflect natural light and create an airy atmosphere, while strategic lighting can highlight focal points and create depth. Mirrors are also excellent tools for making small spaces feel more expansive."
],
"author": "Admin",
"date": "2023-02-28",
"tags": ["design"],
"image": "assets/images/storage.jpg",
"thumbnail": "assets/images/storage.jpg",
"alt": "Small space furniture solutions"
},
{
"id": 6,
"title": "Color psychology in home design",
"excerpt": "Colors have a profound impact on our emotions, mood, and overall well-being. Understanding color psychology can help you create spaces that not only look beautiful but also support your desired atmosphere and lifestyle. Explore how different colors can transform your home and enhance your daily experience. Warm colors like red, orange, and yellow create energy and excitement, making them perfect for social spaces like dining rooms and living areas. These colors stimulate conversation and appetite, making them ideal for areas where you entertain guests or share meals with family.",
"content": [
"Warm colors like red, orange, and yellow create energy and excitement, making them perfect for social spaces like dining rooms and living areas. These colors stimulate conversation and appetite, making them ideal for areas where you entertain guests or share meals with family.",
"Cool colors such as blue, green, and purple promote calmness and relaxation. These hues are excellent choices for bedrooms, bathrooms, and home offices where you want to create a peaceful, focused environment. Different shades can evoke different moods - deep navy creates sophistication while soft lavender promotes tranquility.",
"Neutral colors provide a versatile foundation that allows you to experiment with accent colors and seasonal changes. Whites, grays, and beiges create a timeless backdrop that can be easily updated with colorful accessories, artwork, or seasonal decor without requiring major furniture changes."
],
"author": "Admin",
"date": "2023-03-12",
"tags": ["vintage"],
"image": "assets/images/chairs.jpg",
"thumbnail": "assets/images/chairs.jpg",
"alt": "Color psychology in home design"
}
]
}

1832
data/products.json Normal file

File diff suppressed because it is too large Load diff

49
deploy-config.js Normal file
View file

@ -0,0 +1,49 @@
/**
* Deploy Configuration for KHY Admin Dashboard
*
* This file contains configuration for deploying from the admin repository
* to the main KHY website repository.
*/
module.exports = {
// Default target paths (adjust these based on your setup)
targets: {
// If admin and main website are in the same parent directory
sibling: "../khy_website",
// If admin is in a subdirectory of the main website
parent: "../",
// Absolute path examples (uncomment and modify as needed)
// absolute: "/path/to/your/khy_website",
// absolute: "/Users/george/Documents/khy_website/khy_website",
},
// Which target to use by default
defaultTarget: "sibling",
// Files to copy from admin to main website
filesToCopy: [
{
source: "data/products.json",
target: "data/products.json",
description: "Product catalog data",
},
{
source: "assets/images",
target: "assets/images",
description: "Product images",
isDirectory: true,
},
],
// Required files that must exist in target directory
requiredTargetFiles: ["index.html", "data/products.json"],
// Backup settings
backup: {
enabled: true,
directory: "backups",
timestampFormat: "YYYY-MM-DD_HH-mm-ss",
},
};

175
deploy.js Normal file
View file

@ -0,0 +1,175 @@
#!/usr/bin/env node
/**
* Deploy Script for KHY Admin Dashboard
*
* This script copies the updated products.json and any other changes
* from the admin directory to the main website directory.
*
* Usage: node deploy.js [target-directory]
*/
const fs = require("fs");
const path = require("path");
const { exec } = require("child_process");
const config = require("./deploy-config");
// Configuration
const ADMIN_DIR = __dirname;
// Get target directory from command line or use default
const TARGET_DIR = process.argv[2] || config.targets[config.defaultTarget];
const TARGET_PATH = path.resolve(ADMIN_DIR, TARGET_DIR);
console.log("KHY Admin Deploy Script");
console.log("=======================");
console.log(`Admin Directory: ${ADMIN_DIR}`);
console.log(`Target Directory: ${TARGET_PATH}`);
console.log("");
// Files to copy from admin to main website (from config)
const FILES_TO_COPY = config.filesToCopy;
// Function to copy file or directory
function copyFileOrDir(source, target, isDirectory = false) {
const sourcePath = path.join(ADMIN_DIR, source);
const targetPath = path.join(TARGET_PATH, target);
try {
if (isDirectory) {
// Copy directory recursively
if (fs.existsSync(targetPath)) {
fs.rmSync(targetPath, { recursive: true, force: true });
}
fs.cpSync(sourcePath, targetPath, { recursive: true });
} else {
// Copy file
const targetDir = path.dirname(targetPath);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
fs.copyFileSync(sourcePath, targetPath);
}
return true;
} catch (error) {
console.error(`❌ Error copying ${source}:`, error.message);
return false;
}
}
// Function to create backup
function createBackup() {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const backupDir = path.join(ADMIN_DIR, "backups", timestamp);
try {
fs.mkdirSync(backupDir, { recursive: true });
// Backup current products.json from target
const targetProducts = path.join(TARGET_PATH, "data/products.json");
if (fs.existsSync(targetProducts)) {
fs.copyFileSync(targetProducts, path.join(backupDir, "products.json"));
console.log(`📦 Backup created: ${backupDir}`);
return true;
}
} catch (error) {
console.error("❌ Error creating backup:", error.message);
}
return false;
}
// Function to kill preview server
function killPreviewServer() {
return new Promise((resolve) => {
console.log("Stopping preview server...");
exec("node kill-server.js", (error, stdout, stderr) => {
if (error) {
console.log("No preview server running");
} else {
console.log("Preview server stopped");
}
resolve();
});
});
}
// Main deploy function
async function deploy() {
console.log("Checking target directory...");
// Kill preview server first
await killPreviewServer();
console.log("");
// Check if target directory exists
if (!fs.existsSync(TARGET_PATH)) {
console.error(`Target directory does not exist: ${TARGET_PATH}`);
console.log("Make sure you're running this from the admin directory");
console.log(
"Or specify the correct target directory: node deploy.js /path/to/main/website"
);
process.exit(1);
}
// Check if target has the expected structure
const expectedFiles = config.requiredTargetFiles;
const missingFiles = expectedFiles.filter(
(file) => !fs.existsSync(path.join(TARGET_PATH, file))
);
if (missingFiles.length > 0) {
console.error(
`Target directory missing required files: ${missingFiles.join(", ")}`
);
console.log(
"Make sure you're pointing to the correct main website directory"
);
process.exit(1);
}
console.log("Target directory validated");
console.log("");
// Create backup
console.log("Creating backup...");
createBackup();
console.log("");
// Copy files
console.log("Copying files...");
let successCount = 0;
let totalCount = FILES_TO_COPY.length;
for (const file of FILES_TO_COPY) {
console.log(`${file.description}...`);
const success = copyFileOrDir(file.source, file.target, file.isDirectory);
if (success) {
console.log(` ${file.source}${file.target}`);
successCount++;
} else {
console.log(` Failed to copy ${file.source}`);
}
}
console.log("");
console.log("Deploy Summary");
console.log("==============");
console.log(`Successfully copied: ${successCount}/${totalCount} files`);
if (successCount === totalCount) {
console.log("Deploy completed successfully!");
console.log("");
console.log("Your website has been updated with the latest changes.");
console.log("You can now visit your main website to see the changes.");
} else {
console.log("Deploy completed with some errors.");
console.log("Check the error messages above and try again.");
process.exit(1);
}
}
// Run deploy
deploy().catch((error) => {
console.error("Deploy failed:", error.message);
process.exit(1);
});

1218
index.html Normal file

File diff suppressed because it is too large Load diff

62
kill-server.js Normal file
View file

@ -0,0 +1,62 @@
#!/usr/bin/env node
/**
* Server Killer for KHY Admin Dashboard
*
* This script kills the preview server
* Usage: node kill-server.js
*/
const fs = require("fs");
const path = require("path");
const PID_FILE = path.join(__dirname, ".server.pid");
console.log("KHY Admin Server Killer");
console.log("========================");
console.log("");
// Function to kill server by PID
function killServerByPID() {
try {
if (fs.existsSync(PID_FILE)) {
const pid = fs.readFileSync(PID_FILE, "utf8").trim();
console.log(`Killing server with PID: ${pid}`);
// Kill the process
process.kill(parseInt(pid), "SIGTERM");
// Remove PID file
fs.unlinkSync(PID_FILE);
console.log(`Server killed successfully`);
return true;
} else {
console.log("No server PID file found - server may not be running");
return false;
}
} catch (error) {
if (error.code === "ESRCH") {
console.log("Server process not found - may have already stopped");
// Clean up PID file anyway
if (fs.existsSync(PID_FILE)) {
fs.unlinkSync(PID_FILE);
}
} else {
console.log(`Could not kill server: ${error.message}`);
}
return false;
}
}
// Kill the server
const killed = killServerByPID();
if (killed) {
console.log("");
console.log("Preview server stopped successfully!");
} else {
console.log("");
console.log("Server was not running or already stopped");
}
console.log("");

28
package.json Normal file
View file

@ -0,0 +1,28 @@
{
"name": "khy-admin-dashboard",
"version": "1.0.0",
"description": "KHY Furniture Product Management Admin Dashboard",
"main": "admin.html",
"scripts": {
"start": "python3 -m http.server 8080",
"dev": "python3 -m http.server 8080",
"build": "echo 'No build process needed for static admin dashboard'",
"setup": "node setup-deploy.js",
"deploy": "node deploy.js",
"deploy:prod": "node deploy.js ../",
"preview": "node start-server.js",
"preview:kill": "node kill-server.js",
"preview:open": "echo 'Open http://localhost:8080 to preview changes'"
},
"keywords": [
"admin",
"dashboard",
"product-management",
"furniture",
"khy"
],
"author": "KHY Furniture",
"license": "MIT",
"devDependencies": {},
"dependencies": {}
}

564
product-catalog.html Normal file
View file

@ -0,0 +1,564 @@
<!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" />
<style>
/* Force responsive behavior */
@media (min-width: 640px) {
.sm\:hidden {
display: none !important;
}
.sm\:flex {
display: flex !important;
}
}
</style>
<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="fixed w-full h-20 sm: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-12 sm:h-20 w-auto drop-shadow-sm"
/>
</div>
<!-- Desktop Navigation -->
<ul class="hidden sm: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>
<!-- Mobile Quote Button and Hamburger -->
<div class="sm:hidden flex items-center space-x-3">
<!-- Quote Button -->
<a
href="quote.html"
class="px-3 py-1 border border-black text-black font-playfair text-sm font-normal tracking-wider hover:bg-black hover:text-white transition-colors"
>
Quote
</a>
<!-- Hamburger Button -->
<button
id="mobile-menu-button"
class="text-black hover:text-gray-600 transition-colors"
aria-label="Open mobile menu"
>
<svg
class="w-7 h-7"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
></path>
</svg>
</button>
</div>
</div>
</nav>
</header>
<!-- Mobile Menu Overlay -->
<div
id="mobile-menu-overlay"
class="fixed inset-0 bg-black bg-opacity-50 z-40 hidden transition-opacity duration-300 sm:hidden"
></div>
<!-- Mobile Menu -->
<div
id="mobile-menu"
class="fixed top-0 right-0 h-full w-80 bg-white shadow-xl z-50 transform translate-x-full transition-transform duration-300 sm:hidden"
>
<!-- Mobile Menu Header -->
<div class="flex items-center justify-between p-5 border-b border-gray-200">
<img
src="assets/images/khy_logo.png"
alt="KHY Logo"
class="h-10 w-auto"
/>
<button
id="mobile-menu-close"
class="p-2 text-black hover:text-gray-600 transition-colors"
aria-label="Close mobile menu"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- Mobile Menu Items -->
<nav class="p-5">
<ul class="flex flex-col space-y-4">
<li>
<a
href="index.html"
class="nav-link block text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors py-2"
>Home</a
>
</li>
<li>
<a
href="index.html#products"
class="nav-link block text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors py-2"
>Products</a
>
</li>
<li>
<a
href="index.html#projects-button"
class="nav-link block text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors py-2"
>Projects</a
>
</li>
<li>
<a
href="index.html#clients"
class="nav-link block text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors py-2"
>Clients</a
>
</li>
<li>
<a
href="index.html#about"
class="nav-link block text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors py-2"
>About</a
>
</li>
<li>
<a
href="contact.html"
class="block text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors py-2"
>Contact</a
>
</li>
<li>
<a
href="blog.html"
class="block text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors py-2"
>Blog</a
>
</li>
<li>
<a
href="quote.html"
class="block text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors py-2"
>Quote</a
>
</li>
</ul>
</nav>
</div>
<main>
<!-- Hero Section -->
<section class="relative h-80 mt-20 sm: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-floral-white 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 Separator -->
<div class="border-t border-gray-200"></div>
<!-- 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?v=3.0"></script>
<script src="scripts/products.js?v=3.0"></script>
</body>
</html>

741
product-detail.html Normal file
View file

@ -0,0 +1,741 @@
<!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" />
<style>
/* Force responsive behavior */
@media (min-width: 640px) {
.sm\:hidden {
display: none !important;
}
.sm\:flex {
display: flex !important;
}
}
</style>
<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="fixed w-full h-20 sm: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-12 sm:h-20 w-auto drop-shadow-sm"
/>
</div>
<!-- Desktop Navigation -->
<ul class="hidden sm: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>
<!-- Mobile Quote Button and Hamburger -->
<div class="sm:hidden flex items-center space-x-3">
<!-- Quote Button -->
<a
href="quote.html"
class="px-3 py-1 border border-black text-black font-playfair text-sm font-normal tracking-wider hover:bg-black hover:text-white transition-colors"
>
Quote
</a>
<!-- Hamburger Button -->
<button
id="mobile-menu-button"
class="text-black hover:text-gray-600 transition-colors"
aria-label="Open mobile menu"
>
<svg
class="w-7 h-7"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
></path>
</svg>
</button>
</div>
</div>
</nav>
</header>
<!-- Mobile Menu Overlay -->
<div
id="mobile-menu-overlay"
class="fixed inset-0 bg-black bg-opacity-50 z-40 hidden transition-opacity duration-300 sm:hidden"
></div>
<!-- Mobile Menu -->
<div
id="mobile-menu"
class="fixed top-0 right-0 h-full w-80 bg-white shadow-xl z-50 transform translate-x-full transition-transform duration-300 sm:hidden"
>
<!-- Mobile Menu Header -->
<div
class="flex items-center justify-between p-5 border-b border-gray-200"
>
<img
src="assets/images/khy_logo.png"
alt="KHY Logo"
class="h-10 w-auto"
/>
<button
id="mobile-menu-close"
class="p-2 text-black hover:text-gray-600 transition-colors"
aria-label="Close mobile menu"
>
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</button>
</div>
<!-- Mobile Menu Items -->
<nav class="p-5">
<ul class="flex flex-col space-y-4">
<li>
<a
href="index.html"
class="nav-link block text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors py-2"
>Home</a
>
</li>
<li>
<a
href="index.html#products"
class="nav-link block text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors py-2"
>Products</a
>
</li>
<li>
<a
href="index.html#projects-button"
class="nav-link block text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors py-2"
>Projects</a
>
</li>
<li>
<a
href="index.html#clients"
class="nav-link block text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors py-2"
>Clients</a
>
</li>
<li>
<a
href="index.html#about"
class="nav-link block text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors py-2"
>About</a
>
</li>
<li>
<a
href="contact.html"
class="block text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors py-2"
>Contact</a
>
</li>
<li>
<a
href="blog.html"
class="block text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors py-2"
>Blog</a
>
</li>
<li>
<a
href="quote.html"
class="block text-black hover:text-gray-600 font-playfair text-md font-extralight tracking-wider transition-colors py-2"
>Quote</a
>
</li>
</ul>
</nav>
</div>
<main class="pt-20 sm: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"
id="product-details-title"
>
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 flex-col md:flex-row gap-8">
<!-- Left Section - Product Images -->
<div class="flex flex-col-reverse md:flex-row gap-4">
<!-- Thumbnails -->
<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>
</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"
/>
</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>
<!-- 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-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>
</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 flex-col md:flex-row gap-4 md: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
id="add-to-quote-btn"
class="inline-flex items-center justify-center w-full md: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
id="compare-products-btn"
class="inline-flex items-center justify-center w-full md: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-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>
</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>
</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?v=3.5"></script>
<script src="scripts/quote.js?v=3.5"></script>
</body>
</html>

2073
scripts/main.js Normal file

File diff suppressed because it is too large Load diff

419
scripts/products.js Normal file
View file

@ -0,0 +1,419 @@
// 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) {
// Check if we're in comparison mode
const urlParams = new URLSearchParams(window.location.search);
const returnTo = urlParams.get("returnTo");
const isComparisonMode = returnTo === "comparison";
return `
<div class="group relative bg-light-bg rounded-lg overflow-hidden hover:shadow-lg transition-shadow product-card" data-product-id="${
product.id
}">
<div class="relative h-80 overflow-hidden">
<img
src="${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 ${
isComparisonMode ? "cursor-pointer" : ""
}"
onclick="productManager.viewProduct(${product.id})"
>
${isComparisonMode ? "Add to Comparison" : "View"}
</button>
</div>
</div>
</div>
<div class="p-6">
<h3 class="font-poppins font-semibold text-2xl text-dark-charcoal mb-2">
${product.name}
</h3>
<p class="font-poppins font-medium text-base text-quick-silver">
${product.description}
</p>
</div>
</div>
`;
}
// Filter products by category
filterByCategory(category) {
// Single category helper (not used directly by UI)
this.selectedCategories = new Set([category]);
this.applyFilters();
this.currentPage = 1;
this.renderProducts();
this.updatePagination();
}
// Search products
searchProducts(query) {
if (!query.trim()) {
this.filteredProducts = [...this.products];
} else {
this.filteredProducts = this.products.filter(
(product) =>
product.name.toLowerCase().includes(query.toLowerCase()) ||
product.description.toLowerCase().includes(query.toLowerCase())
);
}
this.currentPage = 1;
this.renderProducts();
this.updatePagination();
}
// Apply selected category filters
applyFilters() {
if (
this.selectedCategories.size === 0 ||
this.selectedCategories.has("all")
) {
this.filteredProducts = [...this.products];
return;
}
this.filteredProducts = this.products.filter((product) =>
this.selectedCategories.has(product.category)
);
}
// Sort products
sortProducts(sortBy) {
switch (sortBy) {
case "name-asc":
this.filteredProducts.sort((a, b) => a.name.localeCompare(b.name));
break;
case "name-desc":
this.filteredProducts.sort((a, b) => b.name.localeCompare(a.name));
break;
default:
// Default sorting by ID
this.filteredProducts.sort((a, b) => a.id - b.id);
}
this.renderProducts();
}
// Change page
changePage(page) {
this.currentPage = page;
this.renderProducts();
this.updatePagination();
}
// Update pagination controls
updatePagination() {
const totalPages = Math.ceil(
this.filteredProducts.length / this.itemsPerPage
);
const paginationContainer = document.getElementById("pagination");
if (!paginationContainer) return;
let paginationHTML = "";
for (let i = 1; i <= totalPages; i++) {
const isActive = i === this.currentPage;
paginationHTML += `
<button
class="w-20 h-15 ${
isActive
? "bg-uc-gold text-white"
: "bg-floral-white text-black hover:bg-uc-gold hover:text-white"
} font-poppins font-normal text-xl rounded-lg flex items-center justify-center transition-colors"
onclick="productManager.changePage(${i})"
>
${i}
</button>
`;
}
if (totalPages > 1 && this.currentPage < totalPages) {
paginationHTML += `
<button
class="w-28 h-15 bg-floral-white text-black font-poppins font-light text-xl rounded-lg flex items-center justify-center hover:bg-uc-gold hover:text-white transition-colors"
onclick="productManager.changePage(${this.currentPage + 1})"
>
Next
</button>
`;
}
paginationContainer.innerHTML = paginationHTML;
}
// Build category multi-select dropdown
renderCategoryFilters() {
const container = document.getElementById("filter-categories");
if (!container) return;
const categoryOptions = this.categories
.map(
(c) => `
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" value="${c.id}" class="category-checkbox category-specific">
<span class="font-poppins text-sm text-black">${c.name}</span>
</label>
`
)
.join("");
const allOption = `
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" value="all" class="category-checkbox category-all">
<span class="font-poppins text-sm text-black">All</span>
</label>
`;
container.innerHTML = allOption + categoryOptions;
// Add event listeners for "All" checkbox behavior
const allCheckbox = container.querySelector(".category-all");
const specificCheckboxes = container.querySelectorAll(".category-specific");
if (allCheckbox) {
allCheckbox.addEventListener("change", (e) => {
const isChecked = e.target.checked;
specificCheckboxes.forEach((checkbox) => {
checkbox.checked = isChecked;
});
});
}
// Update "All" checkbox when specific categories change
specificCheckboxes.forEach((checkbox) => {
checkbox.addEventListener("change", () => {
const allChecked = Array.from(specificCheckboxes).every(
(c) => c.checked
);
const anyChecked = Array.from(specificCheckboxes).some(
(c) => c.checked
);
if (allChecked) {
allCheckbox.checked = true;
} else if (!anyChecked) {
allCheckbox.checked = false;
}
});
});
}
// View product details
viewProduct(productId) {
const product = this.products.find((p) => p.id === productId);
if (product) {
// Check if we're in comparison mode
const urlParams = new URLSearchParams(window.location.search);
const returnTo = urlParams.get("returnTo");
const slot = urlParams.get("slot");
const product1Id = urlParams.get("product1");
const product2Id = urlParams.get("product2");
if (returnTo === "comparison" && slot) {
// Navigate back to comparison page with the selected product
let comparisonUrl = "product-comparison.html?";
if (slot === "1") {
// Replace product 1
comparisonUrl += `product1=${productId}`;
if (product2Id) {
comparisonUrl += `&product2=${product2Id}`;
}
} else {
// Replace product 2
if (product1Id) {
comparisonUrl += `product1=${product1Id}&`;
}
comparisonUrl += `product2=${productId}`;
}
console.log("Navigating to comparison page:", comparisonUrl);
window.location.href = comparisonUrl;
} else {
// Normal mode - navigate to product detail page
window.location.href = `product-detail.html?id=${productId}`;
}
}
}
// Setup event listeners
setupEventListeners() {
// Filter dropdown toggle and outside click
const filterToggle = document.getElementById("filter-toggle");
const filterDropdown = document.getElementById("filter-dropdown");
if (filterToggle && filterDropdown) {
filterToggle.addEventListener("click", () => {
filterDropdown.classList.toggle("hidden");
});
document.addEventListener("click", (e) => {
if (
!filterDropdown.contains(e.target) &&
!filterToggle.contains(e.target)
) {
filterDropdown.classList.add("hidden");
}
});
}
// Sort dropdown
const sortSelect = document.querySelector("select");
if (sortSelect) {
sortSelect.addEventListener("change", (e) => {
this.sortProducts(e.target.value);
});
}
// Apply/clear category filters
const applyBtn = document.getElementById("filter-apply");
const clearBtn = document.getElementById("filter-clear");
if (applyBtn) {
applyBtn.addEventListener("click", () => {
const checks = Array.from(
document.querySelectorAll(".category-checkbox")
);
this.selectedCategories = new Set(
checks.filter((c) => c.checked).map((c) => c.value)
);
this.currentPage = 1;
this.applyFilters();
this.renderProducts();
this.updatePagination();
this.updateResultsCount();
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();
});

605
scripts/quote.js Normal file
View file

@ -0,0 +1,605 @@
// Quote Management System
class QuoteManager {
constructor() {
this.storageKey = "khy_quote_items";
this.quoteItems = this.loadQuoteItems();
this.init();
}
// Load quote items from localStorage
loadQuoteItems() {
try {
const stored = localStorage.getItem(this.storageKey);
return stored ? JSON.parse(stored) : [];
} catch (error) {
console.error("Error loading quote items:", error);
return [];
}
}
// Save quote items to localStorage
saveQuoteItems() {
try {
localStorage.setItem(this.storageKey, JSON.stringify(this.quoteItems));
this.updateQuoteBadge();
} catch (error) {
console.error("Error saving quote items:", error);
}
}
// Add item to quote
addToQuote(productData) {
const {
id,
name,
image,
color = "Default",
size = "Standard",
quantity = 1,
} = productData;
// Check if item already exists with same specifications
const existingItemIndex = this.quoteItems.findIndex(
(item) => item.id === id && item.color === color && item.size === size
);
if (existingItemIndex !== -1) {
// Update quantity of existing item
this.quoteItems[existingItemIndex].quantity += quantity;
} else {
// Add new item
const newItem = {
id,
name,
image,
color,
size,
quantity,
timestamp: new Date().toISOString(),
};
this.quoteItems.push(newItem);
}
this.saveQuoteItems();
this.renderQuoteItems();
this.showAddToQuoteSuccess();
}
// Remove item from quote
removeFromQuote(itemIndex) {
this.quoteItems.splice(itemIndex, 1);
this.saveQuoteItems();
this.renderQuoteItems();
}
// Update item quantity
updateQuantity(itemIndex, newQuantity) {
if (newQuantity > 0) {
this.quoteItems[itemIndex].quantity = newQuantity;
this.saveQuoteItems();
this.renderQuoteItems();
} else {
this.removeFromQuote(itemIndex);
}
}
// Edit a quote item
editQuoteItem(itemIndex) {
const item = this.quoteItems[itemIndex];
if (!item) return;
// Create edit modal
const modal = document.createElement("div");
modal.id = "edit-quote-modal";
modal.className =
"fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50";
modal.innerHTML = `
<div class="bg-white rounded-lg p-8 max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto">
<div class="flex items-center justify-between mb-6">
<h3 class="font-playfair font-semibold text-xl text-black">Edit Quote Item</h3>
<button onclick="quoteManager.closeEditModal()" class="text-gray-500 hover:text-gray-700">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="flex items-center space-x-4 mb-6">
<img src="${item.image}" alt="${
item.name
}" class="w-16 h-16 object-cover rounded-lg">
<div>
<h4 class="font-playfair font-semibold text-lg text-black">${
item.name
}</h4>
<p class="text-sm text-gray-600">Product ID: ${item.id}</p>
</div>
</div>
<form id="edit-quote-form" class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Quantity</label>
<div class="flex items-center space-x-3">
<button type="button" onclick="quoteManager.decrementEditQuantity()" class="w-8 h-8 rounded-full border border-gray-300 flex items-center justify-center hover:bg-gray-100 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"></path>
</svg>
</button>
<input type="number" id="edit-quantity" value="${
item.quantity
}" min="1" class="w-20 text-center border border-gray-300 rounded-lg px-3 py-2 font-playfair">
<button type="button" onclick="quoteManager.incrementEditQuantity()" class="w-8 h-8 rounded-full border border-gray-300 flex items-center justify-center hover:bg-gray-100 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
</button>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Color</label>
<select id="edit-color" class="w-full border border-gray-300 rounded-lg px-3 py-2 font-playfair">
<option value="Black" ${
item.color === "Black" ? "selected" : ""
}>Black</option>
<option value="White" ${
item.color === "White" ? "selected" : ""
}>White</option>
<option value="Brown" ${
item.color === "Brown" ? "selected" : ""
}>Brown</option>
<option value="Gray" ${
item.color === "Gray" ? "selected" : ""
}>Gray</option>
<option value="Beige" ${
item.color === "Beige" ? "selected" : ""
}>Beige</option>
<option value="Navy" ${
item.color === "Navy" ? "selected" : ""
}>Navy</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Size</label>
<select id="edit-size" class="w-full border border-gray-300 rounded-lg px-3 py-2 font-playfair">
<option value="S" ${
item.size === "S" ? "selected" : ""
}>Small (S)</option>
<option value="M" ${
item.size === "M" ? "selected" : ""
}>Medium (M)</option>
<option value="L" ${
item.size === "L" ? "selected" : ""
}>Large (L)</option>
<option value="XL" ${
item.size === "XL" ? "selected" : ""
}>Extra Large (XL)</option>
<option value="Standard" ${
item.size === "Standard" ? "selected" : ""
}>Standard</option>
</select>
</div>
<div class="flex space-x-3 pt-4">
<button type="button" onclick="quoteManager.closeEditModal()" class="flex-1 bg-gray-300 text-gray-700 px-4 py-2 rounded-lg font-playfair hover:bg-gray-400 transition-colors">
Cancel
</button>
<button type="submit" class="flex-1 bg-uc-gold text-white px-4 py-2 rounded-lg font-playfair hover:bg-amber-600 transition-colors">
Save Changes
</button>
</div>
</form>
</div>
`;
document.body.appendChild(modal);
// Add form submission handler
const form = modal.querySelector("#edit-quote-form");
form.addEventListener("submit", (e) => {
e.preventDefault();
this.saveEditChanges(itemIndex);
});
// Close modal when clicking outside
modal.addEventListener("click", (e) => {
if (e.target === modal) {
this.closeEditModal();
}
});
}
// Close edit modal
closeEditModal() {
const modal = document.getElementById("edit-quote-modal");
if (modal) {
modal.remove();
}
}
// Increment quantity in edit modal
incrementEditQuantity() {
const input = document.getElementById("edit-quantity");
if (input) {
input.value = parseInt(input.value) + 1;
}
}
// Decrement quantity in edit modal
decrementEditQuantity() {
const input = document.getElementById("edit-quantity");
if (input && parseInt(input.value) > 1) {
input.value = parseInt(input.value) - 1;
}
}
// Save changes from edit modal
saveEditChanges(itemIndex) {
const quantity = parseInt(document.getElementById("edit-quantity").value);
const color = document.getElementById("edit-color").value;
const size = document.getElementById("edit-size").value;
if (quantity < 1) {
alert("Quantity must be at least 1");
return;
}
// Update the item
this.quoteItems[itemIndex] = {
...this.quoteItems[itemIndex],
quantity: quantity,
color: color,
size: size,
};
this.saveQuoteItems();
this.renderQuoteItems();
this.closeEditModal();
// Show success message
this.showEditSuccess();
}
// Show success message for edit
showEditSuccess() {
const notification = document.createElement("div");
notification.className =
"fixed top-24 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 translate-x-full";
notification.innerHTML = `
<div class="flex items-center space-x-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="font-playfair font-semibold">Quote item updated!</span>
</div>
`;
document.body.appendChild(notification);
// Animate in
setTimeout(() => {
notification.classList.remove("translate-x-full");
}, 100);
// Remove after 3 seconds
setTimeout(() => {
notification.classList.add("translate-x-full");
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
// Clear all quote items
clearQuote() {
this.quoteItems = [];
this.saveQuoteItems();
this.renderQuoteItems();
}
// Get quote items count
getQuoteCount() {
return this.quoteItems.reduce((total, item) => total + item.quantity, 0);
}
// Update quote badge in navigation
updateQuoteBadge() {
const count = this.getQuoteCount();
const quoteLinks = document.querySelectorAll('a[href="quote.html"]');
quoteLinks.forEach((quoteLink) => {
// Remove existing badge
const existingBadge = quoteLink.querySelector(".quote-badge");
if (existingBadge) {
existingBadge.remove();
}
// Add new badge if there are items
if (count > 0) {
const badge = document.createElement("span");
badge.className =
"quote-badge absolute -top-2 -right-2 bg-uc-gold text-white text-xs rounded-full w-5 h-5 flex items-center justify-center font-semibold";
badge.textContent = count > 99 ? "99+" : count;
quoteLink.style.position = "relative";
quoteLink.appendChild(badge);
}
});
}
// Show success message when item is added
showAddToQuoteSuccess() {
// Create success notification
const notification = document.createElement("div");
notification.className =
"fixed top-24 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 translate-x-full";
notification.innerHTML = `
<div class="flex items-center space-x-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="font-playfair font-semibold">Added to quote!</span>
</div>
`;
document.body.appendChild(notification);
// Animate in
setTimeout(() => {
notification.classList.remove("translate-x-full");
}, 100);
// Remove after 3 seconds
setTimeout(() => {
notification.classList.add("translate-x-full");
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
// Render quote items on the quote page
renderQuoteItems() {
const container = document.getElementById("quote-items-container");
const emptyMessage = document.getElementById("empty-quote-message");
const quoteActions = document.getElementById("quote-actions");
if (!container) return;
if (this.quoteItems.length === 0) {
// Show empty state
if (emptyMessage) emptyMessage.style.display = "block";
if (quoteActions) quoteActions.classList.add("hidden");
// Clear any existing items but keep the empty message
const itemsToRemove = container.querySelectorAll(".bg-gray-50");
itemsToRemove.forEach((item) => item.remove());
return;
}
// Hide empty message and show actions
if (emptyMessage) emptyMessage.style.display = "none";
if (quoteActions) quoteActions.classList.remove("hidden");
// Remove any existing items first
const itemsToRemove = container.querySelectorAll(".bg-gray-50");
itemsToRemove.forEach((item) => item.remove());
// Render new items
this.quoteItems.forEach((item, index) => {
const itemElement = document.createElement("div");
itemElement.className =
"bg-gray-50 rounded-lg p-6 border border-gray-200";
itemElement.innerHTML = `
<div class="flex items-center space-x-6">
<!-- Product Image -->
<div class="w-24 h-24 flex-shrink-0">
<img
src="${item.image}"
alt="${item.name}"
class="w-full h-full object-cover rounded-lg"
/>
</div>
<!-- Product Details -->
<div class="flex-1">
<h3 class="font-playfair font-semibold text-xl text-black mb-2">
${item.name}
</h3>
<div class="flex items-center space-x-6 text-sm text-gray-600">
<span><strong>Color:</strong> ${item.color}</span>
<span><strong>Size:</strong> ${item.size}</span>
</div>
</div>
<!-- Quantity Controls -->
<div class="flex items-center space-x-3">
<button
onclick="quoteManager.updateQuantity(${index}, ${
item.quantity - 1
})"
class="w-8 h-8 rounded-full border border-gray-300 flex items-center justify-center hover:bg-gray-100 transition-colors"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"></path>
</svg>
</button>
<span class="font-playfair font-semibold text-lg text-black min-w-[2rem] text-center">
${item.quantity}
</span>
<button
onclick="quoteManager.updateQuantity(${index}, ${
item.quantity + 1
})"
class="w-8 h-8 rounded-full border border-gray-300 flex items-center justify-center hover:bg-gray-100 transition-colors"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
</button>
</div>
<!-- Edit Button -->
<button
onclick="quoteManager.editQuoteItem(${index})"
class="text-blue-500 hover:text-blue-700 transition-colors mr-2"
title="Edit item"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</button>
<!-- Remove Button -->
<button
onclick="quoteManager.removeFromQuote(${index})"
class="text-red-500 hover:text-red-700 transition-colors"
title="Remove item"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
`;
container.appendChild(itemElement);
});
}
// Render quote summary in modal
renderQuoteSummary() {
const summaryContainer = document.getElementById("quote-summary");
if (!summaryContainer) return;
summaryContainer.innerHTML = this.quoteItems
.map(
(item) => `
<div class="flex justify-between items-center py-2 border-b border-gray-100">
<div class="flex-1">
<h5 class="font-playfair font-semibold text-base text-black">${item.name}</h5>
<p class="text-sm text-gray-600">${item.color} ${item.size} Qty: ${item.quantity}</p>
</div>
</div>
`
)
.join("");
}
// Initialize quote manager
init() {
this.updateQuoteBadge();
this.renderQuoteItems();
this.setupEventListeners();
}
// Setup event listeners
setupEventListeners() {
// Clear quote button
const clearBtn = document.getElementById("clear-quote-btn");
if (clearBtn) {
clearBtn.addEventListener("click", () => {
if (confirm("Are you sure you want to clear your quote?")) {
this.clearQuote();
}
});
}
// Request quote button
const requestBtn = document.getElementById("request-quote-btn");
if (requestBtn) {
requestBtn.addEventListener("click", () => {
this.openQuoteModal();
});
}
// Modal close button
const closeBtn = document.getElementById("close-modal-btn");
if (closeBtn) {
closeBtn.addEventListener("click", () => {
this.closeQuoteModal();
});
}
// Modal backdrop click
const modal = document.getElementById("quote-modal");
if (modal) {
modal.addEventListener("click", (e) => {
if (e.target === modal) {
this.closeQuoteModal();
}
});
}
// Quote form submission
const quoteForm = document.getElementById("quote-form");
if (quoteForm) {
quoteForm.addEventListener("submit", (e) => {
e.preventDefault();
this.submitQuoteRequest();
});
}
}
// Open quote modal
openQuoteModal() {
const modal = document.getElementById("quote-modal");
if (modal) {
this.renderQuoteSummary();
modal.classList.remove("hidden");
document.body.style.overflow = "hidden";
}
}
// Close quote modal
closeQuoteModal() {
const modal = document.getElementById("quote-modal");
if (modal) {
modal.classList.add("hidden");
document.body.style.overflow = "auto";
}
}
// Submit quote request
submitQuoteRequest() {
const form = document.getElementById("quote-form");
const formData = new FormData(form);
// Get form data
const quoteData = {
name: formData.get("name"),
email: formData.get("email"),
phone: formData.get("phone"),
company: formData.get("company"),
project: formData.get("project"),
items: this.quoteItems,
timestamp: new Date().toISOString(),
};
// Here you would typically send this data to your server
console.log("Quote request data:", quoteData);
// For now, just show success message
alert("Thank you for your quote request! We will get back to you soon.");
// Clear the quote and close modal
this.clearQuote();
this.closeQuoteModal();
// Reset form
form.reset();
}
}
// Global quote manager instance
const quoteManager = new QuoteManager();
// Global function to add items to quote (called from other pages)
function addToQuote(productData) {
quoteManager.addToQuote(productData);
}
// Initialize when DOM is ready
document.addEventListener("DOMContentLoaded", () => {
// Quote manager is already initialized in constructor
});
// Version: 3.5 - Added edit functionality for quote items

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

161
setup-deploy.js Normal file
View file

@ -0,0 +1,161 @@
#!/usr/bin/env node
/**
* Setup Script for KHY Admin Deploy Configuration
*
* This script helps you configure the deploy paths for your specific setup.
* Run this once to set up the correct paths for your environment.
*/
const fs = require("fs");
const path = require("path");
const readline = require("readline");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function question(prompt) {
return new Promise((resolve) => {
rl.question(prompt, resolve);
});
}
async function setupDeploy() {
console.log("KHY Admin Deploy Setup");
console.log("======================");
console.log("");
console.log("This script will help you configure the deploy paths.");
console.log("You need to specify where your main KHY website is located.");
console.log("");
// Get current admin directory
const adminDir = __dirname;
console.log(`Current admin directory: ${adminDir}`);
console.log("");
// Ask for the main website path
console.log("Where is your main KHY website located?");
console.log("Examples:");
console.log(" - If in same parent directory: ../khy_website");
console.log(" - If in different location: /path/to/your/khy_website");
console.log(" - If admin is inside main website: ../");
console.log("");
const mainWebsitePath = await question("Enter path to main KHY website: ");
if (!mainWebsitePath.trim()) {
console.log("No path provided. Exiting.");
rl.close();
return;
}
// Resolve the path
const resolvedPath = path.resolve(adminDir, mainWebsitePath.trim());
console.log("");
console.log(`Resolved path: ${resolvedPath}`);
// Check if the path exists
if (!fs.existsSync(resolvedPath)) {
console.log("WARNING: The specified path does not exist!");
const continueAnyway = await question("Continue anyway? (y/N): ");
if (continueAnyway.toLowerCase() !== "y") {
console.log("Setup cancelled.");
rl.close();
return;
}
}
// Check for required files
const requiredFiles = ["index.html", "data/products.json"];
const missingFiles = requiredFiles.filter(
(file) => !fs.existsSync(path.join(resolvedPath, file))
);
if (missingFiles.length > 0) {
console.log(`WARNING: Missing required files: ${missingFiles.join(", ")}`);
const continueAnyway = await question("Continue anyway? (y/N): ");
if (continueAnyway.toLowerCase() !== "y") {
console.log("Setup cancelled.");
rl.close();
return;
}
}
// Update the config file
const configPath = path.join(adminDir, "deploy-config.js");
const configContent = `/**
* Deploy Configuration for KHY Admin Dashboard
*
* This file contains configuration for deploying from the admin repository
* to the main KHY website repository.
*
* Generated by setup-deploy.js on ${new Date().toISOString()}
*/
module.exports = {
// Default target paths (adjust these based on your setup)
targets: {
// If admin and main website are in the same parent directory
sibling: "../khy_website",
// If admin is in a subdirectory of the main website
parent: "../",
// Your configured path
main: "${mainWebsitePath.trim()}",
// Absolute path examples (uncomment and modify as needed)
// absolute: "/path/to/your/khy_website",
// absolute: "/Users/george/Documents/khy_website/khy_website",
},
// Which target to use by default
defaultTarget: "main",
// Files to copy from admin to main website
filesToCopy: [
{
source: "data/products.json",
target: "data/products.json",
description: "Product catalog data",
},
{
source: "assets/images",
target: "assets/images",
description: "Product images",
isDirectory: true,
},
],
// Required files that must exist in target directory
requiredTargetFiles: ["index.html", "data/products.json"],
// Backup settings
backup: {
enabled: true,
directory: "backups",
timestampFormat: "YYYY-MM-DD_HH-mm-ss",
}
};`;
try {
fs.writeFileSync(configPath, configContent);
console.log("");
console.log("Configuration updated successfully!");
console.log(`Config file: ${configPath}`);
console.log("");
console.log("You can now run: npm run deploy");
console.log("");
console.log("To test the configuration, run:");
console.log(` node deploy.js "${mainWebsitePath.trim()}"`);
} catch (error) {
console.log(`Error writing config file: ${error.message}`);
}
rl.close();
}
setupDeploy().catch(console.error);

3
src/input.css Normal file
View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

167
start-server.js Normal file
View file

@ -0,0 +1,167 @@
#!/usr/bin/env node
/**
* Server Starter for KHY Admin Dashboard
*
* This script starts a local server and opens the preview pages
* Usage: node start-server.js
*/
const { spawn } = require("child_process");
const { exec } = require("child_process");
const path = require("path");
const fs = require("fs");
const PORT = 8080;
const BASE_URL = `http://localhost:${PORT}`;
const PID_FILE = path.join(__dirname, ".server.pid");
console.log("Starting KHY Admin Preview Server");
console.log("===================================");
console.log("");
// Function to open URL in default browser
function openBrowser(url) {
const start =
process.platform === "darwin"
? "open"
: process.platform === "win32"
? "start"
: "xdg-open";
exec(`${start} ${url}`, (error) => {
if (error) {
console.log(`Please manually open: ${url}`);
}
});
}
// Function to save server PID
function saveServerPID(pid) {
try {
fs.writeFileSync(PID_FILE, pid.toString());
console.log(`Server PID saved: ${pid}`);
} catch (error) {
console.log(`Could not save PID: ${error.message}`);
}
}
// Function to kill server by PID
function killServerByPID() {
try {
if (fs.existsSync(PID_FILE)) {
const pid = fs.readFileSync(PID_FILE, "utf8").trim();
console.log(`Killing server with PID: ${pid}`);
// Kill the process
process.kill(parseInt(pid), "SIGTERM");
// Remove PID file
fs.unlinkSync(PID_FILE);
console.log(`Server killed successfully`);
return true;
}
} catch (error) {
console.log(`Could not kill server: ${error.message}`);
}
return false;
}
// Function to start the server
function startServer() {
console.log(`Starting server on port ${PORT}...`);
// Try Python first, then Node.js http-server
const pythonServer = spawn(
"python3",
["-m", "http.server", PORT.toString()],
{
stdio: "pipe",
cwd: __dirname,
}
);
// Save the server PID
saveServerPID(pythonServer.pid);
pythonServer.stdout.on("data", (data) => {
console.log(`Server output: ${data}`);
});
pythonServer.stderr.on("data", (data) => {
console.log(`Server error: ${data}`);
});
pythonServer.on("close", (code) => {
console.log(`Server stopped with code ${code}`);
// Clean up PID file when server stops
if (fs.existsSync(PID_FILE)) {
fs.unlinkSync(PID_FILE);
}
});
// Wait a moment for server to start
setTimeout(() => {
console.log("");
console.log("Server started successfully!");
console.log("");
console.log("Preview URLs:");
console.log(` Admin Dashboard: ${BASE_URL}/admin.html`);
console.log(` Homepage: ${BASE_URL}/index.html`);
console.log(` Product Catalog: ${BASE_URL}/product-catalog.html`);
console.log(` Product Details: ${BASE_URL}/product-detail.html`);
console.log("");
console.log("Opening preview pages...");
// Open preview pages
setTimeout(() => openBrowser(`${BASE_URL}/admin.html`), 1000);
setTimeout(() => openBrowser(`${BASE_URL}/index.html`), 2000);
setTimeout(() => openBrowser(`${BASE_URL}/product-catalog.html`), 3000);
console.log("");
console.log("Preview environment ready!");
console.log("Press Ctrl+C to stop the server");
console.log("");
}, 2000);
// Handle Ctrl+C
process.on("SIGINT", () => {
console.log("");
console.log("Stopping server...");
pythonServer.kill();
process.exit(0);
});
}
// Check if port is already in use
const net = require("net");
const server = net.createServer();
server.listen(PORT, () => {
server.close(() => {
startServer();
});
});
server.on("error", (err) => {
if (err.code === "EADDRINUSE") {
console.log(`Port ${PORT} is already in use`);
console.log("The server might already be running");
console.log("");
console.log("If server is running, you can access:");
console.log(` Admin Dashboard: ${BASE_URL}/admin.html`);
console.log(` Homepage: ${BASE_URL}/index.html`);
console.log(` Product Catalog: ${BASE_URL}/product-catalog.html`);
console.log(` Product Details: ${BASE_URL}/product-detail.html`);
console.log("");
console.log("Opening preview pages...");
// Open preview pages anyway
setTimeout(() => openBrowser(`${BASE_URL}/admin.html`), 1000);
setTimeout(() => openBrowser(`${BASE_URL}/index.html`), 2000);
setTimeout(() => openBrowser(`${BASE_URL}/product-catalog.html`), 3000);
} else {
console.error("Error starting server:", err.message);
process.exit(1);
}
});

3132
styles/main.css Normal file

File diff suppressed because it is too large Load diff

56
tailwind.config.js Normal file
View file

@ -0,0 +1,56 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./*.html", "./src/**/*.{html,js}", "./scripts/**/*.js"],
theme: {
screens: {
sm: "640px",
md: "768px",
lg: "1024px",
xl: "1280px",
"2xl": "1536px",
},
extend: {
fontFamily: {
montserrat: ["Montserrat", "sans-serif"],
playfair: ["Playfair Display", "serif"],
},
colors: {
white: "#FFFFFF",
"dark-charcoal": "#2F2F2F",
black: "#000000",
"uc-gold": "#B8873F",
"floral-white": "#FCF8F3",
axolotl: "#6F776B",
"dark-charcoal-ii": "#212121",
"eerie-black": "#1A1A1A",
"taupe-gray": "#888888",
"davys-grey": "#595959",
"granite-gray": "#666666",
"light-silver": "#D7D7D7",
"quick-silver": "#A0A0A0",
linen: "#FAF0E6",
"light-bg": "#F4F5F7",
},
spacing: {
396: "396px",
398: "398px",
420: "420px",
259: "259px",
649: "649px",
183: "183px",
150: "150px",
100: "100px",
120: "120px",
60: "240px",
180: "720px",
360: "1440px",
},
textShadow: {
default: "0 2px 4px rgba(0,0,0,0.1)",
lg: "2px 2px 4px rgba(0,0,0,0.3)",
xl: "4px 4px 8px rgba(0,0,0,0.4)",
},
},
},
plugins: [require("tailwindcss-textshadow")],
};