|
| 1 | +// ========================== |
| 2 | +// Helper selectors |
| 3 | +// ========================== |
| 4 | +const q = (s, p = document) => p.querySelector(s); |
| 5 | +const qAll = (s, p = document) => Array.from(p.querySelectorAll(s)); |
| 6 | + |
| 7 | +// ========================== |
1 | 8 | // Mobile nav toggle |
2 | | -const navToggle = document.querySelector('.nav-toggle'); |
3 | | -const navMenu = document.querySelector('.nav'); |
| 9 | +// ========================== |
| 10 | +const navToggle = q('.nav-toggle'); |
| 11 | +const navMenu = q('.nav'); |
4 | 12 |
|
5 | | -navToggle.addEventListener('click', () => { |
6 | | - navMenu.classList.toggle('visible'); |
7 | | -}); |
| 13 | +if (navToggle && navMenu) { |
| 14 | + navToggle.addEventListener('click', () => { |
| 15 | + navMenu.classList.toggle('visible'); |
| 16 | + }); |
| 17 | + |
| 18 | + // Optional: close menu when clicking outside |
| 19 | + document.addEventListener('click', (e) => { |
| 20 | + if (!navMenu.contains(e.target) && !navToggle.contains(e.target)) { |
| 21 | + navMenu.classList.remove('visible'); |
| 22 | + } |
| 23 | + }); |
| 24 | +} |
8 | 25 |
|
| 26 | +// ========================== |
9 | 27 | // Back to top button |
10 | | -const topBtn = document.getElementById('topBtn'); |
11 | | -window.onscroll = function() { |
12 | | - topBtn.style.display = (document.body.scrollTop > 200 || document.documentElement.scrollTop > 200) ? 'flex' : 'none'; |
13 | | -}; |
| 28 | +// ========================== |
| 29 | +const topBtn = q('#topBtn'); |
14 | 30 |
|
15 | | -topBtn.addEventListener('click', () => { |
16 | | - window.scrollTo({ top: 0, behavior: 'smooth' }); |
| 31 | +window.addEventListener('scroll', () => { |
| 32 | + if (!topBtn) return; |
| 33 | + topBtn.style.display = (window.scrollY > 300) ? 'flex' : 'none'; |
17 | 34 | }); |
18 | 35 |
|
19 | | -// Simple selectors |
20 | | -const q = (s, p=document) => p.querySelector(s); |
21 | | -const qAll = (s, p=document) => Array.from(p.querySelectorAll(s)); |
22 | | - |
23 | | -// Mobile nav toggle |
24 | | -const navToggle = q('.nav-toggle'); |
25 | | -navToggle && navToggle.addEventListener('click', () => { |
26 | | - const nav = q('.nav'); |
27 | | - if (!nav) return; |
28 | | - nav.style.display = (nav.style.display === 'flex') ? '' : 'flex'; |
| 36 | +topBtn && topBtn.addEventListener('click', () => { |
| 37 | + window.scrollTo({ top: 0, behavior: 'smooth' }); |
29 | 38 | }); |
30 | 39 |
|
31 | | -// Smooth scroll for in-page anchors and nav |
| 40 | +// ========================== |
| 41 | +// Smooth scroll for anchors |
| 42 | +// ========================== |
32 | 43 | qAll('a[href^="#"], a[href$=".html"]').forEach(a => { |
33 | 44 | a.addEventListener('click', (e) => { |
34 | 45 | const href = a.getAttribute('href'); |
35 | 46 | if (!href) return; |
36 | | - // For same-page anchors |
| 47 | + |
37 | 48 | if (href.startsWith('#')) { |
38 | 49 | const el = document.querySelector(href); |
39 | | - if (el) { e.preventDefault(); el.scrollIntoView({behavior:'smooth', block:'start'}); } |
| 50 | + if (el) { |
| 51 | + e.preventDefault(); |
| 52 | + const headerOffset = 80; // adjust if fixed header |
| 53 | + const elementPosition = el.getBoundingClientRect().top + window.pageYOffset; |
| 54 | + window.scrollTo({ |
| 55 | + top: elementPosition - headerOffset, |
| 56 | + behavior: 'smooth' |
| 57 | + }); |
| 58 | + } |
40 | 59 | } |
41 | | - // For page links, let browser handle navigation |
42 | 60 | }); |
43 | 61 | }); |
44 | 62 |
|
| 63 | +// ========================== |
45 | 64 | // Reveal on scroll |
| 65 | +// ========================== |
46 | 66 | const reveals = qAll('.reveal'); |
47 | | -const obs = new IntersectionObserver(entries => { |
| 67 | +const revealObserver = new IntersectionObserver((entries, obs) => { |
48 | 68 | entries.forEach(entry => { |
49 | 69 | if (entry.isIntersecting) { |
50 | 70 | entry.target.classList.add('visible'); |
51 | 71 | obs.unobserve(entry.target); |
52 | 72 | } |
53 | 73 | }); |
54 | | -}, {threshold: 0.12}); |
55 | | -reveals.forEach(r => obs.observe(r)); |
| 74 | +}, { threshold: 0.12 }); |
| 75 | +reveals.forEach(r => revealObserver.observe(r)); |
56 | 76 |
|
| 77 | +// ========================== |
57 | 78 | // Counters |
| 79 | +// ========================== |
58 | 80 | const counters = qAll('.num'); |
59 | | -const counterObs = new IntersectionObserver(entries => { |
60 | | - entries.forEach(entry => { |
61 | | - if (entry.isIntersecting) { |
62 | | - const el = entry.target; |
63 | | - const target = parseInt(el.dataset.target || el.textContent || '0', 10); |
64 | | - if (!el.dataset.animated) { |
65 | | - let start = 0; |
66 | | - const duration = 1400; |
67 | | - const startTime = performance.now(); |
68 | | - const step = (now) => { |
69 | | - const progress = Math.min((now - startTime) / duration, 1); |
70 | | - el.textContent = Math.floor(progress * target); |
71 | | - if (progress < 1) requestAnimationFrame(step); |
72 | | - else { el.textContent = target; el.dataset.animated = '1'; } |
73 | | - }; |
74 | | - requestAnimationFrame(step); |
75 | | - } |
76 | | - counterObs.unobserve(el); |
77 | | - } |
78 | | - }); |
79 | | -}, {threshold: 0.25}); |
80 | | -counters.forEach(c => counterObs.observe(c)); |
81 | | - |
82 | | -// Gallery lightbox |
83 | | -const galleryItems = qAll('.gallery-item'); |
84 | | -const lightbox = q('#lightbox'); |
85 | | -const lightboxImg = q('#lightboxImg'); |
86 | | -const lightboxClose = q('.lightbox-close'); |
87 | | - |
88 | | -galleryItems.forEach(img => { |
89 | | - img.addEventListener('click', () => { |
90 | | - const full = img.dataset.full || img.src; |
91 | | - lightboxImg.src = full; |
92 | | - lightbox.classList.add('active'); |
93 | | - lightbox.setAttribute('aria-hidden', 'false'); |
94 | | - }); |
95 | | -}); |
96 | | -lightboxClose && lightboxClose.addEventListener('click', () => { |
97 | | - lightbox.classList.remove('active'); lightbox.setAttribute('aria-hidden', 'true'); |
98 | | -}); |
99 | | -lightbox && lightbox.addEventListener('click', (e) => { if (e.target === lightbox) { lightbox.classList.remove('active'); lightbox.setAttribute('aria-hidden', 'true'); } }); |
100 | | - |
101 | | -// Back to top button (static) |
102 | | -const topBtn = q('#topBtn'); |
103 | | -window.addEventListener('scroll', () => { |
104 | | - if (!topBtn) return; |
105 | | - if (window.scrollY > 300) topBtn.style.display = 'flex'; |
106 | | - else topBtn.style.display = 'none'; |
107 | | -}); |
108 | | -topBtn && topBtn.addEventListener('click', () => window.scrollTo({top:0, behavior:'smooth'})); |
109 | | - |
110 | | -// Donation page small helpers (works if donate.html included) |
111 | | -try { |
112 | | - const donateLink = q('#donateLink'); |
113 | | - if (donateLink) { |
114 | | - // set default placeholder (replace with your provider link) |
115 | | - // donateLink.href = 'https://paystack.com/pay/your-code'; |
116 | | - } |
117 | | -} catch(e){/* ignore */} |
118 | | - |
119 | | -// Accessibility: show focus outlines when using keyboard |
120 | | -document.addEventListener('keydown', (e) => { |
121 | | - if (e.key === 'Tab') document.body.classList.add('show-focus'); |
122 | | -}); |
123 | | - |
| 81 | +const counterObserver = new IntersectionObserver((entries, o |
0 commit comments