Spaces:
Running
Running
File size: 5,418 Bytes
5fa412d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
/* =========================================================
# File: script.js
# Adds: theme toggle, mobile menu, hash routing, scroll spy,
# slider, filters, lightbox, form validation.
# ========================================================= */
(function () {
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
// Theme
$('#themeToggle')?.addEventListener('click', () => {
const root = document.documentElement;
const willDark = !root.classList.contains('dark');
root.classList.toggle('dark', willDark);
localStorage.setItem('nnai-theme', willDark ? 'dark' : 'light');
});
// Mobile menu
const menuBtn = $('#menuBtn');
const mobileMenu = $('#mobileMenu');
menuBtn?.addEventListener('click', () => {
const open = !mobileMenu.classList.contains('hidden');
mobileMenu.classList.toggle('hidden', open);
menuBtn.setAttribute('aria-expanded', String(!open));
});
mobileMenu?.addEventListener('click', (e) => {
const link = e.target.closest('[data-route]');
if (link) {
mobileMenu.classList.add('hidden');
menuBtn.setAttribute('aria-expanded', 'false');
}
});
// Routing
function goTo(hash) {
const id = (hash || '#home').replace('#','');
const el = document.getElementById(id);
if (!el) return;
if (location.hash !== '#' + id) history.pushState({ id }, '', '#' + id);
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
document.addEventListener('click', (e) => {
const a = e.target.closest('a[data-route]');
if (!a) return;
const href = a.getAttribute('href');
if (href?.startsWith('#')) {
e.preventDefault(); goTo(href);
}
});
window.addEventListener('popstate', (e) => {
const id = (e.state && e.state.id) || (location.hash.replace('#','') || 'home');
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
window.addEventListener('load', () => goTo(location.hash || '#home'));
// Scroll spy
const sections = $$('.section');
const navLinks = $$('.nav-link');
const spy = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
const id = entry.target.id;
const link = navLinks.find((a) => a.getAttribute('href') === '#' + id);
if (!link) return;
if (entry.isIntersecting) { navLinks.forEach(l => l.classList.remove('active')); link.classList.add('active'); }
});
}, { rootMargin: '-40% 0px -55% 0px', threshold: 0.01 });
sections.forEach((s) => spy.observe(s));
// Slider (simple, accessible)
const slides = $$('.slide');
let idx = slides.findIndex(s => s.classList.contains('is-active'));
if (idx < 0) idx = 0, slides[0]?.classList.add('is-active');
function show(i) {
if (!slides.length) return;
slides[idx]?.classList.remove('is-active');
idx = (i + slides.length) % slides.length;
slides[idx]?.classList.add('is-active');
}
$('.slider-btn.prev')?.addEventListener('click', () => show(idx - 1));
$('.slider-btn.next')?.addEventListener('click', () => show(idx + 1));
let auto = setInterval(() => show(idx + 1), 6000);
// Pause on hover/focus
const slider = $('.slider');
slider?.addEventListener('mouseenter', () => clearInterval(auto));
slider?.addEventListener('mouseleave', () => auto = setInterval(() => show(idx + 1), 6000));
// Portfolio filter
const grid = $('#portfolioGrid');
const filterButtons = $$('.filter-chip');
filterButtons.forEach((btn) => {
btn.addEventListener('click', () => {
filterButtons.forEach((b) => b.classList.remove('active'));
btn.classList.add('active');
const f = btn.dataset.filter;
$$('.portfolio-card', grid).forEach((card) => {
card.style.display = (f === '*' || card.dataset.cat === f) ? '' : 'none';
});
});
});
// Lightbox
const dlg = $('#lightbox');
const dlgImg = $('#lightboxImg');
const dlgClose = $('#lightboxClose');
function openLightbox(url) {
dlgImg.src = url;
if (typeof dlg.showModal === 'function') dlg.showModal();
else dlg.setAttribute('open','true');
}
function closeLightbox() {
dlgImg.src = '';
if (typeof dlg.close === 'function') dlg.close();
else dlg.removeAttribute('open');
}
document.addEventListener('click', (e) => {
const card = e.target.closest('[data-lightbox]');
if (!card) return;
e.preventDefault(); openLightbox(card.dataset.lightbox);
});
dlgClose?.addEventListener('click', closeLightbox);
dlg?.addEventListener('click', (e) => {
const rect = dlgImg.getBoundingClientRect();
const inside = e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
if (!inside) closeLightbox();
});
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && dlg?.open) closeLightbox(); });
// Contact form validation
const form = $('#contactForm');
const formMsg = $('#formMsg');
form?.addEventListener('submit', (e) => {
if (!form.checkValidity()) {
e.preventDefault();
formMsg.textContent = '❗ Please complete all required fields correctly.';
formMsg.className = 'text-sm text-red-600 dark:text-red-400';
return;
}
formMsg.textContent = '✔️ Sending...';
formMsg.className = 'text-sm text-slate-600 dark:text-slate-300';
});
})();
|