reset repo
This commit is contained in:
parent
7f70170fe1
commit
9cdef853c4
631 changed files with 4941 additions and 16827 deletions
1996
scripts/main.js
1996
scripts/main.js
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
605
scripts/quote.js
605
scripts/quote.js
|
|
@ -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
272
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;
|
||||
|
|
@ -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 }];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue