mirror of
https://git.kh3group.com/georgebiri/khy_website.git
synced 2026-07-02 07:03:33 +00:00
polished webspages and completed implementation.
This commit is contained in:
parent
23791a2768
commit
9a3a106fca
7 changed files with 1194 additions and 172 deletions
360
scripts/main.js
360
scripts/main.js
|
|
@ -1,8 +1,366 @@
|
|||
// Update year in footer
|
||||
// Update year in footer and handle smooth scrolling
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Update footer year
|
||||
const footer = document.querySelector("footer p");
|
||||
if (footer) {
|
||||
const currentYear = new Date().getFullYear();
|
||||
footer.innerHTML = footer.innerHTML.replace("2024", currentYear);
|
||||
}
|
||||
|
||||
// Smooth scrolling for navigation links
|
||||
const navLinks = document.querySelectorAll('.nav-link[href^="#"]');
|
||||
|
||||
navLinks.forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const targetId = this.getAttribute("href").substring(1);
|
||||
const targetElement = document.getElementById(targetId);
|
||||
|
||||
if (targetElement) {
|
||||
// Calculate offset for header height (112px = 28 * 4)
|
||||
const headerHeight = 112;
|
||||
const targetPosition = targetElement.offsetTop - headerHeight;
|
||||
|
||||
window.scrollTo({
|
||||
top: targetPosition,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle cross-page navigation links (links that point to other pages with anchors)
|
||||
const crossPageLinks = document.querySelectorAll('.nav-link[href*=".html#"]');
|
||||
|
||||
crossPageLinks.forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
// Don't prevent default - let the browser handle the navigation
|
||||
// The smooth scrolling will work on the target page
|
||||
});
|
||||
});
|
||||
|
||||
// Highlight current page link
|
||||
const currentPage = window.location.pathname.split("/").pop() || "index.html";
|
||||
const currentPageLink = document.querySelector(
|
||||
`.nav-link[href="${currentPage}"]`
|
||||
);
|
||||
if (currentPageLink) {
|
||||
currentPageLink.classList.add("active");
|
||||
}
|
||||
|
||||
// Active link highlighting on scroll
|
||||
const sections = document.querySelectorAll("section[id]");
|
||||
const navItems = document.querySelectorAll('.nav-link[href^="#"]');
|
||||
|
||||
function updateActiveLink() {
|
||||
const scrollPosition = window.scrollY + 150; // Offset for better detection
|
||||
|
||||
sections.forEach((section) => {
|
||||
const sectionTop = section.offsetTop;
|
||||
const sectionHeight = section.offsetHeight;
|
||||
const sectionId = section.getAttribute("id");
|
||||
|
||||
if (
|
||||
scrollPosition >= sectionTop &&
|
||||
scrollPosition < sectionTop + sectionHeight
|
||||
) {
|
||||
// Remove active class from all nav links
|
||||
navItems.forEach((item) => item.classList.remove("active"));
|
||||
|
||||
// Add active class to corresponding nav link
|
||||
const activeLink = document.querySelector(
|
||||
`.nav-link[href="#${sectionId}"]`
|
||||
);
|
||||
if (activeLink) {
|
||||
activeLink.classList.add("active");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for scroll events
|
||||
window.addEventListener("scroll", updateActiveLink);
|
||||
|
||||
// Initial call to set active link on page load
|
||||
updateActiveLink();
|
||||
|
||||
// Blog search functionality (only runs on blog page)
|
||||
const blogSearchInput = document.getElementById("blog-search-input");
|
||||
if (blogSearchInput) {
|
||||
const pagination = document.getElementById("blog-pagination");
|
||||
const pageButtons = document.querySelectorAll(
|
||||
"#blog-pagination .blog-page-btn"
|
||||
);
|
||||
const nextButton = document.getElementById("blog-next-btn");
|
||||
const tagButtons = document.querySelectorAll(".tag-filter");
|
||||
let activeTag = ""; // empty = no tag filter
|
||||
let currentPage = 1;
|
||||
const pageSize = 4; // number of posts per page
|
||||
|
||||
function normalize(text) {
|
||||
return (text || "")
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function getFilteredArticles() {
|
||||
const articles = Array.from(
|
||||
document.querySelectorAll("#main-blog-content article")
|
||||
);
|
||||
// Sort by date descending if data-date is present (YYYY-MM-DD)
|
||||
articles.sort((a, b) => {
|
||||
const ad = a.getAttribute("data-date") || "";
|
||||
const bd = b.getAttribute("data-date") || "";
|
||||
// Fallback: keep original order if dates missing
|
||||
if (!ad && !bd) return 0;
|
||||
return bd.localeCompare(ad);
|
||||
});
|
||||
const query = normalize(blogSearchInput.value.trim());
|
||||
return articles.filter((article) => {
|
||||
const title = article.querySelector("h2")?.textContent || "";
|
||||
const excerpt = article.querySelector("p")?.textContent || "";
|
||||
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);
|
||||
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);
|
||||
|
||||
// Show only items for current page
|
||||
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";
|
||||
});
|
||||
|
||||
// Update pagination visibility and active state
|
||||
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 filterPosts() {
|
||||
// Reset to first page when filters change
|
||||
currentPage = 1;
|
||||
renderPage();
|
||||
}
|
||||
|
||||
blogSearchInput.addEventListener("input", filterPosts);
|
||||
|
||||
// Tag filtering
|
||||
tagButtons.forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const clickedTag = normalize(btn.getAttribute("data-tag"));
|
||||
// Toggle logic: clicking active tag clears it
|
||||
activeTag = activeTag === clickedTag ? "" : clickedTag;
|
||||
|
||||
// Visual active state
|
||||
tagButtons.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");
|
||||
}
|
||||
|
||||
filterPosts();
|
||||
});
|
||||
});
|
||||
|
||||
// Pagination handlers
|
||||
pageButtons.forEach((btn) =>
|
||||
btn.addEventListener("click", () => {
|
||||
currentPage = Number(btn.getAttribute("data-page")) || 1;
|
||||
renderPage();
|
||||
})
|
||||
);
|
||||
if (nextButton) {
|
||||
nextButton.addEventListener("click", () => {
|
||||
currentPage += 1;
|
||||
renderPage();
|
||||
});
|
||||
}
|
||||
// Ensure correct initial state on load
|
||||
renderPage();
|
||||
}
|
||||
|
||||
// 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";
|
||||
});
|
||||
});
|
||||
|
||||
// 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) {
|
||||
const emailInputs = siteFooter.querySelectorAll('input[type="email"]');
|
||||
emailInputs.forEach((emailInput) => {
|
||||
const inputRow = emailInput.closest(".flex") || emailInput.parentElement;
|
||||
let subscribeButton = inputRow ? inputRow.querySelector("button") : null;
|
||||
if (!subscribeButton) {
|
||||
subscribeButton = emailInput.parentElement?.querySelector("button");
|
||||
}
|
||||
if (!subscribeButton) return;
|
||||
|
||||
// Ensure non-submitting behavior
|
||||
subscribeButton.type = "button";
|
||||
|
||||
// Create or reuse feedback element right after the row
|
||||
let feedback = inputRow ? inputRow.nextElementSibling : null;
|
||||
if (!(feedback instanceof HTMLElement) || !feedback.dataset.feedback) {
|
||||
feedback = document.createElement("div");
|
||||
feedback.dataset.feedback = "newsletter";
|
||||
if (inputRow && inputRow.parentElement) {
|
||||
inputRow.parentElement.insertBefore(feedback, inputRow.nextSibling);
|
||||
}
|
||||
}
|
||||
feedback.className =
|
||||
"hidden mt-3 rounded-lg px-4 py-3 font-playfair text-base";
|
||||
|
||||
function showMessage(text, type) {
|
||||
feedback.textContent = text;
|
||||
feedback.className =
|
||||
"mt-3 rounded-lg px-4 py-3 font-playfair text-base";
|
||||
if (type === "success") {
|
||||
feedback.classList.add(
|
||||
"bg-green-50",
|
||||
"border",
|
||||
"border-green-200",
|
||||
"text-green-800"
|
||||
);
|
||||
} else {
|
||||
feedback.classList.add(
|
||||
"bg-red-50",
|
||||
"border",
|
||||
"border-red-200",
|
||||
"text-red-800"
|
||||
);
|
||||
}
|
||||
feedback.classList.remove("hidden");
|
||||
setTimeout(() => feedback.classList.add("hidden"), 3000);
|
||||
}
|
||||
|
||||
subscribeButton.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const value = (emailInput.value || "").trim();
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
// Reset error styles
|
||||
emailInput.classList.remove("border-red-500");
|
||||
|
||||
if (!emailRegex.test(value)) {
|
||||
emailInput.classList.add("border-red-500");
|
||||
showMessage("Please enter a valid email address.", "error");
|
||||
emailInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
showMessage("Thank you for subscribing!", "success");
|
||||
emailInput.value = "";
|
||||
subscribeButton.disabled = true;
|
||||
setTimeout(() => (subscribeButton.disabled = false), 1200);
|
||||
});
|
||||
});
|
||||
}
|
||||
// Story arrow button functionality
|
||||
const storyArrowButton = document.getElementById("story-arrow-button");
|
||||
if (storyArrowButton) {
|
||||
storyArrowButton.addEventListener("click", function () {
|
||||
// You can customize this action - for now, let's scroll to the products section
|
||||
const productsSection = document.getElementById("products");
|
||||
if (productsSection) {
|
||||
const headerHeight = 112;
|
||||
const targetPosition = productsSection.offsetTop - headerHeight;
|
||||
|
||||
window.scrollTo({
|
||||
top: targetPosition,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue