commit d3403cb5e2b3e9cc3a865db24f5fe839e2990dc3 Author: George Birikorang Date: Wed Sep 10 16:56:52 2025 -0700 Initial commit: KHY Admin project diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf6a4af --- /dev/null +++ b/.gitignore @@ -0,0 +1,71 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage +.grunt + +# Bower dependency directory +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons +build/Release + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +logs +*.log + +# Temporary files +tmp/ +temp/ diff --git a/DEPLOY_SETUP.md b/DEPLOY_SETUP.md new file mode 100644 index 0000000..1c32baf --- /dev/null +++ b/DEPLOY_SETUP.md @@ -0,0 +1,150 @@ +# Deploy Setup Guide + +This guide explains how to configure the admin dashboard to deploy to your main KHY website when they are in separate repositories. + +## Repository Structure Examples + +### Example 1: Sibling Directories + +``` +Documents/ +├── khy_website/ # Main website repository +│ ├── index.html +│ ├── data/ +│ └── assets/ +└── khy_admin/ # Admin repository + ├── admin.html + ├── data/ + └── assets/ +``` + +**Setup**: Run `npm run setup` and enter `../khy_website` + +### Example 2: Different Parent Directories + +``` +Documents/ +├── projects/ +│ └── khy_website/ # Main website repository +└── admin_tools/ + └── khy_admin/ # Admin repository +``` + +**Setup**: Run `npm run setup` and enter `../../projects/khy_website` + +### Example 3: Absolute Paths + +``` +/Users/george/Documents/khy_website/ # Main website +/Users/george/Documents/admin_tools/ # Admin repository +``` + +**Setup**: Run `npm run setup` and enter `/Users/george/Documents/khy_website` + +## Setup Process + +1. **Run the setup script**: + + ```bash + npm run setup + ``` + +2. **Enter the path to your main KHY website**: + + - Use relative paths like `../khy_website` or `../../projects/khy_website` + - Use absolute paths like `/full/path/to/khy_website` + +3. **Verify the configuration**: + - The script will check if the path exists + - It will verify required files are present + - It will update `deploy-config.js` with your settings + +## Manual Configuration + +If you prefer to configure manually, edit `deploy-config.js`: + +```javascript +module.exports = { + targets: { + main: "../khy_website", // Change this to your path + }, + defaultTarget: "main", + // ... rest of config +}; +``` + +## Testing the Setup + +After setup, test the configuration: + +```bash +# Test with the configured path +npm run deploy + +# Test with a specific path +node deploy.js /path/to/your/khy_website +``` + +## Troubleshooting + +### "Target directory does not exist" + +- Check the path you entered during setup +- Use absolute paths if relative paths don't work +- Make sure the main website directory exists + +### "Missing required files" + +- Ensure your main website has `index.html` and `data/products.json` +- The admin will create these files if they don't exist + +### "Permission denied" + +- Make sure you have write permissions to the main website directory +- On macOS/Linux, you might need to adjust file permissions + +## Advanced Usage + +### Multiple Targets + +You can configure multiple deployment targets: + +```javascript +targets: { + staging: "../khy_website_staging", + production: "../khy_website", + backup: "/backup/khy_website" +} +``` + +Then deploy to specific targets: + +```bash +node deploy.js staging +node deploy.js production +``` + +### Custom File Mappings + +Modify `filesToCopy` in `deploy-config.js` to copy different files: + +```javascript +filesToCopy: [ + { + source: "data/products.json", + target: "data/products.json", + description: "Product catalog data", + }, + { + source: "assets/images", + target: "assets/images", + description: "Product images", + isDirectory: true, + }, + { + source: "config/settings.json", + target: "config/admin-settings.json", + description: "Admin settings", + }, +]; +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..446e85f --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +# KHY Admin Dashboard + +A **preview/staging environment** for managing KHY Furniture's product catalog with safe testing and one-click deployment to production. + +## Features + +- ✅ **Add Products**: Create new products with full details +- ✅ **Edit Products**: Modify existing product information +- ✅ **Delete Products**: Remove products with confirmation +- ✅ **Preview Products**: See how products will look before saving +- ✅ **Export JSON**: Download updated products.json file +- ✅ **Live Preview**: See exactly how changes will look on the website +- ✅ **One-Click Deploy**: Deploy changes to production with a single command +- ✅ **Automatic Backups**: Safe deployment with backup system +- ✅ **No Technical Knowledge Required**: Simple form-based interface + +## Quick Start + +1. **Setup deploy paths** (first time only): + + ```bash + npm run setup + # Follow the prompts to configure where your main KHY website is located + ``` + +2. **Open the dashboard**: + + ```bash + # Option 1: Double-click admin.html in your file explorer + # Option 2: Run a local server + npm start + # Then open http://localhost:8080/admin.html + ``` + +3. **Preview the website** (optional): + + ```bash + # Option A: Auto-start server and open all preview pages + npm run preview + + # Option B: Start server manually + npm start + # Then open http://localhost:8080/index.html (homepage) + # Open http://localhost:8080/product-catalog.html (catalog) + # Open http://localhost:8080/product-detail.html (product details) + ``` + +4. **Manage products**: + + - Add new products using the form + - Edit existing products by clicking "Edit" + - Delete products by clicking "Delete" + - Preview changes before saving + +5. **Deploy to production**: + + ```bash + # Option A: Automated deploy (recommended) + npm run deploy + # This copies admin/data/products.json → main website data/products.json + # And copies admin/assets/images/ → main website assets/images/ + + # Option B: Manual deploy + # Click "Download products.json" and replace in main website + ``` + +## Complete Workflow + +1. **Make Changes** → Use admin dashboard to add/edit/delete products +2. **Preview Changes** → Open `index.html`, `product-catalog.html`, `product-detail.html` to see how changes look +3. **Confirm Changes** → Review everything thoroughly +4. **Deploy to Production** → Run `npm run deploy` to copy updated files to the main KHY website + +> 📖 **Detailed Workflow**: See [WORKFLOW.md](WORKFLOW.md) for complete instructions + +## File Structure + +``` +admin/ +├── admin.html # Main admin dashboard +├── index.html # Homepage (reference) +├── product-catalog.html # Product catalog page (reference) +├── product-detail.html # Product detail page (reference) +├── data/ +│ └── products.json # Product data file +├── assets/ # Images and resources +├── scripts/ # JavaScript files +├── styles/ # CSS files +├── src/ # Source files +├── tailwind.config.js # Tailwind configuration +├── deploy.js # Deploy script +├── start-server.js # Auto-start server script +├── package.json # Project configuration +├── README.md # This file +└── WORKFLOW.md # Detailed workflow guide +``` + +## Usage Instructions + +### Adding a New Product + +1. Fill out the product form with: + - Product name and description + - Category selection + - Price and model number + - Available sizes and colors + - Stock status and rating +2. Click "Preview Product" to see how it looks +3. Click "Add Product" to save + +### Editing a Product + +1. Click "Edit" on any product in the list +2. The form will auto-fill with existing data +3. Make your changes +4. Click "Preview Product" to see changes +5. Click "Update Product" to save or "Cancel Edit" to abort + +### Deleting a Product + +1. Click "Delete" on any product +2. Confirm the deletion in the popup +3. Product is removed immediately + +### Exporting Changes + +1. After making all your changes +2. Click "Download products.json" +3. Replace the existing `data/products.json` file in your main website +4. Your website will show the updated products + +## Technical Notes + +- **No server required**: Works entirely in the browser +- **No database needed**: Uses JSON file storage +- **Cross-platform**: Works on Windows, Mac, Linux +- **Offline capable**: Works without internet connection +- **Simple deployment**: Just copy files to any web server + +## Support + +For technical support or questions about the admin dashboard, contact your development team. + +## Version History + +- **v1.0.0**: Initial release with full CRUD functionality diff --git a/WORKFLOW.md b/WORKFLOW.md new file mode 100644 index 0000000..0b90b06 --- /dev/null +++ b/WORKFLOW.md @@ -0,0 +1,164 @@ +# KHY Admin Workflow Guide + +## Overview + +This admin dashboard provides a **preview/staging environment** where you can make changes, see exactly how they'll look on the website, and then deploy to production with confidence. + +## Complete Workflow + +### 1. 🎯 **Make Changes** + +- Open `admin.html` in your browser +- Add, edit, or delete products using the form interface +- Use "Preview Product" to see how individual products will look + +### 2. 👀 **Preview Changes** + +#### Option A: Auto-Start Preview (Recommended) + +```bash +# One command to start server and open all preview pages +npm run preview + +# Server will automatically stop when: +# - All preview tabs are closed +# - Deploy command is run +# - Manual stop: npm run preview:kill +``` + +#### Option B: Manual Preview + +- Click "👀 Preview Website" button in admin dashboard +- Or manually open `index.html`, `product-catalog.html`, `product-detail.html` +- Navigate through the site to ensure everything looks correct + +### 3. ✅ **Confirm Changes** + +- Review all changes thoroughly +- Test product links and functionality +- Ensure images load correctly + +### 4. 🚀 **Deploy to Production** + +The deploy process copies updated files from the **admin directory** to the **main KHY website directory**. + +#### Option A: Automated Deploy (Recommended) + +```bash +# Navigate to admin directory +cd admin + +# Copy updated files to main website +npm run deploy +``` + +**What this does:** + +- Copies `admin/data/products.json` → `../data/products.json` (main website) +- Copies `admin/assets/images/` → `../assets/images/` (main website) +- Creates backup of current main website files +- Updates the live KHY website immediately + +#### Option B: Manual Deploy + +1. Click "Download products.json" in the admin dashboard +2. Manually copy the downloaded file to `../data/products.json` +3. Manually copy any new images from `admin/assets/images/` to `../assets/images/` + +## File Structure + +``` +admin/ # Admin/Staging Environment +├── admin.html # Admin dashboard +├── index.html # Homepage preview +├── product-catalog.html # Catalog preview +├── product-detail.html # Product detail preview +├── data/products.json # Staging product data +├── assets/images/ # Staging images +├── deploy.js # Deploy script +└── package.json # NPM scripts + +../ # Main Website (Production) +├── index.html # Live homepage +├── product-catalog.html # Live catalog +├── product-detail.html # Live product details +├── data/products.json # Live product data +└── assets/images/ # Live images +``` + +## Key Benefits + +✅ **Safe Testing**: Make changes without affecting the live site +✅ **Visual Preview**: See exactly how changes will look +✅ **Easy Deployment**: One-click deploy to production +✅ **Backup System**: Automatic backups before deployment +✅ **No Technical Knowledge**: Simple interface for non-technical users + +## Deployment Process + +### What Happens During Deploy: + +1. **Stop Preview Server**: Automatically stops the preview server (if running) +2. **Validation**: Checks that main KHY website directory exists and has required files +3. **Backup**: Creates timestamped backup of current main website data +4. **Copy Files**: + - Copies `admin/data/products.json` → `../data/products.json` (main website) + - Copies `admin/assets/images/` → `../assets/images/` (main website) +5. **Verification**: Confirms all files were copied successfully +6. **Completion**: Main KHY website is updated immediately with new product data + +### Backup System: + +- All deployments create automatic backups in `admin/backups/` +- Backups are timestamped for easy identification +- Can restore from backup if needed + +## Troubleshooting + +### Deploy Fails? + +- Check that you're in the admin directory +- Verify the main website directory exists +- Ensure you have write permissions + +### Changes Not Showing? + +- Clear browser cache (Ctrl+F5 or Cmd+Shift+R) +- Check that the correct `products.json` file was updated +- Verify image paths are correct + +### Need to Restore? + +- Find the backup in `admin/backups/` +- Copy the backup `products.json` to the main website +- Or run the deploy script again + +## Best Practices + +1. **Always Preview**: Use the preview pages before deploying +2. **Test Everything**: Check all product links and images +3. **Deploy Regularly**: Don't let changes pile up +4. **Keep Backups**: The system creates them automatically +5. **Document Changes**: Keep notes of what you changed and why + +## Quick Commands + +```bash +# Start preview server +npm start + +# Deploy to production +npm run deploy + +# View help +npm run preview +``` + +## Support + +If you encounter any issues: + +1. Check the browser console for errors +2. Verify file permissions +3. Ensure all required files exist +4. Contact your development team for assistance diff --git a/admin.html b/admin.html new file mode 100644 index 0000000..0e6f872 --- /dev/null +++ b/admin.html @@ -0,0 +1,728 @@ + + + + + + Product Management - KHY Admin + + + + +
+

Product Management

+ + +
+

Add New Product

+ + + + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+

Available Sizes

+
+
+ + +
+
+ +
+ +
+

Available Colors

+
+
+ + + +
+
+ +
+ +
+ + + +
+
+
+ + +
+
+

Current Products

+
+ + + +
+
+
+
+
+ + + + diff --git a/assets/icons/address.png b/assets/icons/address.png new file mode 100644 index 0000000..28e21f5 Binary files /dev/null and b/assets/icons/address.png differ diff --git a/assets/icons/admin.png b/assets/icons/admin.png new file mode 100644 index 0000000..0a70489 Binary files /dev/null and b/assets/icons/admin.png differ diff --git a/assets/icons/calendar.png b/assets/icons/calendar.png new file mode 100644 index 0000000..ff83c67 Binary files /dev/null and b/assets/icons/calendar.png differ diff --git a/assets/icons/phone.png b/assets/icons/phone.png new file mode 100644 index 0000000..1e83e1a Binary files /dev/null and b/assets/icons/phone.png differ diff --git a/assets/icons/search.png b/assets/icons/search.png new file mode 100644 index 0000000..57ece34 Binary files /dev/null and b/assets/icons/search.png differ diff --git a/assets/icons/shipping.png b/assets/icons/shipping.png new file mode 100644 index 0000000..9fb9a8f Binary files /dev/null and b/assets/icons/shipping.png differ diff --git a/assets/icons/star.png b/assets/icons/star.png new file mode 100644 index 0000000..4f7a231 Binary files /dev/null and b/assets/icons/star.png differ diff --git a/assets/icons/support.png b/assets/icons/support.png new file mode 100644 index 0000000..ebb5cd6 Binary files /dev/null and b/assets/icons/support.png differ diff --git a/assets/icons/tag.png b/assets/icons/tag.png new file mode 100644 index 0000000..a020a6f Binary files /dev/null and b/assets/icons/tag.png differ diff --git a/assets/icons/time.png b/assets/icons/time.png new file mode 100644 index 0000000..d45e13b Binary files /dev/null and b/assets/icons/time.png differ diff --git a/assets/icons/trophy.png b/assets/icons/trophy.png new file mode 100644 index 0000000..c1820f9 Binary files /dev/null and b/assets/icons/trophy.png differ diff --git a/assets/icons/warranty.png b/assets/icons/warranty.png new file mode 100644 index 0000000..6448c7c Binary files /dev/null and b/assets/icons/warranty.png differ diff --git a/assets/images/andreu_world.png b/assets/images/andreu_world.png new file mode 100644 index 0000000..c5e5f66 Binary files /dev/null and b/assets/images/andreu_world.png differ diff --git a/assets/images/asgaard_sofa.png b/assets/images/asgaard_sofa.png new file mode 100644 index 0000000..0b55ef0 Binary files /dev/null and b/assets/images/asgaard_sofa.png differ diff --git a/assets/images/bene.png b/assets/images/bene.png new file mode 100644 index 0000000..21d540f Binary files /dev/null and b/assets/images/bene.png differ diff --git a/assets/images/blog_post_1.png b/assets/images/blog_post_1.png new file mode 100644 index 0000000..73c2647 Binary files /dev/null and b/assets/images/blog_post_1.png differ diff --git a/assets/images/blog_thumb_1.png b/assets/images/blog_thumb_1.png new file mode 100644 index 0000000..9896f63 Binary files /dev/null and b/assets/images/blog_thumb_1.png differ diff --git a/assets/images/cecil_nurse.png b/assets/images/cecil_nurse.png new file mode 100644 index 0000000..fd700c4 Binary files /dev/null and b/assets/images/cecil_nurse.png differ diff --git a/assets/images/chairs.jpg b/assets/images/chairs.jpg new file mode 100644 index 0000000..af8eef0 Binary files /dev/null and b/assets/images/chairs.jpg differ diff --git a/assets/images/conference_room.jpg b/assets/images/conference_room.jpg new file mode 100644 index 0000000..4767b50 Binary files /dev/null and b/assets/images/conference_room.jpg differ diff --git a/assets/images/conference_rooms.jpg b/assets/images/conference_rooms.jpg new file mode 100644 index 0000000..e4bbb79 Binary files /dev/null and b/assets/images/conference_rooms.jpg differ diff --git a/assets/images/dbg.png b/assets/images/dbg.png new file mode 100644 index 0000000..d80a408 Binary files /dev/null and b/assets/images/dbg.png differ diff --git a/assets/images/first_homepage.jpg b/assets/images/first_homepage.jpg new file mode 100644 index 0000000..f88db7d Binary files /dev/null and b/assets/images/first_homepage.jpg differ diff --git a/assets/images/forma_5.png b/assets/images/forma_5.png new file mode 100644 index 0000000..ece711c Binary files /dev/null and b/assets/images/forma_5.png differ diff --git a/assets/images/glico.png b/assets/images/glico.png new file mode 100644 index 0000000..5faff85 Binary files /dev/null and b/assets/images/glico.png differ diff --git a/assets/images/google_accra.jpg b/assets/images/google_accra.jpg new file mode 100644 index 0000000..9407454 Binary files /dev/null and b/assets/images/google_accra.jpg differ diff --git a/assets/images/grow.png b/assets/images/grow.png new file mode 100644 index 0000000..6ec2d70 Binary files /dev/null and b/assets/images/grow.png differ diff --git a/assets/images/khy_logo.png b/assets/images/khy_logo.png new file mode 100644 index 0000000..453d678 Binary files /dev/null and b/assets/images/khy_logo.png differ diff --git a/assets/images/kitchen.JPG b/assets/images/kitchen.JPG new file mode 100644 index 0000000..b08bab1 Binary files /dev/null and b/assets/images/kitchen.JPG differ diff --git a/assets/images/lounge_chair.jpg b/assets/images/lounge_chair.jpg new file mode 100644 index 0000000..4794e0c Binary files /dev/null and b/assets/images/lounge_chair.jpg differ diff --git a/assets/images/mail.png b/assets/images/mail.png new file mode 100644 index 0000000..97d2e58 Binary files /dev/null and b/assets/images/mail.png differ diff --git a/assets/images/norfund.png b/assets/images/norfund.png new file mode 100644 index 0000000..63ef199 Binary files /dev/null and b/assets/images/norfund.png differ diff --git a/assets/images/office_chair.jpg b/assets/images/office_chair.jpg new file mode 100644 index 0000000..5bbe2e8 Binary files /dev/null and b/assets/images/office_chair.jpg differ diff --git a/assets/images/our_story.jpg b/assets/images/our_story.jpg new file mode 100644 index 0000000..7583f1d Binary files /dev/null and b/assets/images/our_story.jpg differ diff --git a/assets/images/outdoor_sofa_set.png b/assets/images/outdoor_sofa_set.png new file mode 100644 index 0000000..b7a00d8 Binary files /dev/null and b/assets/images/outdoor_sofa_set.png differ diff --git a/assets/images/phone.png b/assets/images/phone.png new file mode 100644 index 0000000..5ba105b Binary files /dev/null and b/assets/images/phone.png differ diff --git a/assets/images/pods.jpg b/assets/images/pods.jpg new file mode 100644 index 0000000..0447f43 Binary files /dev/null and b/assets/images/pods.jpg differ diff --git a/assets/images/potty.jpg b/assets/images/potty.jpg new file mode 100644 index 0000000..1fe7884 Binary files /dev/null and b/assets/images/potty.jpg differ diff --git a/assets/images/prod-comparison.jpg b/assets/images/prod-comparison.jpg new file mode 100644 index 0000000..a44e83f Binary files /dev/null and b/assets/images/prod-comparison.jpg differ diff --git a/assets/images/stanbic.png b/assets/images/stanbic.png new file mode 100644 index 0000000..b8e91c8 Binary files /dev/null and b/assets/images/stanbic.png differ diff --git a/assets/images/storage.jpg b/assets/images/storage.jpg new file mode 100644 index 0000000..aae36cd Binary files /dev/null and b/assets/images/storage.jpg differ diff --git a/assets/images/workstation.jpg b/assets/images/workstation.jpg new file mode 100644 index 0000000..ee05008 Binary files /dev/null and b/assets/images/workstation.jpg differ diff --git a/data/blog.json b/data/blog.json new file mode 100644 index 0000000..1c8d54a --- /dev/null +++ b/data/blog.json @@ -0,0 +1,104 @@ +{ + "posts": [ + { + "id": 1, + "title": "Going all-in with millennial design", + "excerpt": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Mus mauris vitae ultricies leo integer malesuada nunc. In nulla posuere sollicitudin aliquam ultrices. Morbi blandit cursus risus at ultrices mi tempus imperdiet. Libero enim sed faucibus turpis in. Cursus mattis molestie a iaculis at erat. Nibh cras pulvinar mattis nunc sed blandit libero. Pellentesque elit ullamcorper dignissim cras tincidunt. Pharetra et ultrices neque ornare aenean euismod elementum. The millennial generation has redefined what it means to create a home that reflects both personal style and contemporary values. This design philosophy embraces clean lines, sustainable materials, and multifunctional spaces that adapt to the ever-changing needs of modern life. From open-concept layouts that foster social interaction to smart home technology that enhances daily convenience, millennial design prioritizes both aesthetics and functionality.", + "content": [ + "Quis hendrerit dolor magna eget est lorem ipsum. Bibendum arcu vitae elementum curabitur vitae nunc sed velit dignissim sodales. Non tellus orci ac auctor augue mauris augue neque gravida in. Nisl condimentum id venenatis a condimentum vitae sapien pellentesque habitant. Sit amet volutpat consequat mauris nunc congue nisi vitae suscipit. Integer enim neque volutpat ac tincidunt vitae semper quis lectus. Nibh tellus molestie nunc non blandit massa enim nec. Amet consectetur adipiscing elit ut aliquam purus sit amet luctus.", + "At tellus at urna condimentum mattis pellentesque id nibh tortor id. Non quam lacus suspendisse faucibus interdum. Interdum velit euismod in pellentesque massa placerat duis ultricies. Platea dictumst quisque sagittis purus sit amet volutpat consequat. Sem viverra aliquet eget sit amet tellus cras adipiscing. Eget lorem dolor sed viverra ipsum nunc aliquet bibendum enim facilisis gravida neque convallis a cras.", + "Mattis rhoncus urna neque viverra justo nec. Aliquet porttitor lacus luctus accumsan tortor posuere ac ut consequat semper viverra nam libero justo laoreet sit amet cursus sit amet dictum sit amet justo. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque elit.", + "The millennial generation has redefined what it means to create a home that reflects both personal style and contemporary values. This design philosophy embraces clean lines, sustainable materials, and multifunctional spaces that adapt to the ever-changing needs of modern life. From open-concept layouts that foster social interaction to smart home technology that enhances daily convenience, millennial design prioritizes both aesthetics and functionality.", + "One of the most significant aspects of millennial design is its emphasis on sustainability and ethical consumption. Young homeowners are increasingly choosing furniture and decor that not only looks good but also aligns with their environmental values. This includes pieces made from responsibly sourced materials, vintage and upcycled items that reduce waste, and locally crafted goods that support small businesses and reduce carbon footprints.", + "The color palette of millennial design often features neutral tones as a foundation, with strategic pops of color that reflect personal taste and current trends. Soft grays, warm whites, and natural wood tones create a calming backdrop, while accent colors like sage green, dusty rose, and deep navy add personality without overwhelming the space. This approach allows for easy updates and seasonal changes without requiring major renovations.", + "Technology integration is another hallmark of millennial design, with smart home features seamlessly incorporated into the aesthetic. From hidden charging stations to voice-controlled lighting systems, technology enhances the living experience without compromising the visual appeal of the space. This balance between high-tech functionality and timeless design creates homes that are both modern and enduring." + ], + "author": "Admin", + "date": "2022-10-14", + "tags": ["design"], + "image": "assets/images/blog_post_1.png", + "thumbnail": "assets/images/blog_thumb_1.png", + "alt": "Going all-in with millennial design" + }, + { + "id": 2, + "title": "How to create the perfect living room", + "excerpt": "Designing the perfect living room requires careful consideration of space, functionality, and personal style. From choosing the right furniture pieces to creating a cohesive color scheme, every element plays a crucial role in crafting a space that feels both welcoming and reflective of your lifestyle. Let's explore the key principles that make a living room truly exceptional. The foundation of any great living room begins with understanding the space you're working with. Consider the room's dimensions, natural light sources, and traffic flow. These factors will guide your furniture placement and help you create a layout that maximizes both comfort and functionality. When selecting furniture, prioritize pieces that serve multiple purposes and create a harmonious balance between form and function.", + "content": [ + "The foundation of any great living room begins with understanding the space you're working with. Consider the room's dimensions, natural light sources, and traffic flow. These factors will guide your furniture placement and help you create a layout that maximizes both comfort and functionality.", + "When selecting furniture, prioritize pieces that serve multiple purposes. A comfortable sofa that can accommodate family gatherings, an accent chair that adds personality, and a coffee table that provides both surface area and storage can transform your living room into a versatile space that adapts to your daily needs.", + "Color and texture play vital roles in creating atmosphere. Choose a color palette that reflects your personal style while maintaining harmony throughout the space. Layer different textures through rugs, throw pillows, and decorative elements to add depth and visual interest." + ], + "author": "Admin", + "date": "2022-11-05", + "tags": ["design"], + "image": "assets/images/conference_room.jpg", + "thumbnail": "assets/images/conference_room.jpg", + "alt": "Perfect living room design" + }, + { + "id": 3, + "title": "Sustainable furniture choices for modern homes", + "excerpt": "As environmental consciousness grows, more homeowners are seeking sustainable furniture options that align with their values. From responsibly sourced materials to eco-friendly manufacturing processes, sustainable furniture offers both environmental benefits and long-term value. Discover how to make informed choices that benefit both your home and the planet. Sustainable furniture begins with the materials used in its construction. Look for pieces made from responsibly harvested wood, such as FSC-certified timber, or consider alternatives like bamboo, which grows rapidly and requires minimal resources. These materials not only reduce environmental impact but also often provide superior durability and timeless appeal.", + "content": [ + "Sustainable furniture begins with the materials used in its construction. Look for pieces made from responsibly harvested wood, such as FSC-certified timber, or consider alternatives like bamboo, which grows rapidly and requires minimal resources. These materials not only reduce environmental impact but also often provide superior durability.", + "Manufacturing processes also play a crucial role in sustainability. Choose furniture from companies that prioritize energy-efficient production methods, minimize waste, and use non-toxic finishes. Many sustainable furniture makers also offer repair services and replacement parts, extending the lifespan of your investment.", + "Consider the full lifecycle of your furniture choices. Opt for timeless designs that won't quickly go out of style, and invest in quality pieces that can be passed down through generations. This approach reduces the need for frequent replacements and creates a more sustainable consumption pattern." + ], + "author": "Admin", + "date": "2022-12-20", + "tags": ["sustainability"], + "image": "assets/images/kitchen.JPG", + "thumbnail": "assets/images/kitchen.JPG", + "alt": "Sustainable furniture choices" + }, + { + "id": 4, + "title": "The art of mixing vintage and contemporary pieces", + "excerpt": "Creating a home that feels both timeless and current requires mastering the art of mixing vintage and contemporary furniture. This approach allows you to tell a story through your decor while maintaining a cohesive and functional space. Learn how to balance different eras and styles to create a home that's uniquely yours. The key to successfully mixing vintage and contemporary pieces lies in finding common threads that unite them. Look for shared design elements such as similar lines, complementary colors, or matching materials. A vintage wooden table might pair beautifully with modern chairs if they share similar wood tones or geometric shapes.", + "content": [ + "The key to successfully mixing vintage and contemporary pieces lies in finding common threads that unite them. Look for shared design elements such as similar lines, complementary colors, or matching materials. A vintage wooden table might pair beautifully with modern chairs if they share similar wood tones or geometric shapes.", + "Scale and proportion are crucial when combining different eras. Ensure that vintage and contemporary pieces work together in terms of size and visual weight. A large vintage armoire might overwhelm a room filled with delicate modern furniture, while smaller vintage accents can add character without dominating the space.", + "Don't be afraid to experiment with unexpected combinations. Sometimes the most interesting interiors come from pairing pieces that seem to have nothing in common. The contrast between a sleek modern sofa and a rustic vintage coffee table can create a dynamic and engaging space that reflects your personal style." + ], + "author": "Admin", + "date": "2023-01-15", + "tags": ["vintage"], + "image": "assets/images/lounge_chair.jpg", + "thumbnail": "assets/images/lounge_chair.jpg", + "alt": "Mixing vintage and contemporary furniture" + }, + { + "id": 5, + "title": "Maximizing small spaces with smart furniture solutions", + "excerpt": "Living in a smaller space doesn't mean sacrificing style or functionality. With the right furniture choices and design strategies, you can create a home that feels spacious, organized, and beautiful. Discover innovative solutions that make the most of every square foot while maintaining your personal aesthetic. Multi-functional furniture is essential in small spaces. Look for pieces that serve multiple purposes, such as ottomans with hidden storage, sofa beds for guest accommodation, or dining tables that can expand when needed. These versatile pieces help you maximize functionality without cluttering your space.", + "content": [ + "Multi-functional furniture is essential in small spaces. Look for pieces that serve multiple purposes, such as ottomans with hidden storage, sofa beds for guest accommodation, or dining tables that can expand when needed. These versatile pieces help you maximize functionality without cluttering your space.", + "Vertical storage solutions can dramatically increase your usable space. Wall-mounted shelves, tall bookcases, and hanging organizers keep your belongings organized while freeing up valuable floor space. Consider custom storage solutions that fit perfectly into awkward corners or underutilized areas.", + "The right color palette and lighting can make a small space feel much larger. Light, neutral colors reflect natural light and create an airy atmosphere, while strategic lighting can highlight focal points and create depth. Mirrors are also excellent tools for making small spaces feel more expansive." + ], + "author": "Admin", + "date": "2023-02-28", + "tags": ["design"], + "image": "assets/images/storage.jpg", + "thumbnail": "assets/images/storage.jpg", + "alt": "Small space furniture solutions" + }, + { + "id": 6, + "title": "Color psychology in home design", + "excerpt": "Colors have a profound impact on our emotions, mood, and overall well-being. Understanding color psychology can help you create spaces that not only look beautiful but also support your desired atmosphere and lifestyle. Explore how different colors can transform your home and enhance your daily experience. Warm colors like red, orange, and yellow create energy and excitement, making them perfect for social spaces like dining rooms and living areas. These colors stimulate conversation and appetite, making them ideal for areas where you entertain guests or share meals with family.", + "content": [ + "Warm colors like red, orange, and yellow create energy and excitement, making them perfect for social spaces like dining rooms and living areas. These colors stimulate conversation and appetite, making them ideal for areas where you entertain guests or share meals with family.", + "Cool colors such as blue, green, and purple promote calmness and relaxation. These hues are excellent choices for bedrooms, bathrooms, and home offices where you want to create a peaceful, focused environment. Different shades can evoke different moods - deep navy creates sophistication while soft lavender promotes tranquility.", + "Neutral colors provide a versatile foundation that allows you to experiment with accent colors and seasonal changes. Whites, grays, and beiges create a timeless backdrop that can be easily updated with colorful accessories, artwork, or seasonal decor without requiring major furniture changes." + ], + "author": "Admin", + "date": "2023-03-12", + "tags": ["vintage"], + "image": "assets/images/chairs.jpg", + "thumbnail": "assets/images/chairs.jpg", + "alt": "Color psychology in home design" + } + ] +} diff --git a/data/products.json b/data/products.json new file mode 100644 index 0000000..3d42c80 --- /dev/null +++ b/data/products.json @@ -0,0 +1,1832 @@ +{ + "products": [ + { + "id": 1, + "name": "Asgaard sofa", + "description": "Premium executive sofa with ergonomic design and premium fabric upholstery. Perfect for executive offices and reception areas.", + "image": "assets/images/asgaard_sofa.png", + "alt": "Asgaard sofa", + "category": "seating", + "modelNo": "SE001", + "tags": ["Chair", "Seating", "Office", "Ergonomic"], + "sizes": ["XS", "S", "M", "L", "XL"], + "colors": [ + { + "name": "Black", + "value": "#000000", + "selected": true + }, + { + "name": "Brown", + "value": "#8B4513", + "selected": false + }, + { + "name": "Gray", + "value": "#808080", + "selected": false + } + ], + "selectedSize": "M", + "selectedColor": "Black", + "price": 250, + "originalPrice": 250, + "rating": "4.4", + "reviews": 88, + "inStock": true, + "images": [ + "assets/images/asgaard_sofa.png", + "assets/images/asgaard_sofa.png", + "assets/images/asgaard_sofa.png", + "assets/images/asgaard_sofa.png" + ], + "descriptionLong": [ + "The Asgaard sofa represents the pinnacle of executive seating design, combining sophisticated aesthetics with uncompromising comfort. Crafted with precision engineering and premium materials, this sofa sets the standard for professional workspace furniture.", + "Featuring a robust engineered wood frame and high-performance fabric upholstery, the Asgaard sofa is built to withstand the demands of daily office use while maintaining its elegant appearance. The ergonomic design provides optimal support for extended meetings and collaborative sessions, while the generous proportions accommodate multiple users comfortably." + ], + "additionalInformation": { + "Material": "Engineered wood frame, premium fabric upholstery", + "Upholstery": "Performance fabric with stain resistance", + "Dimensions": "See size options", + "Warranty": "3 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 88, + "galleryPairs": [ + { + "left": "assets/images/asgaard_sofa.png", + "right": "assets/images/asgaard_sofa.png" + } + ], + "dimensions": "180cm x 85cm x 75cm", + "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" + }, + { + "id": 2, + "name": "Lolito", + "description": "Modern conference table with sleek design and durable construction. Ideal for collaborative meetings and presentations.", + "image": "assets/images/office_chair.jpg", + "alt": "Lolito", + "category": "tables", + "modelNo": "TA002", + "tags": ["Table", "Workstation", "Office", "Conference"], + "sizes": ["S", "M", "L"], + "colors": [ + { + "name": "Natural", + "value": "#D2B48C", + "selected": true + }, + { + "name": "Dark", + "value": "#654321", + "selected": false + }, + { + "name": "White", + "value": "#FFFFFF", + "selected": false + } + ], + "selectedSize": "L", + "selectedColor": "Natural", + "price": 899.99, + "originalPrice": 899.99, + "rating": "4.9", + "reviews": 69, + "inStock": true, + "images": [ + "assets/images/office_chair.jpg", + "assets/images/office_chair.jpg", + "assets/images/office_chair.jpg", + "assets/images/office_chair.jpg" + ], + "descriptionLong": [ + "The Lolito conference table embodies modern workplace design with its clean lines and sophisticated aesthetic. This versatile table is engineered to facilitate productive meetings and collaborative sessions in any professional environment.", + "Constructed from premium materials with a focus on durability and functionality, the Lolito table features integrated cable management and optional power solutions. The spacious surface accommodates multiple users while maintaining an organized, professional appearance that enhances any meeting room or conference space." + ], + "additionalInformation": { + "Material": "Solid wood top, steel frame", + "Finish": "Natural wood or dark stain options", + "Dimensions": "See size options", + "Warranty": "5 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 69, + "galleryPairs": [ + { + "left": "assets/images/office_chair.jpg", + "right": "assets/images/office_chair.jpg" + } + ], + "dimensions": "120cm x 60cm x 75cm", + "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" + }, + { + "id": 3, + "name": "Respira", + "description": "Contemporary outdoor workspace solution with weather-resistant materials. Perfect for outdoor meetings and collaborative areas.", + "image": "assets/images/conference_room.jpg", + "alt": "Respira", + "category": "storage", + "modelNo": "ST003", + "tags": ["Storage", "Cabinet", "Office", "Organization"], + "sizes": ["M", "L", "XL"], + "colors": [ + { + "name": "White", + "value": "#FFFFFF", + "selected": true + }, + { + "name": "Black", + "value": "#000000", + "selected": false + }, + { + "name": "Gray", + "value": "#808080", + "selected": false + } + ], + "selectedSize": "XL", + "selectedColor": "White", + "price": 599.99, + "originalPrice": 599.99, + "rating": "4.8", + "reviews": 12, + "inStock": true, + "images": [ + "assets/images/conference_room.jpg", + "assets/images/conference_room.jpg", + "assets/images/conference_room.jpg", + "assets/images/conference_room.jpg" + ], + "descriptionLong": [ + "Transform any outdoor space into a productive work environment with our Outdoor Bar Table Set. Designed specifically for modern workplaces that embrace outdoor collaboration, this set combines functionality with weather-resistant durability.", + "The set features a sturdy aluminum frame with powder-coated finish and tempered glass top, ensuring years of reliable use in various weather conditions. The ergonomic bar stools provide comfortable seating for extended outdoor work sessions, making it perfect for companies that value flexible workspace solutions." + ], + "additionalInformation": { + "Material": "Aluminum frame, tempered glass top", + "Weather Resistance": "UV and moisture resistant", + "Dimensions": "See size options", + "Warranty": "2 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 12, + "galleryPairs": [ + { + "left": "assets/images/conference_room.jpg", + "right": "assets/images/conference_room.jpg" + } + ], + "dimensions": "100cm x 50cm x 90cm", + "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" + }, + { + "id": 4, + "name": "Respira", + "description": "Versatile storage solution with multiple compartments and secure locking system. Essential for organized office environments.", + "image": "assets/images/conference_rooms.jpg", + "alt": "Respira", + "category": "storage", + "price": 599.99, + "inStock": true, + "modelNo": "ST004", + "tags": ["Storage", "Cabinet", "Office", "Organization"], + "sizes": ["M", "L", "XL"], + "colors": [ + { + "name": "White", + "value": "#FFFFFF", + "selected": true + }, + { + "name": "Black", + "value": "#000000", + "selected": false + }, + { + "name": "Gray", + "value": "#808080", + "selected": false + } + ], + "selectedSize": "XL", + "selectedColor": "White", + "originalPrice": 599.99, + "rating": "4.8", + "reviews": 103, + "images": [ + "assets/images/conference_rooms.jpg", + "assets/images/conference_rooms.jpg", + "assets/images/conference_rooms.jpg", + "assets/images/conference_rooms.jpg" + ], + "descriptionLong": [ + "Maximize your workspace efficiency with our comprehensive storage cabinet system. Designed to meet the diverse storage needs of modern offices, this cabinet provides secure, organized storage for documents, supplies, and personal items.", + "The cabinet features adjustable shelves, integrated cable management, and optional locking mechanisms for enhanced security. The modular design allows for easy customization to fit specific storage requirements, while the premium finish ensures it complements any office aesthetic." + ], + "additionalInformation": { + "Material": "Engineered wood, steel hardware", + "Security": "Optional locking system", + "Dimensions": "See size options", + "Warranty": "3 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 103, + "galleryPairs": [ + { + "left": "assets/images/conference_rooms.jpg", + "right": "assets/images/conference_rooms.jpg" + } + ], + "dimensions": "60cm x 40cm x 120cm", + "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" + }, + { + "id": 5, + "name": "Lounge Chairs", + "description": "Contemporary lounge chair with ergonomic design and premium comfort. Perfect for breakout areas and relaxation spaces.", + "image": "assets/images/lounge_chair.jpg", + "alt": "Lounge Chairs", + "category": "seating", + "price": 299.99, + "inStock": true, + "modelNo": "SE005", + "tags": ["Chair", "Seating", "Office", "Ergonomic"], + "sizes": ["XS", "S", "M", "L", "XL"], + "colors": [ + { + "name": "Black", + "value": "#000000", + "selected": true + }, + { + "name": "Brown", + "value": "#8B4513", + "selected": false + }, + { + "name": "Gray", + "value": "#808080", + "selected": false + } + ], + "selectedSize": "M", + "selectedColor": "Black", + "originalPrice": 299.99, + "rating": "4.4", + "reviews": 25, + "images": [ + "assets/images/lounge_chair.jpg", + "assets/images/lounge_chair.jpg", + "assets/images/lounge_chair.jpg", + "assets/images/lounge_chair.jpg" + ], + "descriptionLong": [ + "Create inviting breakout spaces with our premium lounge chair collection. Designed for comfort and style, these chairs provide the perfect seating solution for informal meetings, relaxation areas, and collaborative spaces.", + "Each chair features an ergonomic design that promotes proper posture while offering exceptional comfort for extended use. The high-quality upholstery and durable construction ensure long-lasting performance in high-traffic office environments." + ], + "additionalInformation": { + "Material": "Steel frame, premium fabric upholstery", + "Comfort": "Ergonomic design with lumbar support", + "Dimensions": "See size options", + "Warranty": "2 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 25, + "galleryPairs": [ + { + "left": "assets/images/lounge_chair.jpg", + "right": "assets/images/lounge_chair.jpg" + } + ], + "dimensions": "140cm x 80cm x 75cm", + "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" + }, + { + "id": 6, + "name": "Leviosa", + "description": "Ergonomic office chair with adjustable features and breathable mesh back. Designed for all-day comfort and productivity.", + "image": "assets/images/office_chair.jpg", + "alt": "Leviosa", + "category": "seating", + "price": 349.99, + "inStock": true, + "modelNo": "SE006", + "tags": ["Chair", "Seating", "Office", "Ergonomic"], + "sizes": ["XS", "S", "M", "L", "XL"], + "colors": [ + { + "name": "Black", + "value": "#000000", + "selected": true + }, + { + "name": "Brown", + "value": "#8B4513", + "selected": false + }, + { + "name": "Gray", + "value": "#808080", + "selected": false + } + ], + "selectedSize": "M", + "selectedColor": "Black", + "originalPrice": 349.99, + "rating": "4.5", + "reviews": 80, + "images": [ + "assets/images/office_chair.jpg", + "assets/images/office_chair.jpg", + "assets/images/office_chair.jpg", + "assets/images/office_chair.jpg" + ], + "descriptionLong": [ + "Enhance workplace productivity with our ergonomic office chair designed for extended use. This chair combines advanced ergonomic features with contemporary design to provide optimal support and comfort throughout the workday.", + "The chair features adjustable height, lumbar support, and breathable mesh back for temperature regulation. The premium materials and construction ensure durability while the ergonomic design helps prevent fatigue and promotes proper posture." + ], + "additionalInformation": { + "Material": "Aluminum base, mesh back, fabric seat", + "Adjustments": "Height, tilt, lumbar support", + "Dimensions": "See size options", + "Warranty": "5 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 80, + "galleryPairs": [ + { + "left": "assets/images/office_chair.jpg", + "right": "assets/images/office_chair.jpg" + } + ], + "dimensions": "100cm x 70cm x 85cm", + "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" + }, + { + "id": 7, + "name": "Lolito", + "description": "Acoustic meeting pod with sound-absorbing materials and integrated technology. Perfect for private conversations and focused work.", + "image": "assets/images/conference_room.jpg", + "alt": "Lolito", + "category": "tables", + "price": 899.99, + "inStock": true, + "modelNo": "TA007", + "tags": ["Table", "Workstation", "Office", "Conference"], + "sizes": ["S", "M", "L"], + "colors": [ + { + "name": "Natural", + "value": "#D2B48C", + "selected": true + }, + { + "name": "Dark", + "value": "#654321", + "selected": false + }, + { + "name": "White", + "value": "#FFFFFF", + "selected": false + } + ], + "selectedSize": "L", + "selectedColor": "Natural", + "originalPrice": 899.99, + "rating": "4.3", + "reviews": 10, + "images": [ + "assets/images/conference_room.jpg", + "assets/images/conference_room.jpg", + "assets/images/conference_room.jpg", + "assets/images/conference_room.jpg" + ], + "descriptionLong": [ + "Create private, distraction-free environments with our acoustic meeting pods. These innovative workspace solutions provide the perfect setting for confidential conversations, focused work sessions, and small team meetings.", + "Each pod features advanced acoustic engineering with sound-absorbing materials that create a quiet, private environment. The integrated ventilation system and comfortable seating ensure extended use comfort, while the modular design allows for easy installation in any office space." + ], + "additionalInformation": { + "Material": "Acoustic panels, steel frame", + "Acoustics": "Sound reduction up to 35dB", + "Dimensions": "See size options", + "Warranty": "3 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 10, + "galleryPairs": [ + { + "left": "assets/images/conference_room.jpg", + "right": "assets/images/conference_room.jpg" + } + ], + "dimensions": "180cm x 90cm x 75cm", + "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" + }, + { + "id": 8, + "name": "Respira", + "description": "Private phone booth with acoustic insulation and ventilation. Ideal for confidential calls and focused work sessions.", + "image": "assets/images/conference_rooms.jpg", + "alt": "Respira", + "category": "storage", + "price": 599.99, + "inStock": true, + "modelNo": "ST008", + "tags": ["Storage", "Cabinet", "Office", "Organization"], + "sizes": ["M", "L", "XL"], + "colors": [ + { + "name": "White", + "value": "#FFFFFF", + "selected": true + }, + { + "name": "Black", + "value": "#000000", + "selected": false + }, + { + "name": "Gray", + "value": "#808080", + "selected": false + } + ], + "selectedSize": "XL", + "selectedColor": "White", + "originalPrice": 599.99, + "rating": "4.7", + "reviews": 89, + "images": [ + "assets/images/conference_rooms.jpg", + "assets/images/conference_rooms.jpg", + "assets/images/conference_rooms.jpg", + "assets/images/conference_rooms.jpg" + ], + "descriptionLong": [ + "Ensure privacy and focus with our professional phone booth solution. Designed for modern open-plan offices, these booths provide the perfect environment for confidential calls, video conferences, and focused work sessions.", + "The booth features advanced acoustic insulation, integrated ventilation, and optional power solutions. The compact design maximizes space efficiency while providing a comfortable, private environment for professional communication needs." + ], + "additionalInformation": { + "Material": "Acoustic panels, tempered glass", + "Privacy": "Sound reduction up to 40dB", + "Dimensions": "See size options", + "Warranty": "2 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 89, + "galleryPairs": [ + { + "left": "assets/images/conference_rooms.jpg", + "right": "assets/images/conference_rooms.jpg" + } + ], + "dimensions": "80cm x 40cm x 180cm", + "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" + }, + { + "id": 9, + "name": "Lounge Chairs", + "description": "Modular workstation with integrated storage and cable management. Designed for efficient workspace organization.", + "image": "assets/images/lounge_chair.jpg", + "alt": "Lounge Chairs", + "category": "seating", + "price": 299.99, + "inStock": true, + "modelNo": "SE009", + "tags": ["Chair", "Seating", "Office", "Ergonomic"], + "sizes": ["XS", "S", "M", "L", "XL"], + "colors": [ + { + "name": "Black", + "value": "#000000", + "selected": true + }, + { + "name": "Brown", + "value": "#8B4513", + "selected": false + }, + { + "name": "Gray", + "value": "#808080", + "selected": false + } + ], + "selectedSize": "M", + "selectedColor": "Black", + "originalPrice": 299.99, + "rating": "4.9", + "reviews": 58, + "images": [ + "assets/images/lounge_chair.jpg", + "assets/images/lounge_chair.jpg", + "assets/images/lounge_chair.jpg", + "assets/images/lounge_chair.jpg" + ], + "descriptionLong": [ + "Optimize your workspace with our modular workstation system. This comprehensive solution combines ergonomic design with intelligent storage and cable management to create highly functional work environments.", + "The workstation features adjustable components, integrated power solutions, and customizable storage options. The modular design allows for easy reconfiguration as workspace needs evolve, while the premium materials ensure long-lasting performance." + ], + "additionalInformation": { + "Material": "Engineered wood, steel frame", + "Modularity": "Fully customizable configuration", + "Dimensions": "See size options", + "Warranty": "5 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 58, + "galleryPairs": [ + { + "left": "assets/images/lounge_chair.jpg", + "right": "assets/images/lounge_chair.jpg" + } + ], + "dimensions": "120cm x 75cm x 85cm", + "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" + }, + { + "id": 10, + "name": "Leviosa", + "description": "Professional conference table with integrated technology and cable management. Perfect for executive meetings and presentations.", + "image": "assets/images/office_chair.jpg", + "alt": "Leviosa", + "category": "seating", + "price": 349.99, + "inStock": true, + "modelNo": "SE010", + "tags": ["Chair", "Seating", "Office", "Ergonomic"], + "sizes": ["XS", "S", "M", "L", "XL"], + "colors": [ + { + "name": "Black", + "value": "#000000", + "selected": true + }, + { + "name": "Brown", + "value": "#8B4513", + "selected": false + }, + { + "name": "Gray", + "value": "#808080", + "selected": false + } + ], + "selectedSize": "M", + "selectedColor": "Black", + "originalPrice": 349.99, + "rating": "4.7", + "reviews": 82, + "images": [ + "assets/images/office_chair.jpg", + "assets/images/office_chair.jpg", + "assets/images/office_chair.jpg", + "assets/images/office_chair.jpg" + ], + "descriptionLong": [ + "Elevate your meeting experience with our professional conference table. Designed for executive environments and important presentations, this table combines sophisticated design with integrated technology solutions.", + "The table features built-in power outlets, cable management systems, and optional AV integration. The premium finish and robust construction ensure it makes a lasting impression while providing the functionality needed for productive meetings." + ], + "additionalInformation": { + "Material": "Solid wood top, steel frame", + "Technology": "Integrated power and cable management", + "Dimensions": "See size options", + "Warranty": "5 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 82, + "galleryPairs": [ + { + "left": "assets/images/office_chair.jpg", + "right": "assets/images/office_chair.jpg" + } + ], + "dimensions": "90cm x 90cm x 85cm", + "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" + }, + { + "id": 11, + "name": "Lolito", + "description": "Professional table solution with durable construction and versatile design. Perfect for meetings, collaboration, and workspace organization.", + "image": "assets/images/conference_room.jpg", + "alt": "Lolito", + "category": "tables", + "price": 899.99, + "inStock": true, + "modelNo": "TA011", + "tags": ["Table", "Workstation", "Office", "Conference"], + "sizes": ["S", "M", "L"], + "colors": [ + { + "name": "Natural", + "value": "#D2B48C", + "selected": true + }, + { + "name": "Dark", + "value": "#654321", + "selected": false + }, + { + "name": "White", + "value": "#FFFFFF", + "selected": false + } + ], + "selectedSize": "L", + "selectedColor": "Natural", + "originalPrice": 899.99, + "rating": "4.3", + "reviews": 38, + "images": [ + "assets/images/conference_room.jpg", + "assets/images/conference_room.jpg", + "assets/images/conference_room.jpg", + "assets/images/conference_room.jpg" + ], + "descriptionLong": [ + "This professional table solution is designed to meet the diverse needs of modern workplaces. Combining durability with versatile design, it provides the perfect foundation for productive meetings and collaborative work.", + "The table features premium materials and construction techniques that ensure years of reliable use. The thoughtful design includes integrated cable management and optional power solutions to support modern work requirements." + ], + "additionalInformation": { + "Material": "Premium materials with durable construction", + "Design": "Contemporary professional aesthetic", + "Dimensions": "See size options", + "Warranty": "3 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 38, + "galleryPairs": [ + { + "left": "assets/images/conference_room.jpg", + "right": "assets/images/conference_room.jpg" + } + ], + "dimensions": "200cm x 100cm x 75cm", + "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" + }, + { + "id": 12, + "name": "Respira", + "description": "Efficient storage solution with intelligent organization features. Essential for maintaining organized and productive work environments.", + "image": "assets/images/conference_rooms.jpg", + "alt": "Respira", + "category": "storage", + "price": 599.99, + "inStock": true, + "modelNo": "ST012", + "tags": ["Storage", "Cabinet", "Office", "Organization"], + "sizes": ["M", "L", "XL"], + "colors": [ + { + "name": "White", + "value": "#FFFFFF", + "selected": true + }, + { + "name": "Black", + "value": "#000000", + "selected": false + }, + { + "name": "Gray", + "value": "#808080", + "selected": false + } + ], + "selectedSize": "XL", + "selectedColor": "White", + "originalPrice": 599.99, + "rating": "4.6", + "reviews": 11, + "images": [ + "assets/images/conference_rooms.jpg", + "assets/images/conference_rooms.jpg", + "assets/images/conference_rooms.jpg", + "assets/images/conference_rooms.jpg" + ], + "descriptionLong": [ + "Maximize workspace efficiency with this intelligent storage solution. Designed to meet the organizational needs of modern offices, it provides secure and accessible storage for all workplace essentials.", + "The storage unit features adjustable components, integrated security options, and premium materials that ensure long-lasting performance. The modular design allows for easy customization to fit specific storage requirements." + ], + "additionalInformation": { + "Material": "Premium materials with durable construction", + "Design": "Contemporary professional aesthetic", + "Dimensions": "See size options", + "Warranty": "3 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 11, + "galleryPairs": [ + { + "left": "assets/images/conference_rooms.jpg", + "right": "assets/images/conference_rooms.jpg" + } + ], + "dimensions": "70cm x 35cm x 160cm", + "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" + }, + { + "id": 13, + "name": "Lounge Chairs", + "description": "Premium seating solution designed for modern office environments. Combines comfort, style, and durability for professional workspaces.", + "image": "assets/images/lounge_chair.jpg", + "alt": "Lounge Chairs", + "category": "seating", + "price": 299.99, + "inStock": true, + "modelNo": "SE013", + "tags": ["Chair", "Seating", "Office", "Ergonomic"], + "sizes": ["XS", "S", "M", "L", "XL"], + "colors": [ + { + "name": "Black", + "value": "#000000", + "selected": true + }, + { + "name": "Brown", + "value": "#8B4513", + "selected": false + }, + { + "name": "Gray", + "value": "#808080", + "selected": false + } + ], + "selectedSize": "M", + "selectedColor": "Black", + "originalPrice": 299.99, + "rating": "5.0", + "reviews": 48, + "images": [ + "assets/images/lounge_chair.jpg", + "assets/images/lounge_chair.jpg", + "assets/images/lounge_chair.jpg", + "assets/images/lounge_chair.jpg" + ], + "descriptionLong": [ + "This premium seating solution is engineered for the modern workplace, offering the perfect balance of comfort, style, and functionality. Designed with ergonomic principles and premium materials, it provides exceptional support for extended use.", + "The chair features advanced ergonomic design with adjustable components and breathable materials. The durable construction ensures long-lasting performance while the contemporary aesthetic enhances any office environment." + ], + "additionalInformation": { + "Material": "Premium materials with durable construction", + "Design": "Contemporary professional aesthetic", + "Dimensions": "See size options", + "Warranty": "3 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 48, + "galleryPairs": [ + { + "left": "assets/images/lounge_chair.jpg", + "right": "assets/images/lounge_chair.jpg" + } + ], + "dimensions": "180cm x 85cm x 75cm", + "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" + }, + { + "id": 14, + "name": "Leviosa", + "description": "Premium seating solution designed for modern office environments. Combines comfort, style, and durability for professional workspaces.", + "image": "assets/images/office_chair.jpg", + "alt": "Leviosa", + "category": "seating", + "price": 349.99, + "inStock": true, + "modelNo": "SE014", + "tags": ["Chair", "Seating", "Office", "Ergonomic"], + "sizes": ["XS", "S", "M", "L", "XL"], + "colors": [ + { + "name": "Black", + "value": "#000000", + "selected": true + }, + { + "name": "Brown", + "value": "#8B4513", + "selected": false + }, + { + "name": "Gray", + "value": "#808080", + "selected": false + } + ], + "selectedSize": "M", + "selectedColor": "Black", + "originalPrice": 349.99, + "rating": "4.1", + "reviews": 50, + "images": [ + "assets/images/office_chair.jpg", + "assets/images/office_chair.jpg", + "assets/images/office_chair.jpg", + "assets/images/office_chair.jpg" + ], + "descriptionLong": [ + "This premium seating solution is engineered for the modern workplace, offering the perfect balance of comfort, style, and functionality. Designed with ergonomic principles and premium materials, it provides exceptional support for extended use.", + "The chair features advanced ergonomic design with adjustable components and breathable materials. The durable construction ensures long-lasting performance while the contemporary aesthetic enhances any office environment." + ], + "additionalInformation": { + "Material": "Premium materials with durable construction", + "Design": "Contemporary professional aesthetic", + "Dimensions": "See size options", + "Warranty": "3 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 50, + "galleryPairs": [ + { + "left": "assets/images/office_chair.jpg", + "right": "assets/images/office_chair.jpg" + } + ], + "dimensions": "65cm x 65cm x 85cm", + "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" + }, + { + "id": 15, + "name": "Lolito", + "description": "Professional table solution with durable construction and versatile design. Perfect for meetings, collaboration, and workspace organization.", + "image": "assets/images/conference_room.jpg", + "alt": "Lolito", + "category": "tables", + "price": 899.99, + "inStock": true, + "modelNo": "TA015", + "tags": ["Table", "Workstation", "Office", "Conference"], + "sizes": ["S", "M", "L"], + "colors": [ + { + "name": "Natural", + "value": "#D2B48C", + "selected": true + }, + { + "name": "Dark", + "value": "#654321", + "selected": false + }, + { + "name": "White", + "value": "#FFFFFF", + "selected": false + } + ], + "selectedSize": "L", + "selectedColor": "Natural", + "originalPrice": 899.99, + "rating": "4.6", + "reviews": 22, + "images": [ + "assets/images/conference_room.jpg", + "assets/images/conference_room.jpg", + "assets/images/conference_room.jpg", + "assets/images/conference_room.jpg" + ], + "descriptionLong": [ + "This professional table solution is designed to meet the diverse needs of modern workplaces. Combining durability with versatile design, it provides the perfect foundation for productive meetings and collaborative work.", + "The table features premium materials and construction techniques that ensure years of reliable use. The thoughtful design includes integrated cable management and optional power solutions to support modern work requirements." + ], + "additionalInformation": { + "Material": "Premium materials with durable construction", + "Design": "Contemporary professional aesthetic", + "Dimensions": "See size options", + "Warranty": "3 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 22, + "galleryPairs": [ + { + "left": "assets/images/conference_room.jpg", + "right": "assets/images/conference_room.jpg" + } + ], + "dimensions": "150cm x 80cm x 75cm", + "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" + }, + { + "id": 16, + "name": "Respira", + "description": "Efficient storage solution with intelligent organization features. Essential for maintaining organized and productive work environments.", + "image": "assets/images/conference_rooms.jpg", + "alt": "Respira", + "category": "storage", + "price": 599.99, + "inStock": true, + "modelNo": "ST016", + "tags": ["Storage", "Cabinet", "Office", "Organization"], + "sizes": ["M", "L", "XL"], + "colors": [ + { + "name": "White", + "value": "#FFFFFF", + "selected": true + }, + { + "name": "Black", + "value": "#000000", + "selected": false + }, + { + "name": "Gray", + "value": "#808080", + "selected": false + } + ], + "selectedSize": "XL", + "selectedColor": "White", + "originalPrice": 599.99, + "rating": "5.0", + "reviews": 95, + "images": [ + "assets/images/conference_rooms.jpg", + "assets/images/conference_rooms.jpg", + "assets/images/conference_rooms.jpg", + "assets/images/conference_rooms.jpg" + ], + "descriptionLong": [ + "Maximize workspace efficiency with this intelligent storage solution. Designed to meet the organizational needs of modern offices, it provides secure and accessible storage for all workplace essentials.", + "The storage unit features adjustable components, integrated security options, and premium materials that ensure long-lasting performance. The modular design allows for easy customization to fit specific storage requirements." + ], + "additionalInformation": { + "Material": "Premium materials with durable construction", + "Design": "Contemporary professional aesthetic", + "Dimensions": "See size options", + "Warranty": "3 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 95, + "galleryPairs": [ + { + "left": "assets/images/conference_rooms.jpg", + "right": "assets/images/conference_rooms.jpg" + } + ], + "dimensions": "60cm x 40cm x 120cm", + "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" + }, + { + "id": 17, + "name": "Workstation", + "description": "Professional table solution with durable construction and versatile design. Perfect for meetings, collaboration, and workspace organization.", + "image": "assets/images/workstation.jpg", + "alt": "Workstation", + "category": "tables", + "price": 499.99, + "inStock": true, + "modelNo": "TA017", + "tags": ["Table", "Workstation", "Office", "Conference"], + "sizes": ["S", "M", "L"], + "colors": [ + { + "name": "Natural", + "value": "#D2B48C", + "selected": true + }, + { + "name": "Dark", + "value": "#654321", + "selected": false + }, + { + "name": "White", + "value": "#FFFFFF", + "selected": false + } + ], + "selectedSize": "L", + "selectedColor": "Natural", + "originalPrice": 499.99, + "rating": "4.2", + "reviews": 104, + "images": [ + "assets/images/workstation.jpg", + "assets/images/workstation.jpg", + "assets/images/workstation.jpg", + "assets/images/workstation.jpg" + ], + "descriptionLong": [ + "This professional table solution is designed to meet the diverse needs of modern workplaces. Combining durability with versatile design, it provides the perfect foundation for productive meetings and collaborative work.", + "The table features premium materials and construction techniques that ensure years of reliable use. The thoughtful design includes integrated cable management and optional power solutions to support modern work requirements." + ], + "additionalInformation": { + "Material": "Premium materials with durable construction", + "Design": "Contemporary professional aesthetic", + "Dimensions": "See size options", + "Warranty": "3 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 104, + "galleryPairs": [ + { + "left": "assets/images/workstation.jpg", + "right": "assets/images/workstation.jpg" + } + ], + "dimensions": "200cm x 100cm x 75cm", + "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" + }, + { + "id": 18, + "name": "Storage Unit", + "description": "Efficient storage solution with intelligent organization features. Essential for maintaining organized and productive work environments.", + "image": "assets/images/storage.jpg", + "alt": "Storage Unit", + "category": "storage", + "price": 259.99, + "inStock": true, + "modelNo": "ST018", + "tags": ["Storage", "Cabinet", "Office", "Organization"], + "sizes": ["M", "L", "XL"], + "colors": [ + { + "name": "White", + "value": "#FFFFFF", + "selected": true + }, + { + "name": "Black", + "value": "#000000", + "selected": false + }, + { + "name": "Gray", + "value": "#808080", + "selected": false + } + ], + "selectedSize": "XL", + "selectedColor": "White", + "originalPrice": 259.99, + "rating": "4.9", + "reviews": 63, + "images": [ + "assets/images/storage.jpg", + "assets/images/storage.jpg", + "assets/images/storage.jpg", + "assets/images/storage.jpg" + ], + "descriptionLong": [ + "Maximize workspace efficiency with this intelligent storage solution. Designed to meet the organizational needs of modern offices, it provides secure and accessible storage for all workplace essentials.", + "The storage unit features adjustable components, integrated security options, and premium materials that ensure long-lasting performance. The modular design allows for easy customization to fit specific storage requirements." + ], + "additionalInformation": { + "Material": "Premium materials with durable construction", + "Design": "Contemporary professional aesthetic", + "Dimensions": "See size options", + "Warranty": "3 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 63, + "galleryPairs": [ + { + "left": "assets/images/storage.jpg", + "right": "assets/images/storage.jpg" + } + ], + "dimensions": "70cm x 35cm x 160cm", + "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" + }, + { + "id": 19, + "name": "Pods", + "description": "Innovative workspace solution designed for modern office needs. Combines functionality with contemporary design for optimal productivity.", + "image": "assets/images/pods.jpg", + "alt": "Pods", + "category": "workspace", + "price": 1299.99, + "inStock": true, + "modelNo": "WO019", + "tags": ["Workspace", "Pod", "Meeting", "Collaboration"], + "sizes": ["M", "L"], + "colors": [ + { + "name": "Blue", + "value": "#4169E1", + "selected": true + }, + { + "name": "Gray", + "value": "#808080", + "selected": false + }, + { + "name": "Black", + "value": "#000000", + "selected": false + } + ], + "selectedSize": "M", + "selectedColor": "Blue", + "originalPrice": 1299.99, + "rating": "4.7", + "reviews": 11, + "images": [ + "assets/images/pods.jpg", + "assets/images/pods.jpg", + "assets/images/pods.jpg", + "assets/images/pods.jpg" + ], + "descriptionLong": [ + "Transform your workspace with this innovative solution designed for modern office environments. Combining cutting-edge design with practical functionality, it creates the perfect environment for focused work and collaboration.", + "The workspace solution features premium materials, intelligent design, and customizable components that adapt to various work requirements. The contemporary aesthetic enhances any office environment while providing the functionality needed for productive work." + ], + "additionalInformation": { + "Material": "Premium materials with durable construction", + "Design": "Contemporary professional aesthetic", + "Dimensions": "See size options", + "Warranty": "3 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 11, + "galleryPairs": [ + { + "left": "assets/images/pods.jpg", + "right": "assets/images/pods.jpg" + } + ], + "dimensions": "140cm x 70cm x 75cm", + "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" + }, + { + "id": 20, + "name": "Phone Booth", + "description": "Innovative workspace solution designed for modern office needs. Combines functionality with contemporary design for optimal productivity.", + "image": "assets/images/phone.png", + "alt": "Phone Booth", + "category": "workspace", + "price": 399.99, + "inStock": true, + "modelNo": "WO020", + "tags": ["Workspace", "Pod", "Meeting", "Collaboration"], + "sizes": ["M", "L"], + "colors": [ + { + "name": "Blue", + "value": "#4169E1", + "selected": true + }, + { + "name": "Gray", + "value": "#808080", + "selected": false + }, + { + "name": "Black", + "value": "#000000", + "selected": false + } + ], + "selectedSize": "M", + "selectedColor": "Blue", + "originalPrice": 399.99, + "rating": "4.4", + "reviews": 23, + "images": [ + "assets/images/phone.png", + "assets/images/phone.png", + "assets/images/phone.png", + "assets/images/phone.png" + ], + "descriptionLong": [ + "Transform your workspace with this innovative solution designed for modern office environments. Combining cutting-edge design with practical functionality, it creates the perfect environment for focused work and collaboration.", + "The workspace solution features premium materials, intelligent design, and customizable components that adapt to various work requirements. The contemporary aesthetic enhances any office environment while providing the functionality needed for productive work." + ], + "additionalInformation": { + "Material": "Premium materials with durable construction", + "Design": "Contemporary professional aesthetic", + "Dimensions": "See size options", + "Warranty": "3 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 23, + "galleryPairs": [ + { + "left": "assets/images/phone.png", + "right": "assets/images/phone.png" + } + ], + "dimensions": "120cm x 60cm x 75cm", + "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" + }, + { + "id": 21, + "name": "Our Story", + "description": "Efficient storage solution with intelligent organization features. Essential for maintaining organized and productive work environments.", + "image": "assets/images/our_story.jpg", + "alt": "Art", + "category": "storage", + "price": 149.99, + "inStock": true, + "modelNo": "ST021", + "tags": ["Storage", "Cabinet", "Office", "Organization"], + "sizes": ["M", "L", "XL"], + "colors": [ + { + "name": "White", + "value": "#FFFFFF", + "selected": true + }, + { + "name": "Black", + "value": "#000000", + "selected": false + }, + { + "name": "Gray", + "value": "#808080", + "selected": false + } + ], + "selectedSize": "XL", + "selectedColor": "White", + "originalPrice": 149.99, + "rating": "4.6", + "reviews": 20, + "images": [ + "assets/images/our_story.jpg", + "assets/images/our_story.jpg", + "assets/images/our_story.jpg", + "assets/images/our_story.jpg" + ], + "descriptionLong": [ + "Maximize workspace efficiency with this intelligent storage solution. Designed to meet the organizational needs of modern offices, it provides secure and accessible storage for all workplace essentials.", + "The storage unit features adjustable components, integrated security options, and premium materials that ensure long-lasting performance. The modular design allows for easy customization to fit specific storage requirements." + ], + "additionalInformation": { + "Material": "Premium materials with durable construction", + "Design": "Contemporary professional aesthetic", + "Dimensions": "See size options", + "Warranty": "3 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 20, + "galleryPairs": [ + { + "left": "assets/images/our_story.jpg", + "right": "assets/images/our_story.jpg" + } + ], + "dimensions": "100cm x 50cm x 90cm", + "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" + }, + { + "id": 22, + "name": "Office Chair Elite", + "description": "Premium seating solution designed for modern office environments. Combines comfort, style, and durability for professional workspaces.", + "image": "assets/images/office_chair.jpg", + "alt": "Office Chair", + "category": "seating", + "price": 399.99, + "inStock": true, + "modelNo": "SE022", + "tags": ["Chair", "Seating", "Office", "Ergonomic"], + "sizes": ["XS", "S", "M", "L", "XL"], + "colors": [ + { + "name": "Black", + "value": "#000000", + "selected": true + }, + { + "name": "Brown", + "value": "#8B4513", + "selected": false + }, + { + "name": "Gray", + "value": "#808080", + "selected": false + } + ], + "selectedSize": "M", + "selectedColor": "Black", + "originalPrice": 399.99, + "rating": "5.0", + "reviews": 102, + "images": [ + "assets/images/office_chair.jpg", + "assets/images/office_chair.jpg", + "assets/images/office_chair.jpg", + "assets/images/office_chair.jpg" + ], + "descriptionLong": [ + "This premium seating solution is engineered for the modern workplace, offering the perfect balance of comfort, style, and functionality. Designed with ergonomic principles and premium materials, it provides exceptional support for extended use.", + "The chair features advanced ergonomic design with adjustable components and breathable materials. The durable construction ensures long-lasting performance while the contemporary aesthetic enhances any office environment." + ], + "additionalInformation": { + "Material": "Premium materials with durable construction", + "Design": "Contemporary professional aesthetic", + "Dimensions": "See size options", + "Warranty": "3 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 102, + "galleryPairs": [ + { + "left": "assets/images/office_chair.jpg", + "right": "assets/images/office_chair.jpg" + } + ], + "dimensions": "90cm x 90cm x 85cm", + "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" + }, + { + "id": 23, + "name": "Conference Room A", + "description": "Professional table solution with durable construction and versatile design. Perfect for meetings, collaboration, and workspace organization.", + "image": "assets/images/conference_room.jpg", + "alt": "Conference Room", + "category": "tables", + "price": 1099.99, + "inStock": true, + "modelNo": "TA023", + "tags": ["Table", "Workstation", "Office", "Conference"], + "sizes": ["S", "M", "L"], + "colors": [ + { + "name": "Natural", + "value": "#D2B48C", + "selected": true + }, + { + "name": "Dark", + "value": "#654321", + "selected": false + }, + { + "name": "White", + "value": "#FFFFFF", + "selected": false + } + ], + "selectedSize": "L", + "selectedColor": "Natural", + "originalPrice": 1099.99, + "rating": "5.0", + "reviews": 34, + "images": [ + "assets/images/conference_room.jpg", + "assets/images/conference_room.jpg", + "assets/images/conference_room.jpg", + "assets/images/conference_room.jpg" + ], + "descriptionLong": [ + "This professional table solution is designed to meet the diverse needs of modern workplaces. Combining durability with versatile design, it provides the perfect foundation for productive meetings and collaborative work.", + "The table features premium materials and construction techniques that ensure years of reliable use. The thoughtful design includes integrated cable management and optional power solutions to support modern work requirements." + ], + "additionalInformation": { + "Material": "Premium materials with durable construction", + "Design": "Contemporary professional aesthetic", + "Dimensions": "See size options", + "Warranty": "3 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 34, + "galleryPairs": [ + { + "left": "assets/images/conference_room.jpg", + "right": "assets/images/conference_room.jpg" + } + ], + "dimensions": "200cm x 100cm x 75cm", + "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" + }, + { + "id": 24, + "name": "Conference Rooms Set", + "description": "Professional table solution with durable construction and versatile design. Perfect for meetings, collaboration, and workspace organization.", + "image": "assets/images/conference_rooms.jpg", + "alt": "Conference Rooms", + "category": "tables", + "price": 2199.99, + "inStock": true, + "modelNo": "TA024", + "tags": ["Table", "Workstation", "Office", "Conference"], + "sizes": ["S", "M", "L"], + "colors": [ + { + "name": "Natural", + "value": "#D2B48C", + "selected": true + }, + { + "name": "Dark", + "value": "#654321", + "selected": false + }, + { + "name": "White", + "value": "#FFFFFF", + "selected": false + } + ], + "selectedSize": "L", + "selectedColor": "Natural", + "originalPrice": 2199.99, + "rating": "4.8", + "reviews": 67, + "images": [ + "assets/images/conference_rooms.jpg", + "assets/images/conference_rooms.jpg", + "assets/images/conference_rooms.jpg", + "assets/images/conference_rooms.jpg" + ], + "descriptionLong": [ + "This professional table solution is designed to meet the diverse needs of modern workplaces. Combining durability with versatile design, it provides the perfect foundation for productive meetings and collaborative work.", + "The table features premium materials and construction techniques that ensure years of reliable use. The thoughtful design includes integrated cable management and optional power solutions to support modern work requirements." + ], + "additionalInformation": { + "Material": "Premium materials with durable construction", + "Design": "Contemporary professional aesthetic", + "Dimensions": "See size options", + "Warranty": "3 years", + "warrantyServiceType": "Standard warranty service", + "coveredInWarranty": "Manufacturing defects", + "notCoveredInWarranty": "Wear and tear not covered" + }, + "reviewsCount": 67, + "galleryPairs": [ + { + "left": "assets/images/conference_rooms.jpg", + "right": "assets/images/conference_rooms.jpg" + } + ], + "dimensions": "140cm x 70cm x 75cm", + "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" + } + ], + "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": 32, + "currentPage": 1, + "totalPages": 2 + } +} diff --git a/deploy-config.js b/deploy-config.js new file mode 100644 index 0000000..0a606a2 --- /dev/null +++ b/deploy-config.js @@ -0,0 +1,49 @@ +/** + * Deploy Configuration for KHY Admin Dashboard + * + * This file contains configuration for deploying from the admin repository + * to the main KHY website repository. + */ + +module.exports = { + // Default target paths (adjust these based on your setup) + targets: { + // If admin and main website are in the same parent directory + sibling: "../khy_website", + + // If admin is in a subdirectory of the main website + parent: "../", + + // Absolute path examples (uncomment and modify as needed) + // absolute: "/path/to/your/khy_website", + // absolute: "/Users/george/Documents/khy_website/khy_website", + }, + + // Which target to use by default + defaultTarget: "sibling", + + // Files to copy from admin to main website + filesToCopy: [ + { + source: "data/products.json", + target: "data/products.json", + description: "Product catalog data", + }, + { + source: "assets/images", + target: "assets/images", + description: "Product images", + isDirectory: true, + }, + ], + + // Required files that must exist in target directory + requiredTargetFiles: ["index.html", "data/products.json"], + + // Backup settings + backup: { + enabled: true, + directory: "backups", + timestampFormat: "YYYY-MM-DD_HH-mm-ss", + }, +}; diff --git a/deploy.js b/deploy.js new file mode 100644 index 0000000..2bc2557 --- /dev/null +++ b/deploy.js @@ -0,0 +1,175 @@ +#!/usr/bin/env node + +/** + * Deploy Script for KHY Admin Dashboard + * + * This script copies the updated products.json and any other changes + * from the admin directory to the main website directory. + * + * Usage: node deploy.js [target-directory] + */ + +const fs = require("fs"); +const path = require("path"); +const { exec } = require("child_process"); +const config = require("./deploy-config"); + +// Configuration +const ADMIN_DIR = __dirname; + +// Get target directory from command line or use default +const TARGET_DIR = process.argv[2] || config.targets[config.defaultTarget]; +const TARGET_PATH = path.resolve(ADMIN_DIR, TARGET_DIR); + +console.log("KHY Admin Deploy Script"); +console.log("======================="); +console.log(`Admin Directory: ${ADMIN_DIR}`); +console.log(`Target Directory: ${TARGET_PATH}`); +console.log(""); + +// Files to copy from admin to main website (from config) +const FILES_TO_COPY = config.filesToCopy; + +// Function to copy file or directory +function copyFileOrDir(source, target, isDirectory = false) { + const sourcePath = path.join(ADMIN_DIR, source); + const targetPath = path.join(TARGET_PATH, target); + + try { + if (isDirectory) { + // Copy directory recursively + if (fs.existsSync(targetPath)) { + fs.rmSync(targetPath, { recursive: true, force: true }); + } + fs.cpSync(sourcePath, targetPath, { recursive: true }); + } else { + // Copy file + const targetDir = path.dirname(targetPath); + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); + } + fs.copyFileSync(sourcePath, targetPath); + } + return true; + } catch (error) { + console.error(`❌ Error copying ${source}:`, error.message); + return false; + } +} + +// Function to create backup +function createBackup() { + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); + const backupDir = path.join(ADMIN_DIR, "backups", timestamp); + + try { + fs.mkdirSync(backupDir, { recursive: true }); + + // Backup current products.json from target + const targetProducts = path.join(TARGET_PATH, "data/products.json"); + if (fs.existsSync(targetProducts)) { + fs.copyFileSync(targetProducts, path.join(backupDir, "products.json")); + console.log(`📦 Backup created: ${backupDir}`); + return true; + } + } catch (error) { + console.error("❌ Error creating backup:", error.message); + } + return false; +} + +// Function to kill preview server +function killPreviewServer() { + return new Promise((resolve) => { + console.log("Stopping preview server..."); + exec("node kill-server.js", (error, stdout, stderr) => { + if (error) { + console.log("No preview server running"); + } else { + console.log("Preview server stopped"); + } + resolve(); + }); + }); +} + +// Main deploy function +async function deploy() { + console.log("Checking target directory..."); + + // Kill preview server first + await killPreviewServer(); + console.log(""); + + // Check if target directory exists + if (!fs.existsSync(TARGET_PATH)) { + console.error(`Target directory does not exist: ${TARGET_PATH}`); + console.log("Make sure you're running this from the admin directory"); + console.log( + "Or specify the correct target directory: node deploy.js /path/to/main/website" + ); + process.exit(1); + } + + // Check if target has the expected structure + const expectedFiles = config.requiredTargetFiles; + const missingFiles = expectedFiles.filter( + (file) => !fs.existsSync(path.join(TARGET_PATH, file)) + ); + + if (missingFiles.length > 0) { + console.error( + `Target directory missing required files: ${missingFiles.join(", ")}` + ); + console.log( + "Make sure you're pointing to the correct main website directory" + ); + process.exit(1); + } + + console.log("Target directory validated"); + console.log(""); + + // Create backup + console.log("Creating backup..."); + createBackup(); + console.log(""); + + // Copy files + console.log("Copying files..."); + let successCount = 0; + let totalCount = FILES_TO_COPY.length; + + for (const file of FILES_TO_COPY) { + console.log(`${file.description}...`); + const success = copyFileOrDir(file.source, file.target, file.isDirectory); + if (success) { + console.log(` ${file.source} → ${file.target}`); + successCount++; + } else { + console.log(` Failed to copy ${file.source}`); + } + } + + console.log(""); + console.log("Deploy Summary"); + console.log("=============="); + console.log(`Successfully copied: ${successCount}/${totalCount} files`); + + if (successCount === totalCount) { + console.log("Deploy completed successfully!"); + console.log(""); + console.log("Your website has been updated with the latest changes."); + console.log("You can now visit your main website to see the changes."); + } else { + console.log("Deploy completed with some errors."); + console.log("Check the error messages above and try again."); + process.exit(1); + } +} + +// Run deploy +deploy().catch((error) => { + console.error("Deploy failed:", error.message); + process.exit(1); +}); diff --git a/index.html b/index.html new file mode 100644 index 0000000..cbe5bb5 --- /dev/null +++ b/index.html @@ -0,0 +1,1218 @@ + + + + + + KHY - Professional Services + + + + + + + +
+ +
+ + + + + +
+ +
+ KHY Logo + +
+ + + +
+ +
+ +
+ +
+ Modern interior design space +
+ + +
+

+ Redefining the Artistry of
+ Furniture +

+
+ +
+
+

+ crafting spaces that feel like home +

+
+
+
+ + +
+
+
+

+ At Khy, we design and furnish office environments where people + thrive. From enterprise headquarters to coworking spaces, we + create workspaces that balance function, comfort, and design + excellence. +

+
+
+
+ + +
+
+ +
+

+ Curated by Space +

+

+ Explore our collection of timeless, curated designs by room +

+
+ + +
+ +
+
+ Lounge Areas +
+

+ Lounge Areas +

+
+ + +
+
+ Workstations +
+

+ Workstations +

+
+ + +
+
+ Conference Rooms +
+

+ Conference Rooms +

+
+
+
+
+ + +
+
+ +
+

+ Our Story +

+
+ + +
+ +
+ Modern office interior + +
+ +
+ + +
+ + + + + +
+
+ + +
+

+ Rooted in Ghana.
+ Inspired by beauty.
+ Timeless by design. +

+ +

+ From our first collection to now, khy has remained dedicated to + artistry, quality, and Ghanaian-inspired sophistication. +
+
+ Khy brings timeless, curated furniture and decor to offices that + tell a story. +

+ + +
+
+
+
+ + +
+
+
+

+ At Khy, we provide end-to-end solutions: interior design, bespoke + manufacturing, furniture supply, professional installation, and + dedicated after-sales care — all under one roof. +

+
+
+
+ + +
+ +
+ + +
+
+
+ +

+ Customer satisfaction +

+ + +
+ + + + + +
+ + +
+
+ "Exceptional Craftsmanship and detail.
+ Our office feels alive." +
+ + Google Ghana, Accra + +
+
+
+
+ + +
+
+
+ +

+ Our Partners +

+ + +
+ +
+ Bene +
+ + +
+ Cecil Nurse. +
+ + +
+ Andreu World +
+ + +
+ Forma 5 +
+
+
+
+
+ + +
+
+
+ +

+ Trusted by Leading Brands +

+ + +
+ +
+ Norfund +
+ + +
+ Glico +
+ + +
+ Stanbic Bank +
+ + +
+ GROW ENGINEERING +
+ + +
+ DBG +
+
+
+
+
+
+ + + + + + + diff --git a/kill-server.js b/kill-server.js new file mode 100644 index 0000000..672069e --- /dev/null +++ b/kill-server.js @@ -0,0 +1,62 @@ +#!/usr/bin/env node + +/** + * Server Killer for KHY Admin Dashboard + * + * This script kills the preview server + * Usage: node kill-server.js + */ + +const fs = require("fs"); +const path = require("path"); + +const PID_FILE = path.join(__dirname, ".server.pid"); + +console.log("KHY Admin Server Killer"); +console.log("========================"); +console.log(""); + +// Function to kill server by PID +function killServerByPID() { + try { + if (fs.existsSync(PID_FILE)) { + const pid = fs.readFileSync(PID_FILE, "utf8").trim(); + console.log(`Killing server with PID: ${pid}`); + + // Kill the process + process.kill(parseInt(pid), "SIGTERM"); + + // Remove PID file + fs.unlinkSync(PID_FILE); + console.log(`Server killed successfully`); + return true; + } else { + console.log("No server PID file found - server may not be running"); + return false; + } + } catch (error) { + if (error.code === "ESRCH") { + console.log("Server process not found - may have already stopped"); + // Clean up PID file anyway + if (fs.existsSync(PID_FILE)) { + fs.unlinkSync(PID_FILE); + } + } else { + console.log(`Could not kill server: ${error.message}`); + } + return false; + } +} + +// Kill the server +const killed = killServerByPID(); + +if (killed) { + console.log(""); + console.log("Preview server stopped successfully!"); +} else { + console.log(""); + console.log("Server was not running or already stopped"); +} + +console.log(""); diff --git a/package.json b/package.json new file mode 100644 index 0000000..04f89e4 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "khy-admin-dashboard", + "version": "1.0.0", + "description": "KHY Furniture Product Management Admin Dashboard", + "main": "admin.html", + "scripts": { + "start": "python3 -m http.server 8080", + "dev": "python3 -m http.server 8080", + "build": "echo 'No build process needed for static admin dashboard'", + "setup": "node setup-deploy.js", + "deploy": "node deploy.js", + "deploy:prod": "node deploy.js ../", + "preview": "node start-server.js", + "preview:kill": "node kill-server.js", + "preview:open": "echo 'Open http://localhost:8080 to preview changes'" + }, + "keywords": [ + "admin", + "dashboard", + "product-management", + "furniture", + "khy" + ], + "author": "KHY Furniture", + "license": "MIT", + "devDependencies": {}, + "dependencies": {} +} diff --git a/product-catalog.html b/product-catalog.html new file mode 100644 index 0000000..c73e97c --- /dev/null +++ b/product-catalog.html @@ -0,0 +1,564 @@ + + + + + + Product Catalog - KHY + + + + + + +
+ +
+ + + + + +
+ +
+ KHY Logo + +
+ + + +
+ +
+ +
+ +
+ Product catalog background + + +
+ + +
+

+ Product catalog +

+
+ + +
+
+
+ +
+ + + + + + + + + + Showing 1–16 of 32 results +
+ + +
+ +
+ Sort by: + +
+
+
+
+
+ + +
+
+ +
+ +
+ + + +
+ + +
+
+
+ +
+
+ Trophy +
+
+

+ High Quality +

+

+ crafted from top materials +

+
+
+ + +
+
+ Guarantee +
+
+

+ Warranty Protection +

+

+ Over 2 years +

+
+
+ + +
+
+ Shipping +
+
+

+ Free Shipping +

+

+ Order over 150 $ +

+
+
+ + +
+
+ Support +
+
+

+ 24 / 7 Support +

+

+ Dedicated support +

+
+
+
+
+
+
+ + +
+ + + + + + + + diff --git a/product-detail.html b/product-detail.html new file mode 100644 index 0000000..84a2724 --- /dev/null +++ b/product-detail.html @@ -0,0 +1,741 @@ + + + + + + Product Detail - KHY + + + + + + +
+ +
+ + + + + +
+ +
+ KHY Logo + +
+ + + +
+ +
+ +
+
+
+

+ Product Details +

+
+
+
+ + +
+
+
+ +
+ +
+
+ Outdoor sofa set +
+
+ Outdoor sofa set 2 +
+
+ Stuart sofa +
+
+ Maya sofa three seater +
+
+ + +
+ Asgaard sofa +
+
+ + +
+ +

+ Asgaard sofa +

+ + +

+ 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. +

+ + +
+

+ Size +

+
+ + + + + + +
+
+ + +
+

+ Color +

+
+ + + +
+
+ + +
+ +
+ + 1 + +
+ + + + +
+ + +
+ + +
+
+ Model No. + : + SS001 +
+
+ Category + : + Sofas +
+
+ Tags + : + Sofa, Chair, Home, Shop +
+
+ Dimension + : + 180cm x 85cm x 75cm +
+
+
+
+
+
+ + +
+
+
+ +
+ +
+ + +
+

+ 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. +

+
+ + +
+
+ Sofa variant left +
+
+ Sofa variant right +
+
+
+
+ + +
+ + + + + +
+
+ + + + + + + + diff --git a/scripts/main.js b/scripts/main.js new file mode 100644 index 0000000..6b2a10d --- /dev/null +++ b/scripts/main.js @@ -0,0 +1,2073 @@ +// Global script initialization test +console.log("=== MAIN.JS LOADED ==="); +console.log("Current pathname:", window.location.pathname); +console.log( + "Is comparison page:", + window.location.pathname.includes("product-comparison.html") +); + +// Update year in footer and handle smooth scrolling +function initSite() { + // Update footer year + const footer = document.querySelector("footer p"); + if (footer) { + const currentYear = new Date().getFullYear(); + footer.innerHTML = footer.innerHTML.replace("2024", currentYear); + } + + // Smooth scrolling for navigation links + const navLinks = document.querySelectorAll('.nav-link[href^="#"]'); + navLinks.forEach((link) => { + link.addEventListener("click", function (e) { + e.preventDefault(); + const targetId = this.getAttribute("href").substring(1); + const targetElement = document.getElementById(targetId); + if (targetElement) { + const headerHeight = 112; + const targetPosition = targetElement.offsetTop - headerHeight; + window.scrollTo({ top: targetPosition, behavior: "smooth" }); + } + }); + }); + + // Handle cross-page navigation links (links that point to other pages with anchors) + const crossPageLinks = document.querySelectorAll('.nav-link[href*=".html#"]'); + crossPageLinks.forEach((link) => { + link.addEventListener("click", function () { + // allow default navigation + }); + }); + + // Remove any lingering .active classes and avoid adding new ones + const allNavLinks = document.querySelectorAll(".nav-link"); + allNavLinks.forEach((a) => a.classList.remove("active")); + + // Do not add page-specific active state anymore + // const currentPage = window.location.pathname.split("/").pop() || "index.html"; + // const currentPageLink = document.querySelector(`.nav-link[href="${currentPage}"]`); + // if (currentPageLink) currentPageLink.classList.add("active"); + + // Disable scroll-based active highlighting + // (Keeping function for potential future use, but it does nothing now.) + function updateActiveLink() { + // no-op: active highlighting disabled globally + } + window.addEventListener("scroll", updateActiveLink); + updateActiveLink(); + + // Quantity controls on product detail page + (function initQuantityControls() { + const decr = document.getElementById("qty-decr"); + const incr = document.getElementById("qty-incr"); + const valueEl = document.getElementById("qty-value"); + if (!decr || !incr || !valueEl) return; // Not on product detail page + + function parseQty() { + const n = parseInt(valueEl.textContent || "1", 10); + return Number.isFinite(n) && n > 0 ? n : 1; + } + decr.addEventListener("click", () => { + const next = Math.max(1, parseQty() - 1); + valueEl.textContent = String(next); + }); + incr.addEventListener("click", () => { + const next = parseQty() + 1; + valueEl.textContent = String(next); + }); + })(); + + // Related products (dynamic) + (async function initRelated() { + const grid = document.getElementById("related-grid"); + if (!grid) return; + + // Don't run this function on product detail pages - let the product detail function handle it + const urlParams = new URLSearchParams(window.location.search); + const productId = urlParams.get("id"); + if (productId) return; + + try { + const res = await fetch("data/products.json", { cache: "no-store" }); + const data = await res.json(); + const products = Array.isArray(data.products) ? data.products : []; + + // Determine current product's category from the title text (fallback seating) + const title = document.querySelector("h1"); + const currentName = (title?.textContent || "").trim(); + const current = products.find( + (p) => (p.name || "").trim() === currentName + ); + const category = current?.category || "seating"; + + // Filter related: same category but not the current product + const related = products.filter( + (p) => p.category === category && p.name !== currentName + ); + + let page = 1; + const pageSize = 4; + + function render() { + grid.innerHTML = ""; + const start = 0; + const end = page * pageSize; // cumulative show-more + related.slice(start, end).forEach((p) => { + const card = document.createElement("article"); + card.className = "rounded-lg overflow-hidden bg-white shadow-sm"; + + // Distinct image background for Asgaard sofa to match others + const imageBgClass = + (p.name || "").toLowerCase() === "asgaard sofa" + ? "bg-linen" + : "bg-white"; + const panelBgClass = "bg-light-bg"; + + card.innerHTML = ` +
+ \"${ +
+
+

${ + p.name + }

+
`; + grid.appendChild(card); + }); + + // Toggle show-more visibility + const btn = document.getElementById("related-show-more"); + if (btn) { + const hasMore = related.length > page * pageSize; + btn.style.display = hasMore ? "inline-flex" : "none"; + // Ensure centered label + btn.classList.add("inline-flex", "items-center", "justify-center"); + } + } + + const btn = document.getElementById("related-show-more"); + if (btn) { + btn.addEventListener("click", () => { + page += 1; + render(); + }); + } + + render(); + } catch (e) { + console.error("Failed to load related products:", e); + } + })(); + + // Blog functionality (only runs on blog page) + (async function initBlog() { + const blogSearchInput = document.getElementById("blog-search-input"); + const mainBlogContent = document.getElementById("main-blog-content"); + const blogCategories = document.getElementById("blog-categories"); + const recentPosts = document.getElementById("recent-posts"); + const pagination = document.getElementById("blog-pagination"); + const pageButtons = document.querySelectorAll( + "#blog-pagination .blog-page-btn" + ); + const nextButton = document.getElementById("blog-next-btn"); + + if (!blogSearchInput || !mainBlogContent) return; + + let allPosts = []; + let filteredPosts = []; + let activeTag = ""; + let currentPage = 1; + const pageSize = 4; + + // Load blog data from JSON + try { + const response = await fetch("data/blog.json", { cache: "no-store" }); + const data = await response.json(); + allPosts = Array.isArray(data.posts) ? data.posts : []; + + // Sort posts by date (newest first) + allPosts.sort((a, b) => new Date(b.date) - new Date(a.date)); + + console.log("Blog posts loaded:", allPosts.length); + } catch (error) { + console.error("Failed to load blog posts:", error); + return; + } + + function normalize(text) { + return (text || "") + .toLowerCase() + .replace(/[^a-z0-9]+/g, " ") + .trim(); + } + + function formatDate(dateString) { + const date = new Date(dateString); + return date.toLocaleDateString("en-US", { + day: "numeric", + month: "short", + year: "numeric", + }); + } + + function getFilteredPosts() { + const query = normalize(blogSearchInput.value.trim()); + return allPosts.filter((post) => { + const title = post.title || ""; + const excerpt = post.excerpt || ""; + const haystack = normalize(`${title} ${excerpt}`); + const matchesQuery = query === "" || haystack.includes(query); + const matchesTag = activeTag === "" || post.tags.includes(activeTag); + return matchesQuery && matchesTag; + }); + } + + function renderBlogPost(post) { + const tagsHtml = post.tags + .map( + (tag) => + `${ + tag.charAt(0).toUpperCase() + tag.slice(1) + }` + ) + .join(", "); + + const contentHtml = post.content + .map((paragraph) => `

${paragraph}

`) + .join(""); + + return ` +
+ +
+ ${post.alt || post.title} +
+ + +
+ +
+ Admin + + ${post.author} + +
+ + +
+ Calendar + + ${formatDate(post.date)} + +
+ + +
+ Tag + + ${ + post.tags[0] + ? post.tags[0].charAt(0).toUpperCase() + + post.tags[0].slice(1) + : "Uncategorized" + } + +
+
+ + +

+ ${post.title} +

+ + +

+ ${post.excerpt} +

+ + + + + + +
+ `; + } + + function renderBlogPosts() { + const start = (currentPage - 1) * pageSize; + const end = start + pageSize; + const postsToShow = filteredPosts.slice(start, end); + + mainBlogContent.innerHTML = postsToShow + .map((post) => renderBlogPost(post)) + .join(""); + + // Re-initialize read more functionality + initReadMore(); + } + + function renderCategories() { + const tagCounts = {}; + allPosts.forEach((post) => { + post.tags.forEach((tag) => { + tagCounts[tag] = (tagCounts[tag] || 0) + 1; + }); + }); + + const categoriesHtml = Object.entries(tagCounts) + .map( + ([tag, count]) => ` + + ` + ) + .join(""); + + blogCategories.innerHTML = categoriesHtml; + + // Add event listeners to category buttons + const categoryButtons = blogCategories.querySelectorAll(".tag-filter"); + categoryButtons.forEach((btn) => { + btn.addEventListener("click", () => { + const clickedTag = btn.getAttribute("data-tag"); + activeTag = activeTag === clickedTag ? "" : clickedTag; + + categoryButtons.forEach((b) => { + b.classList.remove("border-uc-gold", "bg-gray-50", "text-uc-gold"); + b.setAttribute("aria-pressed", "false"); + }); + + if (activeTag) { + btn.classList.add("border-uc-gold", "bg-gray-50", "text-uc-gold"); + btn.setAttribute("aria-pressed", "true"); + } + + currentPage = 1; + filteredPosts = getFilteredPosts(); + renderBlogPosts(); + renderPagination(); + }); + }); + } + + function renderRecentPosts() { + const recentPostsHtml = allPosts + .slice(0, 3) + .map( + (post) => ` +
+ ${post.alt || post.title} +
+

+ ${post.title} +

+

+ ${formatDate(post.date)} +

+
+
+ ` + ) + .join(""); + + recentPosts.innerHTML = recentPostsHtml; + } + + function renderPagination() { + const total = filteredPosts.length; + const totalPages = Math.max(1, Math.ceil(total / pageSize)); + currentPage = Math.min(Math.max(1, currentPage), totalPages); + + if (pagination) { + const hasQuery = blogSearchInput.value.trim().length > 0; + pagination.style.display = hasQuery ? "none" : "flex"; + + pageButtons.forEach((btn) => { + const p = Number(btn.getAttribute("data-page")); + btn.classList.toggle("bg-uc-gold", p === currentPage); + btn.classList.toggle("text-white", p === currentPage); + btn.classList.toggle("bg-linen", p !== currentPage); + btn.classList.toggle("text-black", p !== currentPage); + btn.style.display = p <= totalPages ? "inline-flex" : "none"; + }); + + if (nextButton) { + nextButton.style.display = totalPages > 1 ? "inline-flex" : "none"; + nextButton.disabled = currentPage >= totalPages; + } + } + } + + function initReadMore() { + const articles = document.querySelectorAll("#main-blog-content article"); + + articles.forEach((article) => { + const excerptP = article.querySelector("p"); + const readMoreBtn = article.querySelector(".read-more-toggle"); + if (!excerptP || !readMoreBtn) return; + + const fullExcerpt = excerptP.textContent || ""; + const { text: truncated, truncated: isTruncated } = truncateToWords( + fullExcerpt, + 80 + ); + + if (isTruncated) { + excerptP.dataset.excerptFull = fullExcerpt; + excerptP.textContent = truncated; + readMoreBtn.style.display = "inline-block"; + } else { + readMoreBtn.style.display = "none"; + } + + readMoreBtn.addEventListener("click", (ev) => { + ev.preventDefault(); + if (excerptP.dataset.excerptFull) { + excerptP.textContent = excerptP.dataset.excerptFull; + delete excerptP.dataset.excerptFull; + } + const fullBlock = article.querySelector(".full-content"); + if (fullBlock) fullBlock.classList.remove("hidden"); + readMoreBtn.style.display = "none"; + + // Add "Show less" button at the end of the article + const showLessBtn = document.createElement("button"); + showLessBtn.type = "button"; + showLessBtn.className = + "show-less-toggle inline-block font-playfair font-normal text-base text-black border-b border-black hover:text-gray-600 hover:border-gray-600 transition-colors mt-4"; + showLessBtn.textContent = "Show less"; + showLessBtn.addEventListener("click", (ev) => { + ev.preventDefault(); + // Restore truncated excerpt + excerptP.textContent = truncated; + excerptP.dataset.excerptFull = fullExcerpt; + // Hide full content + if (fullBlock) fullBlock.classList.add("hidden"); + // Show "Read more" button again + readMoreBtn.style.display = "inline-block"; + // Remove "Show less" button + showLessBtn.remove(); + }); + // Insert at the end of the article + article.appendChild(showLessBtn); + }); + }); + } + + // Event listeners + blogSearchInput.addEventListener("input", () => { + currentPage = 1; + filteredPosts = getFilteredPosts(); + renderBlogPosts(); + renderPagination(); + }); + + pageButtons.forEach((btn) => + btn.addEventListener("click", () => { + currentPage = Number(btn.getAttribute("data-page")) || 1; + renderBlogPosts(); + renderPagination(); + }) + ); + + if (nextButton) { + nextButton.addEventListener("click", () => { + currentPage += 1; + renderBlogPosts(); + renderPagination(); + }); + } + + // Initialize everything + filteredPosts = getFilteredPosts(); + renderBlogPosts(); + renderCategories(); + renderRecentPosts(); + renderPagination(); + })(); + + // Inline "Read more" expansion for blog posts + const articlesForExcerpt = document.querySelectorAll( + "#main-blog-content article" + ); + + function truncateToWords(text, maxWords) { + const words = (text || "").trim().split(/\s+/); + if (words.length <= maxWords) return { text, truncated: false }; + return { text: words.slice(0, maxWords).join(" ") + "…", truncated: true }; + } + + articlesForExcerpt.forEach((article) => { + const excerptP = article.querySelector("p"); + const readMoreBtn = article.querySelector(".read-more-toggle"); + if (!excerptP || !readMoreBtn) return; + + const fullExcerpt = excerptP.textContent || ""; + const { text: truncated, truncated: isTruncated } = truncateToWords( + fullExcerpt, + 80 + ); + + if (isTruncated) { + // Store full text and show truncated preview + excerptP.dataset.excerptFull = fullExcerpt; + excerptP.textContent = truncated; + readMoreBtn.style.display = "inline-block"; + } else { + // Nothing to expand; hide the control + readMoreBtn.style.display = "none"; + } + + // Click to expand (delegated fallback added below as well) + readMoreBtn.addEventListener("click", (ev) => { + ev.preventDefault(); + if (excerptP.dataset.excerptFull) { + excerptP.textContent = excerptP.dataset.excerptFull; + delete excerptP.dataset.excerptFull; + } + const fullBlock = article.querySelector(".full-content"); + if (fullBlock) fullBlock.classList.remove("hidden"); + readMoreBtn.style.display = "none"; + + // Add "Show less" button at the end of the article + const showLessBtn = document.createElement("button"); + showLessBtn.type = "button"; + showLessBtn.className = + "show-less-toggle inline-block font-playfair font-normal text-base text-black border-b border-black hover:text-gray-600 hover:border-gray-600 transition-colors mt-4"; + showLessBtn.textContent = "Show less"; + showLessBtn.addEventListener("click", (ev) => { + ev.preventDefault(); + // Restore truncated excerpt + excerptP.textContent = truncated; + excerptP.dataset.excerptFull = fullExcerpt; + // Hide full content + if (fullBlock) fullBlock.classList.add("hidden"); + // Show "Read more" button again + readMoreBtn.style.display = "inline-block"; + // Remove "Show less" button + showLessBtn.remove(); + }); + // Insert at the end of the article + article.appendChild(showLessBtn); + }); + }); + + // Safety net: delegated handler in case markup changes + document.addEventListener("click", (e) => { + const trigger = e.target.closest(".read-more-toggle"); + if (!trigger) return; + e.preventDefault(); + const article = trigger.closest("article"); + if (!article) return; + const excerptP = article.querySelector("p"); + if (excerptP?.dataset?.excerptFull) { + excerptP.textContent = excerptP.dataset.excerptFull; + delete excerptP.dataset.excerptFull; + } + const fullBlock = article.querySelector(".full-content"); + if (fullBlock) fullBlock.classList.remove("hidden"); + trigger.style.display = "none"; + }); + + // Footer newsletter subscribe validation and feedback (works on all pages) + const siteFooter = document.querySelector("footer"); + if (siteFooter) { + const emailInputs = siteFooter.querySelectorAll('input[type="email"]'); + emailInputs.forEach((emailInput) => { + const inputRow = emailInput.closest(".flex") || emailInput.parentElement; + let subscribeButton = inputRow ? inputRow.querySelector("button") : null; + if (!subscribeButton) { + subscribeButton = emailInput.parentElement?.querySelector("button"); + } + if (!subscribeButton) return; + subscribeButton.type = "button"; + let feedback = inputRow ? inputRow.nextElementSibling : null; + if (!(feedback instanceof HTMLElement) || !feedback.dataset.feedback) { + feedback = document.createElement("div"); + feedback.dataset.feedback = "newsletter"; + if (inputRow && inputRow.parentElement) { + inputRow.parentElement.insertBefore(feedback, inputRow.nextSibling); + } + } + feedback.className = + "hidden mt-3 rounded-lg px-4 py-3 font-playfair text-base"; + function showMessage(text, type) { + feedback.textContent = text; + feedback.className = + "mt-3 rounded-lg px-4 py-3 font-playfair text-base"; + if (type === "success") { + feedback.classList.add( + "bg-green-50", + "border", + "border-green-200", + "text-green-800" + ); + } else { + feedback.classList.add( + "bg-red-50", + "border", + "border-red-200", + "text-red-800" + ); + } + feedback.classList.remove("hidden"); + setTimeout(() => feedback.classList.add("hidden"), 3000); + } + subscribeButton.addEventListener("click", (e) => { + e.preventDefault(); + const value = (emailInput.value || "").trim(); + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + emailInput.classList.remove("border-red-500"); + if (!emailRegex.test(value)) { + emailInput.classList.add("border-red-500"); + showMessage("Please enter a valid email address.", "error"); + emailInput.focus(); + return; + } + showMessage("Thank you for subscribing!", "success"); + emailInput.value = ""; + subscribeButton.disabled = true; + setTimeout(() => (subscribeButton.disabled = false), 1200); + }); + }); + } + + // Product detail page functionality + (async function initProductDetail() { + console.log("Product detail script running..."); + + // Check if we're on the product detail page + const productTitle = document.querySelector("h1"); + if (!productTitle) { + console.log("No h1 found, not on product detail page"); + return; + } + + // Get product ID from URL + const urlParams = new URLSearchParams(window.location.search); + const productId = parseInt(urlParams.get("id")); + console.log("Product ID from URL:", productId); + + if (!productId) { + console.log("No product ID found in URL"); + return; + } + + try { + // Load product data + const response = await fetch("data/products.json"); + const data = await response.json(); + const product = data.products.find((p) => p.id === productId); + + if (!product) { + console.error("Product not found:", productId); + return; + } + + console.log("Loading product:", product); + + // Update page title + document.title = `${product.name} - KHY`; + + // Update the "Product Details" title with the actual product name + const productDetailsTitle = document.getElementById( + "product-details-title" + ); + if (productDetailsTitle) { + productDetailsTitle.textContent = product.name; + } + + // Update product title (the main product title, not the breadcrumb) + const allH1s = document.querySelectorAll("h1"); + console.log("All H1 elements:", allH1s); + if (allH1s.length > 1) { + const mainProductTitle = allH1s[1]; // The second h1 should be the product title + console.log("Main product title element:", mainProductTitle); + if (mainProductTitle) { + mainProductTitle.textContent = product.name; + } + } + + // Update product description - find by text content + const allParagraphs = document.querySelectorAll("p"); + console.log("All paragraphs:", allParagraphs); + const descriptionEl = Array.from(allParagraphs).find( + (p) => + p.textContent && + p.textContent.includes( + "Setting the bar as one of the loudest speakers" + ) + ); + console.log("Description element:", descriptionEl); + if (descriptionEl) { + descriptionEl.textContent = product.description; + } + + // Update main product image - find by alt text + const allImages = document.querySelectorAll("img"); + console.log("All images:", allImages); + const mainImage = Array.from(allImages).find( + (img) => img.alt && img.alt.includes("Asgaard sofa") + ); + console.log("Main image element:", mainImage); + if (mainImage) { + mainImage.src = product.image; + mainImage.alt = product.alt || product.name; + } + + // Update thumbnail images - find by alt text + if (product.images && product.images.length > 0) { + const allImages = document.querySelectorAll("img"); + const thumbnails = Array.from(allImages).filter( + (img) => + img.alt && + (img.alt.includes("Outdoor sofa set") || + img.alt.includes("Stuart sofa") || + img.alt.includes("Maya sofa")) + ); + console.log("Thumbnail images found:", thumbnails); + product.images.forEach((imageSrc, index) => { + if (thumbnails[index]) { + thumbnails[index].src = imageSrc; + thumbnails[index].alt = product.alt || product.name; + } + }); + } + + // Update size options - use the specific size-button class + if (product.sizes) { + const sizeButtons = document.querySelectorAll(".size-button"); + console.log("Size buttons found:", sizeButtons); + + // Hide all size buttons first + sizeButtons.forEach((button) => { + button.style.display = "none"; + }); + + // Show and update only the buttons we need + product.sizes.forEach((size, index) => { + if (sizeButtons[index]) { + sizeButtons[index].style.display = "flex"; + sizeButtons[index].textContent = size; + sizeButtons[index].setAttribute("data-size", size); + // Set selected size + if (size === product.selectedSize) { + sizeButtons[index].classList.remove( + "bg-floral-white", + "text-black" + ); + sizeButtons[index].classList.add("bg-uc-gold", "text-white"); + sizeButtons[index].classList.add("selected"); + } else { + sizeButtons[index].classList.remove( + "bg-uc-gold", + "text-white", + "selected" + ); + sizeButtons[index].classList.add("bg-floral-white", "text-black"); + } + } + }); + + // Add click event listeners for size buttons + sizeButtons.forEach((button) => { + button.addEventListener("click", function () { + // Remove selected state from all size buttons + sizeButtons.forEach((btn) => { + btn.classList.remove("bg-uc-gold", "text-white", "selected"); + btn.classList.add("bg-floral-white", "text-black"); + }); + + // Add selected state to clicked button + this.classList.remove("bg-floral-white", "text-black"); + this.classList.add("bg-uc-gold", "text-white", "selected"); + }); + }); + } + + // Update color options - find only the rounded color buttons, not size buttons + if (product.colors) { + const allButtons = document.querySelectorAll("button"); + const colorButtons = Array.from(allButtons).filter( + (button) => + button.classList.contains("w-8") && + button.classList.contains("h-8") && + button.classList.contains("rounded-full") && + !button.textContent // Color buttons should not have text content + ); + console.log("Color buttons found:", colorButtons); + + product.colors.forEach((color, index) => { + if (colorButtons[index]) { + colorButtons[index].style.backgroundColor = color.value; + colorButtons[index].setAttribute( + "data-color", + color.name || color.value + ); + // Set selected color + if (color.selected) { + colorButtons[index].classList.add( + "border-2", + "border-black", + "selected" + ); + } else { + colorButtons[index].classList.remove( + "border-2", + "border-black", + "selected" + ); + } + } + }); + + // Add click event listeners for color buttons + colorButtons.forEach((button) => { + button.addEventListener("click", function () { + // Remove selected state from all color buttons + colorButtons.forEach((btn) => { + btn.classList.remove("border-2", "border-black", "selected"); + }); + + // Add selected state to clicked button + this.classList.add("border-2", "border-black", "selected"); + }); + }); + } + + // Update product metadata - find by text content + const allSpans = document.querySelectorAll("span"); + + // Find and update Model No. + const modelNoLabel = Array.from(allSpans).find( + (span) => span.textContent === "Model No." + ); + if (modelNoLabel && product.modelNo) { + const modelNoValue = + modelNoLabel.parentElement.querySelector("span:last-child"); + if (modelNoValue) { + modelNoValue.textContent = product.modelNo; + } + } + + // Find and update Category + const categoryLabel = Array.from(allSpans).find( + (span) => span.textContent === "Category" + ); + if (categoryLabel && product.category) { + const categoryValue = + categoryLabel.parentElement.querySelector("span:last-child"); + if (categoryValue) { + categoryValue.textContent = + product.category.charAt(0).toUpperCase() + + product.category.slice(1); + } + } + + // Find and update Tags + const tagsLabel = Array.from(allSpans).find( + (span) => span.textContent === "Tags" + ); + if (tagsLabel && product.tags) { + const tagsValue = + tagsLabel.parentElement.querySelector("span:last-child"); + if (tagsValue) { + tagsValue.textContent = product.tags.join(", "); + } + } + + // Find and update Dimensions + const dimensionsLabel = Array.from(allSpans).find( + (span) => span.textContent === "Dimension" + ); + console.log("Dimensions label found:", dimensionsLabel); + console.log("Product dimensions:", product.dimensions); + + if (dimensionsLabel && product.dimensions) { + const dimensionsValue = + dimensionsLabel.parentElement.querySelector("span:last-child"); + console.log("Dimensions value element:", dimensionsValue); + if (dimensionsValue) { + dimensionsValue.textContent = product.dimensions; + console.log("Updated dimensions to:", product.dimensions); + } + } + + // Update description content + if (product.descriptionLong) { + const descriptionContainer = document.querySelector( + ".max-w-5xl.mx-auto.space-y-6" + ); + if (descriptionContainer) { + const descriptionParagraphs = + descriptionContainer.querySelectorAll("p"); + console.log("Description paragraphs found:", descriptionParagraphs); + + // Update each paragraph with the product's description content + product.descriptionLong.forEach((paragraph, index) => { + if (descriptionParagraphs[index]) { + descriptionParagraphs[index].textContent = paragraph; + } + }); + } + } + + // Update gallery images + if (product.galleryPairs) { + // Find gallery images by alt text + const allImages = document.querySelectorAll("img"); + const galleryImages = Array.from(allImages).filter( + (img) => + img.alt && + (img.alt.includes("Sofa variant left") || + img.alt.includes("Sofa variant right")) + ); + console.log("Gallery images found:", galleryImages); + + product.galleryPairs.forEach((pair, index) => { + if (galleryImages[index * 2]) { + galleryImages[index * 2].src = pair.left; + galleryImages[index * 2].alt = product.alt || product.name; + } + if (galleryImages[index * 2 + 1]) { + galleryImages[index * 2 + 1].src = pair.right; + galleryImages[index * 2 + 1].alt = product.alt || product.name; + } + }); + } + + // Update related products section + const relatedGrid = document.getElementById("related-grid"); + if (relatedGrid && product.category) { + console.log("Current product category:", product.category); + console.log( + "All products:", + data.products.map((p) => ({ + id: p.id, + name: p.name, + category: p.category, + })) + ); + + // Filter related products by category (excluding current product) + const allRelatedProducts = data.products.filter( + (p) => p.category === product.category && p.id !== product.id + ); + + console.log("All related products found:", allRelatedProducts); + + let currentPage = 1; + const pageSize = 4; + + function renderRelatedProducts() { + const start = 0; + const end = currentPage * pageSize; + const productsToShow = allRelatedProducts.slice(start, end); + + relatedGrid.innerHTML = productsToShow + .map( + (p) => ` + +
+
+ ${
+                p.alt || p.name
+              } +
+
+

${ + p.name + }

+
+
+
+ ` + ) + .join(""); + + // Handle "Show More" button visibility + const showMoreBtn = document.getElementById("related-show-more"); + if (showMoreBtn) { + const hasMore = allRelatedProducts.length > end; + showMoreBtn.style.display = hasMore ? "inline-flex" : "none"; + showMoreBtn.classList.add( + "inline-flex", + "items-center", + "justify-center" + ); + } + } + + // Add event listener for "Show More" button + const showMoreBtn = document.getElementById("related-show-more"); + if (showMoreBtn) { + showMoreBtn.addEventListener("click", () => { + currentPage += 1; + renderRelatedProducts(); + }); + } + + // Initial render + renderRelatedProducts(); + } + } catch (error) { + console.error("Error loading product data:", error); + } + })(); +} + +// Product Comparison Page Functionality +function initProductComparison() { + console.log("=== INITIALIZING PRODUCT COMPARISON ==="); + + // Prevent running twice (it's invoked in multiple places) + if (window.__cmpInitialized) { + console.log("[Comparison] Already initialized, skipping"); + return; + } + window.__cmpInitialized = true; + + // Get product IDs from URL parameters + const urlParams = new URLSearchParams(window.location.search); + const product1Id = urlParams.get("product1"); + const product2Id = urlParams.get("product2"); + + console.log("URL Parameters:", { product1Id, product2Id }); + + // Keep references to the two products for event handlers + let cmpProduct1 = null; + let cmpProduct2 = null; + + // Fetch product data + fetch("data/products.json") + .then((response) => { + console.log("Fetch response status:", response.status); + return response.json(); + }) + .then((data) => { + console.log("Products data loaded:", data); + console.log("Total products:", data.products.length); + + // Find products by ID + const product1 = data.products.find((p) => p.id == product1Id); + const product2 = data.products.find((p) => p.id == product2Id); + + console.log("Found products:", { product1, product2 }); + + // Update product cards + if (product1) { + console.log("Updating product card 1 with:", product1.name); + cmpProduct1 = product1; + updateProductCard(1, product1); + updateComparisonTable(product1, 1); + } + + if (product2) { + console.log("Updating product card 2 with:", product2.name); + cmpProduct2 = product2; + updateProductCard(2, product2); + updateComparisonTable(product2, 2); + } + + // Populate dropdown + populateProductDropdown(data.products); + + // Update View More link with current comparison state + updateViewMoreLink(product1Id, product2Id); + + // Wire Add To Quote buttons (first -> product1, second -> product2) + wireComparisonAddToQuote(cmpProduct1, cmpProduct2); + }) + .catch((error) => { + console.error("Error loading products:", error); + }); +} + +// Bind the two Add To Quote buttons on the comparison page +function wireComparisonAddToQuote(prod1, prod2) { + try { + const addButtons = Array.from(document.querySelectorAll("button")).filter( + (b) => (b.textContent || "").trim() === "Add To Quote" + ); + + if (addButtons.length === 0) { + console.log("[Comparison] No Add To Quote buttons found"); + return; + } + + // Ensure deterministic order: the first encountered is for product 1 + const btn1 = addButtons[0] || null; + const btn2 = addButtons[1] || null; + + function toQuotePayload(p) { + if (!p) return null; + return { + id: Number(p.id), + name: p.name || "Product", + image: p.image || "", + color: + (p.colors && + p.colors[0] && + (p.colors[0].name || p.colors[0].value)) || + "Default", + size: (p.sizes && p.sizes[0]) || "Standard", + quantity: 1, + }; + } + + if (btn1) { + btn1.type = btn1.getAttribute("type") || "button"; + btn1.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + const payload = toQuotePayload(prod1); + if (!payload) return; + payload.quantity = 1; // force 1 for comparison adds + if (typeof addToQuote === "function") { + addToQuote(payload); + } else { + addToQuoteFallback(payload); + } + }); + } + + if (btn2) { + btn2.type = btn2.getAttribute("type") || "button"; + btn2.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + const payload = toQuotePayload(prod2); + if (!payload) return; + payload.quantity = 1; // force 1 for comparison adds + if (typeof addToQuote === "function") { + addToQuote(payload); + } else { + addToQuoteFallback(payload); + } + }); + } + + // Delegated fallback (in case DOM changes after load) + document.addEventListener("click", (e) => { + const t = e.target.closest && e.target.closest("button"); + if (!t) return; + const label = (t.textContent || "").trim(); + if (label !== "Add To Quote") return; + // Determine which button index this is relative to current NodeList + const currentButtons = Array.from( + document.querySelectorAll("button") + ).filter((b) => (b.textContent || "").trim() === "Add To Quote"); + const idx = currentButtons.indexOf(t); + if (idx === 0) { + const payload = toQuotePayload(prod1); + if (payload) { + payload.quantity = 1; + (typeof addToQuote === "function" ? addToQuote : addToQuoteFallback)( + payload + ); + } + } else if (idx === 1) { + const payload = toQuotePayload(prod2); + if (payload) { + payload.quantity = 1; + (typeof addToQuote === "function" ? addToQuote : addToQuoteFallback)( + payload + ); + } + } + }); + } catch (err) { + console.error("[Comparison] Failed to wire Add To Quote buttons:", err); + } +} + +function updateProductCard(slotNumber, product) { + console.log(`=== UPDATING PRODUCT CARD ${slotNumber} ===`); + console.log("Product data:", product); + + const cardContainer = document.querySelector(`.product-card-${slotNumber}`); + console.log("Card container found:", cardContainer); + + if (cardContainer) { + const imageContainer = cardContainer.querySelector("div"); + const title = cardContainer.querySelector("p"); + + console.log("Image container found:", imageContainer); + console.log("Title element found:", title); + + if (imageContainer) { + console.log("Replacing image container with:", product.image); + // Replace the placeholder div with an image + imageContainer.innerHTML = `${product.name}`; + } + if (title) { + console.log("Updating title to:", product.name); + title.textContent = product.name; + } + } else { + console.log(`Product card container .product-card-${slotNumber} not found`); + } +} + +function updateComparisonTable(product, slotNumber) { + console.log(`=== UPDATING COMPARISON TABLE FOR SLOT ${slotNumber} ===`); + console.log("Product:", product); + + // Update all sections + const sections = ["general", "product", "dimensions", "warranty"]; + + sections.forEach((section) => { + console.log(`Updating ${section} section...`); + updateTableSection(section, product, slotNumber); + }); +} + +function updateTableSection(sectionName, product, slotNumber) { + console.log( + `=== UPDATING TABLE SECTION: ${sectionName} (SLOT ${slotNumber}) ===` + ); + const sectionData = getProductSectionData(product, sectionName); + + // Find the section header + const section = document.querySelector( + `.comparison-table .${sectionName}-section` + ); + if (!section) { + console.log(`Section ${sectionName}-section not found`); + return; + } + + // Find the table that comes after this section header + const sectionTable = section.nextElementSibling; + if (!sectionTable) { + console.log(`Table after ${sectionName}-section not found`); + return; + } + + // Find all flex rows and filter to only those with data columns + const allRows = sectionTable.querySelectorAll(".flex.items-center"); + const rows = Array.from(allRows).filter((row) => + row.querySelector(".column-1") + ); + console.log( + `Found ${rows.length} data rows in ${sectionName} section (out of ${allRows.length} total rows)` + ); + + if (rows.length > 0 && sectionData) { + console.log("Updating rows with data:", sectionData); + + // Update each row with the corresponding data + rows.forEach((row, index) => { + if (index < sectionData.length) { + const column = row.querySelector(`.column-${slotNumber}`); + if (column) { + console.log( + `Updating row ${index + 1} with data: ${sectionData[index]}` + ); + console.log(`Row element:`, row); + console.log(`Column element:`, column); + console.log(`Column text before update: "${column.textContent}"`); + column.textContent = sectionData[index]; + console.log(`Column text after update: "${column.textContent}"`); + } else { + console.log(`Column ${slotNumber} not found in row ${index + 1}`); + } + } + }); + + console.log("Table section updated successfully."); + } else { + console.log("No rows found or section data is empty, skipping update."); + console.log("Rows found:", rows.length); + console.log("Section data:", sectionData); + } +} + +function getProductSectionData(product, sectionName) { + console.log(`=== GETTING PRODUCT SECTION DATA: ${sectionName} ===`); + console.log("Product:", product); + + let sectionData; + switch (sectionName) { + case "general": + sectionData = [ + product.salesPackage || "N/A", // Sales Package + product.modelNo || "N/A", // Model Number + product.additionalInformation?.Material || "N/A", // Secondary Material + product.configuration || "N/A", // Configuration + product.additionalInformation?.Upholstery || "N/A", // Upholstery Material + product.colors?.[0]?.name || "N/A", // Upholstery Color + ]; + break; + case "product": + sectionData = [ + product.fillingMaterial || "N/A", // Filling Material + product.finishType || "N/A", // Finish Type + product.adjustableHeadrest || "N/A", // Adjustable Headrest + product.maxLoadCapacity || "N/A", // Maximum Load Capacity + product.originOfManufacture || "N/A", // Origin of Manufacture + ]; + break; + case "dimensions": + const dims = product.dimensions?.split(" x ") || []; + sectionData = [ + dims[0] || "N/A", // Width + dims[1] || "N/A", // Height + dims[2] || "N/A", // Depth + product.weight || "N/A", // Weight + product.seatHeight || "N/A", // Seat Height + product.legHeight || "N/A", // Leg Height + ]; + break; + case "warranty": + sectionData = [ + product.additionalInformation?.Warranty || "N/A", // Warranty Summary + product.warrantyServiceType || "N/A", // Warranty Service Type + product.coveredInWarranty || "N/A", // Covered in Warranty + product.notCoveredInWarranty || "N/A", // Not Covered in Warranty + product.additionalInformation?.Warranty || "N/A", // Domestic Warranty + ]; + break; + default: + sectionData = []; + } + + console.log(`Section data for ${sectionName}:`, sectionData); + console.log(`Detailed breakdown for ${sectionName}:`); + sectionData.forEach((item, index) => { + console.log(` Item ${index + 1}: "${item}"`); + }); + + return sectionData; +} + +function populateProductDropdown(products) { + const dropdown = document.getElementById("product-dropdown"); + if (dropdown) { + // Clear existing options except the first one + while (dropdown.children.length > 1) { + dropdown.removeChild(dropdown.lastChild); + } + + // Add all products + products.forEach((product) => { + const option = document.createElement("option"); + option.value = product.id; + option.textContent = product.name; + dropdown.appendChild(option); + }); + + // Add event listener for product selection + dropdown.addEventListener("change", function () { + const selectedProductId = this.value; + console.log("Product selected from dropdown:", selectedProductId); + + if (selectedProductId && selectedProductId !== "Choose a Product") { + // Find the selected product + const selectedProduct = products.find((p) => p.id == selectedProductId); + + if (selectedProduct) { + console.log("Selected product found:", selectedProduct); + + // Update the second product slot + updateProductCard(2, selectedProduct); + updateComparisonTable(selectedProduct, 2); + + // Update URL to include the second product + updateURLParameter("product2", selectedProductId); + + // Reset dropdown to default option + this.value = "Choose a Product"; + } + } + }); + } +} + +function updateURLParameter(param, value) { + const url = new URL(window.location); + url.searchParams.set(param, value); + window.history.replaceState({}, "", url); +} + +function updateViewMoreLink(product1Id, product2Id) { + const viewMoreLink = document.querySelector( + 'a[href*="product-catalog.html"]' + ); + if (viewMoreLink) { + // Determine which slot is available (1 or 2) + let availableSlot = 1; + if (!product1Id) { + availableSlot = 1; + } else if (!product2Id) { + availableSlot = 2; + } else { + // Both slots are filled, default to slot 2 for replacement + availableSlot = 2; + } + + let newHref = `product-catalog.html?returnTo=comparison&slot=${availableSlot}`; + if (product1Id) newHref += `&product1=${product1Id}`; + if (product2Id) newHref += `&product2=${product2Id}`; + + viewMoreLink.href = newHref; + console.log("Updated View More link:", newHref); + } +} + +// Initialize product comparison if on comparison page +console.log("=== CHECKING IF COMPARISON PAGE INITIALIZATION SHOULD RUN ==="); +console.log( + "Pathname includes product-comparison.html:", + window.location.pathname.includes("product-comparison.html") +); + +if (window.location.pathname.includes("product-comparison.html")) { + console.log("Product comparison page detected, initializing immediately"); + initProductComparison(); + + // Also try on DOMContentLoaded as backup + document.addEventListener("DOMContentLoaded", function () { + console.log("Product comparison page DOMContentLoaded backup"); + initProductComparison(); + }); +} + +// Product Detail Page - Compare Products functionality +function initProductDetailCompare() { + console.log("=== INITIALIZING PRODUCT DETAIL COMPARE ==="); + console.log("Current URL:", window.location.href); + console.log("Pathname:", window.location.pathname); + + // Try to find the button immediately + let compareButton = document.getElementById("compare-products-btn"); + console.log("Button found by ID:", compareButton); + + if (!compareButton) { + console.log("Button not found by ID, trying text search..."); + const buttons = document.querySelectorAll("button"); + console.log("Total buttons found:", buttons.length); + + buttons.forEach((button, index) => { + console.log(`Button ${index}: "${button.textContent.trim()}"`); + }); + + compareButton = Array.from(buttons).find( + (button) => button.textContent.trim() === "Compare Products" + ); + console.log("Button found by text search:", compareButton); + } + + if (compareButton) { + console.log("=== ADDING CLICK LISTENER ==="); + + // Remove any existing listeners first + compareButton.removeEventListener("click", handleCompareClick); + compareButton.addEventListener("click", handleCompareClick); + + // Also add a direct onclick handler as backup + compareButton.onclick = handleCompareClick; + + console.log("Click listener added successfully"); + console.log("Button element:", compareButton); + console.log("Button text content:", compareButton.textContent); + } else { + console.log("=== BUTTON NOT FOUND ==="); + console.log("Compare Products button not found"); + } +} + +// Separate function for the click handler +function handleCompareClick(event) { + console.log("Compare Products button clicked"); + event.preventDefault(); + event.stopPropagation(); + + // Get current product ID from URL + const urlParams = new URLSearchParams(window.location.search); + const productId = urlParams.get("id"); + + console.log("Product ID from URL:", productId); + + if (productId) { + // Navigate to comparison page with current product as product1 + const comparisonUrl = `product-comparison.html?product1=${productId}`; + console.log("Navigating to:", comparisonUrl); + window.location.href = comparisonUrl; + } else { + // If no product ID, just go to comparison page + console.log("No product ID found, navigating to comparison page"); + window.location.href = "product-comparison.html"; + } +} + +// Initialize immediately if we're on the product detail page +if (window.location.pathname.includes("product-detail.html")) { + console.log( + "Product detail page detected, initializing compare functionality immediately" + ); + initProductDetailCompare(); +} + +// Also try to initialize on DOMContentLoaded +document.addEventListener("DOMContentLoaded", function () { + console.log("DOMContentLoaded event fired"); + if (window.location.pathname.includes("product-detail.html")) { + console.log("Product detail page detected in DOMContentLoaded"); + initProductDetailCompare(); + } +}); + +// Initialize product comparison if on comparison page +if (window.location.pathname.includes("product-comparison.html")) { + document.addEventListener("DOMContentLoaded", function () { + initProductComparison(); + }); +} + +// Initialize comparison functionality if on product catalog page +if (window.location.pathname.includes("product-catalog.html")) { + console.log("=== PRODUCT CATALOG PAGE DETECTED ==="); + document.addEventListener("DOMContentLoaded", function () { + console.log( + "=== DOM CONTENT LOADED - INITIALIZING PRODUCT CATALOG COMPARISON ===" + ); + initProductCatalogComparison(); + }); +} + +// Product Catalog - Handle comparison page returns +function initProductCatalogComparison() { + 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"); + + console.log("Product catalog comparison params:", { + returnTo, + slot, + product1Id, + product2Id, + }); + + if (returnTo === "comparison" && slot) { + console.log("Setting up comparison return functionality for slot:", slot); + + // Note: Product card click handling is now done in products.js viewProduct method + console.log( + "Comparison mode activated - View buttons will navigate to comparison page" + ); + } +} + +// Replace Poppins with Playfair in all font references +function updateFontClasses() { + // Find all elements with font-poppins class and replace with font-playfair + const poppinsElements = document.querySelectorAll(".font-poppins"); + poppinsElements.forEach((element) => { + element.classList.remove("font-poppins"); + element.classList.add("font-playfair"); + }); +} + +// Run font update on page load +document.addEventListener("DOMContentLoaded", function () { + updateFontClasses(); +}); + +// Initialize quote badge on all pages +function initQuoteBadge() { + // Load quote items from localStorage + const storageKey = "khy_quote_items"; + let quoteItems = []; + + try { + const stored = localStorage.getItem(storageKey); + quoteItems = stored ? JSON.parse(stored) : []; + } catch (error) { + console.error("Error loading quote items:", error); + } + + // Calculate total count + const count = quoteItems.reduce((total, item) => total + item.quantity, 0); + + // Update quote badge on all quote links (desktop nav, mobile button, mobile menu) + const quoteLinks = document.querySelectorAll('a[href="quote.html"]'); + quoteLinks.forEach((quoteLink) => { + // Remove existing badge + const existingBadge = quoteLink.querySelector(".quote-badge"); + if (existingBadge) { + existingBadge.remove(); + } + + // Add new badge if there are items + if (count > 0) { + const badge = document.createElement("span"); + badge.className = + "quote-badge absolute -top-2 -right-2 bg-uc-gold text-white text-xs rounded-full w-5 h-5 flex items-center justify-center font-semibold"; + badge.textContent = count > 99 ? "99+" : count; + quoteLink.style.position = "relative"; + quoteLink.appendChild(badge); + } + }); +} + +// Initialize Add To Quote functionality on product detail pages +function initAddToQuote() { + const addToQuoteBtn = document.getElementById("add-to-quote-btn"); + if (addToQuoteBtn) { + try { + // Ensure button is not treated as a submit in case inside a form + if (!addToQuoteBtn.getAttribute("type")) { + addToQuoteBtn.setAttribute("type", "button"); + } + + addToQuoteBtn.addEventListener("click", function (e) { + e.preventDefault(); + e.stopPropagation(); + console.log("[AddToQuote] Direct click captured"); + + const productData = getProductDataFromPage(); + console.log("[AddToQuote] productData:", productData); + if (productData) { + if (typeof addToQuote === "function") { + addToQuote(productData); + } else { + addToQuoteFallback(productData); + } + } else { + console.warn( + "[AddToQuote] No product data found. Check URL id and DOM." + ); + } + }); + } catch (err) { + console.error("[AddToQuote] Failed to bind direct listener:", err); + } + } + + // Delegated fallback in case the button is replaced dynamically + document.addEventListener("click", function (e) { + const btn = e.target.closest && e.target.closest("#add-to-quote-btn"); + if (!btn) return; + e.preventDefault(); + e.stopPropagation(); + console.log("[AddToQuote] Delegated click captured"); + try { + const productData = getProductDataFromPage(); + console.log("[AddToQuote][delegated] productData:", productData); + if (productData) { + if (typeof addToQuote === "function") { + addToQuote(productData); + } else { + addToQuoteFallback(productData); + } + } else { + console.warn("[AddToQuote][delegated] No product data found."); + } + } catch (err) { + console.error("[AddToQuote][delegated] Error handling click:", err); + } + }); +} + +// Get product data from the current page +function getProductDataFromPage() { + // Get product ID from URL + const urlParams = new URLSearchParams(window.location.search); + const productId = urlParams.get("id"); + + if (!productId) { + console.error("No product ID found in URL"); + return null; + } + + // Get selected color and size + const selectedColor = getSelectedColor(); + const selectedSize = getSelectedSize(); + const selectedQuantity = getSelectedQuantity(); + + // Get product name and a best-effort product image + const productName = + document.querySelector("h1")?.textContent?.trim() || "Product"; + let productImage = ""; + + // Helper to guard against invalid selectors (e.g., unescaped brackets) + function qsSafe(selector) { + try { + return document.querySelector(selector); + } catch (e) { + return null; + } + } + + // Try a series of selectors safely + const imageCandidates = [ + ".w-\\[500px\\].h-\\[500px\\] img", + ".w-[500px].h-[500px] img", + ".bg-floral-white img", + "section img[alt]", + ]; + + for (const sel of imageCandidates) { + const el = qsSafe(sel); + if (el && el.src) { + productImage = el.src; + break; + } + } + + if (!productImage) { + const anyImg = qsSafe("img[alt]") || qsSafe("section img") || qsSafe("img"); + productImage = anyImg?.src || ""; + } + + return { + id: parseInt(productId), + name: productName, + image: productImage, + color: selectedColor, + size: selectedSize, + quantity: selectedQuantity, + }; +} + +// Get selected color from the page +function getSelectedColor() { + const colorButtons = document.querySelectorAll("button[data-color]"); + for (let button of colorButtons) { + if ( + button.classList.contains("selected") || + button.classList.contains("border-black") + ) { + return button.getAttribute("data-color") || "Default"; + } + } + return "Default"; +} + +// Get selected size from the page +function getSelectedSize() { + const sizeButtons = document.querySelectorAll("button[data-size]"); + for (let button of sizeButtons) { + if ( + button.classList.contains("selected") || + button.classList.contains("bg-uc-gold") + ) { + return button.getAttribute("data-size") || "Standard"; + } + } + return "Standard"; +} + +// Get selected quantity from the page +function getSelectedQuantity() { + const quantitySpan = document.getElementById("qty-value"); + if (quantitySpan) { + return parseInt(quantitySpan.textContent) || 1; + } + return 1; +} + +// Fallback function to add to quote directly +function addToQuoteFallback(productData) { + const storageKey = "khy_quote_items"; + let quoteItems = []; + + try { + const stored = localStorage.getItem(storageKey); + quoteItems = stored ? JSON.parse(stored) : []; + } catch (error) { + console.error("Error loading quote items:", error); + quoteItems = []; + } + + // Check if item already exists with same specifications + const existingItemIndex = quoteItems.findIndex( + (item) => + item.id === productData.id && + item.color === productData.color && + item.size === productData.size + ); + + if (existingItemIndex !== -1) { + // Update quantity of existing item + quoteItems[existingItemIndex].quantity += productData.quantity; + } else { + // Add new item + const newItem = { + ...productData, + timestamp: new Date().toISOString(), + }; + quoteItems.push(newItem); + } + + // Save to localStorage + try { + localStorage.setItem(storageKey, JSON.stringify(quoteItems)); + + // Update quote badge + initQuoteBadge(); + + // Show success message + showAddToQuoteSuccess(); + } catch (error) { + console.error("Error saving quote items:", error); + } +} + +// Show success message when item is added +function showAddToQuoteSuccess() { + // Create success notification + const notification = document.createElement("div"); + notification.className = + "fixed top-24 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 translate-x-full"; + notification.innerHTML = ` +
+ + + + Added to quote! +
+ `; + + document.body.appendChild(notification); + + // Animate in + setTimeout(() => { + notification.classList.remove("translate-x-full"); + }, 100); + + // Remove after 3 seconds + setTimeout(() => { + notification.classList.add("translate-x-full"); + setTimeout(() => { + if (document.body.contains(notification)) { + document.body.removeChild(notification); + } + }, 300); + }, 3000); +} + +// Initialize quote badge immediately if DOM is already loaded +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initQuoteBadge); +} else { + initQuoteBadge(); +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initSite); +} else { + initSite(); +} + +// Initialize quote badge on page load +document.addEventListener("DOMContentLoaded", function () { + initQuoteBadge(); + initAddToQuote(); + initHeroCarousel(); + initMobileMenu(); +}); + +// Hero Carousel Functionality +function initHeroCarousel() { + const arrowButton = document.getElementById("story-arrow-button"); + const heroImage = document.querySelector("#hero-image img"); + + if (!arrowButton || !heroImage) return; + + // Carousel images array + const carouselImages = [ + "assets/images/our_story.jpg", + "assets/images/first_homepage.jpg", + "assets/images/conference_room.jpg", + "assets/images/lounge_chair.jpg", + "assets/images/kitchen.JPG", + ]; + + let currentImageIndex = 0; + let originalHeight = null; + + // Function to capture original image height on first load + function captureOriginalHeight() { + if (!originalHeight) { + originalHeight = heroImage.offsetHeight; + console.log("Original image height captured:", originalHeight + "px"); + } + } + + // Function to update image with fade transition + function updateImage(newIndex) { + const newImage = new Image(); + newImage.onload = function () { + // Fade out current image + heroImage.style.opacity = "0"; + + setTimeout(() => { + // Change image source + heroImage.src = carouselImages[newIndex]; + heroImage.alt = `Carousel image ${newIndex + 1}`; + + // Apply original height to maintain uniformity + if (originalHeight && newIndex !== 0) { + heroImage.style.height = originalHeight + "px"; + heroImage.style.objectFit = "cover"; + } else if (newIndex === 0) { + // Reset to original for the first image + heroImage.style.height = "auto"; + heroImage.style.objectFit = "initial"; + } + + // Fade in new image + heroImage.style.opacity = "1"; + + // Update indicators + updateIndicators(newIndex); + }, 300); + }; + newImage.src = carouselImages[newIndex]; + } + + // Function to update carousel indicators + function updateIndicators(activeIndex) { + for (let i = 0; i < 5; i++) { + const indicator = document.getElementById(`carousel-indicator-${i}`); + if (indicator) { + indicator.style.opacity = i === activeIndex ? "1" : "0.5"; + } + } + } + + // Arrow button click handler + arrowButton.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + currentImageIndex = (currentImageIndex + 1) % carouselImages.length; + updateImage(currentImageIndex); + }); + + // Indicator click handlers + for (let i = 0; i < 5; i++) { + const indicator = document.getElementById(`carousel-indicator-${i}`); + if (indicator) { + indicator.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + currentImageIndex = i; + updateImage(currentImageIndex); + }); + } + } + + // Capture original height after image loads + if (heroImage.complete) { + captureOriginalHeight(); + } else { + heroImage.addEventListener("load", captureOriginalHeight); + } + + // Auto-advance carousel every 5 seconds + setInterval(() => { + currentImageIndex = (currentImageIndex + 1) % carouselImages.length; + updateImage(currentImageIndex); + }, 5000); +} + +// Mobile Menu Functionality +function initMobileMenu() { + const mobileMenuButton = document.getElementById("mobile-menu-button"); + const mobileMenuClose = document.getElementById("mobile-menu-close"); + const mobileMenu = document.getElementById("mobile-menu"); + const mobileMenuOverlay = document.getElementById("mobile-menu-overlay"); + + if ( + !mobileMenuButton || + !mobileMenuClose || + !mobileMenu || + !mobileMenuOverlay + ) { + console.log("Mobile menu elements not found"); + return; + } + + // Function to open mobile menu + function openMobileMenu() { + mobileMenu.classList.remove("translate-x-full"); + mobileMenuOverlay.classList.remove("hidden"); + document.body.style.overflow = "hidden"; // Prevent background scrolling + } + + // Function to close mobile menu + function closeMobileMenu() { + mobileMenu.classList.add("translate-x-full"); + mobileMenuOverlay.classList.add("hidden"); + document.body.style.overflow = ""; // Restore scrolling + } + + // Event listeners + mobileMenuButton.addEventListener("click", openMobileMenu); + mobileMenuClose.addEventListener("click", closeMobileMenu); + mobileMenuOverlay.addEventListener("click", closeMobileMenu); + + // Close menu when clicking on navigation links + const mobileNavLinks = mobileMenu.querySelectorAll("a"); + mobileNavLinks.forEach((link) => { + link.addEventListener("click", closeMobileMenu); + }); + + // Close menu on escape key + document.addEventListener("keydown", (e) => { + if ( + e.key === "Escape" && + !mobileMenu.classList.contains("translate-x-full") + ) { + closeMobileMenu(); + } + }); + + // Handle window resize - close menu if screen becomes large + window.addEventListener("resize", () => { + if (window.innerWidth >= 640) { + // sm breakpoint + closeMobileMenu(); + } + }); +} + +// Version: 4.8 - Added mobile hamburger menu functionality diff --git a/scripts/products.js b/scripts/products.js new file mode 100644 index 0000000..31dab76 --- /dev/null +++ b/scripts/products.js @@ -0,0 +1,419 @@ +// Product Management System +class ProductManager { + constructor() { + this.products = []; + this.categories = []; + this.pagination = {}; + this.currentPage = 1; + this.itemsPerPage = 16; + this.filteredProducts = []; + this.selectedCategories = new Set(); + } + + // Load products from JSON file + async loadProducts() { + try { + const response = await fetch("/data/products.json"); + const data = await response.json(); + + this.products = data.products; + this.categories = data.categories; + this.pagination = data.pagination; + this.filteredProducts = [...this.products]; + + this.renderProducts(); + this.updatePagination(); + this.updateResultsCount(); + this.setupEventListeners(); + this.renderCategoryFilters(); + + // Check for URL parameters and pre-select category + this.handleUrlParameters(); + } catch (error) { + console.error("Error loading products:", error); + } + } + + // Handle URL parameters for pre-selecting category filters + handleUrlParameters() { + const urlParams = new URLSearchParams(window.location.search); + const category = urlParams.get("category"); + + if (category) { + // Pre-select the category in the filter + this.selectedCategories = new Set([category]); + this.applyFilters(); + this.currentPage = 1; + this.renderProducts(); + this.updatePagination(); + this.updateResultsCount(); + + // Update the UI to show the filter is active + this.updateFilterUI(category); + } + } + + // Update filter UI to show selected category + updateFilterUI(category) { + // Update filter button text to show active filter + const filterToggle = document.getElementById("filter-toggle"); + if (filterToggle) { + const filterText = filterToggle.querySelector("span:last-child"); + if (filterText) { + filterText.textContent = `Filter: ${category}`; + } + } + + // Check the corresponding checkbox in the dropdown + setTimeout(() => { + const checkboxes = document.querySelectorAll(".category-checkbox"); + checkboxes.forEach((checkbox) => { + if (checkbox.value === category) { + checkbox.checked = true; + } + }); + }, 100); + } + + // Render products in the grid + renderProducts() { + const productGrid = document.getElementById("product-grid"); + if (!productGrid) return; + + const startIndex = (this.currentPage - 1) * this.itemsPerPage; + const endIndex = startIndex + this.itemsPerPage; + const productsToShow = this.filteredProducts.slice(startIndex, endIndex); + + productGrid.innerHTML = productsToShow + .map((product) => this.createProductCard(product)) + .join(""); + this.updateResultsCount(); + this.updatePagination(); + } + + // Create individual product card HTML + createProductCard(product) { + // Check if we're in comparison mode + const urlParams = new URLSearchParams(window.location.search); + const returnTo = urlParams.get("returnTo"); + const isComparisonMode = returnTo === "comparison"; + + return ` +
+
+ ${product.alt} + +
+
+ +
+
+
+
+

+ ${product.name} +

+

+ ${product.description} +

+
+
+ `; + } + + // 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 += ` + + `; + } + + if (totalPages > 1 && this.currentPage < totalPages) { + paginationHTML += ` + + `; + } + + paginationContainer.innerHTML = paginationHTML; + } + + // Build category multi-select dropdown + renderCategoryFilters() { + const container = document.getElementById("filter-categories"); + if (!container) return; + + const categoryOptions = this.categories + .map( + (c) => ` + + ` + ) + .join(""); + + const allOption = ` + + `; + + container.innerHTML = allOption + categoryOptions; + + // Add event listeners for "All" checkbox behavior + const allCheckbox = container.querySelector(".category-all"); + const specificCheckboxes = container.querySelectorAll(".category-specific"); + + if (allCheckbox) { + allCheckbox.addEventListener("change", (e) => { + const isChecked = e.target.checked; + specificCheckboxes.forEach((checkbox) => { + checkbox.checked = isChecked; + }); + }); + } + + // Update "All" checkbox when specific categories change + specificCheckboxes.forEach((checkbox) => { + checkbox.addEventListener("change", () => { + const allChecked = Array.from(specificCheckboxes).every( + (c) => c.checked + ); + const anyChecked = Array.from(specificCheckboxes).some( + (c) => c.checked + ); + + if (allChecked) { + allCheckbox.checked = true; + } else if (!anyChecked) { + allCheckbox.checked = false; + } + }); + }); + } + + // View product details + viewProduct(productId) { + const product = this.products.find((p) => p.id === productId); + if (product) { + // Check if we're in comparison mode + const urlParams = new URLSearchParams(window.location.search); + const returnTo = urlParams.get("returnTo"); + const slot = urlParams.get("slot"); + const product1Id = urlParams.get("product1"); + const product2Id = urlParams.get("product2"); + + if (returnTo === "comparison" && slot) { + // Navigate back to comparison page with the selected product + let comparisonUrl = "product-comparison.html?"; + + if (slot === "1") { + // Replace product 1 + comparisonUrl += `product1=${productId}`; + if (product2Id) { + comparisonUrl += `&product2=${product2Id}`; + } + } else { + // Replace product 2 + if (product1Id) { + comparisonUrl += `product1=${product1Id}&`; + } + comparisonUrl += `product2=${productId}`; + } + + console.log("Navigating to comparison page:", comparisonUrl); + window.location.href = comparisonUrl; + } else { + // Normal mode - navigate to product detail page + window.location.href = `product-detail.html?id=${productId}`; + } + } + } + + // Setup event listeners + setupEventListeners() { + // Filter dropdown toggle and outside click + const filterToggle = document.getElementById("filter-toggle"); + const filterDropdown = document.getElementById("filter-dropdown"); + if (filterToggle && filterDropdown) { + filterToggle.addEventListener("click", () => { + filterDropdown.classList.toggle("hidden"); + }); + document.addEventListener("click", (e) => { + if ( + !filterDropdown.contains(e.target) && + !filterToggle.contains(e.target) + ) { + filterDropdown.classList.add("hidden"); + } + }); + } + + // Sort dropdown + const sortSelect = document.querySelector("select"); + if (sortSelect) { + sortSelect.addEventListener("change", (e) => { + this.sortProducts(e.target.value); + }); + } + + // Apply/clear category filters + const applyBtn = document.getElementById("filter-apply"); + const clearBtn = document.getElementById("filter-clear"); + if (applyBtn) { + applyBtn.addEventListener("click", () => { + const checks = Array.from( + document.querySelectorAll(".category-checkbox") + ); + this.selectedCategories = new Set( + checks.filter((c) => c.checked).map((c) => c.value) + ); + this.currentPage = 1; + this.applyFilters(); + this.renderProducts(); + this.updatePagination(); + this.updateResultsCount(); + const dropdown = document.getElementById("filter-dropdown"); + if (dropdown) dropdown.classList.add("hidden"); + }); + } + if (clearBtn) { + clearBtn.addEventListener("click", () => { + const checks = Array.from( + document.querySelectorAll(".category-checkbox") + ); + checks.forEach((c) => (c.checked = false)); + this.selectedCategories.clear(); + this.currentPage = 1; + this.applyFilters(); + this.renderProducts(); + this.updatePagination(); + this.updateResultsCount(); + }); + } + } + + // Update results count + updateResultsCount() { + const resultsElement = document.querySelector(".text-quick-silver"); + if (resultsElement) { + const startIndex = (this.currentPage - 1) * this.itemsPerPage + 1; + const endIndex = Math.min( + startIndex + this.itemsPerPage - 1, + this.filteredProducts.length + ); + resultsElement.textContent = `Showing ${startIndex}–${endIndex} of ${this.filteredProducts.length} results`; + } + } +} + +// Initialize product manager +const productManager = new ProductManager(); + +// Load products when DOM is ready +document.addEventListener("DOMContentLoaded", () => { + productManager.loadProducts(); +}); diff --git a/scripts/quote.js b/scripts/quote.js new file mode 100644 index 0000000..acd743d --- /dev/null +++ b/scripts/quote.js @@ -0,0 +1,605 @@ +// Quote Management System +class QuoteManager { + constructor() { + this.storageKey = "khy_quote_items"; + this.quoteItems = this.loadQuoteItems(); + this.init(); + } + + // Load quote items from localStorage + loadQuoteItems() { + try { + const stored = localStorage.getItem(this.storageKey); + return stored ? JSON.parse(stored) : []; + } catch (error) { + console.error("Error loading quote items:", error); + return []; + } + } + + // Save quote items to localStorage + saveQuoteItems() { + try { + localStorage.setItem(this.storageKey, JSON.stringify(this.quoteItems)); + this.updateQuoteBadge(); + } catch (error) { + console.error("Error saving quote items:", error); + } + } + + // Add item to quote + addToQuote(productData) { + const { + id, + name, + image, + color = "Default", + size = "Standard", + quantity = 1, + } = productData; + + // Check if item already exists with same specifications + const existingItemIndex = this.quoteItems.findIndex( + (item) => item.id === id && item.color === color && item.size === size + ); + + if (existingItemIndex !== -1) { + // Update quantity of existing item + this.quoteItems[existingItemIndex].quantity += quantity; + } else { + // Add new item + const newItem = { + id, + name, + image, + color, + size, + quantity, + timestamp: new Date().toISOString(), + }; + this.quoteItems.push(newItem); + } + + this.saveQuoteItems(); + this.renderQuoteItems(); + this.showAddToQuoteSuccess(); + } + + // Remove item from quote + removeFromQuote(itemIndex) { + this.quoteItems.splice(itemIndex, 1); + this.saveQuoteItems(); + this.renderQuoteItems(); + } + + // Update item quantity + updateQuantity(itemIndex, newQuantity) { + if (newQuantity > 0) { + this.quoteItems[itemIndex].quantity = newQuantity; + this.saveQuoteItems(); + this.renderQuoteItems(); + } else { + this.removeFromQuote(itemIndex); + } + } + + // Edit a quote item + editQuoteItem(itemIndex) { + const item = this.quoteItems[itemIndex]; + if (!item) return; + + // Create edit modal + const modal = document.createElement("div"); + modal.id = "edit-quote-modal"; + modal.className = + "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"; + modal.innerHTML = ` +
+
+

Edit Quote Item

+ +
+ +
+ ${
+      item.name
+    } +
+

${ + item.name + }

+

Product ID: ${item.id}

+
+
+ +
+
+ +
+ + + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ `; + + document.body.appendChild(modal); + + // Add form submission handler + const form = modal.querySelector("#edit-quote-form"); + form.addEventListener("submit", (e) => { + e.preventDefault(); + this.saveEditChanges(itemIndex); + }); + + // Close modal when clicking outside + modal.addEventListener("click", (e) => { + if (e.target === modal) { + this.closeEditModal(); + } + }); + } + + // Close edit modal + closeEditModal() { + const modal = document.getElementById("edit-quote-modal"); + if (modal) { + modal.remove(); + } + } + + // Increment quantity in edit modal + incrementEditQuantity() { + const input = document.getElementById("edit-quantity"); + if (input) { + input.value = parseInt(input.value) + 1; + } + } + + // Decrement quantity in edit modal + decrementEditQuantity() { + const input = document.getElementById("edit-quantity"); + if (input && parseInt(input.value) > 1) { + input.value = parseInt(input.value) - 1; + } + } + + // Save changes from edit modal + saveEditChanges(itemIndex) { + const quantity = parseInt(document.getElementById("edit-quantity").value); + const color = document.getElementById("edit-color").value; + const size = document.getElementById("edit-size").value; + + if (quantity < 1) { + alert("Quantity must be at least 1"); + return; + } + + // Update the item + this.quoteItems[itemIndex] = { + ...this.quoteItems[itemIndex], + quantity: quantity, + color: color, + size: size, + }; + + this.saveQuoteItems(); + this.renderQuoteItems(); + this.closeEditModal(); + + // Show success message + this.showEditSuccess(); + } + + // Show success message for edit + showEditSuccess() { + const notification = document.createElement("div"); + notification.className = + "fixed top-24 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 translate-x-full"; + notification.innerHTML = ` +
+ + + + Quote item updated! +
+ `; + + document.body.appendChild(notification); + + // Animate in + setTimeout(() => { + notification.classList.remove("translate-x-full"); + }, 100); + + // Remove after 3 seconds + setTimeout(() => { + notification.classList.add("translate-x-full"); + setTimeout(() => { + document.body.removeChild(notification); + }, 300); + }, 3000); + } + + // Clear all quote items + clearQuote() { + this.quoteItems = []; + this.saveQuoteItems(); + this.renderQuoteItems(); + } + + // Get quote items count + getQuoteCount() { + return this.quoteItems.reduce((total, item) => total + item.quantity, 0); + } + + // Update quote badge in navigation + updateQuoteBadge() { + const count = this.getQuoteCount(); + const quoteLinks = document.querySelectorAll('a[href="quote.html"]'); + + quoteLinks.forEach((quoteLink) => { + // Remove existing badge + const existingBadge = quoteLink.querySelector(".quote-badge"); + if (existingBadge) { + existingBadge.remove(); + } + + // Add new badge if there are items + if (count > 0) { + const badge = document.createElement("span"); + badge.className = + "quote-badge absolute -top-2 -right-2 bg-uc-gold text-white text-xs rounded-full w-5 h-5 flex items-center justify-center font-semibold"; + badge.textContent = count > 99 ? "99+" : count; + quoteLink.style.position = "relative"; + quoteLink.appendChild(badge); + } + }); + } + + // Show success message when item is added + showAddToQuoteSuccess() { + // Create success notification + const notification = document.createElement("div"); + notification.className = + "fixed top-24 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 translate-x-full"; + notification.innerHTML = ` +
+ + + + Added to quote! +
+ `; + + document.body.appendChild(notification); + + // Animate in + setTimeout(() => { + notification.classList.remove("translate-x-full"); + }, 100); + + // Remove after 3 seconds + setTimeout(() => { + notification.classList.add("translate-x-full"); + setTimeout(() => { + document.body.removeChild(notification); + }, 300); + }, 3000); + } + + // Render quote items on the quote page + renderQuoteItems() { + const container = document.getElementById("quote-items-container"); + const emptyMessage = document.getElementById("empty-quote-message"); + const quoteActions = document.getElementById("quote-actions"); + + if (!container) return; + + if (this.quoteItems.length === 0) { + // Show empty state + if (emptyMessage) emptyMessage.style.display = "block"; + if (quoteActions) quoteActions.classList.add("hidden"); + // Clear any existing items but keep the empty message + const itemsToRemove = container.querySelectorAll(".bg-gray-50"); + itemsToRemove.forEach((item) => item.remove()); + return; + } + + // Hide empty message and show actions + if (emptyMessage) emptyMessage.style.display = "none"; + if (quoteActions) quoteActions.classList.remove("hidden"); + + // Remove any existing items first + const itemsToRemove = container.querySelectorAll(".bg-gray-50"); + itemsToRemove.forEach((item) => item.remove()); + + // Render new items + this.quoteItems.forEach((item, index) => { + const itemElement = document.createElement("div"); + itemElement.className = + "bg-gray-50 rounded-lg p-6 border border-gray-200"; + itemElement.innerHTML = ` +
+ +
+ ${item.name} +
+ + +
+

+ ${item.name} +

+
+ Color: ${item.color} + Size: ${item.size} +
+
+ + +
+ + + ${item.quantity} + + +
+ + + + + + +
+ `; + container.appendChild(itemElement); + }); + } + + // Render quote summary in modal + renderQuoteSummary() { + const summaryContainer = document.getElementById("quote-summary"); + if (!summaryContainer) return; + + summaryContainer.innerHTML = this.quoteItems + .map( + (item) => ` +
+
+
${item.name}
+

${item.color} • ${item.size} • Qty: ${item.quantity}

+
+
+ ` + ) + .join(""); + } + + // Initialize quote manager + init() { + this.updateQuoteBadge(); + this.renderQuoteItems(); + this.setupEventListeners(); + } + + // Setup event listeners + setupEventListeners() { + // Clear quote button + const clearBtn = document.getElementById("clear-quote-btn"); + if (clearBtn) { + clearBtn.addEventListener("click", () => { + if (confirm("Are you sure you want to clear your quote?")) { + this.clearQuote(); + } + }); + } + + // Request quote button + const requestBtn = document.getElementById("request-quote-btn"); + if (requestBtn) { + requestBtn.addEventListener("click", () => { + this.openQuoteModal(); + }); + } + + // Modal close button + const closeBtn = document.getElementById("close-modal-btn"); + if (closeBtn) { + closeBtn.addEventListener("click", () => { + this.closeQuoteModal(); + }); + } + + // Modal backdrop click + const modal = document.getElementById("quote-modal"); + if (modal) { + modal.addEventListener("click", (e) => { + if (e.target === modal) { + this.closeQuoteModal(); + } + }); + } + + // Quote form submission + const quoteForm = document.getElementById("quote-form"); + if (quoteForm) { + quoteForm.addEventListener("submit", (e) => { + e.preventDefault(); + this.submitQuoteRequest(); + }); + } + } + + // Open quote modal + openQuoteModal() { + const modal = document.getElementById("quote-modal"); + if (modal) { + this.renderQuoteSummary(); + modal.classList.remove("hidden"); + document.body.style.overflow = "hidden"; + } + } + + // Close quote modal + closeQuoteModal() { + const modal = document.getElementById("quote-modal"); + if (modal) { + modal.classList.add("hidden"); + document.body.style.overflow = "auto"; + } + } + + // Submit quote request + submitQuoteRequest() { + const form = document.getElementById("quote-form"); + const formData = new FormData(form); + + // Get form data + const quoteData = { + name: formData.get("name"), + email: formData.get("email"), + phone: formData.get("phone"), + company: formData.get("company"), + project: formData.get("project"), + items: this.quoteItems, + timestamp: new Date().toISOString(), + }; + + // Here you would typically send this data to your server + console.log("Quote request data:", quoteData); + + // For now, just show success message + alert("Thank you for your quote request! We will get back to you soon."); + + // Clear the quote and close modal + this.clearQuote(); + this.closeQuoteModal(); + + // Reset form + form.reset(); + } +} + +// Global quote manager instance +const quoteManager = new QuoteManager(); + +// Global function to add items to quote (called from other pages) +function addToQuote(productData) { + quoteManager.addToQuote(productData); +} + +// Initialize when DOM is ready +document.addEventListener("DOMContentLoaded", () => { + // Quote manager is already initialized in constructor +}); + +// Version: 3.5 - Added edit functionality for quote items diff --git a/scripts/update_products_tabs.js b/scripts/update_products_tabs.js new file mode 100644 index 0000000..d2ae51f --- /dev/null +++ b/scripts/update_products_tabs.js @@ -0,0 +1,91 @@ +const fs = require("fs"); +const path = require("path"); + +const productsPath = path.join(__dirname, "../data/products.json"); +const db = JSON.parse(fs.readFileSync(productsPath, "utf8")); + +// Default content for tabs (copy matches current UI text style) +const defaultLong = [ + "Embodying the raw, wayward spirit of rock ‘n’ roll, the Kilburn portable active stereo speaker takes the unmistakable look and sound of Marshall, unplugs the chords, and takes the show on the road.", + "Weighing in under 7 pounds, the Kilburn is a lightweight piece of vintage styled engineering. Setting the bar as one of the loudest speakers in its class, the Kilburn is a compact, stout-hearted hero with a well-balanced audio which boasts a clear midrange and extended highs for a sound that is both articulate and pronounced. The analogue knobs allow you to fine tune the controls to your personal preferences while the guitar-influenced leather strap enables easy and stylish travel.", +]; + +const defaultsByCategory = { + seating: { + additionalInformation: { + Material: "Fabric, engineered wood base", + Upholstery: "Performance fabric", + Dimensions: "See size options", + Warranty: "2 years", + }, + }, + tables: { + additionalInformation: { + Material: "Engineered wood, steel frame", + Finish: "Matte laminate", + Dimensions: "Small/Medium/Large", + Warranty: "2 years", + }, + }, + storage: { + additionalInformation: { + Material: "Powder-coated steel", + Capacity: "Modular shelves", + Dimensions: "Standard/Large/XL", + Warranty: "2 years", + }, + }, + workspace: { + additionalInformation: { + Material: "Acoustic panels, aluminum frame", + Power: "Integrated power module", + Dimensions: "Standard/Large", + Warranty: "2 years", + }, + }, +}; + +function ensureFields(p) { + const cat = + p.category && defaultsByCategory[p.category] ? p.category : "seating"; + const d = defaultsByCategory[cat]; + + if (!Array.isArray(p.descriptionLong) || p.descriptionLong.length === 0) { + p.descriptionLong = defaultLong; + } + + if ( + typeof p.additionalInformation !== "object" || + p.additionalInformation === null + ) { + p.additionalInformation = { ...d.additionalInformation }; + } else { + // Fill any missing keys with defaults without overwriting existing + for (const [k, v] of Object.entries(d.additionalInformation)) { + if ( + p.additionalInformation[k] == null || + p.additionalInformation[k] === "" + ) { + p.additionalInformation[k] = v; + } + } + } + + // Ensure reviewsCount mirrors numeric reviews + const reviewsNum = Number(p.reviews || 0); + p.reviews = Number.isFinite(reviewsNum) ? reviewsNum : 0; + if (p.reviewsCount == null) p.reviewsCount = p.reviews; + + // Ensure galleryPairs (two wide images for tabs). Use product image as fallback. + if (!Array.isArray(p.galleryPairs) || p.galleryPairs.length === 0) { + const img = p.image || "assets/images/asgaard_sofa.png"; + p.galleryPairs = [{ left: img, right: img }]; + } +} + +(db.products || []).forEach(ensureFields); + +fs.writeFileSync(productsPath, JSON.stringify(db, null, 2)); +console.log( + "Ensured tabs data for all products: descriptionLong, additionalInformation, reviewsCount, galleryPairs" +); diff --git a/setup-deploy.js b/setup-deploy.js new file mode 100644 index 0000000..2f21aa8 --- /dev/null +++ b/setup-deploy.js @@ -0,0 +1,161 @@ +#!/usr/bin/env node + +/** + * Setup Script for KHY Admin Deploy Configuration + * + * This script helps you configure the deploy paths for your specific setup. + * Run this once to set up the correct paths for your environment. + */ + +const fs = require("fs"); +const path = require("path"); +const readline = require("readline"); + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +function question(prompt) { + return new Promise((resolve) => { + rl.question(prompt, resolve); + }); +} + +async function setupDeploy() { + console.log("KHY Admin Deploy Setup"); + console.log("======================"); + console.log(""); + console.log("This script will help you configure the deploy paths."); + console.log("You need to specify where your main KHY website is located."); + console.log(""); + + // Get current admin directory + const adminDir = __dirname; + console.log(`Current admin directory: ${adminDir}`); + console.log(""); + + // Ask for the main website path + console.log("Where is your main KHY website located?"); + console.log("Examples:"); + console.log(" - If in same parent directory: ../khy_website"); + console.log(" - If in different location: /path/to/your/khy_website"); + console.log(" - If admin is inside main website: ../"); + console.log(""); + + const mainWebsitePath = await question("Enter path to main KHY website: "); + + if (!mainWebsitePath.trim()) { + console.log("No path provided. Exiting."); + rl.close(); + return; + } + + // Resolve the path + const resolvedPath = path.resolve(adminDir, mainWebsitePath.trim()); + + console.log(""); + console.log(`Resolved path: ${resolvedPath}`); + + // Check if the path exists + if (!fs.existsSync(resolvedPath)) { + console.log("WARNING: The specified path does not exist!"); + const continueAnyway = await question("Continue anyway? (y/N): "); + if (continueAnyway.toLowerCase() !== "y") { + console.log("Setup cancelled."); + rl.close(); + return; + } + } + + // Check for required files + const requiredFiles = ["index.html", "data/products.json"]; + const missingFiles = requiredFiles.filter( + (file) => !fs.existsSync(path.join(resolvedPath, file)) + ); + + if (missingFiles.length > 0) { + console.log(`WARNING: Missing required files: ${missingFiles.join(", ")}`); + const continueAnyway = await question("Continue anyway? (y/N): "); + if (continueAnyway.toLowerCase() !== "y") { + console.log("Setup cancelled."); + rl.close(); + return; + } + } + + // Update the config file + const configPath = path.join(adminDir, "deploy-config.js"); + const configContent = `/** + * Deploy Configuration for KHY Admin Dashboard + * + * This file contains configuration for deploying from the admin repository + * to the main KHY website repository. + * + * Generated by setup-deploy.js on ${new Date().toISOString()} + */ + +module.exports = { + // Default target paths (adjust these based on your setup) + targets: { + // If admin and main website are in the same parent directory + sibling: "../khy_website", + + // If admin is in a subdirectory of the main website + parent: "../", + + // Your configured path + main: "${mainWebsitePath.trim()}", + + // Absolute path examples (uncomment and modify as needed) + // absolute: "/path/to/your/khy_website", + // absolute: "/Users/george/Documents/khy_website/khy_website", + }, + + // Which target to use by default + defaultTarget: "main", + + // Files to copy from admin to main website + filesToCopy: [ + { + source: "data/products.json", + target: "data/products.json", + description: "Product catalog data", + }, + { + source: "assets/images", + target: "assets/images", + description: "Product images", + isDirectory: true, + }, + ], + + // Required files that must exist in target directory + requiredTargetFiles: ["index.html", "data/products.json"], + + // Backup settings + backup: { + enabled: true, + directory: "backups", + timestampFormat: "YYYY-MM-DD_HH-mm-ss", + } +};`; + + try { + fs.writeFileSync(configPath, configContent); + console.log(""); + console.log("Configuration updated successfully!"); + console.log(`Config file: ${configPath}`); + console.log(""); + console.log("You can now run: npm run deploy"); + console.log(""); + console.log("To test the configuration, run:"); + console.log(` node deploy.js "${mainWebsitePath.trim()}"`); + } catch (error) { + console.log(`Error writing config file: ${error.message}`); + } + + rl.close(); +} + +setupDeploy().catch(console.error); diff --git a/src/input.css b/src/input.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/src/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/start-server.js b/start-server.js new file mode 100644 index 0000000..fa2fdff --- /dev/null +++ b/start-server.js @@ -0,0 +1,167 @@ +#!/usr/bin/env node + +/** + * Server Starter for KHY Admin Dashboard + * + * This script starts a local server and opens the preview pages + * Usage: node start-server.js + */ + +const { spawn } = require("child_process"); +const { exec } = require("child_process"); +const path = require("path"); +const fs = require("fs"); + +const PORT = 8080; +const BASE_URL = `http://localhost:${PORT}`; +const PID_FILE = path.join(__dirname, ".server.pid"); + +console.log("Starting KHY Admin Preview Server"); +console.log("==================================="); +console.log(""); + +// Function to open URL in default browser +function openBrowser(url) { + const start = + process.platform === "darwin" + ? "open" + : process.platform === "win32" + ? "start" + : "xdg-open"; + + exec(`${start} ${url}`, (error) => { + if (error) { + console.log(`Please manually open: ${url}`); + } + }); +} + +// Function to save server PID +function saveServerPID(pid) { + try { + fs.writeFileSync(PID_FILE, pid.toString()); + console.log(`Server PID saved: ${pid}`); + } catch (error) { + console.log(`Could not save PID: ${error.message}`); + } +} + +// Function to kill server by PID +function killServerByPID() { + try { + if (fs.existsSync(PID_FILE)) { + const pid = fs.readFileSync(PID_FILE, "utf8").trim(); + console.log(`Killing server with PID: ${pid}`); + + // Kill the process + process.kill(parseInt(pid), "SIGTERM"); + + // Remove PID file + fs.unlinkSync(PID_FILE); + console.log(`Server killed successfully`); + return true; + } + } catch (error) { + console.log(`Could not kill server: ${error.message}`); + } + return false; +} + +// Function to start the server +function startServer() { + console.log(`Starting server on port ${PORT}...`); + + // Try Python first, then Node.js http-server + const pythonServer = spawn( + "python3", + ["-m", "http.server", PORT.toString()], + { + stdio: "pipe", + cwd: __dirname, + } + ); + + // Save the server PID + saveServerPID(pythonServer.pid); + + pythonServer.stdout.on("data", (data) => { + console.log(`Server output: ${data}`); + }); + + pythonServer.stderr.on("data", (data) => { + console.log(`Server error: ${data}`); + }); + + pythonServer.on("close", (code) => { + console.log(`Server stopped with code ${code}`); + // Clean up PID file when server stops + if (fs.existsSync(PID_FILE)) { + fs.unlinkSync(PID_FILE); + } + }); + + // Wait a moment for server to start + setTimeout(() => { + console.log(""); + console.log("Server started successfully!"); + console.log(""); + console.log("Preview URLs:"); + console.log(` Admin Dashboard: ${BASE_URL}/admin.html`); + console.log(` Homepage: ${BASE_URL}/index.html`); + console.log(` Product Catalog: ${BASE_URL}/product-catalog.html`); + console.log(` Product Details: ${BASE_URL}/product-detail.html`); + console.log(""); + console.log("Opening preview pages..."); + + // Open preview pages + setTimeout(() => openBrowser(`${BASE_URL}/admin.html`), 1000); + setTimeout(() => openBrowser(`${BASE_URL}/index.html`), 2000); + setTimeout(() => openBrowser(`${BASE_URL}/product-catalog.html`), 3000); + + console.log(""); + console.log("Preview environment ready!"); + console.log("Press Ctrl+C to stop the server"); + console.log(""); + }, 2000); + + // Handle Ctrl+C + process.on("SIGINT", () => { + console.log(""); + console.log("Stopping server..."); + pythonServer.kill(); + process.exit(0); + }); +} + +// Check if port is already in use +const net = require("net"); +const server = net.createServer(); + +server.listen(PORT, () => { + server.close(() => { + startServer(); + }); +}); + +server.on("error", (err) => { + if (err.code === "EADDRINUSE") { + console.log(`Port ${PORT} is already in use`); + console.log("The server might already be running"); + console.log(""); + console.log("If server is running, you can access:"); + console.log(` Admin Dashboard: ${BASE_URL}/admin.html`); + console.log(` Homepage: ${BASE_URL}/index.html`); + console.log(` Product Catalog: ${BASE_URL}/product-catalog.html`); + console.log(` Product Details: ${BASE_URL}/product-detail.html`); + console.log(""); + console.log("Opening preview pages..."); + + // Open preview pages anyway + setTimeout(() => openBrowser(`${BASE_URL}/admin.html`), 1000); + setTimeout(() => openBrowser(`${BASE_URL}/index.html`), 2000); + setTimeout(() => openBrowser(`${BASE_URL}/product-catalog.html`), 3000); + } else { + console.error("Error starting server:", err.message); + process.exit(1); + } +}); diff --git a/styles/main.css b/styles/main.css new file mode 100644 index 0000000..8e8e588 --- /dev/null +++ b/styles/main.css @@ -0,0 +1,3132 @@ +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +/* +! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS +*/ + +html, +:host { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font-family by default. +2. Use the user's configured `mono` font-feature-settings by default. +3. Use the user's configured `mono` font-variation-settings by default. +4. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-feature-settings: normal; + /* 2 */ + font-variation-settings: normal; + /* 3 */ + font-size: 1em; + /* 4 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + letter-spacing: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +input:where([type='button']), +input:where([type='reset']), +input:where([type='submit']) { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden]:where(:not([hidden="until-found"])) { + display: none; +} + +.\!container { + width: 100% !important; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .\!container { + max-width: 640px !important; + } + + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .\!container { + max-width: 768px !important; + } + + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .\!container { + max-width: 1024px !important; + } + + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .\!container { + max-width: 1280px !important; + } + + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .\!container { + max-width: 1536px !important; + } + + .container { + max-width: 1536px; + } +} + +.pointer-events-none { + pointer-events: none; +} + +.fixed { + position: fixed; +} + +.absolute { + position: absolute; +} + +.relative { + position: relative; +} + +.inset-0 { + inset: 0px; +} + +.inset-y-0 { + top: 0px; + bottom: 0px; +} + +.-right-2 { + right: -0.5rem; +} + +.-top-2 { + top: -0.5rem; +} + +.bottom-4 { + bottom: 1rem; +} + +.left-0 { + left: 0px; +} + +.left-1\/2 { + left: 50%; +} + +.right-0 { + right: 0px; +} + +.right-4 { + right: 1rem; +} + +.top-0 { + top: 0px; +} + +.top-1\/2 { + top: 50%; +} + +.top-12 { + top: 3rem; +} + +.top-24 { + top: 6rem; +} + +.top-80 { + top: 20rem; +} + +.right-2 { + right: 0.5rem; +} + +.top-2 { + top: 0.5rem; +} + +.z-10 { + z-index: 10; +} + +.z-20 { + z-index: 20; +} + +.z-40 { + z-index: 40; +} + +.z-50 { + z-index: 50; +} + +.order-2 { + order: 2; +} + +.order-1 { + order: 1; +} + +.order-3 { + order: 3; +} + +.order-4 { + order: 4; +} + +.mx-2 { + margin-left: 0.5rem; + margin-right: 0.5rem; +} + +.mx-4 { + margin-left: 1rem; + margin-right: 1rem; +} + +.mx-auto { + margin-left: auto; + margin-right: auto; +} + +.-mx-5 { + margin-left: -1.25rem; + margin-right: -1.25rem; +} + +.-mt-1 { + margin-top: -0.25rem; +} + +.-mt-2 { + margin-top: -0.5rem; +} + +.-mt-4 { + margin-top: -1rem; +} + +.-mt-6 { + margin-top: -1.5rem; +} + +.-mt-8 { + margin-top: -2rem; +} + +.mb-0 { + margin-bottom: 0px; +} + +.mb-1 { + margin-bottom: 0.25rem; +} + +.mb-10 { + margin-bottom: 2.5rem; +} + +.mb-12 { + margin-bottom: 3rem; +} + +.mb-16 { + margin-bottom: 4rem; +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.mb-3 { + margin-bottom: 0.75rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.mb-6 { + margin-bottom: 1.5rem; +} + +.mb-8 { + margin-bottom: 2rem; +} + +.ml-8 { + margin-left: 2rem; +} + +.mr-2 { + margin-right: 0.5rem; +} + +.mt-1 { + margin-top: 0.25rem; +} + +.mt-10 { + margin-top: 2.5rem; +} + +.mt-12 { + margin-top: 3rem; +} + +.mt-20 { + margin-top: 5rem; +} + +.mt-28 { + margin-top: 7rem; +} + +.mt-3 { + margin-top: 0.75rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.mt-6 { + margin-top: 1.5rem; +} + +.mt-8 { + margin-top: 2rem; +} + +.-mb-2 { + margin-bottom: -0.5rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.ml-1 { + margin-left: 0.25rem; +} + +.box-border { + box-sizing: border-box; +} + +.block { + display: block; +} + +.inline-block { + display: inline-block; +} + +.flex { + display: flex; +} + +.inline-flex { + display: inline-flex; +} + +.table { + display: table; +} + +.\!grid { + display: grid !important; +} + +.grid { + display: grid; +} + +.hidden { + display: none; +} + +.aspect-square { + aspect-ratio: 1 / 1; +} + +.h-10 { + height: 2.5rem; +} + +.h-12 { + height: 3rem; +} + +.h-16 { + height: 4rem; +} + +.h-20 { + height: 5rem; +} + +.h-24 { + height: 6rem; +} + +.h-28 { + height: 7rem; +} + +.h-3 { + height: 0.75rem; +} + +.h-32 { + height: 8rem; +} + +.h-4 { + height: 1rem; +} + +.h-5 { + height: 1.25rem; +} + +.h-6 { + height: 1.5rem; +} + +.h-7 { + height: 1.75rem; +} + +.h-72 { + height: 18rem; +} + +.h-8 { + height: 2rem; +} + +.h-80 { + height: 20rem; +} + +.h-96 { + height: 24rem; +} + +.h-\[145px\] { + height: 145px; +} + +.h-\[301px\] { + height: 301px; +} + +.h-\[446px\] { + height: 446px; +} + +.h-\[500px\] { + height: 500px; +} + +.h-\[64px\] { + height: 64px; +} + +.h-auto { + height: auto; +} + +.h-full { + height: 100%; +} + +.h-px { + height: 1px; +} + +.h-screen { + height: 100vh; +} + +.h-14 { + height: 3.5rem; +} + +.h-64 { + height: 16rem; +} + +.h-\[56px\] { + height: 56px; +} + +.h-40 { + height: 10rem; +} + +.h-48 { + height: 12rem; +} + +.max-h-\[90vh\] { + max-height: 90vh; +} + +.min-h-\[64px\] { + min-height: 64px; +} + +.min-h-screen { + min-height: 100vh; +} + +.min-h-\[48px\] { + min-height: 48px; +} + +.min-h-\[56px\] { + min-height: 56px; +} + +.w-12 { + width: 3rem; +} + +.w-16 { + width: 4rem; +} + +.w-20 { + width: 5rem; +} + +.w-24 { + width: 6rem; +} + +.w-28 { + width: 7rem; +} + +.w-3 { + width: 0.75rem; +} + +.w-32 { + width: 8rem; +} + +.w-36 { + width: 9rem; +} + +.w-4 { + width: 1rem; +} + +.w-40 { + width: 10rem; +} + +.w-48 { + width: 12rem; +} + +.w-5 { + width: 1.25rem; +} + +.w-56 { + width: 14rem; +} + +.w-6 { + width: 1.5rem; +} + +.w-7 { + width: 1.75rem; +} + +.w-8 { + width: 2rem; +} + +.w-80 { + width: 20rem; +} + +.w-\[180px\] { + width: 180px; +} + +.w-\[245px\] { + width: 245px; +} + +.w-\[380px\] { + width: 380px; +} + +.w-\[440px\] { + width: 440px; +} + +.w-\[500px\] { + width: 500px; +} + +.w-auto { + width: auto; +} + +.w-full { + width: 100%; +} + +.w-1\/2 { + width: 50%; +} + +.min-w-\[2rem\] { + min-width: 2rem; +} + +.min-w-\[96px\] { + min-width: 96px; +} + +.min-w-\[720px\] { + min-width: 720px; +} + +.max-w-2xl { + max-width: 42rem; +} + +.max-w-4xl { + max-width: 56rem; +} + +.max-w-5xl { + max-width: 64rem; +} + +.max-w-6xl { + max-width: 72rem; +} + +.max-w-7xl { + max-width: 80rem; +} + +.max-w-lg { + max-width: 32rem; +} + +.max-w-md { + max-width: 28rem; +} + +.max-w-sm { + max-width: 24rem; +} + +.max-w-xs { + max-width: 20rem; +} + +.max-w-none { + max-width: none; +} + +.flex-1 { + flex: 1 1 0%; +} + +.flex-shrink-0 { + flex-shrink: 0; +} + +.-translate-x-1\/2 { + --tw-translate-x: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-y-1\/2 { + --tw-translate-y: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-x-full { + --tw-translate-x: 100%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.transform { + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.cursor-pointer { + cursor: pointer; +} + +.resize-none { + resize: none; +} + +.resize { + resize: both; +} + +.list-inside { + list-style-position: inside; +} + +.list-disc { + list-style-type: disc; +} + +.appearance-none { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); +} + +.grid-cols-6 { + grid-template-columns: repeat(6, minmax(0, 1fr)); +} + +.grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.flex-row { + flex-direction: row; +} + +.flex-col { + flex-direction: column; +} + +.flex-col-reverse { + flex-direction: column-reverse; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.flex-nowrap { + flex-wrap: nowrap; +} + +.items-start { + align-items: flex-start; +} + +.items-center { + align-items: center; +} + +.items-stretch { + align-items: stretch; +} + +.justify-end { + justify-content: flex-end; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.gap-12 { + gap: 3rem; +} + +.gap-16 { + gap: 4rem; +} + +.gap-2 { + gap: 0.5rem; +} + +.gap-28 { + gap: 7rem; +} + +.gap-4 { + gap: 1rem; +} + +.gap-6 { + gap: 1.5rem; +} + +.gap-8 { + gap: 2rem; +} + +.gap-3 { + gap: 0.75rem; +} + +.gap-y-2 { + row-gap: 0.5rem; +} + +.space-x-10 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(2.5rem * var(--tw-space-x-reverse)); + margin-left: calc(2.5rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-x-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-x-3 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.75rem * var(--tw-space-x-reverse)); + margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-x-4 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(1rem * var(--tw-space-x-reverse)); + margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-x-6 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(1.5rem * var(--tw-space-x-reverse)); + margin-left: calc(1.5rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-x-8 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(2rem * var(--tw-space-x-reverse)); + margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-y-1 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.25rem * var(--tw-space-y-reverse)); +} + +.space-y-16 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(4rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(4rem * var(--tw-space-y-reverse)); +} + +.space-y-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); +} + +.space-y-3 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.75rem * var(--tw-space-y-reverse)); +} + +.space-y-4 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1rem * var(--tw-space-y-reverse)); +} + +.space-y-6 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); +} + +.space-y-8 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(2rem * var(--tw-space-y-reverse)); +} + +.overflow-hidden { + overflow: hidden; +} + +.overflow-visible { + overflow: visible; +} + +.overflow-x-auto { + overflow-x: auto; +} + +.overflow-y-auto { + overflow-y: auto; +} + +.whitespace-nowrap { + white-space: nowrap; +} + +.rounded { + border-radius: 0.25rem; +} + +.rounded-\[10px\] { + border-radius: 10px; +} + +.rounded-\[15px\] { + border-radius: 15px; +} + +.rounded-full { + border-radius: 9999px; +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.rounded-md { + border-radius: 0.375rem; +} + +.rounded-xl { + border-radius: 0.75rem; +} + +.rounded-\[12px\] { + border-radius: 12px; +} + +.border { + border-width: 1px; +} + +.border-2 { + border-width: 2px; +} + +.border-b { + border-bottom-width: 1px; +} + +.border-l { + border-left-width: 1px; +} + +.border-t { + border-top-width: 1px; +} + +.border-black { + --tw-border-opacity: 1; + border-color: rgb(0 0 0 / var(--tw-border-opacity, 1)); +} + +.border-black\/10 { + border-color: rgb(0 0 0 / 0.1); +} + +.border-gray-100 { + --tw-border-opacity: 1; + border-color: rgb(243 244 246 / var(--tw-border-opacity, 1)); +} + +.border-gray-200 { + --tw-border-opacity: 1; + border-color: rgb(229 231 235 / var(--tw-border-opacity, 1)); +} + +.border-gray-300 { + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity, 1)); +} + +.border-green-200 { + --tw-border-opacity: 1; + border-color: rgb(187 247 208 / var(--tw-border-opacity, 1)); +} + +.border-light-silver { + --tw-border-opacity: 1; + border-color: rgb(215 215 215 / var(--tw-border-opacity, 1)); +} + +.border-quick-silver { + --tw-border-opacity: 1; + border-color: rgb(160 160 160 / var(--tw-border-opacity, 1)); +} + +.border-red-200 { + --tw-border-opacity: 1; + border-color: rgb(254 202 202 / var(--tw-border-opacity, 1)); +} + +.border-red-500 { + --tw-border-opacity: 1; + border-color: rgb(239 68 68 / var(--tw-border-opacity, 1)); +} + +.border-uc-gold { + --tw-border-opacity: 1; + border-color: rgb(184 135 63 / var(--tw-border-opacity, 1)); +} + +.border-blue-500 { + --tw-border-opacity: 1; + border-color: rgb(59 130 246 / var(--tw-border-opacity, 1)); +} + +.border-opacity-20 { + --tw-border-opacity: 0.2; +} + +.bg-black { + --tw-bg-opacity: 1; + background-color: rgb(0 0 0 / var(--tw-bg-opacity, 1)); +} + +.bg-blue-50 { + --tw-bg-opacity: 1; + background-color: rgb(239 246 255 / var(--tw-bg-opacity, 1)); +} + +.bg-dark-charcoal { + --tw-bg-opacity: 1; + background-color: rgb(47 47 47 / var(--tw-bg-opacity, 1)); +} + +.bg-floral-white { + --tw-bg-opacity: 1; + background-color: rgb(252 248 243 / var(--tw-bg-opacity, 1)); +} + +.bg-gray-100 { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)); +} + +.bg-gray-200 { + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1)); +} + +.bg-gray-300 { + --tw-bg-opacity: 1; + background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1)); +} + +.bg-gray-50 { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)); +} + +.bg-green-50 { + --tw-bg-opacity: 1; + background-color: rgb(240 253 244 / var(--tw-bg-opacity, 1)); +} + +.bg-green-500 { + --tw-bg-opacity: 1; + background-color: rgb(34 197 94 / var(--tw-bg-opacity, 1)); +} + +.bg-green-600 { + --tw-bg-opacity: 1; + background-color: rgb(22 163 74 / var(--tw-bg-opacity, 1)); +} + +.bg-light-bg { + --tw-bg-opacity: 1; + background-color: rgb(244 245 247 / var(--tw-bg-opacity, 1)); +} + +.bg-light-silver { + --tw-bg-opacity: 1; + background-color: rgb(215 215 215 / var(--tw-bg-opacity, 1)); +} + +.bg-linen { + --tw-bg-opacity: 1; + background-color: rgb(250 240 230 / var(--tw-bg-opacity, 1)); +} + +.bg-purple-500 { + --tw-bg-opacity: 1; + background-color: rgb(168 85 247 / var(--tw-bg-opacity, 1)); +} + +.bg-red-50 { + --tw-bg-opacity: 1; + background-color: rgb(254 242 242 / var(--tw-bg-opacity, 1)); +} + +.bg-transparent { + background-color: transparent; +} + +.bg-uc-gold { + --tw-bg-opacity: 1; + background-color: rgb(184 135 63 / var(--tw-bg-opacity, 1)); +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); +} + +.bg-blue-500 { + --tw-bg-opacity: 1; + background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1)); +} + +.bg-blue-600 { + --tw-bg-opacity: 1; + background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1)); +} + +.bg-green-100 { + --tw-bg-opacity: 1; + background-color: rgb(220 252 231 / var(--tw-bg-opacity, 1)); +} + +.bg-yellow-100 { + --tw-bg-opacity: 1; + background-color: rgb(254 249 195 / var(--tw-bg-opacity, 1)); +} + +.bg-opacity-50 { + --tw-bg-opacity: 0.5; +} + +.bg-opacity-60 { + --tw-bg-opacity: 0.6; +} + +.bg-opacity-70 { + --tw-bg-opacity: 0.7; +} + +.object-contain { + -o-object-fit: contain; + object-fit: contain; +} + +.object-cover { + -o-object-fit: cover; + object-fit: cover; +} + +.object-center { + -o-object-position: center; + object-position: center; +} + +.object-top { + -o-object-position: top; + object-position: top; +} + +.p-2 { + padding: 0.5rem; +} + +.p-3 { + padding: 0.75rem; +} + +.p-4 { + padding: 1rem; +} + +.p-5 { + padding: 1.25rem; +} + +.p-6 { + padding: 1.5rem; +} + +.p-8 { + padding: 2rem; +} + +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.px-5 { + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.px-8 { + padding-left: 2rem; + padding-right: 2rem; +} + +.py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.py-10 { + padding-top: 2.5rem; + padding-bottom: 2.5rem; +} + +.py-12 { + padding-top: 3rem; + padding-bottom: 3rem; +} + +.py-14 { + padding-top: 3.5rem; + padding-bottom: 3.5rem; +} + +.py-16 { + padding-top: 4rem; + padding-bottom: 4rem; +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.py-20 { + padding-top: 5rem; + padding-bottom: 5rem; +} + +.py-3 { + padding-top: 0.75rem; + padding-bottom: 0.75rem; +} + +.py-4 { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.py-6 { + padding-top: 1.5rem; + padding-bottom: 1.5rem; +} + +.py-8 { + padding-top: 2rem; + padding-bottom: 2rem; +} + +.px-0 { + padding-left: 0px; + padding-right: 0px; +} + +.pb-16 { + padding-bottom: 4rem; +} + +.pb-20 { + padding-bottom: 5rem; +} + +.pr-10 { + padding-right: 2.5rem; +} + +.pr-12 { + padding-right: 3rem; +} + +.pr-3 { + padding-right: 0.75rem; +} + +.pr-6 { + padding-right: 1.5rem; +} + +.pt-12 { + padding-top: 3rem; +} + +.pt-2 { + padding-top: 0.5rem; +} + +.pt-28 { + padding-top: 7rem; +} + +.pt-32 { + padding-top: 8rem; +} + +.pt-4 { + padding-top: 1rem; +} + +.pt-6 { + padding-top: 1.5rem; +} + +.pt-8 { + padding-top: 2rem; +} + +.pt-20 { + padding-top: 5rem; +} + +.pb-2 { + padding-bottom: 0.5rem; +} + +.text-left { + text-align: left; +} + +.text-center { + text-align: center; +} + +.text-justify { + text-align: justify; +} + +.font-playfair { + font-family: Playfair Display, serif; +} + +.font-sans { + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +} + +.font-mono { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} + +.text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; +} + +.text-\[20px\] { + font-size: 20px; +} + +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.text-xs { + font-size: 0.75rem; + line-height: 1rem; +} + +.text-\[18px\] { + font-size: 18px; +} + +.font-bold { + font-weight: 700; +} + +.font-extralight { + font-weight: 200; +} + +.font-light { + font-weight: 300; +} + +.font-medium { + font-weight: 500; +} + +.font-normal { + font-weight: 400; +} + +.font-semibold { + font-weight: 600; +} + +.font-thin { + font-weight: 100; +} + +.uppercase { + text-transform: uppercase; +} + +.capitalize { + text-transform: capitalize; +} + +.leading-6 { + line-height: 1.5rem; +} + +.leading-loose { + line-height: 2; +} + +.leading-none { + line-height: 1; +} + +.leading-relaxed { + line-height: 1.625; +} + +.leading-tight { + line-height: 1.25; +} + +.tracking-wider { + letter-spacing: 0.05em; +} + +.text-\[\#333333\] { + --tw-text-opacity: 1; + color: rgb(51 51 51 / var(--tw-text-opacity, 1)); +} + +.text-\[\#3A3A3A\] { + --tw-text-opacity: 1; + color: rgb(58 58 58 / var(--tw-text-opacity, 1)); +} + +.text-axolotl { + --tw-text-opacity: 1; + color: rgb(111 119 107 / var(--tw-text-opacity, 1)); +} + +.text-black { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity, 1)); +} + +.text-blue-500 { + --tw-text-opacity: 1; + color: rgb(59 130 246 / var(--tw-text-opacity, 1)); +} + +.text-dark-charcoal { + --tw-text-opacity: 1; + color: rgb(47 47 47 / var(--tw-text-opacity, 1)); +} + +.text-davys-grey { + --tw-text-opacity: 1; + color: rgb(89 89 89 / var(--tw-text-opacity, 1)); +} + +.text-eerie-black { + --tw-text-opacity: 1; + color: rgb(26 26 26 / var(--tw-text-opacity, 1)); +} + +.text-gray-400 { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity, 1)); +} + +.text-gray-500 { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity, 1)); +} + +.text-gray-600 { + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity, 1)); +} + +.text-gray-700 { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity, 1)); +} + +.text-gray-800 { + --tw-text-opacity: 1; + color: rgb(31 41 55 / var(--tw-text-opacity, 1)); +} + +.text-green-800 { + --tw-text-opacity: 1; + color: rgb(22 101 52 / var(--tw-text-opacity, 1)); +} + +.text-quick-silver { + --tw-text-opacity: 1; + color: rgb(160 160 160 / var(--tw-text-opacity, 1)); +} + +.text-red-500 { + --tw-text-opacity: 1; + color: rgb(239 68 68 / var(--tw-text-opacity, 1)); +} + +.text-red-800 { + --tw-text-opacity: 1; + color: rgb(153 27 27 / var(--tw-text-opacity, 1)); +} + +.text-taupe-gray { + --tw-text-opacity: 1; + color: rgb(136 136 136 / var(--tw-text-opacity, 1)); +} + +.text-uc-gold { + --tw-text-opacity: 1; + color: rgb(184 135 63 / var(--tw-text-opacity, 1)); +} + +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity, 1)); +} + +.text-gray-300 { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity, 1)); +} + +.text-yellow-400 { + --tw-text-opacity: 1; + color: rgb(250 204 21 / var(--tw-text-opacity, 1)); +} + +.text-yellow-800 { + --tw-text-opacity: 1; + color: rgb(133 77 14 / var(--tw-text-opacity, 1)); +} + +.text-opacity-90 { + --tw-text-opacity: 0.9; +} + +.underline { + -webkit-text-decoration-line: underline; + text-decoration-line: underline; +} + +.opacity-0 { + opacity: 0; +} + +.opacity-100 { + opacity: 1; +} + +.opacity-50 { + opacity: 0.5; +} + +.shadow-\[0_8px_24px_rgba\(0\2c 0\2c 0\2c 0\.06\)\] { + --tw-shadow: 0 8px 24px rgba(0,0,0,0.06); + --tw-shadow-colored: 0 8px 24px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-lg { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-sm { + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-xl { + --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow { + --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-md { + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.blur { + --tw-blur: blur(8px); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.drop-shadow-sm { + --tw-drop-shadow: drop-shadow(0 1px 1px rgb(0 0 0 / 0.05)); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.filter { + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.transition { + transition-property: color, background-color, border-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-text-decoration-color; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-text-decoration-color; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-all { + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-colors { + transition-property: color, background-color, border-color, fill, stroke, -webkit-text-decoration-color; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, -webkit-text-decoration-color; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-opacity { + transition-property: opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-shadow { + transition-property: box-shadow; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-transform { + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.duration-200 { + transition-duration: 200ms; +} + +.duration-300 { + transition-duration: 300ms; +} + +.ease-in-out { + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} + +.file\:mr-4::file-selector-button { + margin-right: 1rem; +} + +.file\:rounded-full::file-selector-button { + border-radius: 9999px; +} + +.file\:border-0::file-selector-button { + border-width: 0px; +} + +.file\:bg-blue-50::file-selector-button { + --tw-bg-opacity: 1; + background-color: rgb(239 246 255 / var(--tw-bg-opacity, 1)); +} + +.file\:px-4::file-selector-button { + padding-left: 1rem; + padding-right: 1rem; +} + +.file\:py-2::file-selector-button { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.file\:text-sm::file-selector-button { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.file\:font-semibold::file-selector-button { + font-weight: 600; +} + +.file\:text-blue-700::file-selector-button { + --tw-text-opacity: 1; + color: rgb(29 78 216 / var(--tw-text-opacity, 1)); +} + +.hover\:-translate-y-0\.5:hover { + --tw-translate-y: -0.125rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.hover\:scale-110:hover { + --tw-scale-x: 1.1; + --tw-scale-y: 1.1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.hover\:border-gray-600:hover { + --tw-border-opacity: 1; + border-color: rgb(75 85 99 / var(--tw-border-opacity, 1)); +} + +.hover\:border-uc-gold:hover { + --tw-border-opacity: 1; + border-color: rgb(184 135 63 / var(--tw-border-opacity, 1)); +} + +.hover\:bg-amber-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(217 119 6 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-black:hover { + --tw-bg-opacity: 1; + background-color: rgb(0 0 0 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-gray-100:hover { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-gray-400:hover { + --tw-bg-opacity: 1; + background-color: rgb(156 163 175 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-gray-50:hover { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-light-bg:hover { + --tw-bg-opacity: 1; + background-color: rgb(244 245 247 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-uc-gold:hover { + --tw-bg-opacity: 1; + background-color: rgb(184 135 63 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-uc-gold\/90:hover { + background-color: rgb(184 135 63 / 0.9); +} + +.hover\:bg-opacity-80:hover { + --tw-bg-opacity: 0.8; +} + +.hover\:bg-opacity-90:hover { + --tw-bg-opacity: 0.9; +} + +.hover\:text-black:hover { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity, 1)); +} + +.hover\:text-blue-700:hover { + --tw-text-opacity: 1; + color: rgb(29 78 216 / var(--tw-text-opacity, 1)); +} + +.hover\:text-gray-600:hover { + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity, 1)); +} + +.hover\:text-gray-700:hover { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity, 1)); +} + +.hover\:text-gray-800:hover { + --tw-text-opacity: 1; + color: rgb(31 41 55 / var(--tw-text-opacity, 1)); +} + +.hover\:text-red-700:hover { + --tw-text-opacity: 1; + color: rgb(185 28 28 / var(--tw-text-opacity, 1)); +} + +.hover\:text-white:hover { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity, 1)); +} + +.hover\:underline:hover { + -webkit-text-decoration-line: underline; + text-decoration-line: underline; +} + +.hover\:shadow-lg:hover { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.hover\:shadow-md:hover { + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.hover\:shadow-xl:hover { + --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.hover\:file\:bg-blue-100::file-selector-button:hover { + --tw-bg-opacity: 1; + background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1)); +} + +.focus\:border-black:focus { + --tw-border-opacity: 1; + border-color: rgb(0 0 0 / var(--tw-border-opacity, 1)); +} + +.focus\:border-transparent:focus { + border-color: transparent; +} + +.focus\:border-uc-gold:focus { + --tw-border-opacity: 1; + border-color: rgb(184 135 63 / var(--tw-border-opacity, 1)); +} + +.focus\:outline-none:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.focus\:ring-2:focus { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.focus\:ring-uc-gold:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(184 135 63 / var(--tw-ring-opacity, 1)); +} + +.focus-visible\:ring-2:focus-visible { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.focus-visible\:ring-uc-gold:focus-visible { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(184 135 63 / var(--tw-ring-opacity, 1)); +} + +.group:hover .group-hover\:scale-105 { + --tw-scale-x: 1.05; + --tw-scale-y: 1.05; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.group:hover .group-hover\:scale-110 { + --tw-scale-x: 1.1; + --tw-scale-y: 1.1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.group:hover .group-hover\:stroke-white { + stroke: #FFFFFF; +} + +.group:hover .group-hover\:opacity-100 { + opacity: 1; +} + +@media (min-width: 640px) { + .sm\:order-1 { + order: 1; + } + + .sm\:order-2 { + order: 2; + } + + .sm\:mt-28 { + margin-top: 7rem; + } + + .sm\:mb-6 { + margin-bottom: 1.5rem; + } + + .sm\:mb-8 { + margin-bottom: 2rem; + } + + .sm\:mb-12 { + margin-bottom: 3rem; + } + + .sm\:mt-0 { + margin-top: 0px; + } + + .sm\:flex { + display: flex; + } + + .sm\:hidden { + display: none; + } + + .sm\:h-20 { + height: 5rem; + } + + .sm\:h-28 { + height: 7rem; + } + + .sm\:h-24 { + height: 6rem; + } + + .sm\:h-\[64px\] { + height: 64px; + } + + .sm\:h-32 { + height: 8rem; + } + + .sm\:h-\[500px\] { + height: 500px; + } + + .sm\:min-h-\[64px\] { + min-height: 64px; + } + + .sm\:w-24 { + width: 6rem; + } + + .sm\:w-32 { + width: 8rem; + } + + .sm\:w-\[180px\] { + width: 180px; + } + + .sm\:w-\[300px\] { + width: 300px; + } + + .sm\:w-\[320px\] { + width: 320px; + } + + .sm\:w-\[420px\] { + width: 420px; + } + + .sm\:w-\[380px\] { + width: 380px; + } + + .sm\:w-\[440px\] { + width: 440px; + } + + .sm\:w-\[500px\] { + width: 500px; + } + + .sm\:max-w-md { + max-width: 28rem; + } + + .sm\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .sm\:grid-cols-6 { + grid-template-columns: repeat(6, minmax(0, 1fr)); + } + + .sm\:flex-row { + flex-direction: row; + } + + .sm\:flex-col { + flex-direction: column; + } + + .sm\:gap-4 { + gap: 1rem; + } + + .sm\:gap-6 { + gap: 1.5rem; + } + + .sm\:gap-8 { + gap: 2rem; + } + + .sm\:space-y-6 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); + } + + .sm\:overflow-visible { + overflow: visible; + } + + .sm\:rounded-\[15px\] { + border-radius: 15px; + } + + .sm\:pt-28 { + padding-top: 7rem; + } + + .sm\:pb-0 { + padding-bottom: 0px; + } + + .sm\:text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; + } + + .sm\:text-\[20px\] { + font-size: 20px; + } + + .sm\:text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; + } +} + +@media (min-width: 768px) { + .md\:top-\[24rem\] { + top: 24rem; + } + + .md\:order-none { + order: 0; + } + + .md\:order-1 { + order: 1; + } + + .md\:order-2 { + order: 2; + } + + .md\:order-3 { + order: 3; + } + + .md\:order-4 { + order: 4; + } + + .md\:mt-10 { + margin-top: 2.5rem; + } + + .md\:mb-0 { + margin-bottom: 0px; + } + + .md\:mb-12 { + margin-bottom: 3rem; + } + + .md\:mb-8 { + margin-bottom: 2rem; + } + + .md\:ml-8 { + margin-left: 2rem; + } + + .md\:block { + display: block; + } + + .md\:contents { + display: contents; + } + + .md\:h-\[484px\] { + height: 484px; + } + + .md\:h-28 { + height: 7rem; + } + + .md\:h-32 { + height: 8rem; + } + + .md\:h-\[500px\] { + height: 500px; + } + + .md\:h-\[64px\] { + height: 64px; + } + + .md\:h-80 { + height: 20rem; + } + + .md\:min-h-\[64px\] { + min-height: 64px; + } + + .md\:w-32 { + width: 8rem; + } + + .md\:w-40 { + width: 10rem; + } + + .md\:w-44 { + width: 11rem; + } + + .md\:w-52 { + width: 13rem; + } + + .md\:w-60 { + width: 240px; + } + + .md\:w-28 { + width: 7rem; + } + + .md\:w-\[460px\] { + width: 460px; + } + + .md\:w-\[500px\] { + width: 500px; + } + + .md\:w-\[180px\] { + width: 180px; + } + + .md\:w-\[380px\] { + width: 380px; + } + + .md\:w-\[440px\] { + width: 440px; + } + + .md\:w-20 { + width: 5rem; + } + + .md\:w-auto { + width: auto; + } + + .md\:w-80 { + width: 20rem; + } + + .md\:max-w-md { + max-width: 28rem; + } + + .md\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .md\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .md\:flex-row { + flex-direction: row; + } + + .md\:flex-col { + flex-direction: column; + } + + .md\:items-center { + align-items: center; + } + + .md\:justify-between { + justify-content: space-between; + } + + .md\:gap-12 { + gap: 3rem; + } + + .md\:gap-8 { + gap: 2rem; + } + + .md\:gap-4 { + gap: 1rem; + } + + .md\:gap-6 { + gap: 1.5rem; + } + + .md\:space-y-6 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); + } + + .md\:overflow-visible { + overflow: visible; + } + + .md\:border-l { + border-left-width: 1px; + } + + .md\:border-gray-200 { + --tw-border-opacity: 1; + border-color: rgb(229 231 235 / var(--tw-border-opacity, 1)); + } + + .md\:pr-0 { + padding-right: 0px; + } + + .md\:pt-40 { + padding-top: 10rem; + } + + .md\:pb-0 { + padding-bottom: 0px; + } + + .md\:text-2xl { + font-size: 1.5rem; + line-height: 2rem; + } + + .md\:text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; + } + + .md\:text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; + } + + .md\:text-5xl { + font-size: 3rem; + line-height: 1; + } + + .md\:text-lg { + font-size: 1.125rem; + line-height: 1.75rem; + } + + .md\:text-xl { + font-size: 1.25rem; + line-height: 1.75rem; + } + + .md\:text-\[20px\] { + font-size: 20px; + } + + .md\:text-base { + font-size: 1rem; + line-height: 1.5rem; + } +} + +@media (min-width: 1024px) { + .lg\:top-\[29rem\] { + top: 29rem; + } + + .lg\:order-1 { + order: 1; + } + + .lg\:order-2 { + order: 2; + } + + .lg\:order-3 { + order: 3; + } + + .lg\:col-span-1 { + grid-column: span 1 / span 1; + } + + .lg\:col-span-2 { + grid-column: span 2 / span 2; + } + + .lg\:mx-0 { + margin-left: 0px; + margin-right: 0px; + } + + .lg\:mb-12 { + margin-bottom: 3rem; + } + + .lg\:mb-8 { + margin-bottom: 2rem; + } + + .lg\:mb-0 { + margin-bottom: 0px; + } + + .lg\:mt-0 { + margin-top: 0px; + } + + .lg\:h-32 { + height: 8rem; + } + + .lg\:h-\[500px\] { + height: 500px; + } + + .lg\:h-\[64px\] { + height: 64px; + } + + .lg\:min-h-\[64px\] { + min-height: 64px; + } + + .lg\:w-36 { + width: 9rem; + } + + .lg\:w-44 { + width: 11rem; + } + + .lg\:w-48 { + width: 12rem; + } + + .lg\:w-56 { + width: 14rem; + } + + .lg\:w-64 { + width: 16rem; + } + + .lg\:w-32 { + width: 8rem; + } + + .lg\:w-\[380px\] { + width: 380px; + } + + .lg\:w-\[440px\] { + width: 440px; + } + + .lg\:w-\[500px\] { + width: 500px; + } + + .lg\:w-auto { + width: auto; + } + + .lg\:w-\[180px\] { + width: 180px; + } + + .lg\:w-20 { + width: 5rem; + } + + .lg\:max-w-md { + max-width: 28rem; + } + + .lg\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .lg\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .lg\:grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + + .lg\:flex-row { + flex-direction: row; + } + + .lg\:flex-col { + flex-direction: column; + } + + .lg\:items-start { + align-items: flex-start; + } + + .lg\:gap-12 { + gap: 3rem; + } + + .lg\:gap-16 { + gap: 4rem; + } + + .lg\:gap-6 { + gap: 1.5rem; + } + + .lg\:gap-8 { + gap: 2rem; + } + + .lg\:gap-4 { + gap: 1rem; + } + + .lg\:space-y-6 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); + } + + .lg\:overflow-visible { + overflow: visible; + } + + .lg\:px-0 { + padding-left: 0px; + padding-right: 0px; + } + + .lg\:pr-2 { + padding-right: 0.5rem; + } + + .lg\:pt-48 { + padding-top: 12rem; + } + + .lg\:pb-0 { + padding-bottom: 0px; + } + + .lg\:text-2xl { + font-size: 1.5rem; + line-height: 2rem; + } + + .lg\:text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; + } + + .lg\:text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; + } + + .lg\:text-5xl { + font-size: 3rem; + line-height: 1; + } + + .lg\:text-6xl { + font-size: 3.75rem; + line-height: 1; + } + + .lg\:text-xl { + font-size: 1.25rem; + line-height: 1.75rem; + } + + .lg\:text-\[20px\] { + font-size: 20px; + } + + .lg\:text-base { + font-size: 1rem; + line-height: 1.5rem; + } +} + +@media (min-width: 1280px) { + .xl\:flex-col { + flex-direction: column; + } +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..7b569ce --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,56 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./*.html", "./src/**/*.{html,js}", "./scripts/**/*.js"], + theme: { + screens: { + sm: "640px", + md: "768px", + lg: "1024px", + xl: "1280px", + "2xl": "1536px", + }, + extend: { + fontFamily: { + montserrat: ["Montserrat", "sans-serif"], + playfair: ["Playfair Display", "serif"], + }, + colors: { + white: "#FFFFFF", + "dark-charcoal": "#2F2F2F", + black: "#000000", + "uc-gold": "#B8873F", + "floral-white": "#FCF8F3", + axolotl: "#6F776B", + "dark-charcoal-ii": "#212121", + "eerie-black": "#1A1A1A", + "taupe-gray": "#888888", + "davys-grey": "#595959", + "granite-gray": "#666666", + "light-silver": "#D7D7D7", + "quick-silver": "#A0A0A0", + linen: "#FAF0E6", + "light-bg": "#F4F5F7", + }, + spacing: { + 396: "396px", + 398: "398px", + 420: "420px", + 259: "259px", + 649: "649px", + 183: "183px", + 150: "150px", + 100: "100px", + 120: "120px", + 60: "240px", + 180: "720px", + 360: "1440px", + }, + textShadow: { + default: "0 2px 4px rgba(0,0,0,0.1)", + lg: "2px 2px 4px rgba(0,0,0,0.3)", + xl: "4px 4px 8px rgba(0,0,0,0.4)", + }, + }, + }, + plugins: [require("tailwindcss-textshadow")], +};