Initial commit: KHY Admin project
This commit is contained in:
commit
d3403cb5e2
65 changed files with 12739 additions and 0 deletions
2073
scripts/main.js
Normal file
2073
scripts/main.js
Normal file
File diff suppressed because it is too large
Load diff
419
scripts/products.js
Normal file
419
scripts/products.js
Normal 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
605
scripts/quote.js
Normal 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
|
||||
91
scripts/update_products_tabs.js
Normal file
91
scripts/update_products_tabs.js
Normal 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"
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue