reset repo
97
.gitignore
vendored
|
|
@ -4,8 +4,49 @@ 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
|
||||
pids/
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
|
@ -14,19 +55,7 @@ pids
|
|||
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
|
||||
.nyc_output/
|
||||
|
||||
# Dependency directories
|
||||
jspm_packages/
|
||||
|
|
@ -46,26 +75,30 @@ jspm_packages/
|
|||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
# Nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
# Gatsby files
|
||||
.cache/
|
||||
public
|
||||
|
||||
# Temporary files
|
||||
# Storybook build outputs
|
||||
.out
|
||||
.storybook-out
|
||||
|
||||
# Temporary folders
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
|
|||
150
DEPLOY_SETUP.md
|
|
@ -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
|
|
@ -1,146 +1,217 @@
|
|||
# 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
|
||||
|
||||
- **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
|
||||
- **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 deploy paths** (first time only):
|
||||
### 1. Setup
|
||||
|
||||
```bash
|
||||
npm run setup
|
||||
# Follow the prompts to configure where your main KHY website is located
|
||||
```
|
||||
```bash
|
||||
# Navigate to the admin dashboard directory
|
||||
cd admin-dashboard
|
||||
|
||||
2. **Open the dashboard**:
|
||||
# Install dependencies (optional - for image processing)
|
||||
npm install
|
||||
|
||||
```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
|
||||
```
|
||||
# Start the development server
|
||||
npm start
|
||||
```
|
||||
|
||||
3. **Preview the website** (optional):
|
||||
### 2. Access the Dashboard
|
||||
|
||||
```bash
|
||||
# Option A: Auto-start server and open all preview pages
|
||||
npm run preview
|
||||
Open your browser and go to: `http://localhost:3000`
|
||||
|
||||
# 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)
|
||||
```
|
||||
### 3. Using the Dashboard
|
||||
|
||||
4. **Manage products**:
|
||||
#### Products Section
|
||||
|
||||
- Add new products using the form
|
||||
- Edit existing products by clicking "Edit"
|
||||
- Delete products by clicking "Delete"
|
||||
- Preview changes before saving
|
||||
- **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
|
||||
|
||||
5. **Deploy to production**:
|
||||
#### Categories Section
|
||||
|
||||
```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/
|
||||
- **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)
|
||||
|
||||
# Option B: Manual deploy
|
||||
# Click "Download products.json" and replace in main website
|
||||
```
|
||||
#### Images Section
|
||||
|
||||
## 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
|
||||
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
|
||||
#### Preview Section
|
||||
|
||||
> **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
|
||||
|
||||
```
|
||||
admin/
|
||||
├── admin.html # Main admin dashboard
|
||||
├── index.html # Homepage (reference)
|
||||
├── product-catalog.html # Product catalog page (reference)
|
||||
├── product-detail.html # Product detail page (reference)
|
||||
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 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
|
||||
│ └── 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
|
||||
```
|
||||
|
||||
## Usage Instructions
|
||||
## Configuration
|
||||
|
||||
### Adding a New Product
|
||||
### Main Repository URL
|
||||
|
||||
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
|
||||
Before using the sync feature, update the repository URL in `scripts/sync-to-main.js`:
|
||||
|
||||
### Editing a Product
|
||||
```javascript
|
||||
this.mainRepoUrl = "https://github.com/your-username/khy-website.git";
|
||||
```
|
||||
|
||||
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
|
||||
### Data Structure
|
||||
|
||||
### Deleting a Product
|
||||
The dashboard expects your `products.json` to have this structure:
|
||||
|
||||
1. Click "Delete" on any product
|
||||
2. Confirm the deletion in the popup
|
||||
3. Product is removed immediately
|
||||
```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"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Exporting Changes
|
||||
## Development
|
||||
|
||||
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
|
||||
### Adding New Features
|
||||
|
||||
## 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
|
||||
- **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
|
||||
### 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 technical support or questions about the admin dashboard, contact your development team.
|
||||
|
||||
## Version History
|
||||
|
||||
- **v1.0.0**: Initial release with full CRUD functionality
|
||||
For support or questions about the admin dashboard, please contact the development team.
|
||||
|
|
|
|||
164
WORKFLOW.md
|
|
@ -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
|
||||
104
admin-dashboard/.gitignore
vendored
|
|
@ -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?
|
||||
|
|
@ -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.
|
||||
|
Before Width: | Height: | Size: 884 B |
|
Before Width: | Height: | Size: 811 B |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 803 B |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 4.3 MiB |
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
|
|
@ -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")],
|
||||
};
|
||||
728
admin.html
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 713 B |
|
Before Width: | Height: | Size: 548 B |
|
Before Width: | Height: | Size: 815 B After Width: | Height: | Size: 815 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 889 B |
|
Before Width: | Height: | Size: 749 B |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 500 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 4.8 MiB |
|
Before Width: | Height: | Size: 4.6 MiB |
|
Before Width: | Height: | Size: 802 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.3 MiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 6.3 MiB After Width: | Height: | Size: 6.3 MiB |
|
Before Width: | Height: | Size: 359 KiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 815 B |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 526 KiB |
|
Before Width: | Height: | Size: 1 MiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 408 KiB |
|
Before Width: | Height: | Size: 417 KiB |
|
Before Width: | Height: | Size: 417 KiB After Width: | Height: | Size: 417 KiB |
|
Before Width: | Height: | Size: 272 KiB After Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 180 KiB |
|
Before Width: | Height: | Size: 1 MiB After Width: | Height: | Size: 1 MiB |
|
Before Width: | Height: | Size: 1 MiB After Width: | Height: | Size: 1 MiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 895 KiB After Width: | Height: | Size: 895 KiB |
|
Before Width: | Height: | Size: 896 KiB After Width: | Height: | Size: 896 KiB |
|
Before Width: | Height: | Size: 793 KiB After Width: | Height: | Size: 793 KiB |
|
Before Width: | Height: | Size: 713 KiB After Width: | Height: | Size: 713 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 191 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 337 KiB After Width: | Height: | Size: 337 KiB |