Compare commits
No commits in common. "e4ce417cb50707a7e568a61d4b418a076e1c75c4" and "d3403cb5e2b3e9cc3a865db24f5fe839e2990dc3" have entirely different histories.
e4ce417cb5
...
d3403cb5e2
20
README.md
|
|
@ -4,15 +4,15 @@ A **preview/staging environment** for managing KHY Furniture's product catalog w
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Add Products**: Create new products with full details
|
- ✅ **Add Products**: Create new products with full details
|
||||||
- **Edit Products**: Modify existing product information
|
- ✅ **Edit Products**: Modify existing product information
|
||||||
- **Delete Products**: Remove products with confirmation
|
- ✅ **Delete Products**: Remove products with confirmation
|
||||||
- **Preview Products**: See how products will look before saving
|
- ✅ **Preview Products**: See how products will look before saving
|
||||||
- **Export JSON**: Download updated products.json file
|
- ✅ **Export JSON**: Download updated products.json file
|
||||||
- **Live Preview**: See exactly how changes will look on the website
|
- ✅ **Live Preview**: See exactly how changes will look on the website
|
||||||
- **One-Click Deploy**: Deploy changes to production with a single command
|
- ✅ **One-Click Deploy**: Deploy changes to production with a single command
|
||||||
- **Automatic Backups**: Safe deployment with backup system
|
- ✅ **Automatic Backups**: Safe deployment with backup system
|
||||||
- **No Technical Knowledge Required**: Simple form-based interface
|
- ✅ **No Technical Knowledge Required**: Simple form-based interface
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
|
@ -71,7 +71,7 @@ A **preview/staging environment** for managing KHY Furniture's product catalog w
|
||||||
3. **Confirm Changes** → Review everything thoroughly
|
3. **Confirm Changes** → Review everything thoroughly
|
||||||
4. **Deploy to Production** → Run `npm run deploy` to copy updated files to the main KHY website
|
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
|
> 📖 **Detailed Workflow**: See [WORKFLOW.md](WORKFLOW.md) for complete instructions
|
||||||
|
|
||||||
## File Structure
|
## File Structure
|
||||||
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 884 B |
|
Before Width: | Height: | Size: 815 B |
|
Before Width: | Height: | Size: 811 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 803 B |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
|
@ -1,272 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Product Detail - KHY</title>
|
|
||||||
<link rel="stylesheet" href="assets/styles/main.css" />
|
|
||||||
<style>
|
|
||||||
/* Force responsive behavior */
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
.sm\:hidden {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
.sm\:flex {
|
|
||||||
display: flex !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&family=Playfair+Display:wght@100;200;300;400;500;600;700&family=Poppins:wght@400;500;600&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
</head>
|
|
||||||
<body class="bg-white font-sans text-gray-800">
|
|
||||||
<!-- Header -->
|
|
||||||
<header
|
|
||||||
class="fixed w-full h-20 sm:h-28 top-0 left-0 bg-white shadow-[0_8px_24px_rgba(0,0,0,0.06)] border-b border-black/10 z-50"
|
|
||||||
>
|
|
||||||
<nav class="h-full">
|
|
||||||
<div
|
|
||||||
class="max-w-7xl mx-auto h-full flex items-center justify-between px-5"
|
|
||||||
></div>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="pt-20 sm:pt-28">
|
|
||||||
<!-- Product Details Section -->
|
|
||||||
<section class="relative w-full bg-white py-6">
|
|
||||||
<div class="max-w-7xl mx-auto px-5">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<h1
|
|
||||||
class="font-playfair font-normal text-base text-quick-silver"
|
|
||||||
id="product-details-title"
|
|
||||||
>
|
|
||||||
Product Details
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Main Product Section -->
|
|
||||||
<section class="relative w-full bg-white py-8">
|
|
||||||
<div class="max-w-7xl mx-auto px-5">
|
|
||||||
<div class="flex flex-col md:flex-row gap-8">
|
|
||||||
<!-- Left Section - Product Images -->
|
|
||||||
<div class="flex flex-col-reverse md:flex-row gap-4">
|
|
||||||
<!-- Thumbnails -->
|
|
||||||
<div
|
|
||||||
class="flex flex-row md:flex-col gap-3 md:gap-4 w-full md:w-32 overflow-x-auto md:overflow-visible"
|
|
||||||
>
|
|
||||||
<!-- Thumbnail images will be populated dynamically -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Product Image -->
|
|
||||||
<div
|
|
||||||
class="w-full h-80 md:w-[500px] md:h-[500px] bg-floral-white rounded-lg overflow-hidden"
|
|
||||||
>
|
|
||||||
<!-- Main product image will be populated dynamically -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Section - Product Details -->
|
|
||||||
<div class="w-full md:w-[500px]">
|
|
||||||
<!-- Product Title -->
|
|
||||||
<h1
|
|
||||||
class="font-playfair font-normal text-4xl text-black mb-8"
|
|
||||||
id="product-title"
|
|
||||||
>
|
|
||||||
<!-- Product title will be populated dynamically -->
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<!-- Product Description -->
|
|
||||||
<p
|
|
||||||
class="font-playfair font-normal text-sm text-black mb-8 max-w-md"
|
|
||||||
id="product-description"
|
|
||||||
>
|
|
||||||
<!-- Product description will be populated dynamically -->
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Quantity and Action Buttons -->
|
|
||||||
<div class="flex flex-col md:flex-row gap-4 md:gap-6 mb-3">
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<a
|
|
||||||
href="contact.html"
|
|
||||||
class="inline-flex items-center justify-center w-full md:w-[440px] h-[64px] min-h-[64px] bg-white text-black font-playfair font-light text-[20px] leading-none rounded-[15px] border border-quick-silver hover:bg-black hover:text-white hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-200 box-border whitespace-nowrap mb-4"
|
|
||||||
>
|
|
||||||
Request a Quote
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
id="compare-products-btn"
|
|
||||||
class="inline-flex items-center justify-center w-full md:w-[440px] h-[64px] min-h-[64px] bg-white text-black font-playfair font-light text-[20px] leading-none rounded-[15px] border border-quick-silver hover:bg-black hover:text-white hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-200 box-border whitespace-nowrap"
|
|
||||||
>
|
|
||||||
Compare Products
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Divider -->
|
|
||||||
<div class="w-full h-px bg-light-silver mb-3"></div>
|
|
||||||
|
|
||||||
<!-- Product Specifications -->
|
|
||||||
<div class="mb-8">
|
|
||||||
<div class="space-y-2" id="product-specifications">
|
|
||||||
<!-- Product specifications will be populated dynamically -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Product Metadata -->
|
|
||||||
<div class="space-y-4">
|
|
||||||
<!-- Product metadata will be populated dynamically -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Product Detail Tabs Section -->
|
|
||||||
<section id="product-tabs" class="w-full bg-white">
|
|
||||||
<div class="border-t border-light-silver"></div>
|
|
||||||
<div class="max-w-7xl mx-auto px-5 py-10">
|
|
||||||
<!-- Tabs -->
|
|
||||||
<div class="flex justify-center gap-12 mb-8">
|
|
||||||
<button class="font-playfair text-2xl text-black">
|
|
||||||
Description
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Copy -->
|
|
||||||
<div
|
|
||||||
class="max-w-5xl mx-auto space-y-6 text-center"
|
|
||||||
id="product-description-content"
|
|
||||||
>
|
|
||||||
<!-- Product description content will be populated dynamically -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Images -->
|
|
||||||
<div class="relative mt-10">
|
|
||||||
<div
|
|
||||||
class="grid grid-cols-1 md:grid-cols-2 gap-8"
|
|
||||||
id="product-gallery-images"
|
|
||||||
>
|
|
||||||
<!-- Product gallery images will be populated dynamically -->
|
|
||||||
</div>
|
|
||||||
<!-- Back Arrow -->
|
|
||||||
<button
|
|
||||||
id="gallery-back-btn"
|
|
||||||
class="absolute top-1/2 left-0 transform -translate-y-1/2 bg-white border border-gray-300 rounded-full p-3 shadow-lg hover:shadow-xl transition-all duration-200 opacity-0 pointer-events-none"
|
|
||||||
style="display: none"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6 text-gray-600"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M15 19l-7-7 7-7"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<!-- Forward Arrow -->
|
|
||||||
<button
|
|
||||||
id="gallery-next-btn"
|
|
||||||
class="absolute top-1/2 right-0 transform -translate-y-1/2 bg-white border border-gray-300 rounded-full p-3 shadow-lg hover:shadow-xl transition-all duration-200 opacity-0 pointer-events-none"
|
|
||||||
style="display: none"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6 text-gray-600"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M9 5l7 7-7 7"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- full-width divider before related products -->
|
|
||||||
<div class="w-full border-t border-light-silver"></div>
|
|
||||||
|
|
||||||
<!-- Related Products Section -->
|
|
||||||
<section id="related-products" class="w-full bg-white py-14">
|
|
||||||
<div class="max-w-7xl mx-auto px-5">
|
|
||||||
<h2
|
|
||||||
class="font-playfair text-4xl font-medium text-center text-black mb-10"
|
|
||||||
>
|
|
||||||
Related Products
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="related-grid"
|
|
||||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8"
|
|
||||||
>
|
|
||||||
<!-- Related items will be injected here -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-10 flex justify-center">
|
|
||||||
<button
|
|
||||||
id="related-show-more"
|
|
||||||
class="w-[245px] h-12 border border-uc-gold rounded-md bg-white font-playfair font-semibold text-base text-uc-gold hover:bg-uc-gold hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
Show More
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- full-width divider after related products -->
|
|
||||||
<div class="w-full border-t border-light-silver"></div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Image Enlargement Modal -->
|
|
||||||
<div
|
|
||||||
id="image-modal"
|
|
||||||
class="fixed inset-0 bg-black bg-opacity-90 z-50 hidden flex items-center justify-center p-4 transition-opacity duration-300"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="relative max-w-7xl max-h-full w-full h-full flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<!-- Close Button -->
|
|
||||||
<button
|
|
||||||
id="modal-close-btn"
|
|
||||||
class="absolute top-4 right-4 z-10 bg-white bg-opacity-20 hover:bg-opacity-30 text-white rounded-full p-3 transition-all duration-200 backdrop-blur-sm"
|
|
||||||
aria-label="Close modal"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M6 18L18 6M6 6l12 12"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Enlarged Image -->
|
|
||||||
<img
|
|
||||||
id="modal-image"
|
|
||||||
src=""
|
|
||||||
alt=""
|
|
||||||
class="w-[95vw] h-[95vh] object-contain rounded-lg shadow-2xl transform transition-transform duration-300 scale-95"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="scripts/main.js?v=3.5"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,958 +0,0 @@
|
||||||
/* Admin Dashboard Styles */
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: "Montserrat", sans-serif;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
color: #333;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-container {
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header Styles */
|
|
||||||
.admin-header {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 30px;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left h1 {
|
|
||||||
font-family: "Playfair Display", serif;
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left p {
|
|
||||||
opacity: 0.9;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation Styles */
|
|
||||||
.admin-nav {
|
|
||||||
background: white;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-left {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-right {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn {
|
|
||||||
padding: 15px 25px;
|
|
||||||
border: none;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn:hover {
|
|
||||||
background: #e9ecef;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn-undo {
|
|
||||||
background: #fff3cd !important;
|
|
||||||
color: #856404 !important;
|
|
||||||
border: 1px solid #ffeaa7 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn-undo:hover {
|
|
||||||
background: #ffeaa7 !important;
|
|
||||||
color: #6c5b00 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn.active {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-icon {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main Content */
|
|
||||||
.admin-main {
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
|
||||||
padding: 30px;
|
|
||||||
min-height: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-section {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-section.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
border-bottom: 2px solid #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header h2 {
|
|
||||||
font-family: "Playfair Display", serif;
|
|
||||||
font-size: 2rem;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
padding: 10px 15px;
|
|
||||||
border: 2px solid #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
width: 250px;
|
|
||||||
transition: border-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button Styles */
|
|
||||||
.btn {
|
|
||||||
padding: 12px 24px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
text-align: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success {
|
|
||||||
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background: linear-gradient(135deg, #ff416c 0%, #ff4b2b 100%);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: linear-gradient(135deg, #6c757d 0%, #adb5bd 100%);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline {
|
|
||||||
background: transparent;
|
|
||||||
color: white;
|
|
||||||
border: 2px solid white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline:hover {
|
|
||||||
background: white;
|
|
||||||
color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sm {
|
|
||||||
padding: 8px 16px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Products Grid */
|
|
||||||
.products-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
||||||
gap: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card {
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card:hover {
|
|
||||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
|
||||||
transform: translateY(-5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-image {
|
|
||||||
height: 200px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #f8f9fa;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-image img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
transition: transform 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card:hover .product-image img {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-info {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-info h3 {
|
|
||||||
font-family: "Playfair Display", serif;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-category {
|
|
||||||
color: #667eea;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-description {
|
|
||||||
color: #666;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-actions {
|
|
||||||
padding: 15px 20px;
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
border-top: 1px solid #f8f9fa;
|
|
||||||
background: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Categories List */
|
|
||||||
.categories-list {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-card {
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 20px;
|
|
||||||
background: white;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-card:hover {
|
|
||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-card h3 {
|
|
||||||
font-family: "Playfair Display", serif;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-id {
|
|
||||||
color: #667eea;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-description {
|
|
||||||
color: #666;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Image Manager */
|
|
||||||
.image-manager {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-item {
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-item img {
|
|
||||||
width: 100%;
|
|
||||||
height: 150px;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-item-info {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-item-name {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-item-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Preview Container */
|
|
||||||
.preview-container {
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-container iframe {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal Styles */
|
|
||||||
.modal {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 1000;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background-color: white;
|
|
||||||
margin: 2% auto;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 12px;
|
|
||||||
width: 90%;
|
|
||||||
max-width: 800px;
|
|
||||||
max-height: 90vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
padding: 25px 30px;
|
|
||||||
border-bottom: 1px solid #e9ecef;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
border-radius: 12px 12px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header h3 {
|
|
||||||
font-family: "Playfair Display", serif;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
color: white;
|
|
||||||
opacity: 0.8;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form Styles */
|
|
||||||
form {
|
|
||||||
display: grid;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
textarea,
|
|
||||||
select {
|
|
||||||
padding: 12px 15px;
|
|
||||||
border: 2px solid #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: border-color 0.3s;
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus,
|
|
||||||
textarea:focus,
|
|
||||||
select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Additional Information Section */
|
|
||||||
.additional-info-section {
|
|
||||||
margin-top: 25px;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.additional-info-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.additional-info-section h4 {
|
|
||||||
margin: 0;
|
|
||||||
color: #495057;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
#add-new-field-btn {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
padding: 6px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.additional-info-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.additional-info-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.additional-info-item label {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #6c757d;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.additional-info-item input,
|
|
||||||
.additional-info-item textarea {
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid #ced4da;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.additional-info-item input:focus,
|
|
||||||
.additional-info-item textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #6366f1;
|
|
||||||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.additional-info-item textarea {
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* New field creation form */
|
|
||||||
.new-field-form {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
background-color: #e3f2fd;
|
|
||||||
border: 2px dashed #2196f3;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 15px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-field-form h5 {
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
color: #1976d2;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-field-inputs {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 2fr auto;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-field-inputs input {
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid #2196f3;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-field-inputs button {
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove field button */
|
|
||||||
.remove-field-btn {
|
|
||||||
background: #dc3545;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: 8px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-field-btn:hover {
|
|
||||||
background: #c82333;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Image Delete Confirmation Dialog */
|
|
||||||
.image-delete-confirmation {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 2000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirmation-content {
|
|
||||||
background: white;
|
|
||||||
padding: 30px;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
|
||||||
max-width: 400px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirmation-content h4 {
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
color: #dc3545;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirmation-content p {
|
|
||||||
margin: 10px 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirmation-content .warning {
|
|
||||||
color: #dc3545;
|
|
||||||
font-weight: 500;
|
|
||||||
background: #f8d7da;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid #f5c6cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirmation-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirmation-buttons .btn {
|
|
||||||
min-width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 20px;
|
|
||||||
padding-top: 20px;
|
|
||||||
border-top: 1px solid #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Image Preview */
|
|
||||||
.image-preview {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-preview-item {
|
|
||||||
position: relative;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-preview-item img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100px;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-preview-item .remove-btn {
|
|
||||||
position: absolute;
|
|
||||||
top: 5px;
|
|
||||||
right: 5px;
|
|
||||||
background: rgba(220, 53, 69, 0.9);
|
|
||||||
color: white;
|
|
||||||
border: 2px solid white;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
line-height: 1;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-preview-item .remove-btn:hover {
|
|
||||||
background: rgba(220, 53, 69, 1);
|
|
||||||
transform: scale(1.1);
|
|
||||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sync Status */
|
|
||||||
.sync-status {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sync-step {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #f8f9fa;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sync-step.active {
|
|
||||||
background: #e3f2fd;
|
|
||||||
color: #1976d2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sync-step.completed {
|
|
||||||
background: #e8f5e8;
|
|
||||||
color: #2e7d32;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sync-step.error {
|
|
||||||
background: #ffebee;
|
|
||||||
color: #c62828;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-icon {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-text {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Design */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.admin-container {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-header {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-nav {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-actions {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.products-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
width: 95%;
|
|
||||||
margin: 5% auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Additional info responsive */
|
|
||||||
.additional-info-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-field-inputs {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.additional-info-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading States */
|
|
||||||
.loading {
|
|
||||||
opacity: 0.6;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
display: inline-block;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border: 3px solid #f3f3f3;
|
|
||||||
border-top: 3px solid #667eea;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Success/Error Messages */
|
|
||||||
.message {
|
|
||||||
padding: 15px 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.success {
|
|
||||||
background: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
border: 1px solid #c3e6cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.error {
|
|
||||||
background: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
border: 1px solid #f5c6cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.info {
|
|
||||||
background: #d1ecf1;
|
|
||||||
color: #0c5460;
|
|
||||||
border: 1px solid #bee5eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Products Modal Styles */
|
|
||||||
.products-modal-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
border-bottom: 1px solid #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.products-modal-header p {
|
|
||||||
margin: 0;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #6c757d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.products-list {
|
|
||||||
max-height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 15px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-item:hover {
|
|
||||||
background: #e9ecef;
|
|
||||||
border-color: #6c757d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-info h4 {
|
|
||||||
margin: 0 0 5px 0;
|
|
||||||
color: #333;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-description {
|
|
||||||
margin: 0;
|
|
||||||
color: #6c757d;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-products {
|
|
||||||
text-align: center;
|
|
||||||
color: #6c757d;
|
|
||||||
font-style: italic;
|
|
||||||
padding: 40px 20px;
|
|
||||||
}
|
|
||||||
|
|
@ -1,352 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>KHY Admin Dashboard</title>
|
|
||||||
<link rel="stylesheet" href="css/admin.css" />
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&family=Playfair+Display:wght@100;200;300;400;500;600;700&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="admin-container">
|
|
||||||
<!-- Header -->
|
|
||||||
<header class="admin-header">
|
|
||||||
<div class="header-left">
|
|
||||||
<h1>KHY Admin Dashboard</h1>
|
|
||||||
<p>Product Management System</p>
|
|
||||||
</div>
|
|
||||||
<div class="header-actions">
|
|
||||||
<button id="preview-btn" class="btn btn-secondary">
|
|
||||||
Preview Site
|
|
||||||
</button>
|
|
||||||
<button id="sync-btn" class="btn btn-primary">
|
|
||||||
Sync to Main Site
|
|
||||||
</button>
|
|
||||||
<button id="quick-sync-btn" class="btn btn-secondary">
|
|
||||||
⚡ Quick Sync
|
|
||||||
</button>
|
|
||||||
<button id="logout-btn" class="btn btn-outline">Logout</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Navigation -->
|
|
||||||
<nav class="admin-nav">
|
|
||||||
<div class="nav-left">
|
|
||||||
<button class="nav-btn active" data-section="categories">
|
|
||||||
<span class="nav-icon">🏠</span>
|
|
||||||
Home
|
|
||||||
</button>
|
|
||||||
<button class="nav-btn" data-section="products">
|
|
||||||
<span class="nav-icon">📦</span>
|
|
||||||
Products
|
|
||||||
</button>
|
|
||||||
<button class="nav-btn" data-section="preview">
|
|
||||||
<span class="nav-icon">👁️</span>
|
|
||||||
Preview
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="nav-right">
|
|
||||||
<button class="nav-btn nav-btn-undo" id="undo-changes-btn">
|
|
||||||
<span class="nav-icon">↶</span>
|
|
||||||
Undo
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<main class="admin-main">
|
|
||||||
<!-- Categories Section -->
|
|
||||||
<section id="categories-section" class="content-section active">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>Manage your products here</h2>
|
|
||||||
</div>
|
|
||||||
<div class="categories-list" id="categories-list">
|
|
||||||
<!-- Categories will be loaded here -->
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Products Section -->
|
|
||||||
<section id="products-section" class="content-section">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>Product Management</h2>
|
|
||||||
<div class="section-actions">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="product-search"
|
|
||||||
placeholder="Search products..."
|
|
||||||
class="search-input"
|
|
||||||
/>
|
|
||||||
<button id="add-product-btn" class="btn btn-success">
|
|
||||||
<span>+</span> Add Product
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="products-grid" id="products-grid">
|
|
||||||
<!-- Products will be loaded here -->
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Preview Section -->
|
|
||||||
<section id="preview-section" class="content-section">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>Site Preview</h2>
|
|
||||||
<div class="preview-controls">
|
|
||||||
<button id="preview-catalog" class="btn btn-primary">
|
|
||||||
Product Catalog
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="preview-container">
|
|
||||||
<iframe
|
|
||||||
id="preview-frame"
|
|
||||||
src="../preview/catalog.html"
|
|
||||||
frameborder="0"
|
|
||||||
></iframe>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Category Products Section (Hidden by default) -->
|
|
||||||
<section
|
|
||||||
id="category-products-section"
|
|
||||||
class="content-section"
|
|
||||||
style="display: none"
|
|
||||||
>
|
|
||||||
<div class="section-header">
|
|
||||||
<h2 id="category-products-title">Category Products</h2>
|
|
||||||
<div class="section-actions">
|
|
||||||
<button class="btn btn-secondary" id="back-to-categories-btn">
|
|
||||||
← Back to Categories
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-success" id="add-product-to-category-btn">
|
|
||||||
<span>+</span> Add Product to this Category
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="category-products-container">
|
|
||||||
<div class="category-info">
|
|
||||||
<p class="product-count" id="category-product-count">
|
|
||||||
0 products in this category
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="products-list" id="category-products-list">
|
|
||||||
<!-- Products will be loaded here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Product Modal -->
|
|
||||||
<div id="product-modal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3 id="modal-title">Add Product</h3>
|
|
||||||
<span class="close">×</span>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form id="product-form">
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="product-name">Product Name *</label>
|
|
||||||
<input type="text" id="product-name" name="name" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="product-category">Category *</label>
|
|
||||||
<select id="product-category" name="category" required>
|
|
||||||
<option value="">Select Category</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="product-description">Short Description *</label>
|
|
||||||
<textarea
|
|
||||||
id="product-description"
|
|
||||||
name="description"
|
|
||||||
rows="2"
|
|
||||||
required
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="product-description-long">Long Description</label>
|
|
||||||
<textarea
|
|
||||||
id="product-description-long"
|
|
||||||
name="descriptionLong"
|
|
||||||
rows="4"
|
|
||||||
placeholder="Enter each paragraph on a new line"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="product-material">Material</label>
|
|
||||||
<input type="text" id="product-material" name="material" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="product-design">Design</label>
|
|
||||||
<input type="text" id="product-design" name="design" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="product-use-cases">Use Cases</label>
|
|
||||||
<input type="text" id="product-use-cases" name="useCases" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="product-warranty">Warranty</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="product-warranty"
|
|
||||||
name="warranty"
|
|
||||||
value="5 years"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="product-images">Product Images</label>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
id="product-images"
|
|
||||||
name="images"
|
|
||||||
multiple
|
|
||||||
accept="image/*"
|
|
||||||
/>
|
|
||||||
<div class="image-preview" id="image-preview"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Additional Information Section -->
|
|
||||||
<div
|
|
||||||
class="additional-info-section"
|
|
||||||
id="additional-info-section"
|
|
||||||
style="display: none"
|
|
||||||
>
|
|
||||||
<div class="additional-info-header">
|
|
||||||
<h4>Additional Information</h4>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm btn-secondary"
|
|
||||||
id="add-new-field-btn"
|
|
||||||
>
|
|
||||||
+ Add New Field
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="additional-info-grid" id="additional-info-grid">
|
|
||||||
<!-- Additional info fields will be populated here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-actions">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-secondary"
|
|
||||||
id="cancel-product-btn"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
Save Product
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Category Modal -->
|
|
||||||
<div id="category-modal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3 id="category-modal-title">Add Category</h3>
|
|
||||||
<span class="close">×</span>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form id="category-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="category-id">Category ID *</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="category-id"
|
|
||||||
name="id"
|
|
||||||
required
|
|
||||||
placeholder="e.g., task-chairs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="category-name">Category Name *</label>
|
|
||||||
<input type="text" id="category-name" name="name" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="category-description">Description</label>
|
|
||||||
<textarea
|
|
||||||
id="category-description"
|
|
||||||
name="description"
|
|
||||||
rows="3"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="form-actions">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-secondary"
|
|
||||||
id="cancel-category-btn"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
Save Category
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sync Status Modal -->
|
|
||||||
<div id="sync-modal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3>Syncing to Main Site</h3>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="sync-status">
|
|
||||||
<div class="sync-step" id="sync-step-1">
|
|
||||||
<span class="step-icon">⏳</span>
|
|
||||||
<span class="step-text">Preparing sync...</span>
|
|
||||||
</div>
|
|
||||||
<div class="sync-step" id="sync-step-2">
|
|
||||||
<span class="step-icon">⏳</span>
|
|
||||||
<span class="step-text">Cloning main repository...</span>
|
|
||||||
</div>
|
|
||||||
<div class="sync-step" id="sync-step-3">
|
|
||||||
<span class="step-icon">⏳</span>
|
|
||||||
<span class="step-text">Syncing products...</span>
|
|
||||||
</div>
|
|
||||||
<div class="sync-step" id="sync-step-4">
|
|
||||||
<span class="step-icon">⏳</span>
|
|
||||||
<span class="step-text">Syncing images...</span>
|
|
||||||
</div>
|
|
||||||
<div class="sync-step" id="sync-step-5">
|
|
||||||
<span class="step-icon">⏳</span>
|
|
||||||
<span class="step-text">Committing changes...</span>
|
|
||||||
</div>
|
|
||||||
<div class="sync-step" id="sync-step-6">
|
|
||||||
<span class="step-icon">⏳</span>
|
|
||||||
<span class="step-text">Pushing to main site...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- <script src="js/storageManager.js"></script> No longer needed - using direct saves -->
|
|
||||||
<script src="js/productManager.js"></script>
|
|
||||||
<script src="js/categoryManager.js"></script>
|
|
||||||
<script src="js/imageManager.js"></script>
|
|
||||||
<script src="js/admin.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,543 +0,0 @@
|
||||||
// Main Admin Dashboard Controller
|
|
||||||
class AdminDashboard {
|
|
||||||
constructor() {
|
|
||||||
this.currentSection = "categories";
|
|
||||||
this.productManager = null;
|
|
||||||
this.categoryManager = null;
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.setupEventListeners();
|
|
||||||
this.initializeManagers();
|
|
||||||
this.loadInitialData();
|
|
||||||
|
|
||||||
// Debug: Check if modal exists
|
|
||||||
const modal = document.getElementById("product-modal");
|
|
||||||
console.log("Product modal element exists:", !!modal);
|
|
||||||
if (modal) {
|
|
||||||
console.log("Modal current display:", modal.style.display);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupEventListeners() {
|
|
||||||
// Navigation
|
|
||||||
document.querySelectorAll(".nav-btn").forEach((btn) => {
|
|
||||||
btn.addEventListener("click", (e) => {
|
|
||||||
const button = e.currentTarget;
|
|
||||||
this.switchSection(button.dataset.section);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Header actions
|
|
||||||
document.getElementById("preview-btn").addEventListener("click", () => {
|
|
||||||
this.switchSection("preview");
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("sync-btn").addEventListener("click", () => {
|
|
||||||
this.syncToMainSite();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("quick-sync-btn").addEventListener("click", () => {
|
|
||||||
this.quickSync();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("logout-btn").addEventListener("click", () => {
|
|
||||||
this.logout();
|
|
||||||
});
|
|
||||||
|
|
||||||
document
|
|
||||||
.getElementById("undo-changes-btn")
|
|
||||||
.addEventListener("click", () => {
|
|
||||||
this.undoChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Preview control buttons
|
|
||||||
const catalogBtn = document.getElementById("preview-catalog");
|
|
||||||
|
|
||||||
if (catalogBtn) {
|
|
||||||
catalogBtn.addEventListener("click", () => {
|
|
||||||
this.loadPreview("catalog");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modal close handlers
|
|
||||||
document.querySelectorAll(".close").forEach((closeBtn) => {
|
|
||||||
closeBtn.addEventListener("click", (e) => {
|
|
||||||
const modal = e.target.closest(".modal");
|
|
||||||
modal.style.display = "none";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close modal when clicking outside
|
|
||||||
window.addEventListener("click", (e) => {
|
|
||||||
if (
|
|
||||||
e.target.classList.contains("modal") &&
|
|
||||||
e.target === e.currentTarget
|
|
||||||
) {
|
|
||||||
e.target.style.display = "none";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Keyboard shortcuts
|
|
||||||
document.addEventListener("keydown", (e) => {
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
this.closeAllModals();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeManagers() {
|
|
||||||
this.productManager = new ProductManager(this);
|
|
||||||
this.categoryManager = new CategoryManager(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadInitialData() {
|
|
||||||
try {
|
|
||||||
await this.productManager.loadData();
|
|
||||||
await this.categoryManager.loadData();
|
|
||||||
|
|
||||||
// Render categories immediately since Categories is the default active section
|
|
||||||
this.categoryManager.renderCategories();
|
|
||||||
|
|
||||||
this.showMessage("Dashboard loaded successfully", "success");
|
|
||||||
} catch (error) {
|
|
||||||
this.showMessage(
|
|
||||||
"Error loading dashboard data: " + error.message,
|
|
||||||
"error"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switchSection(sectionName) {
|
|
||||||
// Update navigation
|
|
||||||
document.querySelectorAll(".nav-btn").forEach((btn) => {
|
|
||||||
btn.classList.remove("active");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only update nav button if it's a regular section
|
|
||||||
if (sectionName !== "category-products") {
|
|
||||||
const navBtn = document.querySelector(`[data-section="${sectionName}"]`);
|
|
||||||
if (navBtn) {
|
|
||||||
navBtn.classList.add("active");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update content - hide all sections first
|
|
||||||
document.querySelectorAll(".content-section").forEach((section) => {
|
|
||||||
section.classList.remove("active");
|
|
||||||
section.style.display = "none";
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show the target section
|
|
||||||
const targetSection = document.getElementById(`${sectionName}-section`);
|
|
||||||
if (targetSection) {
|
|
||||||
targetSection.classList.add("active");
|
|
||||||
targetSection.style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentSection = sectionName;
|
|
||||||
|
|
||||||
// Load section-specific data
|
|
||||||
this.loadSectionData(sectionName);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadSectionData(sectionName) {
|
|
||||||
switch (sectionName) {
|
|
||||||
case "categories":
|
|
||||||
this.categoryManager.renderCategories();
|
|
||||||
break;
|
|
||||||
case "products":
|
|
||||||
this.productManager.renderProducts();
|
|
||||||
break;
|
|
||||||
case "preview":
|
|
||||||
this.loadPreview();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPreview(type = "catalog") {
|
|
||||||
const iframe = document.getElementById("preview-frame");
|
|
||||||
const timestamp = new Date().getTime();
|
|
||||||
|
|
||||||
if (type === "catalog") {
|
|
||||||
iframe.src = `../preview/catalog.html?t=${timestamp}`;
|
|
||||||
} else if (type === "detail") {
|
|
||||||
iframe.src = `../preview/detail.html?t=${timestamp}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async syncToMainSite() {
|
|
||||||
const syncModal = document.getElementById("sync-modal");
|
|
||||||
syncModal.style.display = "block";
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Step 1: Copy admin dashboard data to main website
|
|
||||||
if (this.productManager) {
|
|
||||||
await this.copyDataToMainWebsite();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Proceed with main site sync
|
|
||||||
await this.simulateSyncProcess();
|
|
||||||
this.showMessage("Successfully synced to main site!", "success");
|
|
||||||
} catch (error) {
|
|
||||||
this.showMessage("Sync failed: " + error.message, "error");
|
|
||||||
} finally {
|
|
||||||
syncModal.style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async quickSync() {
|
|
||||||
try {
|
|
||||||
console.log("⚡ QUICK SYNC STARTED");
|
|
||||||
|
|
||||||
// Get the current data from localStorage backup
|
|
||||||
const backupData = localStorage.getItem("khy_products_backup");
|
|
||||||
if (!backupData) {
|
|
||||||
this.showMessage(
|
|
||||||
"No changes to sync. Save a product first!",
|
|
||||||
"warning"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = JSON.parse(backupData);
|
|
||||||
console.log("📦 Syncing data:", data.products.length, "products");
|
|
||||||
|
|
||||||
// Create a simple copy command for the user
|
|
||||||
const dataStr = JSON.stringify(data, null, 2);
|
|
||||||
const dataBlob = new Blob([dataStr], { type: "application/json" });
|
|
||||||
const url = URL.createObjectURL(dataBlob);
|
|
||||||
|
|
||||||
// Create download link
|
|
||||||
const downloadLink = document.createElement("a");
|
|
||||||
downloadLink.href = url;
|
|
||||||
downloadLink.download = "products_sync.json";
|
|
||||||
downloadLink.style.display = "none";
|
|
||||||
document.body.appendChild(downloadLink);
|
|
||||||
downloadLink.click();
|
|
||||||
document.body.removeChild(downloadLink);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
// Show instructions
|
|
||||||
this.showMessage(
|
|
||||||
"File downloaded! Copy it to data/products.json",
|
|
||||||
"info"
|
|
||||||
);
|
|
||||||
console.log("📋 QUICK SYNC INSTRUCTIONS:");
|
|
||||||
console.log("1. File 'products_sync.json' has been downloaded");
|
|
||||||
console.log("2. Copy it to replace: data/products.json");
|
|
||||||
console.log("3. Refresh the preview to see changes");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Quick sync failed:", error);
|
|
||||||
this.showMessage("Quick sync failed: " + error.message, "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async copyDataToMainWebsite() {
|
|
||||||
try {
|
|
||||||
console.log("🔄 COPYING DATA TO MAIN WEBSITE");
|
|
||||||
|
|
||||||
// Get the current data from localStorage backup
|
|
||||||
const backupData = localStorage.getItem("khy_products_backup");
|
|
||||||
if (!backupData) {
|
|
||||||
console.log("⚠️ No backup data found in localStorage");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = JSON.parse(backupData);
|
|
||||||
console.log("📦 Data to copy:", data.products.length, "products");
|
|
||||||
|
|
||||||
// Create a simple file copy using Node.js script
|
|
||||||
// This will be handled by the sync-to-main.js script
|
|
||||||
console.log("✅ Data ready for sync - will be processed by sync script");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Error preparing data for sync:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async syncTemporaryChangesToLocal() {
|
|
||||||
try {
|
|
||||||
// Check if there are temporary changes to sync
|
|
||||||
const temporaryChanges =
|
|
||||||
await window.storageManager.getTemporaryChanges();
|
|
||||||
|
|
||||||
if (
|
|
||||||
temporaryChanges.products &&
|
|
||||||
Object.keys(temporaryChanges.products).length > 0
|
|
||||||
) {
|
|
||||||
console.log("🔄 Syncing temporary changes to local files...");
|
|
||||||
|
|
||||||
// Handle image changes first
|
|
||||||
if (temporaryChanges.imageChanges) {
|
|
||||||
console.log("📸 Processing image changes...");
|
|
||||||
await this.processImageChanges(temporaryChanges.imageChanges);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply changes to actual products array
|
|
||||||
Object.values(temporaryChanges.products).forEach((tempProduct) => {
|
|
||||||
const existingIndex = this.productManager.products.findIndex(
|
|
||||||
(p) => p.id === tempProduct.id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingIndex >= 0) {
|
|
||||||
// Update existing product
|
|
||||||
this.productManager.products[existingIndex] = {
|
|
||||||
...this.productManager.products[existingIndex],
|
|
||||||
...tempProduct,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Add new product
|
|
||||||
this.productManager.products.push(tempProduct);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save to actual JSON file
|
|
||||||
await this.productManager.saveData();
|
|
||||||
|
|
||||||
// Clear temporary storage
|
|
||||||
await window.storageManager.clearAllTemporaryData();
|
|
||||||
|
|
||||||
console.log("✅ Temporary changes synced to local files successfully");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Error syncing temporary changes:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async processImageChanges(imageChanges) {
|
|
||||||
console.log("📸 PROCESSING IMAGE CHANGES:");
|
|
||||||
console.log("Image changes:", imageChanges);
|
|
||||||
|
|
||||||
for (const [productId, changes] of Object.entries(imageChanges)) {
|
|
||||||
console.log(`📦 Processing images for product ${productId}:`, changes);
|
|
||||||
|
|
||||||
// Handle new image uploads
|
|
||||||
if (changes.newImages && changes.newImages.length > 0) {
|
|
||||||
console.log("📁 Uploading new images:", changes.newImages);
|
|
||||||
for (const imageRef of changes.newImages) {
|
|
||||||
// Get the actual file from IndexedDB
|
|
||||||
const imageData = await window.storageManager.getImageFile(
|
|
||||||
imageRef.imageId
|
|
||||||
);
|
|
||||||
if (imageData) {
|
|
||||||
await this.uploadImageFile(imageData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle deleted images
|
|
||||||
if (changes.deletedImages && changes.deletedImages.length > 0) {
|
|
||||||
console.log("🗑️ Deleting images:", changes.deletedImages);
|
|
||||||
for (const deletedImage of changes.deletedImages) {
|
|
||||||
await this.deleteImageFile(deletedImage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async uploadImageFile(imageData) {
|
|
||||||
console.log("📤 UPLOADING IMAGE FILE:", imageData.name);
|
|
||||||
|
|
||||||
// In a real implementation, you would:
|
|
||||||
// 1. Create FormData with the file
|
|
||||||
// 2. Upload to server/assets directory
|
|
||||||
// 3. Return the new file path
|
|
||||||
// 4. Handle file naming conflicts
|
|
||||||
|
|
||||||
// For now, simulate the upload process
|
|
||||||
const newPath = `assets/images/products/uploaded/${Date.now()}_${
|
|
||||||
imageData.name
|
|
||||||
}`;
|
|
||||||
console.log(`📁 File would be uploaded to: ${newPath}`);
|
|
||||||
console.log(`📁 File size: ${imageData.size} bytes`);
|
|
||||||
console.log(`📁 File type: ${imageData.type}`);
|
|
||||||
|
|
||||||
// TODO: Implement actual file upload
|
|
||||||
// The actual file is in imageData.file
|
|
||||||
return newPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteImageFile(deletedImage) {
|
|
||||||
console.log("🗑️ DELETING IMAGE FILE:", deletedImage.path);
|
|
||||||
|
|
||||||
// In a real implementation, you would:
|
|
||||||
// 1. Check if file exists
|
|
||||||
// 2. Delete the file from assets directory
|
|
||||||
// 3. Handle any errors gracefully
|
|
||||||
|
|
||||||
// For now, simulate the deletion process
|
|
||||||
console.log(`🗑️ File would be deleted: ${deletedImage.path}`);
|
|
||||||
|
|
||||||
// TODO: Implement actual file deletion
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async simulateSyncProcess() {
|
|
||||||
const steps = [
|
|
||||||
"sync-step-1",
|
|
||||||
"sync-step-2",
|
|
||||||
"sync-step-3",
|
|
||||||
"sync-step-4",
|
|
||||||
"sync-step-5",
|
|
||||||
"sync-step-6",
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = 0; i < steps.length; i++) {
|
|
||||||
const step = document.getElementById(steps[i]);
|
|
||||||
step.classList.add("active");
|
|
||||||
|
|
||||||
// Simulate processing time
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
step.classList.remove("active");
|
|
||||||
step.classList.add("completed");
|
|
||||||
}
|
|
||||||
|
|
||||||
// In a real implementation, this would call the actual sync script
|
|
||||||
console.log("Sync process completed");
|
|
||||||
}
|
|
||||||
|
|
||||||
logout() {
|
|
||||||
if (confirm("Are you sure you want to logout?")) {
|
|
||||||
// In a real implementation, this would clear authentication
|
|
||||||
window.location.href = "login.html";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
undoChanges() {
|
|
||||||
if (
|
|
||||||
confirm(
|
|
||||||
"Are you sure you want to undo all unsaved changes? This action cannot be undone."
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// Call the productManager's undo method
|
|
||||||
if (this.productManager) {
|
|
||||||
this.productManager.undoTemporaryChanges();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAllModals() {
|
|
||||||
document.querySelectorAll(".modal").forEach((modal) => {
|
|
||||||
modal.style.display = "none";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
showMessage(message, type = "info") {
|
|
||||||
// Remove existing messages
|
|
||||||
const existingMessages = document.querySelectorAll(".message");
|
|
||||||
existingMessages.forEach((msg) => msg.remove());
|
|
||||||
|
|
||||||
// Create new message
|
|
||||||
const messageDiv = document.createElement("div");
|
|
||||||
messageDiv.className = `message ${type}`;
|
|
||||||
messageDiv.textContent = message;
|
|
||||||
|
|
||||||
// Insert at the top of the main content
|
|
||||||
const main = document.querySelector(".admin-main");
|
|
||||||
main.insertBefore(messageDiv, main.firstChild);
|
|
||||||
|
|
||||||
// Auto-remove after 5 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
messageDiv.remove();
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility method for other managers to use
|
|
||||||
getData() {
|
|
||||||
return {
|
|
||||||
products: this.productManager.products,
|
|
||||||
categories: this.categoryManager.categories,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility method to refresh current section
|
|
||||||
refreshCurrentSection() {
|
|
||||||
this.loadSectionData(this.currentSection);
|
|
||||||
}
|
|
||||||
|
|
||||||
async undoChanges() {
|
|
||||||
const undoStack = JSON.parse(
|
|
||||||
localStorage.getItem("khy_undo_stack") || "[]"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (undoStack.length === 0) {
|
|
||||||
this.showMessage("No changes to undo.", "info");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastChange = undoStack[undoStack.length - 1];
|
|
||||||
const confirmMessage = `Undo last change: ${lastChange.action.toUpperCase()} "${
|
|
||||||
lastChange.newData.name
|
|
||||||
}"?\n\nThis action cannot be undone.`;
|
|
||||||
|
|
||||||
if (confirm(confirmMessage)) {
|
|
||||||
try {
|
|
||||||
await this.performUndo(lastChange);
|
|
||||||
|
|
||||||
// Remove the undone change from the stack
|
|
||||||
undoStack.pop();
|
|
||||||
localStorage.setItem("khy_undo_stack", JSON.stringify(undoStack));
|
|
||||||
|
|
||||||
this.showMessage(
|
|
||||||
`Successfully undid: ${lastChange.action} "${lastChange.newData.name}"`,
|
|
||||||
"success"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Refresh the current view
|
|
||||||
this.productManager.refreshCurrentView();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ ERROR DURING UNDO:", error);
|
|
||||||
this.showMessage("Error during undo: " + error.message, "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async performUndo(changeEntry) {
|
|
||||||
console.log("⏪ PERFORMING UNDO:", changeEntry);
|
|
||||||
|
|
||||||
if (changeEntry.action === "edit") {
|
|
||||||
// Restore the original product data
|
|
||||||
const index = this.productManager.products.findIndex(
|
|
||||||
(p) => p.id === changeEntry.productId
|
|
||||||
);
|
|
||||||
if (index !== -1) {
|
|
||||||
this.productManager.products[index] = changeEntry.originalData;
|
|
||||||
console.log("✅ Restored original product data");
|
|
||||||
}
|
|
||||||
} else if (changeEntry.action === "add") {
|
|
||||||
// Remove the added product
|
|
||||||
const index = this.productManager.products.findIndex(
|
|
||||||
(p) => p.id === changeEntry.productId
|
|
||||||
);
|
|
||||||
if (index !== -1) {
|
|
||||||
this.productManager.products.splice(index, 1);
|
|
||||||
console.log("✅ Removed added product");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the changes
|
|
||||||
await this.productManager.saveData();
|
|
||||||
console.log("💾 Undo changes saved to file");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize dashboard when DOM is loaded
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
window.adminDashboard = new AdminDashboard();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Global utility functions
|
|
||||||
window.showMessage = (message, type) => {
|
|
||||||
if (window.adminDashboard) {
|
|
||||||
window.adminDashboard.showMessage(message, type);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.refreshDashboard = () => {
|
|
||||||
if (window.adminDashboard) {
|
|
||||||
window.adminDashboard.refreshCurrentSection();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,459 +0,0 @@
|
||||||
// Category Management Class
|
|
||||||
class CategoryManager {
|
|
||||||
constructor(dashboard) {
|
|
||||||
this.dashboard = dashboard;
|
|
||||||
this.categories = [];
|
|
||||||
this.currentCategory = null;
|
|
||||||
this.setupEventListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
setupEventListeners() {
|
|
||||||
// Add category button - removed from UI
|
|
||||||
// document
|
|
||||||
// .getElementById("add-category-btn")
|
|
||||||
// .addEventListener("click", () => {
|
|
||||||
// this.showCategoryModal();
|
|
||||||
// });
|
|
||||||
|
|
||||||
// Category form submit
|
|
||||||
document.getElementById("category-form").addEventListener("submit", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.saveCategory();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cancel button in category modal
|
|
||||||
document.addEventListener("click", (e) => {
|
|
||||||
if (e.target.id === "cancel-category-btn") {
|
|
||||||
e.preventDefault();
|
|
||||||
this.hideCategoryModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// View Products buttons - use event delegation
|
|
||||||
document.addEventListener("click", (e) => {
|
|
||||||
if (e.target.matches("[data-category-id]")) {
|
|
||||||
const categoryId = e.target.getAttribute("data-category-id");
|
|
||||||
this.viewCategoryProducts(categoryId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Back to Categories button
|
|
||||||
document.addEventListener("click", (e) => {
|
|
||||||
if (e.target.id === "back-to-categories-btn") {
|
|
||||||
this.backToHome();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add Product to Category button
|
|
||||||
document.addEventListener("click", (e) => {
|
|
||||||
if (e.target.id === "add-product-to-category-btn") {
|
|
||||||
const categoryId = e.target.getAttribute("data-category-id");
|
|
||||||
this.addProductToCategory(categoryId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Edit Product buttons in category view - use event delegation
|
|
||||||
document.addEventListener("click", (e) => {
|
|
||||||
console.log("Click detected on:", e.target);
|
|
||||||
// Check if the clicked element or its parent has the edit-product action
|
|
||||||
const editButton = e.target.closest("[data-action='edit-product']");
|
|
||||||
if (editButton) {
|
|
||||||
console.log("Edit product button clicked!");
|
|
||||||
e.stopPropagation();
|
|
||||||
const productId = editButton.getAttribute("data-product-id");
|
|
||||||
console.log("Product ID:", productId);
|
|
||||||
this.editProduct(productId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadData() {
|
|
||||||
try {
|
|
||||||
const response = await fetch("../data/products.json");
|
|
||||||
const data = await response.json();
|
|
||||||
this.categories = data.categories || [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading category data:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderCategories() {
|
|
||||||
const container = document.getElementById("categories-list");
|
|
||||||
|
|
||||||
if (this.categories.length === 0) {
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="no-categories">
|
|
||||||
<h3>No categories found</h3>
|
|
||||||
<p>Add your first category to get started!</p>
|
|
||||||
<button class="btn btn-primary" onclick="categoryManager.showCategoryModal()">Add Category</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = "";
|
|
||||||
this.categories.forEach((category) => {
|
|
||||||
const categoryCard = this.createCategoryCard(category);
|
|
||||||
container.appendChild(categoryCard);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createCategoryCard(category) {
|
|
||||||
const card = document.createElement("div");
|
|
||||||
card.className = "category-card";
|
|
||||||
|
|
||||||
// Count products in this category
|
|
||||||
const productCount = this.dashboard.productManager.products.filter(
|
|
||||||
(p) => p.category === category.id
|
|
||||||
).length;
|
|
||||||
|
|
||||||
card.innerHTML = `
|
|
||||||
<div class="category-header">
|
|
||||||
<h3>${category.name}</h3>
|
|
||||||
<span class="category-id">${category.id}</span>
|
|
||||||
</div>
|
|
||||||
<p class="category-description">${
|
|
||||||
category.description || "No description"
|
|
||||||
}</p>
|
|
||||||
<div class="category-stats">
|
|
||||||
<span class="product-count">${productCount} product${
|
|
||||||
productCount !== 1 ? "s" : ""
|
|
||||||
}</span>
|
|
||||||
</div>
|
|
||||||
<div class="category-actions">
|
|
||||||
<button class="btn btn-sm btn-secondary" data-category-id="${
|
|
||||||
category.id
|
|
||||||
}">
|
|
||||||
View Products
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
|
|
||||||
showCategoryModal(category = null) {
|
|
||||||
const modal = document.getElementById("category-modal");
|
|
||||||
const title = document.getElementById("category-modal-title");
|
|
||||||
const form = document.getElementById("category-form");
|
|
||||||
|
|
||||||
this.currentCategory = category;
|
|
||||||
|
|
||||||
if (category) {
|
|
||||||
title.textContent = "Edit Category";
|
|
||||||
this.populateForm(category);
|
|
||||||
} else {
|
|
||||||
title.textContent = "Add Category";
|
|
||||||
form.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
modal.style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
hideCategoryModal() {
|
|
||||||
document.getElementById("category-modal").style.display = "none";
|
|
||||||
this.currentCategory = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
populateForm(category) {
|
|
||||||
document.getElementById("category-id").value = category.id;
|
|
||||||
document.getElementById("category-name").value = category.name;
|
|
||||||
document.getElementById("category-description").value =
|
|
||||||
category.description || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveCategory() {
|
|
||||||
const formData = new FormData(document.getElementById("category-form"));
|
|
||||||
const categoryData = {
|
|
||||||
id: formData.get("id"),
|
|
||||||
name: formData.get("name"),
|
|
||||||
description: formData.get("description"),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Validate required fields
|
|
||||||
if (!categoryData.id || !categoryData.name) {
|
|
||||||
throw new Error("Category ID and name are required");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for duplicate ID (only for new categories)
|
|
||||||
if (!this.currentCategory) {
|
|
||||||
const existingCategory = this.categories.find(
|
|
||||||
(cat) => cat.id === categoryData.id
|
|
||||||
);
|
|
||||||
if (existingCategory) {
|
|
||||||
throw new Error("A category with this ID already exists");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update or add category
|
|
||||||
const existingIndex = this.categories.findIndex(
|
|
||||||
(cat) => cat.id === categoryData.id
|
|
||||||
);
|
|
||||||
if (existingIndex >= 0) {
|
|
||||||
this.categories[existingIndex] = {
|
|
||||||
...this.categories[existingIndex],
|
|
||||||
...categoryData,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
this.categories.push(categoryData);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.saveData();
|
|
||||||
this.renderCategories();
|
|
||||||
this.hideCategoryModal();
|
|
||||||
this.dashboard.showMessage("Category saved successfully!", "success");
|
|
||||||
|
|
||||||
// Update product manager's category select
|
|
||||||
if (this.dashboard.productManager) {
|
|
||||||
this.dashboard.productManager.populateCategorySelect();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.dashboard.showMessage(
|
|
||||||
"Error saving category: " + error.message,
|
|
||||||
"error"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
editCategory(id) {
|
|
||||||
const category = this.categories.find((cat) => cat.id === id);
|
|
||||||
if (category) {
|
|
||||||
this.showCategoryModal(category);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewCategoryProducts(categoryId) {
|
|
||||||
const category = this.categories.find((cat) => cat.id === categoryId);
|
|
||||||
if (!category) return;
|
|
||||||
|
|
||||||
// Get products in this category
|
|
||||||
const productsInCategory = this.dashboard.productManager.products.filter(
|
|
||||||
(p) => p.category === categoryId
|
|
||||||
);
|
|
||||||
|
|
||||||
// Switch to the dedicated category products section
|
|
||||||
this.dashboard.switchSection("category-products");
|
|
||||||
|
|
||||||
// Render the category products
|
|
||||||
this.showCategoryProductsScreen(category, productsInCategory);
|
|
||||||
}
|
|
||||||
|
|
||||||
showCategoryProductsScreen(category, products) {
|
|
||||||
console.log("showCategoryProductsScreen called with category:", category);
|
|
||||||
console.log("Products in category:", products);
|
|
||||||
|
|
||||||
// Update the section title
|
|
||||||
const title = document.getElementById("category-products-title");
|
|
||||||
if (title) {
|
|
||||||
title.textContent = `${category.name} Products`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the product count
|
|
||||||
const productCount = document.getElementById("category-product-count");
|
|
||||||
if (productCount) {
|
|
||||||
productCount.textContent = `${products.length} product${
|
|
||||||
products.length !== 1 ? "s" : ""
|
|
||||||
} in this category`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the add product button
|
|
||||||
const addButton = document.getElementById("add-product-to-category-btn");
|
|
||||||
if (addButton) {
|
|
||||||
addButton.setAttribute("data-category-id", category.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the products list
|
|
||||||
const productsList = document.getElementById("category-products-list");
|
|
||||||
if (productsList) {
|
|
||||||
productsList.innerHTML =
|
|
||||||
products.length === 0
|
|
||||||
? '<p class="no-products">No products in this category yet. Click "Add Product to this Category" to get started!</p>'
|
|
||||||
: products
|
|
||||||
.map((product) => {
|
|
||||||
console.log(
|
|
||||||
"Rendering product:",
|
|
||||||
product.name,
|
|
||||||
"with ID:",
|
|
||||||
product.id
|
|
||||||
);
|
|
||||||
return `
|
|
||||||
<div class="product-card">
|
|
||||||
<div class="product-info">
|
|
||||||
<h4>${product.name}</h4>
|
|
||||||
<p class="product-description">${
|
|
||||||
product.description || "No description"
|
|
||||||
}</p>
|
|
||||||
</div>
|
|
||||||
<div class="product-actions">
|
|
||||||
<button class="btn btn-sm btn-primary" data-action="edit-product" data-product-id="${
|
|
||||||
product.id
|
|
||||||
}">
|
|
||||||
✏️ Edit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
backToHome() {
|
|
||||||
// Switch back to home section
|
|
||||||
this.dashboard.switchSection("categories");
|
|
||||||
}
|
|
||||||
|
|
||||||
setupModalEventListeners() {
|
|
||||||
const modal = document.getElementById("products-modal");
|
|
||||||
if (!modal) return;
|
|
||||||
|
|
||||||
// Close modal
|
|
||||||
modal.addEventListener("click", (e) => {
|
|
||||||
if (
|
|
||||||
e.target.matches("[data-action='close-modal']") ||
|
|
||||||
e.target.classList.contains("modal")
|
|
||||||
) {
|
|
||||||
modal.style.display = "none";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add product to category
|
|
||||||
modal.addEventListener("click", (e) => {
|
|
||||||
if (e.target.matches("[data-action='add-product']")) {
|
|
||||||
const categoryId = e.target.getAttribute("data-category-id");
|
|
||||||
this.addProductToCategory(categoryId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// View product details
|
|
||||||
modal.addEventListener("click", (e) => {
|
|
||||||
if (e.target.matches("[data-action='view-product']")) {
|
|
||||||
const productId = e.target.getAttribute("data-product-id");
|
|
||||||
this.viewProductDetails(productId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Edit product
|
|
||||||
modal.addEventListener("click", (e) => {
|
|
||||||
if (e.target.matches("[data-action='edit-product']")) {
|
|
||||||
e.stopPropagation();
|
|
||||||
const productId = e.target.getAttribute("data-product-id");
|
|
||||||
this.editProduct(productId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addProductToCategory(categoryId) {
|
|
||||||
// Show the product modal directly without switching sections
|
|
||||||
this.dashboard.productManager.showProductModal();
|
|
||||||
|
|
||||||
// Pre-select the category in the form after the modal is shown
|
|
||||||
setTimeout(() => {
|
|
||||||
const categorySelect = document.getElementById("product-category");
|
|
||||||
if (categorySelect) {
|
|
||||||
categorySelect.value = categoryId;
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
viewProductDetails(productId) {
|
|
||||||
// Close the products modal immediately
|
|
||||||
const modal = document.getElementById("products-modal");
|
|
||||||
if (modal) {
|
|
||||||
modal.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Switch to products section and show product details
|
|
||||||
this.dashboard.switchSection("products");
|
|
||||||
// You could implement a product details view here
|
|
||||||
this.dashboard.showMessage(
|
|
||||||
`Viewing product details for ID: ${productId}`,
|
|
||||||
"info"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
editProduct(productId) {
|
|
||||||
console.log("editProduct called with ID:", productId);
|
|
||||||
console.log("Dashboard productManager:", this.dashboard.productManager);
|
|
||||||
// Edit the product directly (we're already in the products section)
|
|
||||||
this.dashboard.productManager.editProduct(productId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteCategory(id) {
|
|
||||||
const category = this.categories.find((cat) => cat.id === id);
|
|
||||||
if (!category) return;
|
|
||||||
|
|
||||||
// Check if category has products
|
|
||||||
const productsInCategory = this.dashboard.productManager.products.filter(
|
|
||||||
(p) => p.category === id
|
|
||||||
);
|
|
||||||
if (productsInCategory.length > 0) {
|
|
||||||
this.dashboard.showMessage(
|
|
||||||
`Cannot delete category "${category.name}" because it contains ${productsInCategory.length} product(s). Please move or delete the products first.`,
|
|
||||||
"error"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
confirm(
|
|
||||||
`Are you sure you want to delete the category "${category.name}"? This action cannot be undone.`
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
this.categories = this.categories.filter((cat) => cat.id !== id);
|
|
||||||
await this.saveData();
|
|
||||||
this.renderCategories();
|
|
||||||
this.dashboard.showMessage("Category deleted successfully!", "success");
|
|
||||||
|
|
||||||
// Update product manager's category select
|
|
||||||
if (this.dashboard.productManager) {
|
|
||||||
this.dashboard.productManager.populateCategorySelect();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.dashboard.showMessage(
|
|
||||||
"Error deleting category: " + error.message,
|
|
||||||
"error"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveData() {
|
|
||||||
const data = {
|
|
||||||
products: this.dashboard.productManager.products,
|
|
||||||
categories: this.categories,
|
|
||||||
};
|
|
||||||
|
|
||||||
// In a real implementation, this would save to a server
|
|
||||||
console.log("Saving category data:", data);
|
|
||||||
|
|
||||||
// Simulate API call
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve();
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility method to get category by ID
|
|
||||||
getCategoryById(id) {
|
|
||||||
return this.categories.find((cat) => cat.id === id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility method to get category name by ID
|
|
||||||
getCategoryName(id) {
|
|
||||||
const category = this.getCategoryById(id);
|
|
||||||
return category ? category.name : id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility method to validate category ID format
|
|
||||||
validateCategoryId(id) {
|
|
||||||
// Category IDs should be lowercase, hyphen-separated
|
|
||||||
return /^[a-z0-9-]+$/.test(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make CategoryManager available globally
|
|
||||||
window.CategoryManager = CategoryManager;
|
|
||||||
|
|
@ -1,338 +0,0 @@
|
||||||
// Image Management Class
|
|
||||||
class ImageManager {
|
|
||||||
constructor(dashboard) {
|
|
||||||
this.dashboard = dashboard;
|
|
||||||
this.images = [];
|
|
||||||
this.setupEventListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
setupEventListeners() {
|
|
||||||
// Upload images button
|
|
||||||
document
|
|
||||||
.getElementById("upload-images-btn")
|
|
||||||
.addEventListener("click", () => {
|
|
||||||
this.showUploadModal();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadData() {
|
|
||||||
try {
|
|
||||||
// In a real implementation, this would fetch from a server
|
|
||||||
// For now, we'll collect images from products
|
|
||||||
this.collectImagesFromProducts();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading image data:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
collectImagesFromProducts() {
|
|
||||||
this.images = [];
|
|
||||||
const products = this.dashboard.productManager.products;
|
|
||||||
|
|
||||||
products.forEach((product) => {
|
|
||||||
if (product.images && Array.isArray(product.images)) {
|
|
||||||
product.images.forEach((imagePath) => {
|
|
||||||
if (!this.images.find((img) => img.path === imagePath)) {
|
|
||||||
this.images.push({
|
|
||||||
path: imagePath,
|
|
||||||
name: this.extractFileName(imagePath),
|
|
||||||
productId: product.id,
|
|
||||||
productName: product.name,
|
|
||||||
size: "Unknown", // Would be fetched from server
|
|
||||||
uploadDate: "Unknown", // Would be fetched from server
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
extractFileName(path) {
|
|
||||||
return path.split("/").pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
renderImages() {
|
|
||||||
const container = document.getElementById("image-manager");
|
|
||||||
|
|
||||||
if (this.images.length === 0) {
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="no-images">
|
|
||||||
<h3>No images found</h3>
|
|
||||||
<p>Upload images to get started!</p>
|
|
||||||
<button class="btn btn-primary" onclick="imageManager.showUploadModal()">Upload Images</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = "";
|
|
||||||
this.images.forEach((image) => {
|
|
||||||
const imageItem = this.createImageItem(image);
|
|
||||||
container.appendChild(imageItem);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createImageItem(image) {
|
|
||||||
const item = document.createElement("div");
|
|
||||||
item.className = "image-item";
|
|
||||||
|
|
||||||
item.innerHTML = `
|
|
||||||
<div class="image-preview">
|
|
||||||
<img src="${image.path}" alt="${image.name}" onerror="this.src='data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"150\" viewBox=\"0 0 200 150\"><rect width=\"200\" height=\"150\" fill=\"%23f0f0f0\"/><text x=\"50%\" y=\"50%\" text-anchor=\"middle\" dy=\".3em\" fill=\"%23999\">Image Not Found</text></svg>'">
|
|
||||||
</div>
|
|
||||||
<div class="image-item-info">
|
|
||||||
<div class="image-item-name" title="${image.name}">${image.name}</div>
|
|
||||||
<div class="image-meta">
|
|
||||||
<span class="image-product">Product: ${image.productName}</span>
|
|
||||||
<span class="image-size">Size: ${image.size}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="image-item-actions">
|
|
||||||
<button class="btn btn-sm btn-primary" onclick="imageManager.viewImage('${image.path}')">
|
|
||||||
👁️ View
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-secondary" onclick="imageManager.renameImage('${image.path}')">
|
|
||||||
✏️ Rename
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-danger" onclick="imageManager.deleteImage('${image.path}')">
|
|
||||||
🗑️ Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
showUploadModal() {
|
|
||||||
// Create upload modal if it doesn't exist
|
|
||||||
let uploadModal = document.getElementById("upload-modal");
|
|
||||||
if (!uploadModal) {
|
|
||||||
uploadModal = this.createUploadModal();
|
|
||||||
document.body.appendChild(uploadModal);
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadModal.style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
createUploadModal() {
|
|
||||||
const modal = document.createElement("div");
|
|
||||||
modal.id = "upload-modal";
|
|
||||||
modal.className = "modal";
|
|
||||||
|
|
||||||
modal.innerHTML = `
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3>Upload Images</h3>
|
|
||||||
<span class="close" onclick="this.closest('.modal').style.display='none'">×</span>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="upload-area" id="upload-area">
|
|
||||||
<div class="upload-content">
|
|
||||||
<div class="upload-icon">📤</div>
|
|
||||||
<h4>Drag & Drop images here</h4>
|
|
||||||
<p>or click to select files</p>
|
|
||||||
<input type="file" id="file-input" multiple accept="image/*" style="display: none;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="upload-preview" id="upload-preview"></div>
|
|
||||||
<div class="upload-actions">
|
|
||||||
<button class="btn btn-secondary" onclick="this.closest('.modal').style.display='none'">Cancel</button>
|
|
||||||
<button class="btn btn-primary" onclick="imageManager.processUpload()">Upload Images</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Setup drag and drop
|
|
||||||
const uploadArea = modal.querySelector("#upload-area");
|
|
||||||
const fileInput = modal.querySelector("#file-input");
|
|
||||||
|
|
||||||
uploadArea.addEventListener("click", () => fileInput.click());
|
|
||||||
uploadArea.addEventListener("dragover", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
uploadArea.classList.add("drag-over");
|
|
||||||
});
|
|
||||||
uploadArea.addEventListener("dragleave", () => {
|
|
||||||
uploadArea.classList.remove("drag-over");
|
|
||||||
});
|
|
||||||
uploadArea.addEventListener("drop", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
uploadArea.classList.remove("drag-over");
|
|
||||||
this.handleFileSelect(e.dataTransfer.files);
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput.addEventListener("change", (e) => {
|
|
||||||
this.handleFileSelect(e.target.files);
|
|
||||||
});
|
|
||||||
|
|
||||||
return modal;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFileSelect(files) {
|
|
||||||
const preview = document.getElementById("upload-preview");
|
|
||||||
preview.innerHTML = "";
|
|
||||||
|
|
||||||
Array.from(files).forEach((file) => {
|
|
||||||
if (file.type.startsWith("image/")) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
const imageDiv = document.createElement("div");
|
|
||||||
imageDiv.className = "upload-preview-item";
|
|
||||||
imageDiv.innerHTML = `
|
|
||||||
<img src="${e.target.result}" alt="${file.name}">
|
|
||||||
<div class="upload-preview-info">
|
|
||||||
<span class="upload-filename">${file.name}</span>
|
|
||||||
<span class="upload-filesize">${this.formatFileSize(
|
|
||||||
file.size
|
|
||||||
)}</span>
|
|
||||||
</div>
|
|
||||||
<button class="remove-upload-btn" onclick="this.parentElement.remove()">×</button>
|
|
||||||
`;
|
|
||||||
preview.appendChild(imageDiv);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
formatFileSize(bytes) {
|
|
||||||
if (bytes === 0) return "0 Bytes";
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
async processUpload() {
|
|
||||||
const fileInput = document.getElementById("file-input");
|
|
||||||
const files = fileInput.files;
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
|
||||||
this.dashboard.showMessage("Please select files to upload", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.dashboard.showMessage("Uploading images...", "info");
|
|
||||||
|
|
||||||
const uploadedImages = await this.uploadImages(files);
|
|
||||||
|
|
||||||
// Add to images list
|
|
||||||
uploadedImages.forEach((imagePath) => {
|
|
||||||
const fileName = this.extractFileName(imagePath);
|
|
||||||
this.images.push({
|
|
||||||
path: imagePath,
|
|
||||||
name: fileName,
|
|
||||||
productId: null,
|
|
||||||
productName: "Unassigned",
|
|
||||||
size: "Unknown",
|
|
||||||
uploadDate: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.renderImages();
|
|
||||||
document.getElementById("upload-modal").style.display = "none";
|
|
||||||
this.dashboard.showMessage(
|
|
||||||
`Successfully uploaded ${uploadedImages.length} image(s)!`,
|
|
||||||
"success"
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.dashboard.showMessage(
|
|
||||||
"Error uploading images: " + error.message,
|
|
||||||
"error"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async uploadImages(files) {
|
|
||||||
const uploadedImages = [];
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
if (file.type.startsWith("image/")) {
|
|
||||||
// In a real implementation, you'd upload to a server
|
|
||||||
const filename = `uploaded_${Date.now()}_${file.name}`;
|
|
||||||
const imagePath = `assets/images/products/${filename}`;
|
|
||||||
uploadedImages.push(imagePath);
|
|
||||||
|
|
||||||
console.log(`Would upload: ${file.name} to ${imagePath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return uploadedImages;
|
|
||||||
}
|
|
||||||
|
|
||||||
viewImage(imagePath) {
|
|
||||||
// Create image viewer modal
|
|
||||||
const viewer = document.createElement("div");
|
|
||||||
viewer.className = "modal image-viewer";
|
|
||||||
viewer.innerHTML = `
|
|
||||||
<div class="modal-content image-viewer-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3>Image Viewer</h3>
|
|
||||||
<span class="close" onclick="this.closest('.modal').remove()">×</span>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<img src="${imagePath}" alt="Full size image" style="max-width: 100%; max-height: 70vh; object-fit: contain;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.body.appendChild(viewer);
|
|
||||||
viewer.style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
renameImage(imagePath) {
|
|
||||||
const currentName = this.extractFileName(imagePath);
|
|
||||||
const newName = prompt("Enter new name for the image:", currentName);
|
|
||||||
|
|
||||||
if (newName && newName !== currentName) {
|
|
||||||
// In a real implementation, you'd rename the file on the server
|
|
||||||
console.log(`Would rename ${imagePath} to ${newName}`);
|
|
||||||
this.dashboard.showMessage(
|
|
||||||
"Image rename functionality would be implemented here",
|
|
||||||
"info"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteImage(imagePath) {
|
|
||||||
if (
|
|
||||||
confirm(
|
|
||||||
"Are you sure you want to delete this image? This action cannot be undone."
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
// Check if image is used by any products
|
|
||||||
const productsUsingImage =
|
|
||||||
this.dashboard.productManager.products.filter(
|
|
||||||
(product) => product.images && product.images.includes(imagePath)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (productsUsingImage.length > 0) {
|
|
||||||
this.dashboard.showMessage(
|
|
||||||
`Cannot delete image because it's used by ${productsUsingImage.length} product(s). Please remove it from the products first.`,
|
|
||||||
"error"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove from images list
|
|
||||||
this.images = this.images.filter((img) => img.path !== imagePath);
|
|
||||||
|
|
||||||
// In a real implementation, you'd delete from server
|
|
||||||
console.log(`Would delete image: ${imagePath}`);
|
|
||||||
|
|
||||||
this.renderImages();
|
|
||||||
this.dashboard.showMessage("Image deleted successfully!", "success");
|
|
||||||
} catch (error) {
|
|
||||||
this.dashboard.showMessage(
|
|
||||||
"Error deleting image: " + error.message,
|
|
||||||
"error"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make ImageManager available globally
|
|
||||||
window.ImageManager = ImageManager;
|
|
||||||
|
|
@ -1,245 +0,0 @@
|
||||||
class StorageManager {
|
|
||||||
constructor() {
|
|
||||||
this.dbName = "KHYAdminStorage";
|
|
||||||
this.dbVersion = 1;
|
|
||||||
this.db = null;
|
|
||||||
this.initDB();
|
|
||||||
}
|
|
||||||
|
|
||||||
async initDB() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = indexedDB.open(this.dbName, this.dbVersion);
|
|
||||||
|
|
||||||
request.onerror = () => {
|
|
||||||
console.error("Failed to open IndexedDB:", request.error);
|
|
||||||
reject(request.error);
|
|
||||||
};
|
|
||||||
|
|
||||||
request.onsuccess = () => {
|
|
||||||
this.db = request.result;
|
|
||||||
console.log("✅ IndexedDB initialized successfully");
|
|
||||||
resolve(this.db);
|
|
||||||
};
|
|
||||||
|
|
||||||
request.onupgradeneeded = (event) => {
|
|
||||||
const db = event.target.result;
|
|
||||||
|
|
||||||
// Create stores for different types of data
|
|
||||||
if (!db.objectStoreNames.contains("temporaryChanges")) {
|
|
||||||
db.createObjectStore("temporaryChanges", { keyPath: "id" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!db.objectStoreNames.contains("imageFiles")) {
|
|
||||||
db.createObjectStore("imageFiles", { keyPath: "id" });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ IndexedDB stores created");
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveTemporaryChanges(changes) {
|
|
||||||
if (!this.db) {
|
|
||||||
await this.initDB();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const transaction = this.db.transaction(
|
|
||||||
["temporaryChanges"],
|
|
||||||
"readwrite"
|
|
||||||
);
|
|
||||||
const store = transaction.objectStore("temporaryChanges");
|
|
||||||
|
|
||||||
const changeData = {
|
|
||||||
id: "current",
|
|
||||||
data: changes,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const request = store.put(changeData);
|
|
||||||
|
|
||||||
request.onsuccess = () => {
|
|
||||||
console.log("💾 Temporary changes saved to IndexedDB");
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
request.onerror = () => {
|
|
||||||
console.error("Failed to save temporary changes:", request.error);
|
|
||||||
reject(request.error);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTemporaryChanges() {
|
|
||||||
if (!this.db) {
|
|
||||||
await this.initDB();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const transaction = this.db.transaction(["temporaryChanges"], "readonly");
|
|
||||||
const store = transaction.objectStore("temporaryChanges");
|
|
||||||
const request = store.get("current");
|
|
||||||
|
|
||||||
request.onsuccess = () => {
|
|
||||||
const result = request.result;
|
|
||||||
if (result && result.data) {
|
|
||||||
console.log("📦 Retrieved temporary changes from IndexedDB");
|
|
||||||
resolve(result.data);
|
|
||||||
} else {
|
|
||||||
console.log("📦 No temporary changes found in IndexedDB");
|
|
||||||
resolve({});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
request.onerror = () => {
|
|
||||||
console.error("Failed to get temporary changes:", request.error);
|
|
||||||
reject(request.error);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveImageFile(imageId, file) {
|
|
||||||
if (!this.db) {
|
|
||||||
await this.initDB();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const transaction = this.db.transaction(["imageFiles"], "readwrite");
|
|
||||||
const store = transaction.objectStore("imageFiles");
|
|
||||||
|
|
||||||
const imageData = {
|
|
||||||
id: imageId,
|
|
||||||
file: file,
|
|
||||||
name: file.name,
|
|
||||||
size: file.size,
|
|
||||||
type: file.type,
|
|
||||||
lastModified: file.lastModified,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const request = store.put(imageData);
|
|
||||||
|
|
||||||
request.onsuccess = () => {
|
|
||||||
console.log(`📁 Image file saved to IndexedDB: ${file.name}`);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
request.onerror = () => {
|
|
||||||
console.error("Failed to save image file:", request.error);
|
|
||||||
reject(request.error);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getImageFile(imageId) {
|
|
||||||
if (!this.db) {
|
|
||||||
await this.initDB();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const transaction = this.db.transaction(["imageFiles"], "readonly");
|
|
||||||
const store = transaction.objectStore("imageFiles");
|
|
||||||
const request = store.get(imageId);
|
|
||||||
|
|
||||||
request.onsuccess = () => {
|
|
||||||
const result = request.result;
|
|
||||||
if (result) {
|
|
||||||
console.log(`📁 Retrieved image file from IndexedDB: ${result.name}`);
|
|
||||||
resolve(result);
|
|
||||||
} else {
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
request.onerror = () => {
|
|
||||||
console.error("Failed to get image file:", request.error);
|
|
||||||
reject(request.error);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteImageFile(imageId) {
|
|
||||||
if (!this.db) {
|
|
||||||
await this.initDB();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const transaction = this.db.transaction(["imageFiles"], "readwrite");
|
|
||||||
const store = transaction.objectStore("imageFiles");
|
|
||||||
const request = store.delete(imageId);
|
|
||||||
|
|
||||||
request.onsuccess = () => {
|
|
||||||
console.log(`🗑️ Image file deleted from IndexedDB: ${imageId}`);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
request.onerror = () => {
|
|
||||||
console.error("Failed to delete image file:", request.error);
|
|
||||||
reject(request.error);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async clearAllTemporaryData() {
|
|
||||||
if (!this.db) {
|
|
||||||
await this.initDB();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const transaction = this.db.transaction(
|
|
||||||
["temporaryChanges", "imageFiles"],
|
|
||||||
"readwrite"
|
|
||||||
);
|
|
||||||
|
|
||||||
const changeStore = transaction.objectStore("temporaryChanges");
|
|
||||||
const imageStore = transaction.objectStore("imageFiles");
|
|
||||||
|
|
||||||
const changeRequest = changeStore.clear();
|
|
||||||
const imageRequest = imageStore.clear();
|
|
||||||
|
|
||||||
let completed = 0;
|
|
||||||
const total = 2;
|
|
||||||
|
|
||||||
const checkComplete = () => {
|
|
||||||
completed++;
|
|
||||||
if (completed === total) {
|
|
||||||
console.log("🧹 All temporary data cleared from IndexedDB");
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
changeRequest.onsuccess = checkComplete;
|
|
||||||
imageRequest.onsuccess = checkComplete;
|
|
||||||
|
|
||||||
changeRequest.onerror = () => reject(changeRequest.error);
|
|
||||||
imageRequest.onerror = () => reject(imageRequest.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAllImageFiles() {
|
|
||||||
if (!this.db) {
|
|
||||||
await this.initDB();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const transaction = this.db.transaction(["imageFiles"], "readonly");
|
|
||||||
const store = transaction.objectStore("imageFiles");
|
|
||||||
const request = store.getAll();
|
|
||||||
|
|
||||||
request.onsuccess = () => {
|
|
||||||
console.log(
|
|
||||||
`📁 Retrieved ${request.result.length} image files from IndexedDB`
|
|
||||||
);
|
|
||||||
resolve(request.result);
|
|
||||||
};
|
|
||||||
|
|
||||||
request.onerror = () => {
|
|
||||||
console.error("Failed to get all image files:", request.error);
|
|
||||||
reject(request.error);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create global instance
|
|
||||||
window.storageManager = new StorageManager();
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
echo "🚀 Starting KHY Admin Dashboard..."
|
|
||||||
echo "📱 Dashboard will be available at: http://localhost:3000"
|
|
||||||
echo "🔧 To stop the server, press Ctrl+C"
|
|
||||||
echo ""
|
|
||||||
python3 -m http.server 3000
|
|
||||||