Spaces:
Running
Running
| /* ========================================================= | |
| # 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'; | |
| }); | |
| })(); | |