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 submitButton = form.querySelector("button[type='submit']"); const action = (form.getAttribute("action") || "").trim(); const hasExternalEndpoint = /^https?:\/\//i.test(action); const hasPlaceholderEndpoint = action.includes("YOUR_ID_HERE"); const method = (form.getAttribute("method") || "post").toUpperCase(); const buttonText = submitButton ? submitButton.textContent : ""; const validators = { email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) }; const setStatus = (message, state = "") => { if (!status) { return; } status.textContent = message; status.classList.toggle("is-success", state === "success"); status.classList.toggle("is-error", state === "error"); }; const setSubmitting = (isSubmitting) => { if (!submitButton) { return; } submitButton.disabled = isSubmitting; submitButton.setAttribute("aria-busy", String(isSubmitting)); submitButton.textContent = isSubmitting ? "Sending..." : buttonText; }; 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(); setStatus("Please review the highlighted fields and try again.", "error"); const firstInvalid = form.querySelector("[aria-invalid='true']"); if (firstInvalid) { firstInvalid.focus(); } return; } if (!hasExternalEndpoint || hasPlaceholderEndpoint) { event.preventDefault(); setStatus("Contact form is currently unavailable. Please try again shortly.", "error"); return; } event.preventDefault(); setSubmitting(true); setStatus("Sending your message..."); fetch(action, { method, headers: { Accept: "application/json" }, body: new FormData(form) }) .then(async (response) => { if (response.ok) { form.reset(); fields.forEach((field) => setFieldError(field, "")); setStatus("Thank you. Your contact request was sent and noted.", "success"); return; } let errorMessage = "Sorry, your message could not be sent. Please try again shortly."; try { const payload = await response.json(); if (payload && Array.isArray(payload.errors) && payload.errors.length > 0) { errorMessage = payload.errors.map((item) => item.message).join(" "); } } catch (parseError) { // Keep fallback message when response body is not JSON. } throw new Error(errorMessage); }) .catch((error) => { setStatus(error.message, "error"); }) .finally(() => { setSubmitting(false); }); }); }