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