Add remaining admin dashboard files (final chunk)
This commit is contained in:
parent
758728a093
commit
7f70170fe1
513 changed files with 7857 additions and 0 deletions
1561
admin-dashboard/scripts/main.js
Normal file
1561
admin-dashboard/scripts/main.js
Normal file
File diff suppressed because it is too large
Load diff
596
admin-dashboard/scripts/products.js
Normal file
596
admin-dashboard/scripts/products.js
Normal file
|
|
@ -0,0 +1,596 @@
|
|||
// Helper function to fix image paths for admin dashboard preview
|
||||
function fixImagePath(imagePath) {
|
||||
if (!imagePath) return imagePath;
|
||||
// If the path starts with "assets/", prepend "../" for admin dashboard preview
|
||||
if (imagePath.startsWith("assets/")) {
|
||||
return "../" + imagePath;
|
||||
}
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
// Product Management System
|
||||
class ProductManager {
|
||||
constructor() {
|
||||
this.products = [];
|
||||
this.categories = [];
|
||||
this.pagination = {};
|
||||
this.currentPage = 1;
|
||||
this.itemsPerPage = 16;
|
||||
this.filteredProducts = [];
|
||||
this.selectedCategories = new Set();
|
||||
this.tempProducts = null; // For temporary data from admin dashboard
|
||||
}
|
||||
|
||||
// Load products from JSON file
|
||||
async loadProducts() {
|
||||
try {
|
||||
const response = await fetch("/data/products.json");
|
||||
const data = await response.json();
|
||||
|
||||
this.products = data.products;
|
||||
this.categories = data.categories;
|
||||
this.pagination = data.pagination;
|
||||
this.filteredProducts = [...this.products];
|
||||
|
||||
this.renderProducts();
|
||||
this.updatePagination();
|
||||
this.updateResultsCount();
|
||||
this.setupEventListeners();
|
||||
this.renderCategoryFilters();
|
||||
|
||||
// Check for URL parameters and pre-select category
|
||||
this.handleUrlParameters();
|
||||
} catch (error) {
|
||||
console.error("Error loading products:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle URL parameters for pre-selecting category filters
|
||||
handleUrlParameters() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const category = urlParams.get("category");
|
||||
|
||||
if (category) {
|
||||
// Pre-select the category in the filter
|
||||
this.selectedCategories = new Set([category]);
|
||||
this.applyFilters();
|
||||
this.currentPage = 1;
|
||||
this.renderProducts();
|
||||
this.updatePagination();
|
||||
this.updateResultsCount();
|
||||
|
||||
// Update the UI to show the filter is active
|
||||
this.updateFilterUI(category);
|
||||
}
|
||||
}
|
||||
|
||||
// Update filter UI to show selected category
|
||||
updateFilterUI(categoryId) {
|
||||
// Find the category object to get the display name
|
||||
const category = this.categories.find((c) => c.id === categoryId);
|
||||
const displayName = category ? category.name : categoryId;
|
||||
|
||||
// Update filter button text to show active filter
|
||||
const filterToggle = document.getElementById("filter-toggle");
|
||||
if (filterToggle) {
|
||||
const filterText = filterToggle.querySelector("span:last-child");
|
||||
if (filterText) {
|
||||
filterText.textContent = `Filter: ${displayName}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Check the corresponding checkbox in the dropdown
|
||||
setTimeout(() => {
|
||||
const checkboxes = document.querySelectorAll(".category-checkbox");
|
||||
checkboxes.forEach((checkbox) => {
|
||||
if (checkbox.value === categoryId) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Update filter button text based on selected categories
|
||||
updateFilterButtonText() {
|
||||
const filterToggle = document.getElementById("filter-toggle");
|
||||
if (!filterToggle) return;
|
||||
|
||||
const filterText = filterToggle.querySelector("span:last-child");
|
||||
if (!filterText) return;
|
||||
|
||||
// If no categories selected or "all" is selected, show default text
|
||||
if (
|
||||
this.selectedCategories.size === 0 ||
|
||||
this.selectedCategories.has("all")
|
||||
) {
|
||||
filterText.textContent = "Filter";
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the first selected category and find its display name
|
||||
const firstSelectedCategory = Array.from(this.selectedCategories)[0];
|
||||
const category = this.categories.find(
|
||||
(c) => c.id === firstSelectedCategory
|
||||
);
|
||||
|
||||
if (category) {
|
||||
filterText.textContent = `Filter: ${category.name}`;
|
||||
} else {
|
||||
filterText.textContent = "Filter";
|
||||
}
|
||||
}
|
||||
|
||||
// Render products in the grid
|
||||
renderProducts(productsToRender = null) {
|
||||
const productGrid = document.getElementById("product-grid");
|
||||
if (!productGrid) return;
|
||||
|
||||
// Use temporary products if provided, otherwise use filtered products
|
||||
let sourceProducts = productsToRender || this.filteredProducts;
|
||||
|
||||
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
|
||||
const endIndex = startIndex + this.itemsPerPage;
|
||||
const productsToShow = sourceProducts.slice(startIndex, endIndex);
|
||||
|
||||
productGrid.innerHTML = productsToShow
|
||||
.map((product) => this.createProductCard(product))
|
||||
.join("");
|
||||
this.updateResultsCount();
|
||||
this.updatePagination();
|
||||
|
||||
// Re-add image enlargement listeners and lazy loading after products are rendered
|
||||
setTimeout(() => {
|
||||
addImageEnlargementListeners();
|
||||
this.initLazyLoading();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// Create individual product card HTML
|
||||
createProductCard(product) {
|
||||
// Check if we're in comparison mode
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const returnTo = urlParams.get("returnTo");
|
||||
const isComparisonMode = returnTo === "comparison";
|
||||
|
||||
return `
|
||||
<div class="group relative bg-light-bg rounded-lg overflow-hidden hover:shadow-lg transition-shadow product-card" data-product-id="${
|
||||
product.id
|
||||
}">
|
||||
<div class="relative h-80 overflow-hidden">
|
||||
<img
|
||||
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='320' viewBox='0 0 400 320'%3E%3Crect width='400' height='320' fill='%23f3f4f6'/%3E%3C/svg%3E"
|
||||
data-src="${fixImagePath(product.image)}"
|
||||
alt="${product.alt}"
|
||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300 cursor-pointer lazy-load"
|
||||
data-enlarge-src="${fixImagePath(product.image)}"
|
||||
loading="lazy"
|
||||
/>
|
||||
<!-- Hover Overlay -->
|
||||
<div class="absolute inset-0 bg-dark-charcoal bg-opacity-70 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<button
|
||||
class="bg-white text-uc-gold font-poppins font-semibold px-8 py-3 rounded-md hover:bg-uc-gold hover:text-white transition-colors ${
|
||||
isComparisonMode ? "cursor-pointer" : ""
|
||||
}"
|
||||
onclick="productManager.viewProduct(${product.id})"
|
||||
>
|
||||
${isComparisonMode ? "Add to Comparison" : "View"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<h3 class="font-poppins font-semibold text-2xl text-dark-charcoal mb-2">
|
||||
${product.name}
|
||||
</h3>
|
||||
<p class="font-poppins font-medium text-base text-quick-silver">
|
||||
${product.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Filter products by category
|
||||
filterByCategory(category) {
|
||||
// Single category helper (not used directly by UI)
|
||||
this.selectedCategories = new Set([category]);
|
||||
this.applyFilters();
|
||||
this.currentPage = 1;
|
||||
this.renderProducts();
|
||||
this.updatePagination();
|
||||
}
|
||||
|
||||
// Search products
|
||||
searchProducts(query) {
|
||||
if (!query.trim()) {
|
||||
this.filteredProducts = [...this.products];
|
||||
} else {
|
||||
this.filteredProducts = this.products.filter(
|
||||
(product) =>
|
||||
product.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
product.description.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
}
|
||||
this.currentPage = 1;
|
||||
this.renderProducts();
|
||||
this.updatePagination();
|
||||
}
|
||||
|
||||
// Apply selected category filters
|
||||
applyFilters() {
|
||||
if (
|
||||
this.selectedCategories.size === 0 ||
|
||||
this.selectedCategories.has("all")
|
||||
) {
|
||||
this.filteredProducts = [...this.products];
|
||||
return;
|
||||
}
|
||||
this.filteredProducts = this.products.filter((product) =>
|
||||
this.selectedCategories.has(product.category)
|
||||
);
|
||||
}
|
||||
|
||||
// Sort products
|
||||
sortProducts(sortBy) {
|
||||
switch (sortBy) {
|
||||
case "name-asc":
|
||||
this.filteredProducts.sort((a, b) => a.name.localeCompare(b.name));
|
||||
break;
|
||||
case "name-desc":
|
||||
this.filteredProducts.sort((a, b) => b.name.localeCompare(a.name));
|
||||
break;
|
||||
default:
|
||||
// Default sorting by ID
|
||||
this.filteredProducts.sort((a, b) => a.id - b.id);
|
||||
}
|
||||
this.renderProducts();
|
||||
}
|
||||
|
||||
// Change page
|
||||
changePage(page) {
|
||||
this.currentPage = page;
|
||||
this.renderProducts();
|
||||
this.updatePagination();
|
||||
}
|
||||
|
||||
// Update pagination controls
|
||||
updatePagination() {
|
||||
const totalPages = Math.ceil(
|
||||
this.filteredProducts.length / this.itemsPerPage
|
||||
);
|
||||
const paginationContainer = document.getElementById("pagination");
|
||||
|
||||
if (!paginationContainer) return;
|
||||
|
||||
let paginationHTML = "";
|
||||
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
const isActive = i === this.currentPage;
|
||||
paginationHTML += `
|
||||
<button
|
||||
class="w-20 h-15 ${
|
||||
isActive
|
||||
? "bg-uc-gold text-white"
|
||||
: "bg-floral-white text-black hover:bg-uc-gold hover:text-white"
|
||||
} font-poppins font-normal text-xl rounded-lg flex items-center justify-center transition-colors"
|
||||
onclick="productManager.changePage(${i})"
|
||||
>
|
||||
${i}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
if (totalPages > 1 && this.currentPage < totalPages) {
|
||||
paginationHTML += `
|
||||
<button
|
||||
class="w-28 h-15 bg-floral-white text-black font-poppins font-light text-xl rounded-lg flex items-center justify-center hover:bg-uc-gold hover:text-white transition-colors"
|
||||
onclick="productManager.changePage(${this.currentPage + 1})"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
paginationContainer.innerHTML = paginationHTML;
|
||||
}
|
||||
|
||||
// Build category multi-select dropdown
|
||||
renderCategoryFilters() {
|
||||
const container = document.getElementById("filter-categories");
|
||||
if (!container) return;
|
||||
|
||||
const categoryOptions = this.categories
|
||||
.map(
|
||||
(c) => `
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" value="${c.id}" class="category-checkbox category-specific">
|
||||
<span class="font-poppins text-sm text-black">${c.name}</span>
|
||||
</label>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
const allOption = `
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" value="all" class="category-checkbox category-all">
|
||||
<span class="font-poppins text-sm text-black">All</span>
|
||||
</label>
|
||||
`;
|
||||
|
||||
container.innerHTML = allOption + categoryOptions;
|
||||
|
||||
// Add event listeners for "All" checkbox behavior
|
||||
const allCheckbox = container.querySelector(".category-all");
|
||||
const specificCheckboxes = container.querySelectorAll(".category-specific");
|
||||
|
||||
if (allCheckbox) {
|
||||
allCheckbox.addEventListener("change", (e) => {
|
||||
const isChecked = e.target.checked;
|
||||
specificCheckboxes.forEach((checkbox) => {
|
||||
checkbox.checked = isChecked;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Update "All" checkbox when specific categories change
|
||||
specificCheckboxes.forEach((checkbox) => {
|
||||
checkbox.addEventListener("change", () => {
|
||||
const allChecked = Array.from(specificCheckboxes).every(
|
||||
(c) => c.checked
|
||||
);
|
||||
const anyChecked = Array.from(specificCheckboxes).some(
|
||||
(c) => c.checked
|
||||
);
|
||||
|
||||
if (allChecked) {
|
||||
allCheckbox.checked = true;
|
||||
} else if (!anyChecked) {
|
||||
allCheckbox.checked = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// View product details
|
||||
viewProduct(productId) {
|
||||
const product = this.products.find((p) => p.id === productId);
|
||||
if (product) {
|
||||
// Check if we're in comparison mode
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const returnTo = urlParams.get("returnTo");
|
||||
const slot = urlParams.get("slot");
|
||||
const product1Id = urlParams.get("product1");
|
||||
const product2Id = urlParams.get("product2");
|
||||
|
||||
if (returnTo === "comparison" && slot) {
|
||||
// Navigate back to comparison page with the selected product
|
||||
let comparisonUrl = "product-comparison.html?";
|
||||
|
||||
if (slot === "1") {
|
||||
// Replace product 1
|
||||
comparisonUrl += `product1=${productId}`;
|
||||
if (product2Id) {
|
||||
comparisonUrl += `&product2=${product2Id}`;
|
||||
}
|
||||
} else {
|
||||
// Replace product 2
|
||||
if (product1Id) {
|
||||
comparisonUrl += `product1=${product1Id}&`;
|
||||
}
|
||||
comparisonUrl += `product2=${productId}`;
|
||||
}
|
||||
|
||||
console.log("Navigating to comparison page:", comparisonUrl);
|
||||
window.location.href = comparisonUrl;
|
||||
} else {
|
||||
// Normal mode - navigate to product detail page
|
||||
window.location.href = `../product-detail.html?id=${productId}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
setupEventListeners() {
|
||||
// Filter dropdown toggle and outside click
|
||||
const filterToggle = document.getElementById("filter-toggle");
|
||||
const filterDropdown = document.getElementById("filter-dropdown");
|
||||
if (filterToggle && filterDropdown) {
|
||||
filterToggle.addEventListener("click", () => {
|
||||
filterDropdown.classList.toggle("hidden");
|
||||
});
|
||||
document.addEventListener("click", (e) => {
|
||||
if (
|
||||
!filterDropdown.contains(e.target) &&
|
||||
!filterToggle.contains(e.target)
|
||||
) {
|
||||
filterDropdown.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Sort dropdown
|
||||
const sortSelect = document.querySelector("select");
|
||||
if (sortSelect) {
|
||||
sortSelect.addEventListener("change", (e) => {
|
||||
this.sortProducts(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
// Apply/clear category filters
|
||||
const applyBtn = document.getElementById("filter-apply");
|
||||
const clearBtn = document.getElementById("filter-clear");
|
||||
if (applyBtn) {
|
||||
applyBtn.addEventListener("click", () => {
|
||||
const checks = Array.from(
|
||||
document.querySelectorAll(".category-checkbox")
|
||||
);
|
||||
this.selectedCategories = new Set(
|
||||
checks.filter((c) => c.checked).map((c) => c.value)
|
||||
);
|
||||
this.currentPage = 1;
|
||||
this.applyFilters();
|
||||
this.renderProducts();
|
||||
this.updatePagination();
|
||||
this.updateResultsCount();
|
||||
|
||||
// Update filter button text to show selected category
|
||||
this.updateFilterButtonText();
|
||||
|
||||
const dropdown = document.getElementById("filter-dropdown");
|
||||
if (dropdown) dropdown.classList.add("hidden");
|
||||
});
|
||||
}
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener("click", () => {
|
||||
const checks = Array.from(
|
||||
document.querySelectorAll(".category-checkbox")
|
||||
);
|
||||
checks.forEach((c) => (c.checked = false));
|
||||
this.selectedCategories.clear();
|
||||
this.currentPage = 1;
|
||||
this.applyFilters();
|
||||
this.renderProducts();
|
||||
this.updatePagination();
|
||||
this.updateResultsCount();
|
||||
|
||||
// Update filter button text to show default
|
||||
this.updateFilterButtonText();
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize lazy loading
|
||||
this.initLazyLoading();
|
||||
}
|
||||
|
||||
// Initialize lazy loading for images
|
||||
initLazyLoading() {
|
||||
// Use Intersection Observer for better performance
|
||||
if ("IntersectionObserver" in window) {
|
||||
const imageObserver = new IntersectionObserver(
|
||||
(entries, observer) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target;
|
||||
if (img.dataset.src) {
|
||||
img.src = img.dataset.src;
|
||||
img.classList.remove("lazy-load");
|
||||
img.classList.add("loaded");
|
||||
observer.unobserve(img);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
rootMargin: "50px 0px", // Start loading 50px before image comes into view
|
||||
threshold: 0.01,
|
||||
}
|
||||
);
|
||||
|
||||
// Observe all lazy-loaded images
|
||||
const lazyImages = document.querySelectorAll(".lazy-load");
|
||||
lazyImages.forEach((img) => imageObserver.observe(img));
|
||||
} else {
|
||||
// Fallback for older browsers - load all images immediately
|
||||
const lazyImages = document.querySelectorAll(".lazy-load");
|
||||
lazyImages.forEach((img) => {
|
||||
if (img.dataset.src) {
|
||||
img.src = img.dataset.src;
|
||||
img.classList.remove("lazy-load");
|
||||
img.classList.add("loaded");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update results count
|
||||
updateResultsCount() {
|
||||
const resultsElement = document.querySelector(".text-quick-silver");
|
||||
if (resultsElement) {
|
||||
const startIndex = (this.currentPage - 1) * this.itemsPerPage + 1;
|
||||
const endIndex = Math.min(
|
||||
startIndex + this.itemsPerPage - 1,
|
||||
this.filteredProducts.length
|
||||
);
|
||||
resultsElement.textContent = `Showing ${startIndex}–${endIndex} of ${this.filteredProducts.length} results`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize product manager
|
||||
const productManager = new ProductManager();
|
||||
window.productManager = productManager; // Make it globally available
|
||||
|
||||
// Image enlargement modal functionality
|
||||
function addImageEnlargementListeners() {
|
||||
const productImages = document.querySelectorAll(
|
||||
"#product-grid img[data-enlarge-src]"
|
||||
);
|
||||
const modal = document.getElementById("image-modal");
|
||||
const modalImage = document.getElementById("modal-image");
|
||||
const modalCloseBtn = document.getElementById("modal-close-btn");
|
||||
|
||||
if (!modal || !modalImage || !modalCloseBtn) return;
|
||||
|
||||
productImages.forEach((img) => {
|
||||
img.addEventListener("click", function (e) {
|
||||
e.preventDefault(); // Prevent any default behavior
|
||||
e.stopPropagation(); // Stop event bubbling to parent elements
|
||||
|
||||
const imageSrc = this.getAttribute("data-enlarge-src");
|
||||
const imageAlt = this.getAttribute("alt");
|
||||
|
||||
modalImage.src = imageSrc;
|
||||
modalImage.alt = imageAlt;
|
||||
|
||||
// Show modal with animation
|
||||
modal.classList.remove("hidden");
|
||||
document.body.style.overflow = "hidden"; // Prevent background scrolling
|
||||
|
||||
// Trigger animation after a brief delay
|
||||
setTimeout(() => {
|
||||
modalImage.classList.remove("scale-95");
|
||||
modalImage.classList.add("scale-100");
|
||||
}, 10);
|
||||
});
|
||||
});
|
||||
|
||||
// Close modal functionality
|
||||
function closeModal() {
|
||||
// Animate out
|
||||
modalImage.classList.remove("scale-100");
|
||||
modalImage.classList.add("scale-95");
|
||||
|
||||
// Hide modal after animation
|
||||
setTimeout(() => {
|
||||
modal.classList.add("hidden");
|
||||
document.body.style.overflow = ""; // Restore scrolling
|
||||
}, 300);
|
||||
}
|
||||
|
||||
modalCloseBtn.addEventListener("click", closeModal);
|
||||
|
||||
// Close modal when clicking outside the image
|
||||
modal.addEventListener("click", function (e) {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal with Escape key
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape" && !modal.classList.contains("hidden")) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load products when DOM is ready
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
productManager.loadProducts();
|
||||
|
||||
// Add image enlargement listeners after products are loaded
|
||||
setTimeout(() => {
|
||||
addImageEnlargementListeners();
|
||||
}, 100);
|
||||
});
|
||||
272
admin-dashboard/scripts/sync-to-main.js
Normal file
272
admin-dashboard/scripts/sync-to-main.js
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { execSync } = require("child_process");
|
||||
|
||||
class AdminSync {
|
||||
constructor() {
|
||||
this.mainRepoPath = "temp-main";
|
||||
this.adminRepoPath = process.cwd();
|
||||
this.mainRepoUrl = "https://git.kh3group.com/george/website.git";
|
||||
this.syncLog = [];
|
||||
}
|
||||
|
||||
async syncToMain() {
|
||||
console.log("🚀 Starting sync to main site...");
|
||||
this.log("Starting sync process");
|
||||
|
||||
try {
|
||||
// Step 1: Clone main repository
|
||||
await this.cloneMainRepo();
|
||||
|
||||
// Step 2: Sync products.json
|
||||
await this.syncProducts();
|
||||
|
||||
// Step 3: Sync images
|
||||
await this.syncImages();
|
||||
|
||||
// Step 4: Validate changes
|
||||
await this.validateChanges();
|
||||
|
||||
// Step 5: Commit and push
|
||||
await this.commitAndPush();
|
||||
|
||||
// Step 6: Cleanup
|
||||
this.cleanup();
|
||||
|
||||
console.log("✅ Successfully synced to main site!");
|
||||
console.log("🌐 Your changes should be live shortly.");
|
||||
this.log("Sync completed successfully");
|
||||
} catch (error) {
|
||||
console.error("❌ Sync failed:", error.message);
|
||||
this.log(`Sync failed: ${error.message}`, "error");
|
||||
this.cleanup();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async cloneMainRepo() {
|
||||
console.log("📥 Cloning main repository...");
|
||||
this.log("Cloning main repository");
|
||||
|
||||
if (fs.existsSync(this.mainRepoPath)) {
|
||||
execSync(`rm -rf ${this.mainRepoPath}`);
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(`git clone ${this.mainRepoUrl} ${this.mainRepoPath}`, {
|
||||
stdio: "pipe",
|
||||
});
|
||||
execSync(`cd ${this.mainRepoPath} && git checkout main`, {
|
||||
stdio: "pipe",
|
||||
});
|
||||
this.log("Main repository cloned successfully");
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to clone repository: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async syncProducts() {
|
||||
console.log("📦 Syncing products.json...");
|
||||
this.log("Syncing products data");
|
||||
|
||||
const adminProductsPath = path.join(
|
||||
this.adminRepoPath,
|
||||
"data/products.json"
|
||||
);
|
||||
const mainProductsPath = path.join(this.mainRepoPath, "data/products.json");
|
||||
|
||||
if (!fs.existsSync(adminProductsPath)) {
|
||||
throw new Error("products.json not found in admin repo");
|
||||
}
|
||||
|
||||
// Ensure main data directory exists
|
||||
const mainDataDir = path.dirname(mainProductsPath);
|
||||
if (!fs.existsSync(mainDataDir)) {
|
||||
fs.mkdirSync(mainDataDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Copy products.json
|
||||
fs.copyFileSync(adminProductsPath, mainProductsPath);
|
||||
console.log(" ✅ Products synced");
|
||||
this.log("Products data synced successfully");
|
||||
}
|
||||
|
||||
async syncImages() {
|
||||
console.log("🖼️ Syncing images...");
|
||||
this.log("Syncing product images");
|
||||
|
||||
const adminImagesPath = path.join(
|
||||
this.adminRepoPath,
|
||||
"assets/images/products"
|
||||
);
|
||||
const mainImagesPath = path.join(
|
||||
this.mainRepoPath,
|
||||
"assets/images/products"
|
||||
);
|
||||
|
||||
// Create main images directory if it doesn't exist
|
||||
if (!fs.existsSync(mainImagesPath)) {
|
||||
fs.mkdirSync(mainImagesPath, { recursive: true });
|
||||
}
|
||||
|
||||
let imagesProcessed = 0;
|
||||
let imagesRemoved = 0;
|
||||
|
||||
if (fs.existsSync(adminImagesPath)) {
|
||||
// Get all files in admin images directory
|
||||
const adminImages = fs.readdirSync(adminImagesPath);
|
||||
const mainImages = fs.existsSync(mainImagesPath)
|
||||
? fs.readdirSync(mainImagesPath)
|
||||
: [];
|
||||
|
||||
// Copy new/updated images
|
||||
for (const image of adminImages) {
|
||||
const srcPath = path.join(adminImagesPath, image);
|
||||
const destPath = path.join(mainImagesPath, image);
|
||||
|
||||
if (fs.statSync(srcPath).isFile()) {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
console.log(` 📸 Copied: ${image}`);
|
||||
imagesProcessed++;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove orphaned images (in main but not in admin)
|
||||
for (const image of mainImages) {
|
||||
const adminImagePath = path.join(adminImagesPath, image);
|
||||
if (!fs.existsSync(adminImagePath)) {
|
||||
fs.unlinkSync(path.join(mainImagesPath, image));
|
||||
console.log(` 🗑️ Removed: ${image}`);
|
||||
imagesRemoved++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
` ✅ Images synced (${imagesProcessed} copied, ${imagesRemoved} removed)`
|
||||
);
|
||||
this.log(
|
||||
`Images synced: ${imagesProcessed} copied, ${imagesRemoved} removed`
|
||||
);
|
||||
}
|
||||
|
||||
async validateChanges() {
|
||||
console.log("🔍 Validating changes...");
|
||||
this.log("Validating synced data");
|
||||
|
||||
// Validate JSON
|
||||
const productsPath = path.join(this.mainRepoPath, "data/products.json");
|
||||
if (!fs.existsSync(productsPath)) {
|
||||
throw new Error("products.json not found after sync");
|
||||
}
|
||||
|
||||
let productsData;
|
||||
try {
|
||||
productsData = JSON.parse(fs.readFileSync(productsPath, "utf8"));
|
||||
} catch (error) {
|
||||
throw new Error("Invalid JSON in products.json");
|
||||
}
|
||||
|
||||
if (!productsData.products || !Array.isArray(productsData.products)) {
|
||||
throw new Error("Invalid products.json structure");
|
||||
}
|
||||
|
||||
// Check if all referenced images exist
|
||||
let missingImages = 0;
|
||||
for (const product of productsData.products) {
|
||||
if (product.images) {
|
||||
for (const imagePath of product.images) {
|
||||
const fullPath = path.join(this.mainRepoPath, imagePath);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.warn(`⚠️ Warning: Image not found: ${imagePath}`);
|
||||
missingImages++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (missingImages > 0) {
|
||||
console.warn(`⚠️ Found ${missingImages} missing images`);
|
||||
}
|
||||
|
||||
console.log(" ✅ Validation passed");
|
||||
this.log(`Validation passed (${missingImages} missing images)`);
|
||||
}
|
||||
|
||||
async commitAndPush() {
|
||||
console.log("💾 Committing and pushing changes...");
|
||||
this.log("Committing and pushing changes");
|
||||
|
||||
try {
|
||||
// Add all changes
|
||||
execSync(`cd ${this.mainRepoPath} && git add .`, { stdio: "pipe" });
|
||||
|
||||
// Check if there are changes to commit
|
||||
try {
|
||||
execSync(`cd ${this.mainRepoPath} && git diff --cached --quiet`, {
|
||||
stdio: "pipe",
|
||||
});
|
||||
console.log(" ℹ️ No changes to commit");
|
||||
this.log("No changes to commit");
|
||||
return;
|
||||
} catch (error) {
|
||||
// There are changes, proceed with commit
|
||||
}
|
||||
|
||||
// Commit changes
|
||||
const commitMessage = `Update products and images from admin dashboard - ${new Date().toISOString()}`;
|
||||
execSync(`cd ${this.mainRepoPath} && git commit -m "${commitMessage}"`, {
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
// Push to main branch
|
||||
execSync(`cd ${this.mainRepoPath} && git push origin main`, {
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
console.log(" ✅ Changes pushed to main repository");
|
||||
this.log("Changes pushed successfully");
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to commit/push: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
console.log("🧹 Cleaning up...");
|
||||
this.log("Cleaning up temporary files");
|
||||
if (fs.existsSync(this.mainRepoPath)) {
|
||||
execSync(`rm -rf ${this.mainRepoPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
log(message, type = "info") {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logEntry = {
|
||||
timestamp,
|
||||
type,
|
||||
message,
|
||||
};
|
||||
this.syncLog.push(logEntry);
|
||||
|
||||
// Save log to file
|
||||
const logPath = path.join(this.adminRepoPath, "sync-log.json");
|
||||
fs.writeFileSync(logPath, JSON.stringify(this.syncLog, null, 2));
|
||||
}
|
||||
|
||||
// Method to get sync history
|
||||
getSyncHistory() {
|
||||
const logPath = path.join(this.adminRepoPath, "sync-log.json");
|
||||
if (fs.existsSync(logPath)) {
|
||||
return JSON.parse(fs.readFileSync(logPath, "utf8"));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Run sync if called directly
|
||||
if (require.main === module) {
|
||||
const sync = new AdminSync();
|
||||
sync.syncToMain();
|
||||
}
|
||||
|
||||
module.exports = AdminSync;
|
||||
91
admin-dashboard/scripts/update_products_tabs.js
Normal file
91
admin-dashboard/scripts/update_products_tabs.js
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const productsPath = path.join(__dirname, "../data/products.json");
|
||||
const db = JSON.parse(fs.readFileSync(productsPath, "utf8"));
|
||||
|
||||
// Default content for tabs (copy matches current UI text style)
|
||||
const defaultLong = [
|
||||
"Embodying the raw, wayward spirit of rock ‘n’ roll, the Kilburn portable active stereo speaker takes the unmistakable look and sound of Marshall, unplugs the chords, and takes the show on the road.",
|
||||
"Weighing in under 7 pounds, the Kilburn is a lightweight piece of vintage styled engineering. Setting the bar as one of the loudest speakers in its class, the Kilburn is a compact, stout-hearted hero with a well-balanced audio which boasts a clear midrange and extended highs for a sound that is both articulate and pronounced. The analogue knobs allow you to fine tune the controls to your personal preferences while the guitar-influenced leather strap enables easy and stylish travel.",
|
||||
];
|
||||
|
||||
const defaultsByCategory = {
|
||||
seating: {
|
||||
additionalInformation: {
|
||||
Material: "Fabric, engineered wood base",
|
||||
Upholstery: "Performance fabric",
|
||||
Dimensions: "See size options",
|
||||
Warranty: "2 years",
|
||||
},
|
||||
},
|
||||
tables: {
|
||||
additionalInformation: {
|
||||
Material: "Engineered wood, steel frame",
|
||||
Finish: "Matte laminate",
|
||||
Dimensions: "Small/Medium/Large",
|
||||
Warranty: "2 years",
|
||||
},
|
||||
},
|
||||
storage: {
|
||||
additionalInformation: {
|
||||
Material: "Powder-coated steel",
|
||||
Capacity: "Modular shelves",
|
||||
Dimensions: "Standard/Large/XL",
|
||||
Warranty: "2 years",
|
||||
},
|
||||
},
|
||||
workspace: {
|
||||
additionalInformation: {
|
||||
Material: "Acoustic panels, aluminum frame",
|
||||
Power: "Integrated power module",
|
||||
Dimensions: "Standard/Large",
|
||||
Warranty: "2 years",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function ensureFields(p) {
|
||||
const cat =
|
||||
p.category && defaultsByCategory[p.category] ? p.category : "seating";
|
||||
const d = defaultsByCategory[cat];
|
||||
|
||||
if (!Array.isArray(p.descriptionLong) || p.descriptionLong.length === 0) {
|
||||
p.descriptionLong = defaultLong;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof p.additionalInformation !== "object" ||
|
||||
p.additionalInformation === null
|
||||
) {
|
||||
p.additionalInformation = { ...d.additionalInformation };
|
||||
} else {
|
||||
// Fill any missing keys with defaults without overwriting existing
|
||||
for (const [k, v] of Object.entries(d.additionalInformation)) {
|
||||
if (
|
||||
p.additionalInformation[k] == null ||
|
||||
p.additionalInformation[k] === ""
|
||||
) {
|
||||
p.additionalInformation[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure reviewsCount mirrors numeric reviews
|
||||
const reviewsNum = Number(p.reviews || 0);
|
||||
p.reviews = Number.isFinite(reviewsNum) ? reviewsNum : 0;
|
||||
if (p.reviewsCount == null) p.reviewsCount = p.reviews;
|
||||
|
||||
// Ensure galleryPairs (two wide images for tabs). Use product image as fallback.
|
||||
if (!Array.isArray(p.galleryPairs) || p.galleryPairs.length === 0) {
|
||||
const img = p.image || "../assets/images/asgaard_sofa.png";
|
||||
p.galleryPairs = [{ left: img, right: img }];
|
||||
}
|
||||
}
|
||||
|
||||
(db.products || []).forEach(ensureFields);
|
||||
|
||||
fs.writeFileSync(productsPath, JSON.stringify(db, null, 2));
|
||||
console.log(
|
||||
"Ensured tabs data for all products: descriptionLong, additionalInformation, reviewsCount, galleryPairs"
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue