portfolio-website/js/script.js

223 lines
5.6 KiB
JavaScript

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, ""));
});
}