document.addEventListener("DOMContentLoaded", () => { setupMobileNavigation(); setupScrollAnimations(); setupProjectFiltering(); setupContactForm(); setupSmoothScrollClose(); }); function setupMobileNavigation() { const toggle = document.querySelector("[data-menu-toggle]"); const mobileNav = document.querySelector("[data-mobile-nav]"); if (!toggle || !mobileNav) { return; } const updateState = (isOpen) => { toggle.classList.toggle("is-open", isOpen); toggle.setAttribute("aria-expanded", String(isOpen)); mobileNav.classList.toggle("is-open", isOpen); document.body.classList.toggle("menu-open", isOpen); }; toggle.addEventListener("click", () => { const isOpen = toggle.getAttribute("aria-expanded") === "true"; updateState(!isOpen); }); mobileNav.querySelectorAll("a").forEach((link) => { link.addEventListener("click", () => updateState(false)); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { updateState(false); } }); document.addEventListener("click", (event) => { const clickedInsideMenu = mobileNav.contains(event.target); const clickedToggle = toggle.contains(event.target); if (!clickedInsideMenu && !clickedToggle) { updateState(false); } }); } function setupSmoothScrollClose() { const samePageLinks = document.querySelectorAll('a[href^="#"]'); samePageLinks.forEach((link) => { link.addEventListener("click", (event) => { const targetId = link.getAttribute("href"); if (!targetId || targetId === "#") { return; } const target = document.querySelector(targetId); if (!target) { return; } event.preventDefault(); target.scrollIntoView({ behavior: "smooth", block: "start" }); }); }); } function setupScrollAnimations() { const animatedItems = document.querySelectorAll("[data-animate]"); if (!animatedItems.length) { return; } if (!("IntersectionObserver" in window)) { animatedItems.forEach((item) => item.classList.add("is-visible")); return; } const observer = new IntersectionObserver( (entries, currentObserver) => { entries.forEach((entry) => { if (!entry.isIntersecting) { return; } entry.target.classList.add("is-visible"); currentObserver.unobserve(entry.target); }); }, { threshold: 0.12, rootMargin: "0px 0px -40px 0px" } ); animatedItems.forEach((item) => observer.observe(item)); } function setupProjectFiltering() { const filterButtons = document.querySelectorAll("[data-filter]"); const filterItems = document.querySelectorAll("[data-category]"); if (!filterButtons.length || !filterItems.length) { return; } filterButtons.forEach((button) => { button.addEventListener("click", () => { const filterValue = button.dataset.filter; filterButtons.forEach((otherButton) => { otherButton.classList.toggle("is-active", otherButton === button); otherButton.setAttribute("aria-pressed", String(otherButton === button)); }); filterItems.forEach((item) => { const categories = (item.dataset.category || "").split(" "); const shouldShow = filterValue === "all" || categories.includes(filterValue); item.classList.toggle("is-hidden", !shouldShow); }); }); }); } function setupContactForm() { const form = document.querySelector("[data-contact-form]"); if (!form) { return; } const fields = Array.from(form.querySelectorAll("[data-required], [type='email']")); const status = form.querySelector("[data-form-status]"); const validators = { email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) }; const setFieldError = (field, message) => { const wrapper = field.closest(".form-field"); const error = wrapper ? wrapper.querySelector(".form-field__error") : null; field.setAttribute("aria-invalid", String(Boolean(message))); if (wrapper) { wrapper.classList.toggle("is-invalid", Boolean(message)); } if (error) { error.textContent = message || ""; } }; const validateField = (field) => { const rawValue = field.value.trim(); const label = field.dataset.label || "This field"; if (field.hasAttribute("data-required") && !rawValue) { setFieldError(field, `${label} is required.`); return false; } if (field.type === "email" && rawValue && !validators.email(rawValue)) { setFieldError(field, "Enter a valid email address."); return false; } setFieldError(field, ""); return true; }; fields.forEach((field) => { field.addEventListener("blur", () => validateField(field)); field.addEventListener("input", () => { if (field.getAttribute("aria-invalid") === "true") { validateField(field); } }); }); form.addEventListener("submit", (event) => { let isValid = true; fields.forEach((field) => { if (!validateField(field)) { isValid = false; } }); if (!isValid) { event.preventDefault(); if (status) { status.textContent = "Please review the highlighted fields and try again."; status.classList.remove("is-success"); } const firstInvalid = form.querySelector("[aria-invalid='true']"); if (firstInvalid) { firstInvalid.focus(); } return; } event.preventDefault(); if (status) { status.textContent = "Thanks. Your message structure is validated and ready for backend integration."; status.classList.add("is-success"); } form.reset(); fields.forEach((field) => setFieldError(field, "")); }); }