reset repo

This commit is contained in:
George Birikorang 2025-09-19 21:51:10 -07:00
parent 7f70170fe1
commit 9cdef853c4
631 changed files with 4941 additions and 16827 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,13 @@
// 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() {
@ -8,6 +18,7 @@ class ProductManager {
this.itemsPerPage = 16;
this.filteredProducts = [];
this.selectedCategories = new Set();
this.tempProducts = null; // For temporary data from admin dashboard
}
// Load products from JSON file
@ -54,13 +65,17 @@ class ProductManager {
}
// Update filter UI to show selected category
updateFilterUI(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: ${category}`;
filterText.textContent = `Filter: ${displayName}`;
}
}
@ -68,27 +83,66 @@ class ProductManager {
setTimeout(() => {
const checkboxes = document.querySelectorAll(".category-checkbox");
checkboxes.forEach((checkbox) => {
if (checkbox.value === category) {
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() {
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 = this.filteredProducts.slice(startIndex, endIndex);
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
@ -104,9 +158,12 @@ class ProductManager {
}">
<div class="relative h-80 overflow-hidden">
<img
src="${product.image}"
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"
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">
@ -328,7 +385,7 @@ class ProductManager {
window.location.href = comparisonUrl;
} else {
// Normal mode - navigate to product detail page
window.location.href = `product-detail.html?id=${productId}`;
window.location.href = `../product-detail.html?id=${productId}`;
}
}
}
@ -376,6 +433,10 @@ class ProductManager {
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");
});
@ -392,6 +453,52 @@ class ProductManager {
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");
}
});
}
}
@ -412,8 +519,78 @@ class ProductManager {
// 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

@ -1,605 +0,0 @@
// Quote Management System
class QuoteManager {
constructor() {
this.storageKey = "khy_quote_items";
this.quoteItems = this.loadQuoteItems();
this.init();
}
// Load quote items from localStorage
loadQuoteItems() {
try {
const stored = localStorage.getItem(this.storageKey);
return stored ? JSON.parse(stored) : [];
} catch (error) {
console.error("Error loading quote items:", error);
return [];
}
}
// Save quote items to localStorage
saveQuoteItems() {
try {
localStorage.setItem(this.storageKey, JSON.stringify(this.quoteItems));
this.updateQuoteBadge();
} catch (error) {
console.error("Error saving quote items:", error);
}
}
// Add item to quote
addToQuote(productData) {
const {
id,
name,
image,
color = "Default",
size = "Standard",
quantity = 1,
} = productData;
// Check if item already exists with same specifications
const existingItemIndex = this.quoteItems.findIndex(
(item) => item.id === id && item.color === color && item.size === size
);
if (existingItemIndex !== -1) {
// Update quantity of existing item
this.quoteItems[existingItemIndex].quantity += quantity;
} else {
// Add new item
const newItem = {
id,
name,
image,
color,
size,
quantity,
timestamp: new Date().toISOString(),
};
this.quoteItems.push(newItem);
}
this.saveQuoteItems();
this.renderQuoteItems();
this.showAddToQuoteSuccess();
}
// Remove item from quote
removeFromQuote(itemIndex) {
this.quoteItems.splice(itemIndex, 1);
this.saveQuoteItems();
this.renderQuoteItems();
}
// Update item quantity
updateQuantity(itemIndex, newQuantity) {
if (newQuantity > 0) {
this.quoteItems[itemIndex].quantity = newQuantity;
this.saveQuoteItems();
this.renderQuoteItems();
} else {
this.removeFromQuote(itemIndex);
}
}
// Edit a quote item
editQuoteItem(itemIndex) {
const item = this.quoteItems[itemIndex];
if (!item) return;
// Create edit modal
const modal = document.createElement("div");
modal.id = "edit-quote-modal";
modal.className =
"fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50";
modal.innerHTML = `
<div class="bg-white rounded-lg p-8 max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto">
<div class="flex items-center justify-between mb-6">
<h3 class="font-playfair font-semibold text-xl text-black">Edit Quote Item</h3>
<button onclick="quoteManager.closeEditModal()" class="text-gray-500 hover:text-gray-700">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="flex items-center space-x-4 mb-6">
<img src="${item.image}" alt="${
item.name
}" class="w-16 h-16 object-cover rounded-lg">
<div>
<h4 class="font-playfair font-semibold text-lg text-black">${
item.name
}</h4>
<p class="text-sm text-gray-600">Product ID: ${item.id}</p>
</div>
</div>
<form id="edit-quote-form" class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Quantity</label>
<div class="flex items-center space-x-3">
<button type="button" onclick="quoteManager.decrementEditQuantity()" class="w-8 h-8 rounded-full border border-gray-300 flex items-center justify-center hover:bg-gray-100 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"></path>
</svg>
</button>
<input type="number" id="edit-quantity" value="${
item.quantity
}" min="1" class="w-20 text-center border border-gray-300 rounded-lg px-3 py-2 font-playfair">
<button type="button" onclick="quoteManager.incrementEditQuantity()" class="w-8 h-8 rounded-full border border-gray-300 flex items-center justify-center hover:bg-gray-100 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
</button>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Color</label>
<select id="edit-color" class="w-full border border-gray-300 rounded-lg px-3 py-2 font-playfair">
<option value="Black" ${
item.color === "Black" ? "selected" : ""
}>Black</option>
<option value="White" ${
item.color === "White" ? "selected" : ""
}>White</option>
<option value="Brown" ${
item.color === "Brown" ? "selected" : ""
}>Brown</option>
<option value="Gray" ${
item.color === "Gray" ? "selected" : ""
}>Gray</option>
<option value="Beige" ${
item.color === "Beige" ? "selected" : ""
}>Beige</option>
<option value="Navy" ${
item.color === "Navy" ? "selected" : ""
}>Navy</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Size</label>
<select id="edit-size" class="w-full border border-gray-300 rounded-lg px-3 py-2 font-playfair">
<option value="S" ${
item.size === "S" ? "selected" : ""
}>Small (S)</option>
<option value="M" ${
item.size === "M" ? "selected" : ""
}>Medium (M)</option>
<option value="L" ${
item.size === "L" ? "selected" : ""
}>Large (L)</option>
<option value="XL" ${
item.size === "XL" ? "selected" : ""
}>Extra Large (XL)</option>
<option value="Standard" ${
item.size === "Standard" ? "selected" : ""
}>Standard</option>
</select>
</div>
<div class="flex space-x-3 pt-4">
<button type="button" onclick="quoteManager.closeEditModal()" class="flex-1 bg-gray-300 text-gray-700 px-4 py-2 rounded-lg font-playfair hover:bg-gray-400 transition-colors">
Cancel
</button>
<button type="submit" class="flex-1 bg-uc-gold text-white px-4 py-2 rounded-lg font-playfair hover:bg-amber-600 transition-colors">
Save Changes
</button>
</div>
</form>
</div>
`;
document.body.appendChild(modal);
// Add form submission handler
const form = modal.querySelector("#edit-quote-form");
form.addEventListener("submit", (e) => {
e.preventDefault();
this.saveEditChanges(itemIndex);
});
// Close modal when clicking outside
modal.addEventListener("click", (e) => {
if (e.target === modal) {
this.closeEditModal();
}
});
}
// Close edit modal
closeEditModal() {
const modal = document.getElementById("edit-quote-modal");
if (modal) {
modal.remove();
}
}
// Increment quantity in edit modal
incrementEditQuantity() {
const input = document.getElementById("edit-quantity");
if (input) {
input.value = parseInt(input.value) + 1;
}
}
// Decrement quantity in edit modal
decrementEditQuantity() {
const input = document.getElementById("edit-quantity");
if (input && parseInt(input.value) > 1) {
input.value = parseInt(input.value) - 1;
}
}
// Save changes from edit modal
saveEditChanges(itemIndex) {
const quantity = parseInt(document.getElementById("edit-quantity").value);
const color = document.getElementById("edit-color").value;
const size = document.getElementById("edit-size").value;
if (quantity < 1) {
alert("Quantity must be at least 1");
return;
}
// Update the item
this.quoteItems[itemIndex] = {
...this.quoteItems[itemIndex],
quantity: quantity,
color: color,
size: size,
};
this.saveQuoteItems();
this.renderQuoteItems();
this.closeEditModal();
// Show success message
this.showEditSuccess();
}
// Show success message for edit
showEditSuccess() {
const notification = document.createElement("div");
notification.className =
"fixed top-24 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 translate-x-full";
notification.innerHTML = `
<div class="flex items-center space-x-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="font-playfair font-semibold">Quote item updated!</span>
</div>
`;
document.body.appendChild(notification);
// Animate in
setTimeout(() => {
notification.classList.remove("translate-x-full");
}, 100);
// Remove after 3 seconds
setTimeout(() => {
notification.classList.add("translate-x-full");
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
// Clear all quote items
clearQuote() {
this.quoteItems = [];
this.saveQuoteItems();
this.renderQuoteItems();
}
// Get quote items count
getQuoteCount() {
return this.quoteItems.reduce((total, item) => total + item.quantity, 0);
}
// Update quote badge in navigation
updateQuoteBadge() {
const count = this.getQuoteCount();
const quoteLinks = document.querySelectorAll('a[href="quote.html"]');
quoteLinks.forEach((quoteLink) => {
// Remove existing badge
const existingBadge = quoteLink.querySelector(".quote-badge");
if (existingBadge) {
existingBadge.remove();
}
// Add new badge if there are items
if (count > 0) {
const badge = document.createElement("span");
badge.className =
"quote-badge absolute -top-2 -right-2 bg-uc-gold text-white text-xs rounded-full w-5 h-5 flex items-center justify-center font-semibold";
badge.textContent = count > 99 ? "99+" : count;
quoteLink.style.position = "relative";
quoteLink.appendChild(badge);
}
});
}
// Show success message when item is added
showAddToQuoteSuccess() {
// Create success notification
const notification = document.createElement("div");
notification.className =
"fixed top-24 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 translate-x-full";
notification.innerHTML = `
<div class="flex items-center space-x-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="font-playfair font-semibold">Added to quote!</span>
</div>
`;
document.body.appendChild(notification);
// Animate in
setTimeout(() => {
notification.classList.remove("translate-x-full");
}, 100);
// Remove after 3 seconds
setTimeout(() => {
notification.classList.add("translate-x-full");
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
// Render quote items on the quote page
renderQuoteItems() {
const container = document.getElementById("quote-items-container");
const emptyMessage = document.getElementById("empty-quote-message");
const quoteActions = document.getElementById("quote-actions");
if (!container) return;
if (this.quoteItems.length === 0) {
// Show empty state
if (emptyMessage) emptyMessage.style.display = "block";
if (quoteActions) quoteActions.classList.add("hidden");
// Clear any existing items but keep the empty message
const itemsToRemove = container.querySelectorAll(".bg-gray-50");
itemsToRemove.forEach((item) => item.remove());
return;
}
// Hide empty message and show actions
if (emptyMessage) emptyMessage.style.display = "none";
if (quoteActions) quoteActions.classList.remove("hidden");
// Remove any existing items first
const itemsToRemove = container.querySelectorAll(".bg-gray-50");
itemsToRemove.forEach((item) => item.remove());
// Render new items
this.quoteItems.forEach((item, index) => {
const itemElement = document.createElement("div");
itemElement.className =
"bg-gray-50 rounded-lg p-6 border border-gray-200";
itemElement.innerHTML = `
<div class="flex items-center space-x-6">
<!-- Product Image -->
<div class="w-24 h-24 flex-shrink-0">
<img
src="${item.image}"
alt="${item.name}"
class="w-full h-full object-cover rounded-lg"
/>
</div>
<!-- Product Details -->
<div class="flex-1">
<h3 class="font-playfair font-semibold text-xl text-black mb-2">
${item.name}
</h3>
<div class="flex items-center space-x-6 text-sm text-gray-600">
<span><strong>Color:</strong> ${item.color}</span>
<span><strong>Size:</strong> ${item.size}</span>
</div>
</div>
<!-- Quantity Controls -->
<div class="flex items-center space-x-3">
<button
onclick="quoteManager.updateQuantity(${index}, ${
item.quantity - 1
})"
class="w-8 h-8 rounded-full border border-gray-300 flex items-center justify-center hover:bg-gray-100 transition-colors"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"></path>
</svg>
</button>
<span class="font-playfair font-semibold text-lg text-black min-w-[2rem] text-center">
${item.quantity}
</span>
<button
onclick="quoteManager.updateQuantity(${index}, ${
item.quantity + 1
})"
class="w-8 h-8 rounded-full border border-gray-300 flex items-center justify-center hover:bg-gray-100 transition-colors"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
</button>
</div>
<!-- Edit Button -->
<button
onclick="quoteManager.editQuoteItem(${index})"
class="text-blue-500 hover:text-blue-700 transition-colors mr-2"
title="Edit item"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</button>
<!-- Remove Button -->
<button
onclick="quoteManager.removeFromQuote(${index})"
class="text-red-500 hover:text-red-700 transition-colors"
title="Remove item"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
`;
container.appendChild(itemElement);
});
}
// Render quote summary in modal
renderQuoteSummary() {
const summaryContainer = document.getElementById("quote-summary");
if (!summaryContainer) return;
summaryContainer.innerHTML = this.quoteItems
.map(
(item) => `
<div class="flex justify-between items-center py-2 border-b border-gray-100">
<div class="flex-1">
<h5 class="font-playfair font-semibold text-base text-black">${item.name}</h5>
<p class="text-sm text-gray-600">${item.color} • ${item.size} • Qty: ${item.quantity}</p>
</div>
</div>
`
)
.join("");
}
// Initialize quote manager
init() {
this.updateQuoteBadge();
this.renderQuoteItems();
this.setupEventListeners();
}
// Setup event listeners
setupEventListeners() {
// Clear quote button
const clearBtn = document.getElementById("clear-quote-btn");
if (clearBtn) {
clearBtn.addEventListener("click", () => {
if (confirm("Are you sure you want to clear your quote?")) {
this.clearQuote();
}
});
}
// Request quote button
const requestBtn = document.getElementById("request-quote-btn");
if (requestBtn) {
requestBtn.addEventListener("click", () => {
this.openQuoteModal();
});
}
// Modal close button
const closeBtn = document.getElementById("close-modal-btn");
if (closeBtn) {
closeBtn.addEventListener("click", () => {
this.closeQuoteModal();
});
}
// Modal backdrop click
const modal = document.getElementById("quote-modal");
if (modal) {
modal.addEventListener("click", (e) => {
if (e.target === modal) {
this.closeQuoteModal();
}
});
}
// Quote form submission
const quoteForm = document.getElementById("quote-form");
if (quoteForm) {
quoteForm.addEventListener("submit", (e) => {
e.preventDefault();
this.submitQuoteRequest();
});
}
}
// Open quote modal
openQuoteModal() {
const modal = document.getElementById("quote-modal");
if (modal) {
this.renderQuoteSummary();
modal.classList.remove("hidden");
document.body.style.overflow = "hidden";
}
}
// Close quote modal
closeQuoteModal() {
const modal = document.getElementById("quote-modal");
if (modal) {
modal.classList.add("hidden");
document.body.style.overflow = "auto";
}
}
// Submit quote request
submitQuoteRequest() {
const form = document.getElementById("quote-form");
const formData = new FormData(form);
// Get form data
const quoteData = {
name: formData.get("name"),
email: formData.get("email"),
phone: formData.get("phone"),
company: formData.get("company"),
project: formData.get("project"),
items: this.quoteItems,
timestamp: new Date().toISOString(),
};
// Here you would typically send this data to your server
console.log("Quote request data:", quoteData);
// For now, just show success message
alert("Thank you for your quote request! We will get back to you soon.");
// Clear the quote and close modal
this.clearQuote();
this.closeQuoteModal();
// Reset form
form.reset();
}
}
// Global quote manager instance
const quoteManager = new QuoteManager();
// Global function to add items to quote (called from other pages)
function addToQuote(productData) {
quoteManager.addToQuote(productData);
}
// Initialize when DOM is ready
document.addEventListener("DOMContentLoaded", () => {
// Quote manager is already initialized in constructor
});
// Version: 3.5 - Added edit functionality for quote items

272
scripts/sync-to-main.js Normal file
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

@ -78,7 +78,7 @@ function ensureFields(p) {
// 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";
const img = p.image || "../assets/images/asgaard_sofa.png";
p.galleryPairs = [{ left: img, right: img }];
}
}