Add remaining admin dashboard files (final chunk)

This commit is contained in:
George Birikorang 2025-09-19 21:37:57 -07:00
parent 758728a093
commit 7f70170fe1
513 changed files with 7857 additions and 0 deletions

File diff suppressed because it is too large Load diff

View 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);
});

View 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;

View 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"
);