Add core admin dashboard files (HTML, CSS, JS)

This commit is contained in:
George Birikorang 2025-09-19 21:32:46 -07:00
parent 889578f180
commit 4d590fd822
9 changed files with 4663 additions and 0 deletions

View file

@ -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();
}
};

View file

@ -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 = `
<div class="no-categories">
<h3>No categories found</h3>
<p>Add your first category to get started!</p>
<button class="btn btn-primary" onclick="categoryManager.showCategoryModal()">Add Category</button>
</div>
`;
return;
}
container.innerHTML = "";
this.categories.forEach((category) => {
const categoryCard = this.createCategoryCard(category);
container.appendChild(categoryCard);
});
}
createCategoryCard(category) {
const card = document.createElement("div");
card.className = "category-card";
// Count products in this category
const productCount = this.dashboard.productManager.products.filter(
(p) => p.category === category.id
).length;
card.innerHTML = `
<div class="category-header">
<h3>${category.name}</h3>
<span class="category-id">${category.id}</span>
</div>
<p class="category-description">${
category.description || "No description"
}</p>
<div class="category-stats">
<span class="product-count">${productCount} product${
productCount !== 1 ? "s" : ""
}</span>
</div>
<div class="category-actions">
<button class="btn btn-sm btn-secondary" data-category-id="${
category.id
}">
View Products
</button>
</div>
`;
return card;
}
showCategoryModal(category = null) {
const modal = document.getElementById("category-modal");
const title = document.getElementById("category-modal-title");
const form = document.getElementById("category-form");
this.currentCategory = category;
if (category) {
title.textContent = "Edit Category";
this.populateForm(category);
} else {
title.textContent = "Add Category";
form.reset();
}
modal.style.display = "block";
}
hideCategoryModal() {
document.getElementById("category-modal").style.display = "none";
this.currentCategory = null;
}
populateForm(category) {
document.getElementById("category-id").value = category.id;
document.getElementById("category-name").value = category.name;
document.getElementById("category-description").value =
category.description || "";
}
async saveCategory() {
const formData = new FormData(document.getElementById("category-form"));
const categoryData = {
id: formData.get("id"),
name: formData.get("name"),
description: formData.get("description"),
};
try {
// Validate required fields
if (!categoryData.id || !categoryData.name) {
throw new Error("Category ID and name are required");
}
// Check for duplicate ID (only for new categories)
if (!this.currentCategory) {
const existingCategory = this.categories.find(
(cat) => cat.id === categoryData.id
);
if (existingCategory) {
throw new Error("A category with this ID already exists");
}
}
// Update or add category
const existingIndex = this.categories.findIndex(
(cat) => cat.id === categoryData.id
);
if (existingIndex >= 0) {
this.categories[existingIndex] = {
...this.categories[existingIndex],
...categoryData,
};
} else {
this.categories.push(categoryData);
}
await this.saveData();
this.renderCategories();
this.hideCategoryModal();
this.dashboard.showMessage("Category saved successfully!", "success");
// Update product manager's category select
if (this.dashboard.productManager) {
this.dashboard.productManager.populateCategorySelect();
}
} catch (error) {
this.dashboard.showMessage(
"Error saving category: " + error.message,
"error"
);
}
}
editCategory(id) {
const category = this.categories.find((cat) => cat.id === id);
if (category) {
this.showCategoryModal(category);
}
}
viewCategoryProducts(categoryId) {
const category = this.categories.find((cat) => cat.id === categoryId);
if (!category) return;
// Get products in this category
const productsInCategory = this.dashboard.productManager.products.filter(
(p) => p.category === categoryId
);
// Switch to the dedicated category products section
this.dashboard.switchSection("category-products");
// Render the category products
this.showCategoryProductsScreen(category, productsInCategory);
}
showCategoryProductsScreen(category, products) {
console.log("showCategoryProductsScreen called with category:", category);
console.log("Products in category:", products);
// Update the section title
const title = document.getElementById("category-products-title");
if (title) {
title.textContent = `${category.name} Products`;
}
// Update the product count
const productCount = document.getElementById("category-product-count");
if (productCount) {
productCount.textContent = `${products.length} product${
products.length !== 1 ? "s" : ""
} in this category`;
}
// Update the add product button
const addButton = document.getElementById("add-product-to-category-btn");
if (addButton) {
addButton.setAttribute("data-category-id", category.id);
}
// Update the products list
const productsList = document.getElementById("category-products-list");
if (productsList) {
productsList.innerHTML =
products.length === 0
? '<p class="no-products">No products in this category yet. Click "Add Product to this Category" to get started!</p>'
: products
.map((product) => {
console.log(
"Rendering product:",
product.name,
"with ID:",
product.id
);
return `
<div class="product-card">
<div class="product-info">
<h4>${product.name}</h4>
<p class="product-description">${
product.description || "No description"
}</p>
</div>
<div class="product-actions">
<button class="btn btn-sm btn-primary" data-action="edit-product" data-product-id="${
product.id
}">
✏️ Edit
</button>
</div>
</div>
`;
})
.join("");
}
}
backToHome() {
// Switch back to home section
this.dashboard.switchSection("categories");
}
setupModalEventListeners() {
const modal = document.getElementById("products-modal");
if (!modal) return;
// Close modal
modal.addEventListener("click", (e) => {
if (
e.target.matches("[data-action='close-modal']") ||
e.target.classList.contains("modal")
) {
modal.style.display = "none";
}
});
// Add product to category
modal.addEventListener("click", (e) => {
if (e.target.matches("[data-action='add-product']")) {
const categoryId = e.target.getAttribute("data-category-id");
this.addProductToCategory(categoryId);
}
});
// View product details
modal.addEventListener("click", (e) => {
if (e.target.matches("[data-action='view-product']")) {
const productId = e.target.getAttribute("data-product-id");
this.viewProductDetails(productId);
}
});
// Edit product
modal.addEventListener("click", (e) => {
if (e.target.matches("[data-action='edit-product']")) {
e.stopPropagation();
const productId = e.target.getAttribute("data-product-id");
this.editProduct(productId);
}
});
}
addProductToCategory(categoryId) {
// Show the product modal directly without switching sections
this.dashboard.productManager.showProductModal();
// Pre-select the category in the form after the modal is shown
setTimeout(() => {
const categorySelect = document.getElementById("product-category");
if (categorySelect) {
categorySelect.value = categoryId;
}
}, 200);
}
viewProductDetails(productId) {
// Close the products modal immediately
const modal = document.getElementById("products-modal");
if (modal) {
modal.remove();
}
// Switch to products section and show product details
this.dashboard.switchSection("products");
// You could implement a product details view here
this.dashboard.showMessage(
`Viewing product details for ID: ${productId}`,
"info"
);
}
editProduct(productId) {
console.log("editProduct called with ID:", productId);
console.log("Dashboard productManager:", this.dashboard.productManager);
// Edit the product directly (we're already in the products section)
this.dashboard.productManager.editProduct(productId);
}
async deleteCategory(id) {
const category = this.categories.find((cat) => cat.id === id);
if (!category) return;
// Check if category has products
const productsInCategory = this.dashboard.productManager.products.filter(
(p) => p.category === id
);
if (productsInCategory.length > 0) {
this.dashboard.showMessage(
`Cannot delete category "${category.name}" because it contains ${productsInCategory.length} product(s). Please move or delete the products first.`,
"error"
);
return;
}
if (
confirm(
`Are you sure you want to delete the category "${category.name}"? This action cannot be undone.`
)
) {
try {
this.categories = this.categories.filter((cat) => cat.id !== id);
await this.saveData();
this.renderCategories();
this.dashboard.showMessage("Category deleted successfully!", "success");
// Update product manager's category select
if (this.dashboard.productManager) {
this.dashboard.productManager.populateCategorySelect();
}
} catch (error) {
this.dashboard.showMessage(
"Error deleting category: " + error.message,
"error"
);
}
}
}
async saveData() {
const data = {
products: this.dashboard.productManager.products,
categories: this.categories,
};
// In a real implementation, this would save to a server
console.log("Saving category data:", data);
// Simulate API call
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 500);
});
}
// Utility method to get category by ID
getCategoryById(id) {
return this.categories.find((cat) => cat.id === id);
}
// Utility method to get category name by ID
getCategoryName(id) {
const category = this.getCategoryById(id);
return category ? category.name : id;
}
// Utility method to validate category ID format
validateCategoryId(id) {
// Category IDs should be lowercase, hyphen-separated
return /^[a-z0-9-]+$/.test(id);
}
}
// Make CategoryManager available globally
window.CategoryManager = CategoryManager;

View file

@ -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 = `
<div class="no-images">
<h3>No images found</h3>
<p>Upload images to get started!</p>
<button class="btn btn-primary" onclick="imageManager.showUploadModal()">Upload Images</button>
</div>
`;
return;
}
container.innerHTML = "";
this.images.forEach((image) => {
const imageItem = this.createImageItem(image);
container.appendChild(imageItem);
});
}
createImageItem(image) {
const item = document.createElement("div");
item.className = "image-item";
item.innerHTML = `
<div class="image-preview">
<img src="${image.path}" alt="${image.name}" onerror="this.src='data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"150\" viewBox=\"0 0 200 150\"><rect width=\"200\" height=\"150\" fill=\"%23f0f0f0\"/><text x=\"50%\" y=\"50%\" text-anchor=\"middle\" dy=\".3em\" fill=\"%23999\">Image Not Found</text></svg>'">
</div>
<div class="image-item-info">
<div class="image-item-name" title="${image.name}">${image.name}</div>
<div class="image-meta">
<span class="image-product">Product: ${image.productName}</span>
<span class="image-size">Size: ${image.size}</span>
</div>
</div>
<div class="image-item-actions">
<button class="btn btn-sm btn-primary" onclick="imageManager.viewImage('${image.path}')">
👁️ View
</button>
<button class="btn btn-sm btn-secondary" onclick="imageManager.renameImage('${image.path}')">
✏️ Rename
</button>
<button class="btn btn-sm btn-danger" onclick="imageManager.deleteImage('${image.path}')">
🗑️ Delete
</button>
</div>
`;
return item;
}
showUploadModal() {
// Create upload modal if it doesn't exist
let uploadModal = document.getElementById("upload-modal");
if (!uploadModal) {
uploadModal = this.createUploadModal();
document.body.appendChild(uploadModal);
}
uploadModal.style.display = "block";
}
createUploadModal() {
const modal = document.createElement("div");
modal.id = "upload-modal";
modal.className = "modal";
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h3>Upload Images</h3>
<span class="close" onclick="this.closest('.modal').style.display='none'">&times;</span>
</div>
<div class="modal-body">
<div class="upload-area" id="upload-area">
<div class="upload-content">
<div class="upload-icon">📤</div>
<h4>Drag & Drop images here</h4>
<p>or click to select files</p>
<input type="file" id="file-input" multiple accept="image/*" style="display: none;">
</div>
</div>
<div class="upload-preview" id="upload-preview"></div>
<div class="upload-actions">
<button class="btn btn-secondary" onclick="this.closest('.modal').style.display='none'">Cancel</button>
<button class="btn btn-primary" onclick="imageManager.processUpload()">Upload Images</button>
</div>
</div>
</div>
`;
// Setup drag and drop
const uploadArea = modal.querySelector("#upload-area");
const fileInput = modal.querySelector("#file-input");
uploadArea.addEventListener("click", () => fileInput.click());
uploadArea.addEventListener("dragover", (e) => {
e.preventDefault();
uploadArea.classList.add("drag-over");
});
uploadArea.addEventListener("dragleave", () => {
uploadArea.classList.remove("drag-over");
});
uploadArea.addEventListener("drop", (e) => {
e.preventDefault();
uploadArea.classList.remove("drag-over");
this.handleFileSelect(e.dataTransfer.files);
});
fileInput.addEventListener("change", (e) => {
this.handleFileSelect(e.target.files);
});
return modal;
}
handleFileSelect(files) {
const preview = document.getElementById("upload-preview");
preview.innerHTML = "";
Array.from(files).forEach((file) => {
if (file.type.startsWith("image/")) {
const reader = new FileReader();
reader.onload = (e) => {
const imageDiv = document.createElement("div");
imageDiv.className = "upload-preview-item";
imageDiv.innerHTML = `
<img src="${e.target.result}" alt="${file.name}">
<div class="upload-preview-info">
<span class="upload-filename">${file.name}</span>
<span class="upload-filesize">${this.formatFileSize(
file.size
)}</span>
</div>
<button class="remove-upload-btn" onclick="this.parentElement.remove()">×</button>
`;
preview.appendChild(imageDiv);
};
reader.readAsDataURL(file);
}
});
}
formatFileSize(bytes) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
async processUpload() {
const fileInput = document.getElementById("file-input");
const files = fileInput.files;
if (files.length === 0) {
this.dashboard.showMessage("Please select files to upload", "error");
return;
}
try {
this.dashboard.showMessage("Uploading images...", "info");
const uploadedImages = await this.uploadImages(files);
// Add to images list
uploadedImages.forEach((imagePath) => {
const fileName = this.extractFileName(imagePath);
this.images.push({
path: imagePath,
name: fileName,
productId: null,
productName: "Unassigned",
size: "Unknown",
uploadDate: new Date().toISOString(),
});
});
this.renderImages();
document.getElementById("upload-modal").style.display = "none";
this.dashboard.showMessage(
`Successfully uploaded ${uploadedImages.length} image(s)!`,
"success"
);
} catch (error) {
this.dashboard.showMessage(
"Error uploading images: " + error.message,
"error"
);
}
}
async uploadImages(files) {
const uploadedImages = [];
for (const file of files) {
if (file.type.startsWith("image/")) {
// In a real implementation, you'd upload to a server
const filename = `uploaded_${Date.now()}_${file.name}`;
const imagePath = `assets/images/products/${filename}`;
uploadedImages.push(imagePath);
console.log(`Would upload: ${file.name} to ${imagePath}`);
}
}
return uploadedImages;
}
viewImage(imagePath) {
// Create image viewer modal
const viewer = document.createElement("div");
viewer.className = "modal image-viewer";
viewer.innerHTML = `
<div class="modal-content image-viewer-content">
<div class="modal-header">
<h3>Image Viewer</h3>
<span class="close" onclick="this.closest('.modal').remove()">&times;</span>
</div>
<div class="modal-body">
<img src="${imagePath}" alt="Full size image" style="max-width: 100%; max-height: 70vh; object-fit: contain;">
</div>
</div>
`;
document.body.appendChild(viewer);
viewer.style.display = "block";
}
renameImage(imagePath) {
const currentName = this.extractFileName(imagePath);
const newName = prompt("Enter new name for the image:", currentName);
if (newName && newName !== currentName) {
// In a real implementation, you'd rename the file on the server
console.log(`Would rename ${imagePath} to ${newName}`);
this.dashboard.showMessage(
"Image rename functionality would be implemented here",
"info"
);
}
}
async deleteImage(imagePath) {
if (
confirm(
"Are you sure you want to delete this image? This action cannot be undone."
)
) {
try {
// Check if image is used by any products
const productsUsingImage =
this.dashboard.productManager.products.filter(
(product) => product.images && product.images.includes(imagePath)
);
if (productsUsingImage.length > 0) {
this.dashboard.showMessage(
`Cannot delete image because it's used by ${productsUsingImage.length} product(s). Please remove it from the products first.`,
"error"
);
return;
}
// Remove from images list
this.images = this.images.filter((img) => img.path !== imagePath);
// In a real implementation, you'd delete from server
console.log(`Would delete image: ${imagePath}`);
this.renderImages();
this.dashboard.showMessage("Image deleted successfully!", "success");
} catch (error) {
this.dashboard.showMessage(
"Error deleting image: " + error.message,
"error"
);
}
}
}
}
// Make ImageManager available globally
window.ImageManager = ImageManager;

File diff suppressed because it is too large Load diff

View file

@ -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();