diff --git a/README.md b/README.md index 446e85f..68f54d0 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,15 @@ A **preview/staging environment** for managing KHY Furniture's product catalog w ## Features -- ✅ **Add Products**: Create new products with full details -- ✅ **Edit Products**: Modify existing product information -- ✅ **Delete Products**: Remove products with confirmation -- ✅ **Preview Products**: See how products will look before saving -- ✅ **Export JSON**: Download updated products.json file -- ✅ **Live Preview**: See exactly how changes will look on the website -- ✅ **One-Click Deploy**: Deploy changes to production with a single command -- ✅ **Automatic Backups**: Safe deployment with backup system -- ✅ **No Technical Knowledge Required**: Simple form-based interface +- **Add Products**: Create new products with full details +- **Edit Products**: Modify existing product information +- **Delete Products**: Remove products with confirmation +- **Preview Products**: See how products will look before saving +- **Export JSON**: Download updated products.json file +- **Live Preview**: See exactly how changes will look on the website +- **One-Click Deploy**: Deploy changes to production with a single command +- **Automatic Backups**: Safe deployment with backup system +- **No Technical Knowledge Required**: Simple form-based interface ## Quick Start @@ -71,7 +71,7 @@ A **preview/staging environment** for managing KHY Furniture's product catalog w 3. **Confirm Changes** → Review everything thoroughly 4. **Deploy to Production** → Run `npm run deploy` to copy updated files to the main KHY website -> 📖 **Detailed Workflow**: See [WORKFLOW.md](WORKFLOW.md) for complete instructions +> **Detailed Workflow**: See [WORKFLOW.md](WORKFLOW.md) for complete instructions ## File Structure diff --git a/admin-dashboard/assets/icons/address.png b/admin-dashboard/assets/icons/address.png new file mode 100644 index 0000000..28e21f5 Binary files /dev/null and b/admin-dashboard/assets/icons/address.png differ diff --git a/admin-dashboard/assets/icons/mail.png b/admin-dashboard/assets/icons/mail.png new file mode 100644 index 0000000..97d2e58 Binary files /dev/null and b/admin-dashboard/assets/icons/mail.png differ diff --git a/admin-dashboard/assets/icons/phone.png b/admin-dashboard/assets/icons/phone.png new file mode 100644 index 0000000..1e83e1a Binary files /dev/null and b/admin-dashboard/assets/icons/phone.png differ diff --git a/admin-dashboard/assets/icons/phone2.png b/admin-dashboard/assets/icons/phone2.png new file mode 100644 index 0000000..5ba105b Binary files /dev/null and b/admin-dashboard/assets/icons/phone2.png differ diff --git a/admin-dashboard/assets/icons/shipping.png b/admin-dashboard/assets/icons/shipping.png new file mode 100644 index 0000000..9fb9a8f Binary files /dev/null and b/admin-dashboard/assets/icons/shipping.png differ diff --git a/admin-dashboard/assets/icons/star.png b/admin-dashboard/assets/icons/star.png new file mode 100644 index 0000000..4f7a231 Binary files /dev/null and b/admin-dashboard/assets/icons/star.png differ diff --git a/admin-dashboard/assets/icons/support.png b/admin-dashboard/assets/icons/support.png new file mode 100644 index 0000000..ebb5cd6 Binary files /dev/null and b/admin-dashboard/assets/icons/support.png differ diff --git a/admin-dashboard/assets/icons/time.png b/admin-dashboard/assets/icons/time.png new file mode 100644 index 0000000..d45e13b Binary files /dev/null and b/admin-dashboard/assets/icons/time.png differ diff --git a/admin-dashboard/assets/icons/trophy.png b/admin-dashboard/assets/icons/trophy.png new file mode 100644 index 0000000..c1820f9 Binary files /dev/null and b/admin-dashboard/assets/icons/trophy.png differ diff --git a/admin-dashboard/assets/icons/warranty.png b/admin-dashboard/assets/icons/warranty.png new file mode 100644 index 0000000..6448c7c Binary files /dev/null and b/admin-dashboard/assets/icons/warranty.png differ diff --git a/admin-dashboard/product-detail.html b/admin-dashboard/product-detail.html new file mode 100644 index 0000000..d6c091a --- /dev/null +++ b/admin-dashboard/product-detail.html @@ -0,0 +1,272 @@ + + + + + + Product Detail - KHY + + + + + + +
+ +
+ +
+ +
+
+
+

+ Product Details +

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

+ +

+ + +

+ +

+ + +
+ + + Request a Quote + + +
+ + +
+ + +
+
+ +
+
+ + +
+ +
+
+
+
+
+ + +
+
+
+ +
+ +
+ + +
+ +
+ + +
+ + + + + +
+
+
+ + +
+ + + + + +
+
+ + + + + + + diff --git a/admin-dashboard/src/css/admin.css b/admin-dashboard/src/css/admin.css new file mode 100644 index 0000000..5595da3 --- /dev/null +++ b/admin-dashboard/src/css/admin.css @@ -0,0 +1,958 @@ +/* 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; +} diff --git a/admin-dashboard/src/index.html b/admin-dashboard/src/index.html new file mode 100644 index 0000000..0440fa5 --- /dev/null +++ b/admin-dashboard/src/index.html @@ -0,0 +1,352 @@ + + + + + + KHY Admin Dashboard + + + + +
+ +
+
+

KHY Admin Dashboard

+

Product Management System

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

Manage your products here

+
+
+ +
+
+ + +
+
+

Product Management

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

Site Preview

+
+ +
+
+
+ +
+
+ + + +
+
+ + + + + + + + + + + + + + + + + diff --git a/admin-dashboard/src/js/admin.js b/admin-dashboard/src/js/admin.js new file mode 100644 index 0000000..0ea018e --- /dev/null +++ b/admin-dashboard/src/js/admin.js @@ -0,0 +1,543 @@ +// 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(); + } +}; diff --git a/admin-dashboard/src/js/categoryManager.js b/admin-dashboard/src/js/categoryManager.js new file mode 100644 index 0000000..afa48ca --- /dev/null +++ b/admin-dashboard/src/js/categoryManager.js @@ -0,0 +1,459 @@ +// 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 = ` +
+

No categories found

+

Add your first category to get started!

+ +
+ `; + 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 = ` +
+

${category.name}

+ ${category.id} +
+

${ + category.description || "No description" + }

+
+ ${productCount} product${ + productCount !== 1 ? "s" : "" + } +
+
+ +
+ `; + 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 + ? '

No products in this category yet. Click "Add Product to this Category" to get started!

' + : products + .map((product) => { + console.log( + "Rendering product:", + product.name, + "with ID:", + product.id + ); + return ` +
+
+

${product.name}

+

${ + product.description || "No description" + }

+
+
+ +
+
+ `; + }) + .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; diff --git a/admin-dashboard/src/js/imageManager.js b/admin-dashboard/src/js/imageManager.js new file mode 100644 index 0000000..8944dc6 --- /dev/null +++ b/admin-dashboard/src/js/imageManager.js @@ -0,0 +1,338 @@ +// 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 = ` +
+

No images found

+

Upload images to get started!

+ +
+ `; + 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 = ` +
+ ${image.name}Image Not Found'"> +
+
+
${image.name}
+
+ Product: ${image.productName} + Size: ${image.size} +
+
+
+ + + +
+ `; + 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 = ` + + `; + + // 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 = ` + ${file.name} +
+ ${file.name} + ${this.formatFileSize( + file.size + )} +
+ + `; + 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 = ` + + `; + + 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; diff --git a/admin-dashboard/src/js/productManager.js b/admin-dashboard/src/js/productManager.js new file mode 100644 index 0000000..a96605a --- /dev/null +++ b/admin-dashboard/src/js/productManager.js @@ -0,0 +1,1490 @@ +// Product Management Class +class ProductManager { + constructor(dashboard) { + this.dashboard = dashboard; + this.products = []; + this.categories = []; + this.currentProduct = null; + this.currentBlobUrls = []; + this.setupEventListeners(); + console.log("✅ ProductManager initialized with direct save system"); + } + + setupEventListeners() { + // Add product button + document + .getElementById("add-product-btn") + .addEventListener("click", async () => { + await this.showProductModal(); + }); + + // Product form submit + document.getElementById("product-form").addEventListener("submit", (e) => { + e.preventDefault(); + this.saveProduct(); + }); + + // Product search + document.getElementById("product-search").addEventListener("input", (e) => { + this.filterProducts(e.target.value); + }); + + // Image upload preview + document + .getElementById("product-images") + .addEventListener("change", async (e) => { + await this.previewImages(e.target.files); + }); + + // Add new additional field and image removal + document.addEventListener("click", (e) => { + if (e.target.id === "add-new-field-btn") { + this.showNewFieldForm(); + } else if (e.target.id === "save-new-field-btn") { + this.addNewField(); + } else if (e.target.id === "cancel-new-field-btn") { + this.hideNewFieldForm(); + } else if (e.target.matches(".remove-field-btn")) { + this.removeField(e.target); + } else if (e.target.matches("[data-action='remove-image']")) { + e.preventDefault(); + e.stopPropagation(); + const imageIndex = parseInt(e.target.getAttribute("data-image-index")); + this.removeExistingImage(imageIndex); + } else if (e.target.id === "cancel-product-btn") { + // Handle cancel button in product modal + e.preventDefault(); + this.hideProductModal(); + } + }); + } + + async loadData() { + try { + // Try to load from admin dashboard's local copy first + let response; + try { + response = await fetch("../data/products.json"); + console.log("✅ Loaded from admin dashboard local copy"); + } catch (localError) { + // Fallback to main website if admin local copy doesn't exist + response = await fetch("../../data/products.json"); + console.log("✅ Loaded from main website (fallback)"); + } + + const data = await response.json(); + this.products = data.products || []; + this.categories = data.categories || []; + this.populateCategorySelect(); + console.log("✅ Loaded data:", this.products.length, "products"); + } catch (error) { + console.error("Error loading product data:", error); + throw error; + } + } + + populateCategorySelect() { + const categorySelect = document.getElementById("product-category"); + categorySelect.innerHTML = ''; + + this.categories.forEach((category) => { + const option = document.createElement("option"); + option.value = category.id; + option.textContent = category.name; + categorySelect.appendChild(option); + }); + } + + renderProducts(productsToRender = null) { + const grid = document.getElementById("products-grid"); + + // Don't display any products, just show empty state + grid.innerHTML = ` +
+

Product Management

+

Use the "Add Product" button above to create new products.

+
+ `; + } + + createProductCard(product) { + const card = document.createElement("div"); + card.className = "product-card"; + + const category = this.categories.find((cat) => cat.id === product.category); + const categoryName = category ? category.name : product.category; + + card.innerHTML = ` +
+ No Image' + }" + alt="${product.name}" + onerror="this.src='data:image/svg+xml,No Image'"> +
+
+

${product.name}

+

${categoryName}

+

${ + product.description + ? product.description.substring(0, 100) + "..." + : "No description" + }

+
+ ID: ${product.id} + ${ + product.additionalInformation?.Warranty || + "No warranty info" + } +
+
+
+ + + +
+ `; + return card; + } + + async showProductModal(product = null) { + console.log("showProductModal called with product:", product); + const modal = document.getElementById("product-modal"); + console.log("Modal element:", modal); + const title = document.getElementById("modal-title"); + const form = document.getElementById("product-form"); + const additionalInfoSection = document.getElementById( + "additional-info-section" + ); + + this.currentProduct = product; + + if (product) { + title.textContent = "Edit Product"; + await this.populateForm(product); + this.populateAdditionalInfo(product); + additionalInfoSection.style.display = "block"; + } else { + title.textContent = "Add Product"; + form.reset(); + document.getElementById("product-warranty").value = "5 years"; + document.getElementById("image-preview").innerHTML = ""; + additionalInfoSection.style.display = "none"; + } + + console.log("Setting modal display to block"); + modal.style.display = "block"; + } + + hideProductModal() { + // Clean up blob URLs to prevent memory leaks + this.cleanupBlobUrls(); + + if (this.currentProduct && this.currentProduct.images) { + this.currentProduct.images.forEach((img) => { + if (img.startsWith("blob:")) { + URL.revokeObjectURL(img); + } + }); + } + + document.getElementById("product-modal").style.display = "none"; + this.currentProduct = null; + } + + async populateForm(product) { + document.getElementById("product-name").value = product.name || ""; + document.getElementById("product-category").value = product.category || ""; + document.getElementById("product-description").value = + product.description || ""; + + // Handle descriptionLong array + if (product.descriptionLong && Array.isArray(product.descriptionLong)) { + document.getElementById("product-description-long").value = + product.descriptionLong.join("\n"); + } else { + document.getElementById("product-description-long").value = + product.descriptionLong || ""; + } + + // Populate additional information + if (product.additionalInformation) { + document.getElementById("product-material").value = + product.additionalInformation.Material || ""; + document.getElementById("product-design").value = + product.additionalInformation.Design || ""; + document.getElementById("product-use-cases").value = + product.additionalInformation["Use Cases"] || ""; + } + + document.getElementById("product-warranty").value = + product.additionalInformation?.Warranty || "5 years"; + + // Clean up any expired blob URLs in the product data + // BUT preserve newly uploaded images that should be in IndexedDB + if (product.images) { + const originalCount = product.images.length; + const validImages = []; + + for (const img of product.images) { + if (img.startsWith("blob:")) { + // Check if this blob URL corresponds to a newly uploaded image in IndexedDB + try { + const imageData = await this.findImageDataInIndexedDB( + img, + product.id + ); + if (imageData && imageData.file) { + // This is a newly uploaded image - keep it and recreate blob URL + const newBlobUrl = URL.createObjectURL(imageData.file); + validImages.push(newBlobUrl); + + // Store for cleanup + if (!this.currentBlobUrls) this.currentBlobUrls = []; + this.currentBlobUrls.push(newBlobUrl); + + console.log( + `📁 Preserved newly uploaded image: ${imageData.name}` + ); + } else { + console.log(`🧹 Removed expired blob URL: ${img}`); + } + } catch (error) { + console.log(`🧹 Removed invalid blob URL: ${img}`); + } + } else { + // Regular image path - keep it + validImages.push(img); + } + } + + product.images = validImages; + const cleanedCount = product.images.length; + if (originalCount !== cleanedCount) { + console.log( + `🧹 Cleaned ${ + originalCount - cleanedCount + } expired blob URLs from product data` + ); + } + } + + // Show existing images + await this.showExistingImages(product); + } + + populateAdditionalInfo(product) { + const additionalInfoGrid = document.getElementById("additional-info-grid"); + + if (!product.additionalInformation) { + additionalInfoGrid.innerHTML = + "

No additional information available.

"; + return; + } + + // Clear existing content + additionalInfoGrid.innerHTML = ""; + + // Get all additional information fields + const additionalFields = product.additionalInformation; + + // Fields that are already shown in the main form (exclude these) + const excludedFields = ["Material", "Design", "Use Cases", "Warranty"]; + + // Create form fields for each additional information field + Object.keys(additionalFields).forEach((key) => { + if (!excludedFields.includes(key)) { + const value = additionalFields[key]; + const fieldId = `additional-${key + .toLowerCase() + .replace(/[^a-z0-9]/g, "-")}`; + + // Determine if it should be a textarea (for longer content) + const isLongContent = value && value.length > 50; + + const fieldElement = document.createElement("div"); + fieldElement.className = "additional-info-item"; + + if (isLongContent) { + fieldElement.innerHTML = ` + + + `; + } else { + fieldElement.innerHTML = ` + + + `; + } + + additionalInfoGrid.appendChild(fieldElement); + } + }); + + // If no additional fields to show, display a message + if (additionalInfoGrid.children.length === 0) { + additionalInfoGrid.innerHTML = + "

No additional information fields to display.

"; + } + } + + showNewFieldForm() { + const additionalInfoGrid = document.getElementById("additional-info-grid"); + + // Remove any existing new field form + this.hideNewFieldForm(); + + // Create new field form + const newFieldForm = document.createElement("div"); + newFieldForm.className = "new-field-form"; + newFieldForm.innerHTML = ` +
Add New Field
+
+ + + + +
+ `; + + // Insert at the beginning of the grid + additionalInfoGrid.insertBefore( + newFieldForm, + additionalInfoGrid.firstChild + ); + + // Focus on the title input + document.getElementById("new-field-title").focus(); + } + + hideNewFieldForm() { + const existingForm = document.querySelector(".new-field-form"); + if (existingForm) { + existingForm.remove(); + } + } + + addNewField() { + const titleInput = document.getElementById("new-field-title"); + const valueInput = document.getElementById("new-field-value"); + + const title = titleInput.value.trim(); + const value = valueInput.value.trim(); + + if (!title || !value) { + alert("Both title and value are required!"); + return; + } + + // Check if field already exists + const existingField = document.querySelector( + `[name="additionalInfo.${title}"]` + ); + if (existingField) { + alert("A field with this title already exists!"); + return; + } + + // Create new field element + const additionalInfoGrid = document.getElementById("additional-info-grid"); + const fieldId = `additional-${title + .toLowerCase() + .replace(/[^a-z0-9]/g, "-")}`; + const isLongContent = value.length > 50; + + const fieldElement = document.createElement("div"); + fieldElement.className = "additional-info-item"; + + if (isLongContent) { + fieldElement.innerHTML = ` + + + `; + } else { + fieldElement.innerHTML = ` + + + `; + } + + // Insert after the new field form + const newFieldForm = document.querySelector(".new-field-form"); + additionalInfoGrid.insertBefore(fieldElement, newFieldForm.nextSibling); + + // Hide the form and clear inputs + this.hideNewFieldForm(); + } + + removeField(button) { + const fieldName = button.getAttribute("data-field-name"); + if (confirm(`Are you sure you want to remove the "${fieldName}" field?`)) { + const fieldElement = button.closest(".additional-info-item"); + fieldElement.remove(); + } + } + + async showExistingImages(product) { + const preview = document.getElementById("image-preview"); + preview.innerHTML = ""; + + console.log( + `🖼️ showExistingImages called with ${ + product.images ? product.images.length : 0 + } images` + ); + + if (product.images && product.images.length > 0) { + // Clean up any existing blob URLs + this.cleanupBlobUrls(); + + console.log( + `🖼️ Processing ${product.images.length} images:`, + product.images + ); + + for (let index = 0; index < product.images.length; index++) { + const imagePath = product.images[index]; + const imageDiv = document.createElement("div"); + imageDiv.className = "image-preview-item"; + + // Handle different types of image paths + let imageSrc; + if (imagePath.startsWith("blob:")) { + // This is an expired blob URL - try to find the image in IndexedDB + console.log(`⚠️ Found expired blob URL: ${imagePath}`); + try { + const imageData = await this.findImageDataInIndexedDB( + imagePath, + product.id + ); + if (imageData && imageData.file) { + // Create new blob URL from stored file + const newBlobUrl = URL.createObjectURL(imageData.file); + imageSrc = newBlobUrl; + + // Store the new blob URL for cleanup later + if (!this.currentBlobUrls) this.currentBlobUrls = []; + this.currentBlobUrls.push(newBlobUrl); + + console.log( + `📁 Recreated blob URL from IndexedDB: ${imageData.name}` + ); + } else { + // Show error placeholder + imageSrc = + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2VlZSIvPjx0ZXh0IHg9IjUwIiB5PSI1MCIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjEyIiBmaWxsPSIjOTk5IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSI+SW1hZ2UgRXJyb3I8L3RleHQ+PC9zdmc+"; + console.log(`❌ No IndexedDB data found for expired blob URL`); + } + } catch (error) { + console.error("❌ Error loading IndexedDB image:", error); + imageSrc = + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2VlZSIvPjx0ZXh0IHg9IjUwIiB5PSI1MCIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjEyIiBmaWxsPSIjOTk5IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSI+SW1hZ2UgRXJyb3I8L3RleHQ+PC9zdmc+"; + } + } else if ( + imagePath.includes("product_") && + imagePath.includes("_cover.jpg") + ) { + // This is a newly uploaded image that should be in IndexedDB + try { + const imageData = await this.findImageDataInIndexedDB( + imagePath, + product.id + ); + if (imageData && imageData.file) { + // Create new blob URL from stored file + const newBlobUrl = URL.createObjectURL(imageData.file); + imageSrc = newBlobUrl; + + // Store the new blob URL for cleanup later + if (!this.currentBlobUrls) this.currentBlobUrls = []; + this.currentBlobUrls.push(newBlobUrl); + + console.log(`📁 Displaying IndexedDB image: ${imageData.name}`); + } else { + // Fallback to original path + imageSrc = this.fixImagePath(imagePath); + console.log(`📷 Fallback to original path: ${imagePath}`); + } + } catch (error) { + console.error("❌ Error loading IndexedDB image:", error); + imageSrc = this.fixImagePath(imagePath); + } + } else { + // This is an existing image path - fix it for admin dashboard + imageSrc = this.fixImagePath(imagePath); + console.log(`📷 Displaying original image: ${imagePath}`); + } + + console.log( + `🖼️ Creating image ${index + 1} with src: ${imageSrc.substring( + 0, + 50 + )}...` + ); + + imageDiv.innerHTML = ` + Product image ${
+          index + 1
+        } + + `; + preview.appendChild(imageDiv); + console.log(`✅ Image ${index + 1} added to DOM`); + } + } + } + + async findImageDataInIndexedDB(imagePath, productId) { + try { + // Get all image files from IndexedDB + const allImageFiles = await window.storageManager.getAllImageFiles(); + console.log( + `🔍 Searching IndexedDB for image. Total files: ${allImageFiles.length}` + ); + + // Extract filename from the image path + const filename = imagePath.split("/").pop(); + console.log( + `🔍 Looking for filename: ${filename}, Product ID: ${productId}` + ); + + // Find the matching image data with multiple strategies + let matchingImage = null; + + // Strategy 1: Direct filename match + matchingImage = allImageFiles.find((img) => img.name === filename); + if (matchingImage) { + console.log(`✅ Found by filename match: ${matchingImage.name}`); + return matchingImage; + } + + // Strategy 2: Match by product ID (for newly uploaded images) + matchingImage = allImageFiles.find( + (img) => + img.id.includes(productId.toString()) && + (img.name.includes("cover") || + img.name.includes("jpg") || + img.name.includes("jpeg") || + img.name.includes("png")) + ); + if (matchingImage) { + console.log(`✅ Found by product ID match: ${matchingImage.name}`); + return matchingImage; + } + + // Strategy 3: Match by partial filename (for generated paths) + if (filename.includes("product_") || filename.includes("cover")) { + matchingImage = allImageFiles.find((img) => + img.id.includes(productId.toString()) + ); + if (matchingImage) { + console.log(`✅ Found by partial match: ${matchingImage.name}`); + return matchingImage; + } + } + + // Strategy 4: If it's a blob URL, try to find any image for this product + if (imagePath.startsWith("blob:")) { + matchingImage = allImageFiles.find((img) => + img.id.includes(productId.toString()) + ); + if (matchingImage) { + console.log(`✅ Found blob URL replacement: ${matchingImage.name}`); + return matchingImage; + } + } + + console.log(`❌ No matching image found in IndexedDB for: ${filename}`); + console.log( + `Available images:`, + allImageFiles.map((img) => ({ id: img.id, name: img.name })) + ); + return null; + } catch (error) { + console.error("❌ Error searching IndexedDB for image:", error); + return null; + } + } + + cleanupBlobUrls() { + if (this.currentBlobUrls) { + this.currentBlobUrls.forEach((url) => { + URL.revokeObjectURL(url); + }); + this.currentBlobUrls = []; + } + } + + fixImagePath(imagePath) { + // Fix image path for admin dashboard + if (imagePath && !imagePath.startsWith("http")) { + // Remove any leading slashes and ensure we have the correct path + let cleanPath = imagePath.replace(/^\/+/, ""); + + // If it starts with 'assets/', use it as is + if (cleanPath.startsWith("assets/")) { + return "/" + cleanPath; + } + + // If it starts with 'src/assets/', remove the 'src/' part + if (cleanPath.startsWith("src/assets/")) { + return "/" + cleanPath.replace("src/", ""); + } + + // For other paths, assume they need to be prefixed with 'assets/' + return "/" + cleanPath; + } + return imagePath; + } + + async saveProduct() { + console.log("🚀 SAVE PRODUCT STARTED - DIRECT SAVE MODE"); + console.log( + "Current product (editing):", + this.currentProduct ? "YES" : "NO" + ); + + const formData = new FormData(document.getElementById("product-form")); + const productData = this.extractFormData(formData); + + try { + // Track changes for undo before making any modifications + await this.trackChangesForUndo(productData); + + // Handle images - separate existing paths from blob URLs + const existingImages = (productData.images || []).filter( + (img) => !img.startsWith("blob:") + ); + + console.log("🖼️ IMAGE PROCESSING:"); + console.log("Existing images (non-blob):", existingImages); + + // Handle new file uploads + const imageInput = document.getElementById("product-images"); + if (imageInput.files.length > 0) { + console.log("📁 New files to upload:", imageInput.files.length); + const newImages = await this.uploadImages(imageInput.files); + productData.images = [...existingImages, ...newImages]; + console.log("🆕 New image paths:", newImages); + } else { + // Only keep existing non-blob images + productData.images = existingImages; + console.log("📷 No new files, keeping existing images"); + } + + console.log("🖼️ Final images array:", productData.images); + + // Generate new ID if adding new product + if (!this.currentProduct) { + productData.id = this.getNextProductId(); + console.log("🆔 NEW PRODUCT - Generated ID:", productData.id); + } else { + productData.id = this.currentProduct.id; + console.log("✏️ EDITING PRODUCT - Using existing ID:", productData.id); + } + + console.log("📦 FINAL PRODUCT DATA BEFORE DIRECT SAVE:", productData); + + // Save directly to actual data + await this.saveDirectlyToData(productData); + + // Close modal and show success message + this.hideProductModal(); + this.dashboard.showMessage("Product saved successfully!", "success"); + + // Refresh the current view to show changes + this.refreshCurrentView(); + } catch (error) { + console.error("❌ ERROR SAVING PRODUCT:", error); + this.dashboard.showMessage( + "Error saving product: " + error.message, + "error" + ); + } + } + + // Track changes for undo functionality + async trackChangesForUndo(productData) { + console.log("📝 TRACKING CHANGES FOR UNDO"); + + // Get current undo stack from localStorage + let undoStack = JSON.parse(localStorage.getItem("khy_undo_stack") || "[]"); + + if (this.currentProduct) { + // Editing existing product - store the original state + const originalProduct = { ...this.currentProduct }; + const changeEntry = { + id: Date.now(), + timestamp: new Date().toISOString(), + action: "edit", + productId: productData.id, + originalData: originalProduct, + newData: { ...productData }, + }; + + undoStack.push(changeEntry); + console.log("📌 Tracked EDIT change:", changeEntry); + } else { + // Adding new product - just track the addition + const changeEntry = { + id: Date.now(), + timestamp: new Date().toISOString(), + action: "add", + productId: productData.id, + newData: { ...productData }, + }; + + undoStack.push(changeEntry); + console.log("📌 Tracked ADD change:", changeEntry); + } + + // Keep only last 10 changes to prevent excessive memory usage + if (undoStack.length > 10) { + undoStack = undoStack.slice(-10); + } + + localStorage.setItem("khy_undo_stack", JSON.stringify(undoStack)); + console.log("💾 Undo stack updated. Total changes:", undoStack.length); + } + + // Save directly to the actual products.json + async saveDirectlyToData(productData) { + console.log("💾 SAVING DIRECTLY TO DATA"); + + if (this.currentProduct) { + // Editing existing product + const index = this.products.findIndex((p) => p.id === productData.id); + if (index !== -1) { + this.products[index] = productData; + console.log("✏️ Updated existing product at index:", index); + } + } else { + // Adding new product + this.products.push(productData); + console.log( + "➕ Added new product. Total products:", + this.products.length + ); + } + + // Save to file + await this.saveData(); + console.log("💾 Data saved to products.json"); + } + + // Save data to the products.json file + async saveData() { + try { + const dataToSave = { + products: this.products, + categories: this.categories, + }; + + console.log("💾 SAVING DATA TO FILE:"); + console.log("📦 Products to save:", this.products.length); + console.log("📂 Categories to save:", this.categories.length); + + // Store in localStorage for backup + localStorage.setItem("khy_products_backup", JSON.stringify(dataToSave)); + + // Write to admin dashboard's local copy for sync + await this.writeToAdminLocal(dataToSave); + + // Create a downloadable file for manual sync + this.createDownloadableFile(dataToSave); + + // Show instructions for manual sync + console.log("📋 TO SYNC TO MAIN WEBSITE:"); + console.log("1. Two files have been downloaded:"); + console.log(" - 'products_updated.json' (for main website)"); + console.log(" - 'admin_local_products.json' (for admin dashboard)"); + console.log("2. QUICK SYNC - Copy this command and run it in terminal:"); + console.log( + " cp ~/Downloads/products_updated.json ../data/products.json" + ); + console.log(" (from the admin-dashboard directory)"); + console.log("3. Refresh the preview to see changes"); + console.log( + "💡 The admin dashboard will now show your changes immediately!" + ); + } catch (error) { + console.error("❌ Error saving data:", error); + throw error; + } + } + + // Write to admin dashboard's local copy using a simple approach + async writeToAdminLocal(data) { + try { + // Since we can't write files directly from browser, we'll use a different approach + // Store the data in a special localStorage key that can be accessed by a sync script + localStorage.setItem("khy_admin_local_copy", JSON.stringify(data)); + console.log("✅ Data stored for admin local copy"); + + // Also create a downloadable file specifically for admin local copy + const dataStr = JSON.stringify(data, null, 2); + const dataBlob = new Blob([dataStr], { type: "application/json" }); + const url = URL.createObjectURL(dataBlob); + + const downloadLink = document.createElement("a"); + downloadLink.href = url; + downloadLink.download = "admin_local_products.json"; + downloadLink.style.display = "none"; + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); + URL.revokeObjectURL(url); + + console.log( + "✅ Admin local copy downloaded as 'admin_local_products.json'" + ); + } catch (error) { + console.log("⚠️ Could not create admin local copy:", error); + } + } + + // Create a downloadable file for manual sync + createDownloadableFile(data) { + try { + const dataStr = JSON.stringify(data, null, 2); + const dataBlob = new Blob([dataStr], { type: "application/json" }); + const url = URL.createObjectURL(dataBlob); + + // Create a temporary download link + const downloadLink = document.createElement("a"); + downloadLink.href = url; + downloadLink.download = "products_updated.json"; + downloadLink.style.display = "none"; + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); + URL.revokeObjectURL(url); + + console.log("✅ Updated products.json downloaded for manual sync"); + } catch (error) { + console.log("⚠️ Could not create download file:", error); + } + } + + // Refresh current view to show changes immediately + refreshCurrentView() { + console.log("🔄 REFRESHING CURRENT VIEW"); + + // If we're in category view, refresh that + if (this.dashboard.currentSection === "category-products") { + // Get the current category from the URL or state + const categoryButton = document.querySelector("[data-category-id]"); + if (categoryButton) { + const categoryId = categoryButton.getAttribute("data-category-id"); + // Refresh the category products view + if (this.dashboard.categoryManager) { + this.dashboard.categoryManager.viewCategoryProducts(categoryId); + } + } + } + + // Always refresh the preview if it exists + this.refreshPreview(); + } + + // Refresh the preview iframe + refreshPreview() { + console.log("🖼️ REFRESHING PREVIEW"); + const iframe = document.getElementById("preview-iframe"); + if (iframe && iframe.src) { + // Add a timestamp to force refresh + const url = new URL(iframe.src); + url.searchParams.set("_refresh", Date.now()); + iframe.src = url.toString(); + console.log("🔄 Preview refreshed with timestamp"); + } + } + + extractFormData(formData) { + console.log("🔍 EXTRACTING FORM DATA:"); + console.log("FormData entries:", Array.from(formData.entries())); + + const productData = { + name: formData.get("name"), + category: formData.get("category"), + description: formData.get("description"), + warranty: formData.get("warranty"), + }; + + console.log("📝 Basic product data:", productData); + + // Handle descriptionLong + const descriptionLong = formData.get("descriptionLong"); + if (descriptionLong) { + productData.descriptionLong = descriptionLong + .split("\n") + .filter((line) => line.trim()); + console.log("📄 DescriptionLong array:", productData.descriptionLong); + } + + // Handle additional information + const material = formData.get("material"); + const design = formData.get("design"); + const useCases = formData.get("useCases"); + const warranty = formData.get("warranty"); + + console.log("🔧 Additional info fields:", { + material, + design, + useCases, + warranty, + }); + + // Start with existing additional information or create new object + productData.additionalInformation = + this.currentProduct?.additionalInformation || {}; + + // Update main fields + if (material) productData.additionalInformation.Material = material; + if (design) productData.additionalInformation.Design = design; + if (useCases) productData.additionalInformation["Use Cases"] = useCases; + if (warranty) productData.additionalInformation.Warranty = warranty; + + // Handle all additional information fields from the form + for (const [key, value] of formData.entries()) { + if (key.startsWith("additionalInfo.")) { + const fieldName = key.replace("additionalInfo.", ""); + if (value && value.trim()) { + productData.additionalInformation[fieldName] = value.trim(); + } + } + } + + console.log( + "📋 Additional Information object:", + productData.additionalInformation + ); + + // Preserve existing images + if (this.currentProduct && this.currentProduct.images) { + productData.images = [...this.currentProduct.images]; + console.log("🖼️ Existing images:", productData.images); + } + + console.log("✅ FINAL EXTRACTED PRODUCT DATA:", productData); + return productData; + } + + getNextProductId() { + const maxId = Math.max(...this.products.map((p) => p.id), 0); + return maxId + 1; + } + + async editProduct(id) { + console.log( + "ProductManager.editProduct called with ID:", + id, + "Type:", + typeof id + ); + console.log("Available products:", this.products.length); + // Convert string ID to number for comparison + const numericId = typeof id === "string" ? parseInt(id, 10) : id; + console.log("Looking for numeric ID:", numericId); + const product = this.products.find((p) => p.id === numericId); + console.log("Found product:", product); + if (product) { + console.log("Showing product modal for:", product.name); + await this.showProductModal(product); + } else { + console.log("Product not found with ID:", numericId); + // Debug: show first few product IDs for comparison + console.log( + "First 5 product IDs:", + this.products + .slice(0, 5) + .map((p) => ({ id: p.id, name: p.name, type: typeof p.id })) + ); + } + } + + async duplicateProduct(id) { + const numericId = typeof id === "string" ? parseInt(id, 10) : id; + const product = this.products.find((p) => p.id === numericId); + if (product) { + const duplicatedProduct = { + ...product, + id: this.getNextProductId(), + name: product.name + " (Copy)", + }; + await this.showProductModal(duplicatedProduct); + } + } + + async deleteProduct(id) { + const numericId = typeof id === "string" ? parseInt(id, 10) : id; + const product = this.products.find((p) => p.id === numericId); + if (!product) return; + + if ( + confirm( + `Are you sure you want to delete "${product.name}"? This action cannot be undone.` + ) + ) { + try { + // Remove product images if any + if (product.images) { + await this.deleteProductImages(product.images); + } + + // Remove from products array + this.products = this.products.filter((p) => p.id !== numericId); + await this.saveData(); + this.renderProducts(); + this.dashboard.showMessage("Product deleted successfully!", "success"); + } catch (error) { + this.dashboard.showMessage( + "Error deleting product: " + error.message, + "error" + ); + } + } + } + + removeExistingImage(index) { + console.log("removeExistingImage called with index:", index); + if (this.currentProduct && this.currentProduct.images) { + const imagePath = this.currentProduct.images[index]; + const imageName = imagePath ? imagePath.split("/").pop() : "this image"; + + console.log("Showing confirmation dialog for:", imageName); + this.showImageDeleteConfirmation(imageName, index, imagePath); + } + } + + showImageDeleteConfirmation(imageName, index, imagePath) { + // Create custom confirmation dialog + const confirmationDiv = document.createElement("div"); + confirmationDiv.className = "image-delete-confirmation"; + confirmationDiv.innerHTML = ` +
+

Delete Image

+

Do you really want to delete ${imageName}?

+

This action is irreversible and will permanently remove the image from this product.

+
+ + +
+
+ `; + + // Add to modal + const modal = document.getElementById("product-modal"); + modal.appendChild(confirmationDiv); + + // Add event listeners + document + .getElementById("cancel-image-delete") + .addEventListener("click", () => { + confirmationDiv.remove(); + }); + + document + .getElementById("confirm-image-delete") + .addEventListener("click", async () => { + const imageToRemove = this.currentProduct.images[index]; + + // Track deleted image for sync + this.trackDeletedImage(imagePath, imageName); + + // Clean up blob URL if it's a blob + if (imageToRemove && imageToRemove.startsWith("blob:")) { + URL.revokeObjectURL(imageToRemove); + } + + this.currentProduct.images.splice(index, 1); + await this.showExistingImages(this.currentProduct); + console.log(`Image removed: ${imageName}`); + confirmationDiv.remove(); + }); + } + + async trackDeletedImage(imagePath, imageName) { + console.log("🗑️ TRACKING DELETED IMAGE:"); + console.log("Image path:", imagePath); + console.log("Image name:", imageName); + + try { + // Get current temporary changes + const temporaryChanges = + await window.storageManager.getTemporaryChanges(); + + // Initialize image tracking if not exists + temporaryChanges.imageChanges = temporaryChanges.imageChanges || {}; + + // Find the product ID (could be current product or new product being created) + const productId = this.currentProduct ? this.currentProduct.id : "new"; + + temporaryChanges.imageChanges[productId] = temporaryChanges.imageChanges[ + productId + ] || { + newImages: [], + deletedImages: [], + blobImages: [], + finalImages: [], + }; + + // Add to deleted images if not already there + const existingDeleted = temporaryChanges.imageChanges[ + productId + ].deletedImages.find((img) => img.path === imagePath); + + if (!existingDeleted) { + temporaryChanges.imageChanges[productId].deletedImages.push({ + path: imagePath, + name: imageName, + timestamp: Date.now(), + }); + } + + // Save updated tracking + await window.storageManager.saveTemporaryChanges(temporaryChanges); + + console.log( + "📸 Updated image tracking:", + temporaryChanges.imageChanges[productId] + ); + } catch (error) { + console.error("❌ Error tracking deleted image:", error); + } + } + + filterProducts(searchTerm) { + if (!searchTerm) { + this.renderProducts(); + return; + } + + const filtered = this.products.filter( + (product) => + product.name.toLowerCase().includes(searchTerm.toLowerCase()) || + product.description.toLowerCase().includes(searchTerm.toLowerCase()) || + product.category.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + this.renderProducts(filtered); + } + + // Temporary Storage Methods + async addToTemporaryStorage(productData) { + console.log("💾 ADDING TO TEMPORARY STORAGE:"); + + try { + // Get existing temporary storage or create new + let temporaryChanges = await window.storageManager.getTemporaryChanges(); + + console.log("📦 Existing temporary changes:", temporaryChanges); + + // Initialize image tracking if not exists + temporaryChanges.imageChanges = temporaryChanges.imageChanges || {}; + temporaryChanges.imageChanges[productData.id] = temporaryChanges + .imageChanges[productData.id] || { + newImages: [], + deletedImages: [], + }; + + // Track image changes + await this.trackImageChanges(productData, temporaryChanges); + + // Store the product change + temporaryChanges.products = temporaryChanges.products || {}; + + const finalProductData = { + ...productData, + timestamp: Date.now(), + action: this.currentProduct ? "updated" : "created", + }; + + temporaryChanges.products[productData.id] = finalProductData; + + console.log("🏷️ Product metadata:", { + id: productData.id, + timestamp: finalProductData.timestamp, + action: finalProductData.action, + }); + + console.log( + "📸 Image changes tracked:", + temporaryChanges.imageChanges[productData.id] + ); + + console.log("📋 FINAL PRODUCT STRUCTURE IN TEMPORARY STORAGE:"); + console.log(JSON.stringify(finalProductData, null, 2)); + + // Save to IndexedDB + await window.storageManager.saveTemporaryChanges(temporaryChanges); + + console.log( + "✅ COMPLETE TEMPORARY STORAGE STRUCTURE SAVED TO INDEXEDDB:" + ); + console.log(JSON.stringify(temporaryChanges, null, 2)); + } catch (error) { + console.error("❌ Error saving to temporary storage:", error); + } + } + + async trackImageChanges(productData, temporaryChanges) { + const productId = productData.id; + const currentImages = this.currentProduct + ? this.currentProduct.images || [] + : []; + const newImages = productData.images || []; + + console.log("📸 TRACKING IMAGE CHANGES:"); + console.log("Current images:", currentImages); + console.log("New images:", newImages); + + // Find new images (blob URLs or new file uploads) + const newImageFiles = []; + const imageInput = document.getElementById("product-images"); + if (imageInput && imageInput.files.length > 0) { + for (const file of Array.from(imageInput.files)) { + // Save file to IndexedDB and get reference + const imageId = `${productId}_${Date.now()}_${file.name}`; + await window.storageManager.saveImageFile(imageId, file); + + newImageFiles.push({ + imageId: imageId, + name: file.name, + size: file.size, + type: file.type, + lastModified: file.lastModified, + }); + } + } + + // Find deleted images + const deletedImages = []; + currentImages.forEach((oldImage) => { + if (!newImages.includes(oldImage)) { + deletedImages.push({ + path: oldImage, + name: oldImage.split("/").pop(), + }); + } + }); + + // Find blob URLs (temporary preview images) + const blobImages = newImages.filter((img) => img.startsWith("blob:")); + + // Update image tracking + temporaryChanges.imageChanges[productId] = { + newImages: newImageFiles, + deletedImages: deletedImages, + blobImages: blobImages, + finalImages: newImages.filter((img) => !img.startsWith("blob:")), + }; + + console.log("📁 New image files:", newImageFiles); + console.log("🗑️ Deleted images:", deletedImages); + console.log("🖼️ Blob images (temporary):", blobImages); + } + + async updatePreviewWithTemporaryData() { + try { + // Get temporary changes + const temporaryChanges = + await window.storageManager.getTemporaryChanges(); + + if ( + temporaryChanges.products && + Object.keys(temporaryChanges.products).length > 0 + ) { + // Create a merged dataset for preview + const previewData = this.createPreviewData(temporaryChanges); + + // Update preview iframe with temporary data + this.updatePreviewIframe(previewData); + } + } catch (error) { + console.error("❌ Error updating preview with temporary data:", error); + } + } + + createPreviewData(temporaryChanges) { + // Start with original products + const previewProducts = [...this.products]; + + // Apply temporary changes + if (temporaryChanges.products) { + Object.values(temporaryChanges.products).forEach((tempProduct) => { + const existingIndex = previewProducts.findIndex( + (p) => p.id === tempProduct.id + ); + + if (existingIndex >= 0) { + // Update existing product + previewProducts[existingIndex] = { + ...previewProducts[existingIndex], + ...tempProduct, + }; + } else { + // Add new product + previewProducts.push(tempProduct); + } + }); + } + + return { + products: previewProducts, + categories: this.categories, + }; + } + + updatePreviewIframe(previewData) { + // Send data to preview iframe + const iframe = document.getElementById("preview-iframe"); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage( + { + type: "UPDATE_PREVIEW_DATA", + data: previewData, + }, + "*" + ); + } + } + + async undoTemporaryChanges() { + try { + const temporaryChanges = + await window.storageManager.getTemporaryChanges(); + + if ( + !temporaryChanges.products || + Object.keys(temporaryChanges.products).length === 0 + ) { + this.dashboard.showMessage("No temporary changes to undo.", "info"); + return; + } + + // Clear temporary storage + await window.storageManager.clearAllTemporaryData(); + + // Update preview with original data + this.updatePreviewWithTemporaryData(); + + // Refresh product list + this.renderProducts(); + + this.dashboard.showMessage("Temporary changes undone!", "success"); + } catch (error) { + console.error("❌ Error undoing changes:", error); + this.dashboard.showMessage( + "Error undoing changes. Please try again.", + "error" + ); + } + } + + async previewImages(files) { + if (!files || files.length === 0) return; + + console.log(`📁 Previewing ${files.length} new image(s)`); + + // Add new images to current product's images array + for (const file of Array.from(files)) { + if (file.type.startsWith("image/")) { + console.log(`📷 Processing image: ${file.name}`); + + // Create a temporary URL for preview + const tempUrl = URL.createObjectURL(file); + + // Store blob URL for cleanup + if (!this.currentBlobUrls) this.currentBlobUrls = []; + this.currentBlobUrls.push(tempUrl); + + // Add to current product's images array + if (this.currentProduct) { + if (!this.currentProduct.images) { + this.currentProduct.images = []; + } + // Add temporary URL for immediate preview + this.currentProduct.images.push(tempUrl); + console.log( + `📁 Added image to existing product. Total images: ${this.currentProduct.images.length}` + ); + } else { + // If no current product (adding new product), create a temporary one for preview + this.currentProduct = { + images: [tempUrl], + }; + console.log(`📁 Created new product with image. Total images: 1`); + } + } + } + + // Refresh the display to show all images (existing + new) + if (this.currentProduct) { + console.log( + `🖼️ Refreshing display with ${this.currentProduct.images.length} total images` + ); + await this.showExistingImages(this.currentProduct); + } + } + + 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 + // For now, we'll create a local path + const filename = `product_${Date.now()}_${file.name}`; + const imagePath = `assets/images/products/${filename}`; + uploadedImages.push(imagePath); + + // In a real implementation, you'd save the file + console.log(`Would upload: ${file.name} to ${imagePath}`); + } + } + + return uploadedImages; + } + + async deleteProductImages(imagePaths) { + // In a real implementation, you'd delete from server + console.log("Would delete images:", imagePaths); + } + + async saveData() { + const data = { + products: this.products, + categories: this.categories, + }; + + // In a real implementation, this would save to a server + // For now, we'll just update the local data + console.log("Saving data:", data); + + // Simulate API call + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 500); + }); + } +} + +// Make ProductManager available globally +window.ProductManager = ProductManager; diff --git a/admin-dashboard/src/js/storageManager.js b/admin-dashboard/src/js/storageManager.js new file mode 100644 index 0000000..f3974f4 --- /dev/null +++ b/admin-dashboard/src/js/storageManager.js @@ -0,0 +1,245 @@ +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(); diff --git a/admin-dashboard/start.sh b/admin-dashboard/start.sh new file mode 100755 index 0000000..6334a08 --- /dev/null +++ b/admin-dashboard/start.sh @@ -0,0 +1,6 @@ +#!/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