refactor: remove blog and quote pages along with related scripts and data, streamline navigation and enhance overall site structure
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
George Birikorang 2025-09-11 16:09:47 -07:00
parent be36d6aa3c
commit 8fe919c449
11 changed files with 274 additions and 3127 deletions

View file

@ -160,445 +160,6 @@ function initSite() {
}
})();
// Blog functionality (only runs on blog page)
(async function initBlog() {
const blogSearchInput = document.getElementById("blog-search-input");
const mainBlogContent = document.getElementById("main-blog-content");
const blogCategories = document.getElementById("blog-categories");
const recentPosts = document.getElementById("recent-posts");
const pagination = document.getElementById("blog-pagination");
const pageButtons = document.querySelectorAll(
"#blog-pagination .blog-page-btn"
);
const nextButton = document.getElementById("blog-next-btn");
if (!blogSearchInput || !mainBlogContent) return;
let allPosts = [];
let filteredPosts = [];
let activeTag = "";
let currentPage = 1;
const pageSize = 4;
// Load blog data from JSON
try {
const response = await fetch("data/blog.json", { cache: "no-store" });
const data = await response.json();
allPosts = Array.isArray(data.posts) ? data.posts : [];
// Sort posts by date (newest first)
allPosts.sort((a, b) => new Date(b.date) - new Date(a.date));
console.log("Blog posts loaded:", allPosts.length);
} catch (error) {
console.error("Failed to load blog posts:", error);
return;
}
function normalize(text) {
return (text || "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, " ")
.trim();
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString("en-US", {
day: "numeric",
month: "short",
year: "numeric",
});
}
function getFilteredPosts() {
const query = normalize(blogSearchInput.value.trim());
return allPosts.filter((post) => {
const title = post.title || "";
const excerpt = post.excerpt || "";
const haystack = normalize(`${title} ${excerpt}`);
const matchesQuery = query === "" || haystack.includes(query);
const matchesTag = activeTag === "" || post.tags.includes(activeTag);
return matchesQuery && matchesTag;
});
}
function renderBlogPost(post) {
const tagsHtml = post.tags
.map(
(tag) =>
`<span class="font-playfair font-normal text-base text-gray-500">${
tag.charAt(0).toUpperCase() + tag.slice(1)
}</span>`
)
.join(", ");
const contentHtml = post.content
.map((paragraph) => `<p class="mb-4">${paragraph}</p>`)
.join("");
return `
<article class="mb-16" data-tags="${post.tags.join(",")}" data-date="${
post.date
}">
<!-- Featured Image -->
<div class="mb-8">
<img
src="${post.image}"
alt="${post.alt || post.title}"
class="w-full h-96 object-cover rounded-lg"
/>
</div>
<!-- Post Meta -->
<div class="flex items-center space-x-8 mb-6">
<!-- Admin -->
<div class="flex items-center space-x-2">
<img src="assets/icons/admin.png" alt="Admin" class="w-5 h-5" />
<span class="font-playfair font-normal text-base text-gray-500">
${post.author}
</span>
</div>
<!-- Date -->
<div class="flex items-center space-x-2">
<img src="assets/icons/calendar.png" alt="Calendar" class="w-5 h-5" />
<span class="font-playfair font-normal text-base text-gray-500">
${formatDate(post.date)}
</span>
</div>
<!-- Category -->
<div class="flex items-center space-x-2">
<img src="assets/icons/tag.png" alt="Tag" class="w-6 h-6" />
<span class="font-playfair font-normal text-base text-gray-500">
${
post.tags[0]
? post.tags[0].charAt(0).toUpperCase() +
post.tags[0].slice(1)
: "Uncategorized"
}
</span>
</div>
</div>
<!-- Post Title -->
<h2 class="font-playfair font-medium text-3xl md:text-4xl leading-tight text-black mb-6">
${post.title}
</h2>
<!-- Post Excerpt -->
<p class="font-playfair font-normal text-base leading-relaxed text-gray-500 mb-8 text-justify">
${post.excerpt}
</p>
<!-- Read More Link -->
<button
type="button"
class="read-more-toggle inline-block font-playfair font-normal text-base text-black border-b border-black hover:text-gray-600 hover:border-gray-600 transition-colors"
>
Read more
</button>
<!-- Full content (initially hidden) -->
<div class="full-content hidden font-playfair font-normal text-base leading-relaxed text-gray-500 mt-6 text-justify">
${contentHtml}
</div>
</article>
`;
}
function renderBlogPosts() {
const start = (currentPage - 1) * pageSize;
const end = start + pageSize;
const postsToShow = filteredPosts.slice(start, end);
mainBlogContent.innerHTML = postsToShow
.map((post) => renderBlogPost(post))
.join("");
// Re-initialize read more functionality
initReadMore();
}
function renderCategories() {
const tagCounts = {};
allPosts.forEach((post) => {
post.tags.forEach((tag) => {
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
});
});
const categoriesHtml = Object.entries(tagCounts)
.map(
([tag, count]) => `
<button
type="button"
class="flex justify-between items-center w-full text-left tag-filter px-4 py-3 rounded-lg border border-gray-300 hover:border-uc-gold hover:bg-gray-50 transition-colors"
data-tag="${tag}"
aria-pressed="false"
>
<span class="font-playfair font-normal text-base">
${tag.charAt(0).toUpperCase() + tag.slice(1)}
</span>
<span class="font-playfair font-normal text-base">
(${count})
</span>
</button>
`
)
.join("");
blogCategories.innerHTML = categoriesHtml;
// Add event listeners to category buttons
const categoryButtons = blogCategories.querySelectorAll(".tag-filter");
categoryButtons.forEach((btn) => {
btn.addEventListener("click", () => {
const clickedTag = btn.getAttribute("data-tag");
activeTag = activeTag === clickedTag ? "" : clickedTag;
categoryButtons.forEach((b) => {
b.classList.remove("border-uc-gold", "bg-gray-50", "text-uc-gold");
b.setAttribute("aria-pressed", "false");
});
if (activeTag) {
btn.classList.add("border-uc-gold", "bg-gray-50", "text-uc-gold");
btn.setAttribute("aria-pressed", "true");
}
currentPage = 1;
filteredPosts = getFilteredPosts();
renderBlogPosts();
renderPagination();
});
});
}
function renderRecentPosts() {
const recentPostsHtml = allPosts
.slice(0, 3)
.map(
(post) => `
<div class="flex space-x-4">
<img
src="${post.thumbnail}"
alt="${post.alt || post.title}"
class="w-20 h-20 object-cover rounded-lg"
/>
<div>
<h4 class="font-playfair font-normal text-sm text-black mb-2 leading-tight">
${post.title}
</h4>
<p class="font-playfair font-normal text-xs text-gray-500">
${formatDate(post.date)}
</p>
</div>
</div>
`
)
.join("");
recentPosts.innerHTML = recentPostsHtml;
}
function renderPagination() {
const total = filteredPosts.length;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
currentPage = Math.min(Math.max(1, currentPage), totalPages);
if (pagination) {
const hasQuery = blogSearchInput.value.trim().length > 0;
pagination.style.display = hasQuery ? "none" : "flex";
pageButtons.forEach((btn) => {
const p = Number(btn.getAttribute("data-page"));
btn.classList.toggle("bg-uc-gold", p === currentPage);
btn.classList.toggle("text-white", p === currentPage);
btn.classList.toggle("bg-linen", p !== currentPage);
btn.classList.toggle("text-black", p !== currentPage);
btn.style.display = p <= totalPages ? "inline-flex" : "none";
});
if (nextButton) {
nextButton.style.display = totalPages > 1 ? "inline-flex" : "none";
nextButton.disabled = currentPage >= totalPages;
}
}
}
function initReadMore() {
const articles = document.querySelectorAll("#main-blog-content article");
articles.forEach((article) => {
const excerptP = article.querySelector("p");
const readMoreBtn = article.querySelector(".read-more-toggle");
if (!excerptP || !readMoreBtn) return;
const fullExcerpt = excerptP.textContent || "";
const { text: truncated, truncated: isTruncated } = truncateToWords(
fullExcerpt,
80
);
if (isTruncated) {
excerptP.dataset.excerptFull = fullExcerpt;
excerptP.textContent = truncated;
readMoreBtn.style.display = "inline-block";
} else {
readMoreBtn.style.display = "none";
}
readMoreBtn.addEventListener("click", (ev) => {
ev.preventDefault();
if (excerptP.dataset.excerptFull) {
excerptP.textContent = excerptP.dataset.excerptFull;
delete excerptP.dataset.excerptFull;
}
const fullBlock = article.querySelector(".full-content");
if (fullBlock) fullBlock.classList.remove("hidden");
readMoreBtn.style.display = "none";
// Add "Show less" button at the end of the article
const showLessBtn = document.createElement("button");
showLessBtn.type = "button";
showLessBtn.className =
"show-less-toggle inline-block font-playfair font-normal text-base text-black border-b border-black hover:text-gray-600 hover:border-gray-600 transition-colors mt-4";
showLessBtn.textContent = "Show less";
showLessBtn.addEventListener("click", (ev) => {
ev.preventDefault();
// Restore truncated excerpt
excerptP.textContent = truncated;
excerptP.dataset.excerptFull = fullExcerpt;
// Hide full content
if (fullBlock) fullBlock.classList.add("hidden");
// Show "Read more" button again
readMoreBtn.style.display = "inline-block";
// Remove "Show less" button
showLessBtn.remove();
});
// Insert at the end of the article
article.appendChild(showLessBtn);
});
});
}
// Event listeners
blogSearchInput.addEventListener("input", () => {
currentPage = 1;
filteredPosts = getFilteredPosts();
renderBlogPosts();
renderPagination();
});
pageButtons.forEach((btn) =>
btn.addEventListener("click", () => {
currentPage = Number(btn.getAttribute("data-page")) || 1;
renderBlogPosts();
renderPagination();
})
);
if (nextButton) {
nextButton.addEventListener("click", () => {
currentPage += 1;
renderBlogPosts();
renderPagination();
});
}
// Initialize everything
filteredPosts = getFilteredPosts();
renderBlogPosts();
renderCategories();
renderRecentPosts();
renderPagination();
})();
// Inline "Read more" expansion for blog posts
const articlesForExcerpt = document.querySelectorAll(
"#main-blog-content article"
);
function truncateToWords(text, maxWords) {
const words = (text || "").trim().split(/\s+/);
if (words.length <= maxWords) return { text, truncated: false };
return { text: words.slice(0, maxWords).join(" ") + "…", truncated: true };
}
articlesForExcerpt.forEach((article) => {
const excerptP = article.querySelector("p");
const readMoreBtn = article.querySelector(".read-more-toggle");
if (!excerptP || !readMoreBtn) return;
const fullExcerpt = excerptP.textContent || "";
const { text: truncated, truncated: isTruncated } = truncateToWords(
fullExcerpt,
80
);
if (isTruncated) {
// Store full text and show truncated preview
excerptP.dataset.excerptFull = fullExcerpt;
excerptP.textContent = truncated;
readMoreBtn.style.display = "inline-block";
} else {
// Nothing to expand; hide the control
readMoreBtn.style.display = "none";
}
// Click to expand (delegated fallback added below as well)
readMoreBtn.addEventListener("click", (ev) => {
ev.preventDefault();
if (excerptP.dataset.excerptFull) {
excerptP.textContent = excerptP.dataset.excerptFull;
delete excerptP.dataset.excerptFull;
}
const fullBlock = article.querySelector(".full-content");
if (fullBlock) fullBlock.classList.remove("hidden");
readMoreBtn.style.display = "none";
// Add "Show less" button at the end of the article
const showLessBtn = document.createElement("button");
showLessBtn.type = "button";
showLessBtn.className =
"show-less-toggle inline-block font-playfair font-normal text-base text-black border-b border-black hover:text-gray-600 hover:border-gray-600 transition-colors mt-4";
showLessBtn.textContent = "Show less";
showLessBtn.addEventListener("click", (ev) => {
ev.preventDefault();
// Restore truncated excerpt
excerptP.textContent = truncated;
excerptP.dataset.excerptFull = fullExcerpt;
// Hide full content
if (fullBlock) fullBlock.classList.add("hidden");
// Show "Read more" button again
readMoreBtn.style.display = "inline-block";
// Remove "Show less" button
showLessBtn.remove();
});
// Insert at the end of the article
article.appendChild(showLessBtn);
});
});
// Safety net: delegated handler in case markup changes
document.addEventListener("click", (e) => {
const trigger = e.target.closest(".read-more-toggle");
if (!trigger) return;
e.preventDefault();
const article = trigger.closest("article");
if (!article) return;
const excerptP = article.querySelector("p");
if (excerptP?.dataset?.excerptFull) {
excerptP.textContent = excerptP.dataset.excerptFull;
delete excerptP.dataset.excerptFull;
}
const fullBlock = article.querySelector(".full-content");
if (fullBlock) fullBlock.classList.remove("hidden");
trigger.style.display = "none";
});
// Footer newsletter subscribe validation and feedback (works on all pages)
const siteFooter = document.querySelector("footer");
if (siteFooter) {
@ -1108,113 +669,12 @@ function initProductComparison() {
// Update View More link with current comparison state
updateViewMoreLink(product1Id, product2Id);
// Wire Add To Quote buttons (first -> product1, second -> product2)
wireComparisonAddToQuote(cmpProduct1, cmpProduct2);
})
.catch((error) => {
console.error("Error loading products:", error);
});
}
// Bind the two Add To Quote buttons on the comparison page
function wireComparisonAddToQuote(prod1, prod2) {
try {
const addButtons = Array.from(document.querySelectorAll("button")).filter(
(b) => (b.textContent || "").trim() === "Add To Quote"
);
if (addButtons.length === 0) {
console.log("[Comparison] No Add To Quote buttons found");
return;
}
// Ensure deterministic order: the first encountered is for product 1
const btn1 = addButtons[0] || null;
const btn2 = addButtons[1] || null;
function toQuotePayload(p) {
if (!p) return null;
return {
id: Number(p.id),
name: p.name || "Product",
image: p.image || "",
color:
(p.colors &&
p.colors[0] &&
(p.colors[0].name || p.colors[0].value)) ||
"Default",
size: (p.sizes && p.sizes[0]) || "Standard",
quantity: 1,
};
}
if (btn1) {
btn1.type = btn1.getAttribute("type") || "button";
btn1.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const payload = toQuotePayload(prod1);
if (!payload) return;
payload.quantity = 1; // force 1 for comparison adds
if (typeof addToQuote === "function") {
addToQuote(payload);
} else {
addToQuoteFallback(payload);
}
});
}
if (btn2) {
btn2.type = btn2.getAttribute("type") || "button";
btn2.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const payload = toQuotePayload(prod2);
if (!payload) return;
payload.quantity = 1; // force 1 for comparison adds
if (typeof addToQuote === "function") {
addToQuote(payload);
} else {
addToQuoteFallback(payload);
}
});
}
// Delegated fallback (in case DOM changes after load)
document.addEventListener("click", (e) => {
const t = e.target.closest && e.target.closest("button");
if (!t) return;
const label = (t.textContent || "").trim();
if (label !== "Add To Quote") return;
// Determine which button index this is relative to current NodeList
const currentButtons = Array.from(
document.querySelectorAll("button")
).filter((b) => (b.textContent || "").trim() === "Add To Quote");
const idx = currentButtons.indexOf(t);
if (idx === 0) {
const payload = toQuotePayload(prod1);
if (payload) {
payload.quantity = 1;
(typeof addToQuote === "function" ? addToQuote : addToQuoteFallback)(
payload
);
}
} else if (idx === 1) {
const payload = toQuotePayload(prod2);
if (payload) {
payload.quantity = 1;
(typeof addToQuote === "function" ? addToQuote : addToQuoteFallback)(
payload
);
}
}
});
} catch (err) {
console.error("[Comparison] Failed to wire Add To Quote buttons:", err);
}
}
function updateProductCard(slotNumber, product) {
console.log(`=== UPDATING PRODUCT CARD ${slotNumber} ===`);
console.log("Product data:", product);
@ -1610,297 +1070,14 @@ document.addEventListener("DOMContentLoaded", function () {
updateFontClasses();
});
// Initialize quote badge on all pages
function initQuoteBadge() {
// Load quote items from localStorage
const storageKey = "khy_quote_items";
let quoteItems = [];
try {
const stored = localStorage.getItem(storageKey);
quoteItems = stored ? JSON.parse(stored) : [];
} catch (error) {
console.error("Error loading quote items:", error);
}
// Calculate total count
const count = quoteItems.reduce((total, item) => total + item.quantity, 0);
// Update quote badge on all quote links (desktop nav, mobile button, mobile menu)
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);
}
});
}
// Initialize Add To Quote functionality on product detail pages
function initAddToQuote() {
const addToQuoteBtn = document.getElementById("add-to-quote-btn");
if (addToQuoteBtn) {
try {
// Ensure button is not treated as a submit in case inside a form
if (!addToQuoteBtn.getAttribute("type")) {
addToQuoteBtn.setAttribute("type", "button");
}
addToQuoteBtn.addEventListener("click", function (e) {
e.preventDefault();
e.stopPropagation();
console.log("[AddToQuote] Direct click captured");
const productData = getProductDataFromPage();
console.log("[AddToQuote] productData:", productData);
if (productData) {
if (typeof addToQuote === "function") {
addToQuote(productData);
} else {
addToQuoteFallback(productData);
}
} else {
console.warn(
"[AddToQuote] No product data found. Check URL id and DOM."
);
}
});
} catch (err) {
console.error("[AddToQuote] Failed to bind direct listener:", err);
}
}
// Delegated fallback in case the button is replaced dynamically
document.addEventListener("click", function (e) {
const btn = e.target.closest && e.target.closest("#add-to-quote-btn");
if (!btn) return;
e.preventDefault();
e.stopPropagation();
console.log("[AddToQuote] Delegated click captured");
try {
const productData = getProductDataFromPage();
console.log("[AddToQuote][delegated] productData:", productData);
if (productData) {
if (typeof addToQuote === "function") {
addToQuote(productData);
} else {
addToQuoteFallback(productData);
}
} else {
console.warn("[AddToQuote][delegated] No product data found.");
}
} catch (err) {
console.error("[AddToQuote][delegated] Error handling click:", err);
}
});
}
// Get product data from the current page
function getProductDataFromPage() {
// Get product ID from URL
const urlParams = new URLSearchParams(window.location.search);
const productId = urlParams.get("id");
if (!productId) {
console.error("No product ID found in URL");
return null;
}
// Get selected color and size
const selectedColor = getSelectedColor();
const selectedSize = getSelectedSize();
const selectedQuantity = getSelectedQuantity();
// Get product name and a best-effort product image
const productName =
document.querySelector("h1")?.textContent?.trim() || "Product";
let productImage = "";
// Helper to guard against invalid selectors (e.g., unescaped brackets)
function qsSafe(selector) {
try {
return document.querySelector(selector);
} catch (e) {
return null;
}
}
// Try a series of selectors safely
const imageCandidates = [
".w-\\[500px\\].h-\\[500px\\] img",
".w-[500px].h-[500px] img",
".bg-floral-white img",
"section img[alt]",
];
for (const sel of imageCandidates) {
const el = qsSafe(sel);
if (el && el.src) {
productImage = el.src;
break;
}
}
if (!productImage) {
const anyImg = qsSafe("img[alt]") || qsSafe("section img") || qsSafe("img");
productImage = anyImg?.src || "";
}
return {
id: parseInt(productId),
name: productName,
image: productImage,
color: selectedColor,
size: selectedSize,
quantity: selectedQuantity,
};
}
// Get selected color from the page
function getSelectedColor() {
const colorButtons = document.querySelectorAll("button[data-color]");
for (let button of colorButtons) {
if (
button.classList.contains("selected") ||
button.classList.contains("border-black")
) {
return button.getAttribute("data-color") || "Default";
}
}
return "Default";
}
// Get selected size from the page
function getSelectedSize() {
const sizeButtons = document.querySelectorAll("button[data-size]");
for (let button of sizeButtons) {
if (
button.classList.contains("selected") ||
button.classList.contains("bg-uc-gold")
) {
return button.getAttribute("data-size") || "Standard";
}
}
return "Standard";
}
// Get selected quantity from the page
function getSelectedQuantity() {
const quantitySpan = document.getElementById("qty-value");
if (quantitySpan) {
return parseInt(quantitySpan.textContent) || 1;
}
return 1;
}
// Fallback function to add to quote directly
function addToQuoteFallback(productData) {
const storageKey = "khy_quote_items";
let quoteItems = [];
try {
const stored = localStorage.getItem(storageKey);
quoteItems = stored ? JSON.parse(stored) : [];
} catch (error) {
console.error("Error loading quote items:", error);
quoteItems = [];
}
// Check if item already exists with same specifications
const existingItemIndex = quoteItems.findIndex(
(item) =>
item.id === productData.id &&
item.color === productData.color &&
item.size === productData.size
);
if (existingItemIndex !== -1) {
// Update quantity of existing item
quoteItems[existingItemIndex].quantity += productData.quantity;
} else {
// Add new item
const newItem = {
...productData,
timestamp: new Date().toISOString(),
};
quoteItems.push(newItem);
}
// Save to localStorage
try {
localStorage.setItem(storageKey, JSON.stringify(quoteItems));
// Update quote badge
initQuoteBadge();
// Show success message
showAddToQuoteSuccess();
} catch (error) {
console.error("Error saving quote items:", error);
}
}
// Show success message when item is added
function 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(() => {
if (document.body.contains(notification)) {
document.body.removeChild(notification);
}
}, 300);
}, 3000);
}
// Initialize quote badge immediately if DOM is already loaded
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initQuoteBadge);
} else {
initQuoteBadge();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initSite);
} else {
initSite();
}
// Initialize quote badge on page load
// Initialize page functionality
document.addEventListener("DOMContentLoaded", function () {
initQuoteBadge();
initAddToQuote();
initHeroCarousel();
initMobileMenu();
});

View file

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