reset repo

This commit is contained in:
George Birikorang 2025-09-19 21:51:10 -07:00
parent 7f70170fe1
commit 9cdef853c4
631 changed files with 4941 additions and 16827 deletions

97
.gitignore vendored
View file

@ -4,8 +4,49 @@ npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs/
*.log
sync-log.json
# Temporary files
temp-main/
.tmp/
.temp/
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# Build outputs
dist/
build/
# Cache
.cache/
.parcel-cache/
# Runtime data # Runtime data
pids pids/
*.pid *.pid
*.seed *.seed
*.pid.lock *.pid.lock
@ -14,19 +55,7 @@ pids
coverage/ coverage/
# nyc test coverage # nyc test coverage
.nyc_output .nyc_output/
# Grunt intermediate storage
.grunt
# Bower dependency directory
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons
build/Release
# Dependency directories # Dependency directories
jspm_packages/ jspm_packages/
@ -46,26 +75,30 @@ jspm_packages/
# dotenv environment variables file # dotenv environment variables file
.env .env
# IDE files # next.js build output
.vscode/ .next
.idea/
*.swp
*.swo
*~
# OS generated files # Nuxt.js build output
.DS_Store .nuxt
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs # Gatsby files
logs .cache/
*.log public
# Temporary files # Storybook build outputs
.out
.storybook-out
# Temporary folders
tmp/ tmp/
temp/ temp/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View file

@ -1,150 +0,0 @@
# 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",
},
];
```

279
README.md
View file

@ -1,146 +1,217 @@
# KHY Admin Dashboard # KHY Admin Dashboard
A **preview/staging environment** for managing KHY Furniture's product catalog with safe testing and one-click deployment to production. A comprehensive product management dashboard for the KHY website, allowing you to manage products, categories, and images with live preview capabilities.
## Features ## Features
- **Add Products**: Create new products with full details - **Product Management**: Add, edit, delete, and duplicate products
- **Edit Products**: Modify existing product information - **Category Management**: Organize products into categories
- **Delete Products**: Remove products with confirmation - **Image Management**: Upload, organize, and manage product images
- **Preview Products**: See how products will look before saving - **Live Preview**: Preview how changes will look on the actual website
- **Export JSON**: Download updated products.json file - **Sync to Main Site**: Deploy changes to your main website repository
- **Live Preview**: See exactly how changes will look on the website - **Responsive Design**: Works on desktop, tablet, and mobile devices
- **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 ## Quick Start
1. **Setup deploy paths** (first time only): ### 1. Setup
```bash ```bash
npm run setup # Navigate to the admin dashboard directory
# Follow the prompts to configure where your main KHY website is located cd admin-dashboard
```
2. **Open the dashboard**: # Install dependencies (optional - for image processing)
npm install
```bash # Start the development server
# Option 1: Double-click admin.html in your file explorer npm start
# Option 2: Run a local server ```
npm start
# Then open http://localhost:8080/admin.html
```
3. **Preview the website** (optional): ### 2. Access the Dashboard
```bash Open your browser and go to: `http://localhost:3000`
# Option A: Auto-start server and open all preview pages
npm run preview
# Option B: Start server manually ### 3. Using the Dashboard
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**: #### Products Section
- Add new products using the form - **Add Product**: Click "Add Product" to create a new product
- Edit existing products by clicking "Edit" - **Edit Product**: Click the "Edit" button on any product card
- Delete products by clicking "Delete" - **Duplicate Product**: Click "Duplicate" to create a copy of an existing product
- Preview changes before saving - **Delete Product**: Click "Delete" to remove a product (with confirmation)
- **Search**: Use the search bar to find specific products
5. **Deploy to production**: #### Categories Section
```bash - **Add Category**: Create new product categories
# Option A: Automated deploy (recommended) - **Edit Category**: Modify existing categories
npm run deploy - **View Products**: See all products in a specific category
# This copies admin/data/products.json → main website data/products.json - **Delete Category**: Remove categories (only if no products are using them)
# And copies admin/assets/images/ → main website assets/images/
# Option B: Manual deploy #### Images Section
# Click "Download products.json" and replace in main website
```
## Complete Workflow - **Upload Images**: Drag and drop or click to upload new images
- **View Images**: See all uploaded images with metadata
- **Rename Images**: Change image names
- **Delete Images**: Remove unused images
1. **Make Changes** → Use admin dashboard to add/edit/delete products #### Preview Section
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 - **Product Catalog**: See how your product catalog looks to customers
- **Product Detail**: Preview individual product pages
- **Interactive**: Click on products in catalog to view details
### 4. Syncing to Main Site
When you're ready to deploy your changes:
1. Click the "Sync to Main Site" button
2. Watch the sync progress in the modal
3. Your changes will be committed and pushed to the main repository
## File Structure ## File Structure
``` ```
admin/ admin-dashboard/
├── admin.html # Main admin dashboard ├── src/
├── index.html # Homepage (reference) │ ├── index.html # Main dashboard interface
├── product-catalog.html # Product catalog page (reference) │ ├── css/
├── product-detail.html # Product detail page (reference) │ │ └── admin.css # Dashboard styling
│ └── js/
│ ├── admin.js # Main dashboard controller
│ ├── productManager.js # Product management logic
│ ├── categoryManager.js # Category management logic
│ └── imageManager.js # Image management logic
├── data/ ├── data/
│ └── products.json # Product data file │ └── products.json # Product and category data
├── assets/ # Images and resources ├── assets/
├── scripts/ # JavaScript files │ └── images/
├── styles/ # CSS files │ └── products/ # Product images
├── src/ # Source files ├── preview/
├── tailwind.config.js # Tailwind configuration │ ├── catalog.html # Product catalog preview
├── deploy.js # Deploy script │ └── detail.html # Product detail preview
├── start-server.js # Auto-start server script ├── scripts/
├── package.json # Project configuration │ └── sync-to-main.js # Sync script for deployment
├── README.md # This file ├── package.json # Node.js dependencies
└── WORKFLOW.md # Detailed workflow guide └── README.md # This file
``` ```
## Usage Instructions ## Configuration
### Adding a New Product ### Main Repository URL
1. Fill out the product form with: Before using the sync feature, update the repository URL in `scripts/sync-to-main.js`:
- 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 ```javascript
this.mainRepoUrl = "https://github.com/your-username/khy-website.git";
```
1. Click "Edit" on any product in the list ### Data Structure
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 The dashboard expects your `products.json` to have this structure:
1. Click "Delete" on any product ```json
2. Confirm the deletion in the popup {
3. Product is removed immediately "products": [
{
"id": 1,
"name": "Product Name",
"description": "Short description",
"descriptionLong": ["Paragraph 1", "Paragraph 2"],
"category": "category-id",
"image": "assets/images/products/image.jpg",
"images": [
"assets/images/products/image1.jpg",
"assets/images/products/image2.jpg"
],
"galleryPairs": [
"assets/images/products/image1.jpg",
"assets/images/products/image2.jpg"
],
"additionalInformation": {
"Material": "Product material",
"Design": "Design description",
"Use Cases": "Use case description"
},
"warranty": "5 years"
}
],
"categories": [
{
"id": "category-id",
"name": "Category Name",
"description": "Category description"
}
]
}
```
### Exporting Changes ## Development
1. After making all your changes ### Adding New Features
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 1. **New Product Fields**: Add form fields in `src/index.html` and handle them in `productManager.js`
2. **New Category Fields**: Extend the category form and logic in `categoryManager.js`
3. **New Image Features**: Add functionality in `imageManager.js`
- **No server required**: Works entirely in the browser ### Styling
- **No database needed**: Uses JSON file storage
- **Cross-platform**: Works on Windows, Mac, Linux The dashboard uses a custom CSS framework with:
- **Offline capable**: Works without internet connection
- **Simple deployment**: Just copy files to any web server - **Colors**: Gradient-based color scheme
- **Typography**: Montserrat + Playfair Display fonts
- **Components**: Card-based layout with hover effects
- **Responsive**: Mobile-first responsive design
### API Integration
Currently, the dashboard works with local JSON files. To integrate with a real API:
1. Replace `fetch('data/products.json')` calls with API endpoints
2. Update the `saveData()` methods to POST to your API
3. Handle authentication and error states
## Deployment
### Option 1: Separate Repository (Recommended)
1. Move this admin dashboard to its own repository
2. Update the sync script with your main repository URL
3. Deploy the admin dashboard to a separate domain/subdomain
4. Use the sync feature to deploy changes to your main site
### Option 2: Subdirectory
1. Keep the admin dashboard in a subdirectory of your main site
2. Add authentication to protect the admin area
3. Use the sync feature to update the main site files
## Security Considerations
- **Authentication**: Add login functionality before deploying
- **File Uploads**: Validate and sanitize uploaded images
- **Data Validation**: Validate all form inputs
- **Access Control**: Restrict access to authorized users only
## Troubleshooting
### Common Issues
1. **Images not loading**: Check file paths and ensure images exist
2. **Sync fails**: Verify repository URL and Git credentials
3. **Preview not working**: Ensure the preview HTML files are accessible
4. **Data not saving**: Check browser console for JavaScript errors
### Getting Help
- Check the browser console for error messages
- Verify all file paths are correct
- Ensure the data structure matches the expected format
- Test with a small dataset first
## License
This admin dashboard is part of the KHY website project.
## Support ## Support
For technical support or questions about the admin dashboard, contact your development team. For support or questions about the admin dashboard, please contact the development team.
## Version History
- **v1.0.0**: Initial release with full CRUD functionality

View file

@ -1,164 +0,0 @@
# 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

View file

@ -1,104 +0,0 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs/
*.log
sync-log.json
# Temporary files
temp-main/
.tmp/
.temp/
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# Build outputs
dist/
build/
# Cache
.cache/
.parcel-cache/
# Runtime data
pids/
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
# nyc test coverage
.nyc_output/
# 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
# next.js build output
.next
# Nuxt.js build output
.nuxt
# Gatsby files
.cache/
public
# Storybook build outputs
.out
.storybook-out
# Temporary folders
tmp/
temp/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View file

@ -1,217 +0,0 @@
# KHY Admin Dashboard
A comprehensive product management dashboard for the KHY website, allowing you to manage products, categories, and images with live preview capabilities.
## Features
- **Product Management**: Add, edit, delete, and duplicate products
- **Category Management**: Organize products into categories
- **Image Management**: Upload, organize, and manage product images
- **Live Preview**: Preview how changes will look on the actual website
- **Sync to Main Site**: Deploy changes to your main website repository
- **Responsive Design**: Works on desktop, tablet, and mobile devices
## Quick Start
### 1. Setup
```bash
# Navigate to the admin dashboard directory
cd admin-dashboard
# Install dependencies (optional - for image processing)
npm install
# Start the development server
npm start
```
### 2. Access the Dashboard
Open your browser and go to: `http://localhost:3000`
### 3. Using the Dashboard
#### Products Section
- **Add Product**: Click "Add Product" to create a new product
- **Edit Product**: Click the "Edit" button on any product card
- **Duplicate Product**: Click "Duplicate" to create a copy of an existing product
- **Delete Product**: Click "Delete" to remove a product (with confirmation)
- **Search**: Use the search bar to find specific products
#### Categories Section
- **Add Category**: Create new product categories
- **Edit Category**: Modify existing categories
- **View Products**: See all products in a specific category
- **Delete Category**: Remove categories (only if no products are using them)
#### Images Section
- **Upload Images**: Drag and drop or click to upload new images
- **View Images**: See all uploaded images with metadata
- **Rename Images**: Change image names
- **Delete Images**: Remove unused images
#### Preview Section
- **Product Catalog**: See how your product catalog looks to customers
- **Product Detail**: Preview individual product pages
- **Interactive**: Click on products in catalog to view details
### 4. Syncing to Main Site
When you're ready to deploy your changes:
1. Click the "Sync to Main Site" button
2. Watch the sync progress in the modal
3. Your changes will be committed and pushed to the main repository
## File Structure
```
admin-dashboard/
├── src/
│ ├── index.html # Main dashboard interface
│ ├── css/
│ │ └── admin.css # Dashboard styling
│ └── js/
│ ├── admin.js # Main dashboard controller
│ ├── productManager.js # Product management logic
│ ├── categoryManager.js # Category management logic
│ └── imageManager.js # Image management logic
├── data/
│ └── products.json # Product and category data
├── assets/
│ └── images/
│ └── products/ # Product images
├── preview/
│ ├── catalog.html # Product catalog preview
│ └── detail.html # Product detail preview
├── scripts/
│ └── sync-to-main.js # Sync script for deployment
├── package.json # Node.js dependencies
└── README.md # This file
```
## Configuration
### Main Repository URL
Before using the sync feature, update the repository URL in `scripts/sync-to-main.js`:
```javascript
this.mainRepoUrl = "https://github.com/your-username/khy-website.git";
```
### Data Structure
The dashboard expects your `products.json` to have this structure:
```json
{
"products": [
{
"id": 1,
"name": "Product Name",
"description": "Short description",
"descriptionLong": ["Paragraph 1", "Paragraph 2"],
"category": "category-id",
"image": "assets/images/products/image.jpg",
"images": [
"assets/images/products/image1.jpg",
"assets/images/products/image2.jpg"
],
"galleryPairs": [
"assets/images/products/image1.jpg",
"assets/images/products/image2.jpg"
],
"additionalInformation": {
"Material": "Product material",
"Design": "Design description",
"Use Cases": "Use case description"
},
"warranty": "5 years"
}
],
"categories": [
{
"id": "category-id",
"name": "Category Name",
"description": "Category description"
}
]
}
```
## Development
### Adding New Features
1. **New Product Fields**: Add form fields in `src/index.html` and handle them in `productManager.js`
2. **New Category Fields**: Extend the category form and logic in `categoryManager.js`
3. **New Image Features**: Add functionality in `imageManager.js`
### Styling
The dashboard uses a custom CSS framework with:
- **Colors**: Gradient-based color scheme
- **Typography**: Montserrat + Playfair Display fonts
- **Components**: Card-based layout with hover effects
- **Responsive**: Mobile-first responsive design
### API Integration
Currently, the dashboard works with local JSON files. To integrate with a real API:
1. Replace `fetch('data/products.json')` calls with API endpoints
2. Update the `saveData()` methods to POST to your API
3. Handle authentication and error states
## Deployment
### Option 1: Separate Repository (Recommended)
1. Move this admin dashboard to its own repository
2. Update the sync script with your main repository URL
3. Deploy the admin dashboard to a separate domain/subdomain
4. Use the sync feature to deploy changes to your main site
### Option 2: Subdirectory
1. Keep the admin dashboard in a subdirectory of your main site
2. Add authentication to protect the admin area
3. Use the sync feature to update the main site files
## Security Considerations
- **Authentication**: Add login functionality before deploying
- **File Uploads**: Validate and sanitize uploaded images
- **Data Validation**: Validate all form inputs
- **Access Control**: Restrict access to authorized users only
## Troubleshooting
### Common Issues
1. **Images not loading**: Check file paths and ensure images exist
2. **Sync fails**: Verify repository URL and Git credentials
3. **Preview not working**: Ensure the preview HTML files are accessible
4. **Data not saving**: Check browser console for JavaScript errors
### Getting Help
- Check the browser console for error messages
- Verify all file paths are correct
- Ensure the data structure matches the expected format
- Test with a small dataset first
## License
This admin dashboard is part of the KHY website project.
## Support
For support or questions about the admin dashboard, please contact the development team.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 MiB

File diff suppressed because it is too large Load diff

View file

@ -1,24 +0,0 @@
{
"name": "khy-admin",
"version": "1.0.0",
"description": "KHY Product Management Dashboard",
"main": "src/index.html",
"scripts": {
"start": "python3 -m http.server 3000",
"sync": "node scripts/sync-to-main.js",
"dev": "python3 -m http.server 3000"
},
"dependencies": {
"sharp": "^0.32.0"
},
"devDependencies": {
"nodemon": "^3.0.0"
},
"keywords": [
"admin",
"dashboard",
"product-management"
],
"author": "KHY",
"license": "MIT"
}

View file

@ -1,272 +0,0 @@
<!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="assets/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"
></div>
</nav>
</header>
<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"
>
<!-- Thumbnail images will be populated dynamically -->
</div>
<!-- Main Product Image -->
<div
class="w-full h-80 md:w-[500px] md:h-[500px] bg-floral-white rounded-lg overflow-hidden"
>
<!-- Main product image will be populated dynamically -->
</div>
</div>
<!-- Right Section - Product Details -->
<div class="w-full md:w-[500px]">
<!-- Product Title -->
<h1
class="font-playfair font-normal text-4xl text-black mb-8"
id="product-title"
>
<!-- Product title will be populated dynamically -->
</h1>
<!-- Product Description -->
<p
class="font-playfair font-normal text-sm text-black mb-8 max-w-md"
id="product-description"
>
<!-- Product description will be populated dynamically -->
</p>
<!-- Quantity and Action Buttons -->
<div class="flex flex-col md:flex-row gap-4 md:gap-6 mb-3">
<!-- Action Buttons -->
<a
href="contact.html"
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 mb-4"
>
Request a Quote
</a>
<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-3"></div>
<!-- Product Specifications -->
<div class="mb-8">
<div class="space-y-2" id="product-specifications">
<!-- Product specifications will be populated dynamically -->
</div>
</div>
<!-- Product Metadata -->
<div class="space-y-4">
<!-- Product metadata will be populated dynamically -->
</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 text-center"
id="product-description-content"
>
<!-- Product description content will be populated dynamically -->
</div>
<!-- Images -->
<div class="relative mt-10">
<div
class="grid grid-cols-1 md:grid-cols-2 gap-8"
id="product-gallery-images"
>
<!-- Product gallery images will be populated dynamically -->
</div>
<!-- Back Arrow -->
<button
id="gallery-back-btn"
class="absolute top-1/2 left-0 transform -translate-y-1/2 bg-white border border-gray-300 rounded-full p-3 shadow-lg hover:shadow-xl transition-all duration-200 opacity-0 pointer-events-none"
style="display: none"
>
<svg
class="w-6 h-6 text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 19l-7-7 7-7"
></path>
</svg>
</button>
<!-- Forward Arrow -->
<button
id="gallery-next-btn"
class="absolute top-1/2 right-0 transform -translate-y-1/2 bg-white border border-gray-300 rounded-full p-3 shadow-lg hover:shadow-xl transition-all duration-200 opacity-0 pointer-events-none"
style="display: none"
>
<svg
class="w-6 h-6 text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
></path>
</svg>
</button>
</div>
</div>
</section>
<!-- 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>
<!-- Image Enlargement Modal -->
<div
id="image-modal"
class="fixed inset-0 bg-black bg-opacity-90 z-50 hidden flex items-center justify-center p-4 transition-opacity duration-300"
>
<div
class="relative max-w-7xl max-h-full w-full h-full flex items-center justify-center"
>
<!-- Close Button -->
<button
id="modal-close-btn"
class="absolute top-4 right-4 z-10 bg-white bg-opacity-20 hover:bg-opacity-30 text-white rounded-full p-3 transition-all duration-200 backdrop-blur-sm"
aria-label="Close modal"
>
<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>
<!-- Enlarged Image -->
<img
id="modal-image"
src=""
alt=""
class="w-[95vw] h-[95vh] object-contain rounded-lg shadow-2xl transform transition-transform duration-300 scale-95"
/>
</div>
</div>
<script src="scripts/main.js?v=3.5"></script>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,91 +0,0 @@
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"
);

View file

@ -1,56 +0,0 @@
/** @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")],
};

View file

@ -1,728 +0,0 @@
<!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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 713 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 548 B

View file

Before

Width:  |  Height:  |  Size: 815 B

After

Width:  |  Height:  |  Size: 815 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 889 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 802 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View file

Before

Width:  |  Height:  |  Size: 6.3 MiB

After

Width:  |  Height:  |  Size: 6.3 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 815 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 KiB

View file

Before

Width:  |  Height:  |  Size: 417 KiB

After

Width:  |  Height:  |  Size: 417 KiB

Before After
Before After

Some files were not shown because too many files have changed in this diff Show more