mirror of
https://git.kh3group.com/georgebiri/khy_website.git
synced 2026-07-02 07:03:33 +00:00
feat: add quote page
This commit is contained in:
parent
ec844c6c88
commit
0eab10dc09
9 changed files with 2057 additions and 242 deletions
739
scripts/main.js
739
scripts/main.js
|
|
@ -160,62 +160,258 @@ function initSite() {
|
|||
}
|
||||
})();
|
||||
|
||||
// Blog search functionality (only runs on blog page)
|
||||
const blogSearchInput = document.getElementById("blog-search-input");
|
||||
if (blogSearchInput) {
|
||||
// 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");
|
||||
const blogTagButtons = document.querySelectorAll(".tag-filter");
|
||||
|
||||
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 getFilteredArticles() {
|
||||
const articles = Array.from(
|
||||
document.querySelectorAll("#main-blog-content article")
|
||||
);
|
||||
articles.sort((a, b) => {
|
||||
const ad = a.getAttribute("data-date") || "";
|
||||
const bd = b.getAttribute("data-date") || "";
|
||||
if (!ad && !bd) return 0;
|
||||
return bd.localeCompare(ad);
|
||||
|
||||
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 articles.filter((article) => {
|
||||
const title = article.querySelector("h2")?.textContent || "";
|
||||
const excerpt = article.querySelector("p")?.textContent || "";
|
||||
return allPosts.filter((post) => {
|
||||
const title = post.title || "";
|
||||
const excerpt = post.excerpt || "";
|
||||
const haystack = normalize(`${title} ${excerpt}`);
|
||||
const tagList = (article.getAttribute("data-tags") || "")
|
||||
.split(",")
|
||||
.map((t) => normalize(t));
|
||||
const matchesQuery = query === "" || haystack.includes(query);
|
||||
const matchesTag = activeTag === "" || tagList.includes(activeTag);
|
||||
const matchesTag = activeTag === "" || post.tags.includes(activeTag);
|
||||
return matchesQuery && matchesTag;
|
||||
});
|
||||
}
|
||||
function renderPage() {
|
||||
const filtered = getFilteredArticles();
|
||||
const total = filtered.length;
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
currentPage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
|
||||
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 toShow = new Set(filtered.slice(start, end));
|
||||
document.querySelectorAll("#main-blog-content article").forEach((a) => {
|
||||
a.style.display = toShow.has(a) ? "" : "none";
|
||||
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);
|
||||
|
|
@ -224,46 +420,101 @@ function initSite() {
|
|||
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 filterPosts() {
|
||||
currentPage = 1;
|
||||
renderPage();
|
||||
}
|
||||
blogSearchInput.addEventListener("input", filterPosts);
|
||||
blogTagButtons.forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const clickedTag = normalize(btn.getAttribute("data-tag"));
|
||||
activeTag = activeTag === clickedTag ? "" : clickedTag;
|
||||
blogTagButtons.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");
|
||||
|
||||
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";
|
||||
}
|
||||
filterPosts();
|
||||
|
||||
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;
|
||||
renderPage();
|
||||
renderBlogPosts();
|
||||
renderPagination();
|
||||
})
|
||||
);
|
||||
|
||||
if (nextButton) {
|
||||
nextButton.addEventListener("click", () => {
|
||||
currentPage += 1;
|
||||
renderPage();
|
||||
renderBlogPosts();
|
||||
renderPagination();
|
||||
});
|
||||
}
|
||||
renderPage();
|
||||
}
|
||||
|
||||
// Initialize everything
|
||||
filteredPosts = getFilteredPosts();
|
||||
renderBlogPosts();
|
||||
renderCategories();
|
||||
renderRecentPosts();
|
||||
renderPagination();
|
||||
})();
|
||||
|
||||
// Inline "Read more" expansion for blog posts
|
||||
const articlesForExcerpt = document.querySelectorAll(
|
||||
|
|
@ -307,6 +558,27 @@ function initSite() {
|
|||
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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -444,6 +716,14 @@ function initSite() {
|
|||
// Update page title
|
||||
document.title = `${product.name} - KHY`;
|
||||
|
||||
// Update the "Product Details" title with the actual product name
|
||||
const productDetailsTitle = document.getElementById(
|
||||
"product-details-title"
|
||||
);
|
||||
if (productDetailsTitle) {
|
||||
productDetailsTitle.textContent = product.name;
|
||||
}
|
||||
|
||||
// Update product title (the main product title, not the breadcrumb)
|
||||
const allH1s = document.querySelectorAll("h1");
|
||||
console.log("All H1 elements:", allH1s);
|
||||
|
|
@ -516,6 +796,7 @@ function initSite() {
|
|||
if (sizeButtons[index]) {
|
||||
sizeButtons[index].style.display = "flex";
|
||||
sizeButtons[index].textContent = size;
|
||||
sizeButtons[index].setAttribute("data-size", size);
|
||||
// Set selected size
|
||||
if (size === product.selectedSize) {
|
||||
sizeButtons[index].classList.remove(
|
||||
|
|
@ -523,12 +804,32 @@ function initSite() {
|
|||
"text-black"
|
||||
);
|
||||
sizeButtons[index].classList.add("bg-uc-gold", "text-white");
|
||||
sizeButtons[index].classList.add("selected");
|
||||
} else {
|
||||
sizeButtons[index].classList.remove("bg-uc-gold", "text-white");
|
||||
sizeButtons[index].classList.remove(
|
||||
"bg-uc-gold",
|
||||
"text-white",
|
||||
"selected"
|
||||
);
|
||||
sizeButtons[index].classList.add("bg-floral-white", "text-black");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add click event listeners for size buttons
|
||||
sizeButtons.forEach((button) => {
|
||||
button.addEventListener("click", function () {
|
||||
// Remove selected state from all size buttons
|
||||
sizeButtons.forEach((btn) => {
|
||||
btn.classList.remove("bg-uc-gold", "text-white", "selected");
|
||||
btn.classList.add("bg-floral-white", "text-black");
|
||||
});
|
||||
|
||||
// Add selected state to clicked button
|
||||
this.classList.remove("bg-floral-white", "text-black");
|
||||
this.classList.add("bg-uc-gold", "text-white", "selected");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Update color options - find only the rounded color buttons, not size buttons
|
||||
|
|
@ -546,14 +847,39 @@ function initSite() {
|
|||
product.colors.forEach((color, index) => {
|
||||
if (colorButtons[index]) {
|
||||
colorButtons[index].style.backgroundColor = color.value;
|
||||
colorButtons[index].setAttribute(
|
||||
"data-color",
|
||||
color.name || color.value
|
||||
);
|
||||
// Set selected color
|
||||
if (color.selected) {
|
||||
colorButtons[index].classList.add("border-2", "border-black");
|
||||
colorButtons[index].classList.add(
|
||||
"border-2",
|
||||
"border-black",
|
||||
"selected"
|
||||
);
|
||||
} else {
|
||||
colorButtons[index].classList.remove("border-2", "border-black");
|
||||
colorButtons[index].classList.remove(
|
||||
"border-2",
|
||||
"border-black",
|
||||
"selected"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add click event listeners for color buttons
|
||||
colorButtons.forEach((button) => {
|
||||
button.addEventListener("click", function () {
|
||||
// Remove selected state from all color buttons
|
||||
colorButtons.forEach((btn) => {
|
||||
btn.classList.remove("border-2", "border-black", "selected");
|
||||
});
|
||||
|
||||
// Add selected state to clicked button
|
||||
this.classList.add("border-2", "border-black", "selected");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Update product metadata - find by text content
|
||||
|
|
@ -671,36 +997,69 @@ function initSite() {
|
|||
);
|
||||
|
||||
// Filter related products by category (excluding current product)
|
||||
const relatedProducts = data.products
|
||||
.filter((p) => p.category === product.category && p.id !== product.id)
|
||||
.slice(0, 4);
|
||||
const allRelatedProducts = data.products.filter(
|
||||
(p) => p.category === product.category && p.id !== product.id
|
||||
);
|
||||
|
||||
console.log("Related products found:", relatedProducts);
|
||||
console.log("All related products found:", allRelatedProducts);
|
||||
|
||||
relatedGrid.innerHTML = relatedProducts
|
||||
.map(
|
||||
(p) => `
|
||||
<a href="product-detail.html?id=${p.id}" class="block">
|
||||
<div class="rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-lg transition-shadow">
|
||||
<div class="h-72 ${
|
||||
p.name.toLowerCase().includes("asgaard")
|
||||
? "bg-linen"
|
||||
: "bg-white"
|
||||
} flex items-center justify-center">
|
||||
<img src="${p.image}" alt="${
|
||||
p.alt || p.name
|
||||
}" class="w-full h-full object-cover" />
|
||||
let currentPage = 1;
|
||||
const pageSize = 4;
|
||||
|
||||
function renderRelatedProducts() {
|
||||
const start = 0;
|
||||
const end = currentPage * pageSize;
|
||||
const productsToShow = allRelatedProducts.slice(start, end);
|
||||
|
||||
relatedGrid.innerHTML = productsToShow
|
||||
.map(
|
||||
(p) => `
|
||||
<a href="product-detail.html?id=${p.id}" class="block">
|
||||
<div class="rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-lg transition-shadow">
|
||||
<div class="h-72 ${
|
||||
p.name.toLowerCase().includes("asgaard")
|
||||
? "bg-linen"
|
||||
: "bg-white"
|
||||
} flex items-center justify-center">
|
||||
<img src="${p.image}" alt="${
|
||||
p.alt || p.name
|
||||
}" class="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div class="bg-light-bg p-6">
|
||||
<h3 class="font-poppins font-semibold text-2xl text-[#3A3A3A] mb-0">${
|
||||
p.name
|
||||
}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-light-bg p-6">
|
||||
<h3 class="font-poppins font-semibold text-2xl text-[#3A3A3A] mb-0">${
|
||||
p.name
|
||||
}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
</a>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
// Handle "Show More" button visibility
|
||||
const showMoreBtn = document.getElementById("related-show-more");
|
||||
if (showMoreBtn) {
|
||||
const hasMore = allRelatedProducts.length > end;
|
||||
showMoreBtn.style.display = hasMore ? "inline-flex" : "none";
|
||||
showMoreBtn.classList.add(
|
||||
"inline-flex",
|
||||
"items-center",
|
||||
"justify-center"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add event listener for "Show More" button
|
||||
const showMoreBtn = document.getElementById("related-show-more");
|
||||
if (showMoreBtn) {
|
||||
showMoreBtn.addEventListener("click", () => {
|
||||
currentPage += 1;
|
||||
renderRelatedProducts();
|
||||
});
|
||||
}
|
||||
|
||||
// Initial render
|
||||
renderRelatedProducts();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading product data:", error);
|
||||
|
|
@ -1154,8 +1513,230 @@ 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
|
||||
const quoteLink = document.querySelector('a[href="quote.html"]');
|
||||
if (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) {
|
||||
addToQuoteBtn.addEventListener("click", function () {
|
||||
// Get product data from the page
|
||||
const productData = getProductDataFromPage();
|
||||
if (productData) {
|
||||
// Add to quote using the global function
|
||||
if (typeof addToQuote === "function") {
|
||||
addToQuote(productData);
|
||||
} else {
|
||||
// Fallback: directly use localStorage
|
||||
addToQuoteFallback(productData);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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 image (you might need to adjust these selectors)
|
||||
const productName = document.querySelector("h1")?.textContent || "Product";
|
||||
const productImage =
|
||||
document.querySelector(".w-\\[500px\\].h-\\[500px\\] img")?.src ||
|
||||
document.querySelector(".w-[500px].h-[500px] img")?.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
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
initQuoteBadge();
|
||||
initAddToQuote();
|
||||
});
|
||||
|
||||
// Version: 4.2 - Moved Show Less button to end of blog post content
|
||||
|
|
|
|||
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 quoteLink = document.querySelector('a[href="quote.html"]');
|
||||
|
||||
if (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
|
||||
Loading…
Add table
Add a link
Reference in a new issue