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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0 products in this category
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ โณ
+ Preparing sync...
+
+
+ โณ
+ Cloning main repository...
+
+
+ โณ
+ Syncing products...
+
+
+ โณ
+ Syncing images...
+
+
+ โณ
+ Committing changes...
+
+
+ โณ
+ Pushing to main site...
+
+
+
+
+
+
+
+
+
+
+
+
+
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.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 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 = `
+
+
+
+
+
+
๐ค
+
Drag & Drop images here
+
or click to select files
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ // 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}
+ ${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,
'">
+
+
+
${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 = `
+
+
+ `;
+ 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