Initial commit: KHY Admin project

This commit is contained in:
George Birikorang 2025-09-10 16:56:52 -07:00
commit d3403cb5e2
65 changed files with 12739 additions and 0 deletions

2073
scripts/main.js Normal file

File diff suppressed because it is too large Load diff

419
scripts/products.js Normal file
View file

@ -0,0 +1,419 @@
// Product Management System
class ProductManager {
constructor() {
this.products = [];
this.categories = [];
this.pagination = {};
this.currentPage = 1;
this.itemsPerPage = 16;
this.filteredProducts = [];
this.selectedCategories = new Set();
}
// 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(category) {
// 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}`;
}
}
// Check the corresponding checkbox in the dropdown
setTimeout(() => {
const checkboxes = document.querySelectorAll(".category-checkbox");
checkboxes.forEach((checkbox) => {
if (checkbox.value === category) {
checkbox.checked = true;
}
});
}, 100);
}
// Render products in the grid
renderProducts() {
const productGrid = document.getElementById("product-grid");
if (!productGrid) return;
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
const endIndex = startIndex + this.itemsPerPage;
const productsToShow = this.filteredProducts.slice(startIndex, endIndex);
productGrid.innerHTML = productsToShow
.map((product) => this.createProductCard(product))
.join("");
this.updateResultsCount();
this.updatePagination();
}
// 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="${product.image}"
alt="${product.alt}"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
<!-- 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();
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 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();
// Load products when DOM is ready
document.addEventListener("DOMContentLoaded", () => {
productManager.loadProducts();
});

605
scripts/quote.js Normal file
View file

@ -0,0 +1,605 @@
// 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

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