1/* DigiByte main app â vanilla ES module 2 * Replaces: main2o.js, plugins (AOS, Slick, Lity, Waypoints, Parallax, jQuery) 3 */ 4 5const prefersReducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches; 6const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); 7const $ = (sel, root = document) => root.querySelector(sel); 8 9// ---------- Sticky nav state ---------- 10function initNav() { 11 const nav = $('.nav'); 12 if (!nav) return; 13 const onScroll = () => nav.classList.toggle('is-scrolled', window.scrollY > 8); 14 onScroll(); 15 window.addEventListener('scroll', onScroll, { passive: true }); 16 17 const toggle = $('.nav__toggle', nav); 18 if (toggle) { 19 toggle.addEventListener('click', () => { 20 const open = nav.classList.toggle('is-open'); 21 toggle.setAttribute('aria-expanded', String(open)); 22 }); 23 } 24 25 // Close mobile menu on link click 26 $$('.nav__list a', nav).forEach(a => { 27 a.addEventListener('click', () => nav.classList.remove('is-open')); 28 }); 29 30 // Active link via IntersectionObserver on sections 31 const links = $$('.nav__list a[href^="#"]'); 32 const map = new Map(); 33 links.forEach(a => { 34 const id = a.getAttribute('href').slice(1); 35 const sec = document.getElementById(id); 36 if (sec) map.set(sec, a); 37 }); 38 if (map.size) { 39 const io = new IntersectionObserver(entries => { 40 entries.forEach(e => { 41 const link = map.get(e.target); 42 if (!link) return; 43 if (e.isIntersecting) { 44 links.forEach(l => l.classList.remove('is-active')); 45 link.classList.add('is-active'); 46 } 47 }); 48 }, { rootMargin: '-40% 0px -55% 0px', threshold: 0 }); 49 map.forEach((_, sec) => io.observe(sec)); 50 } 51} 52 53// ---------- Reveal on scroll ---------- 54function initReveal() { 55 const items = $$('[data-reveal]'); 56 if (!items.length || prefersReducedMotion) { 57 items.forEach(i => i.classList.add('is-visible')); 58 return; 59 } 60 const io = new IntersectionObserver((entries) => { 61 entries.forEach(e => { 62 if (e.isIntersecting) { 63 const delay = parseInt(e.target.dataset.revealDelay || '0', 10); 64 setTimeout(() => e.target.classList.add('is-visible'), delay); 65 io.unobserve(e.target); 66 } 67 }); 68 }, { threshold: 0.12, rootMargin: '0px 0px -10% 0px' }); 69 items.forEach(i => io.observe(i)); 70} 71 72// ---------- Count-up ---------- 73function animateCount(el, target, opts = {}) { 74 const { duration = 1400, decimals = 0, prefix = '', suffix = '' } = opts; 75 const start = performance.now(); 76 const from = parseFloat(el.dataset._from || '0'); 77 const ease = t => 1 - Math.pow(1 - t, 3); 78 const tick = (now) => { 79 const t = Math.min(1, (now - start) / duration); 80 const v = from + (target - from) * ease(t); 81 el.textContent = prefix + v.toLocaleString(undefined, { 82 minimumFractionDigits: decimals, 83 maximumFractionDigits: decimals, 84 }) + suffix; 85 if (t < 1) requestAnimationFrame(tick); 86 else el.dataset._from = String(target); 87 }; 88 requestAnimationFrame(tick); 89} 90 91function initCounters() { 92 const items = $$('[data-count]'); 93 if (!items.length) return; 94 if (prefersReducedMotion) { 95 items.forEach(el => { 96 const t = parseFloat(el.dataset.count); 97 el.textContent = (el.dataset.prefix || '') + t.toLocaleString() + (el.dataset.suffix || ''); 98 }); 99 return; 100 } 101 const io = new IntersectionObserver((entries) => { 102 entries.forEach(e => { 103 if (e.isIntersecting) { 104 const target = parseFloat(e.target.dataset.count); 105 animateCount(e.target, target, { 106 duration: parseInt(e.target.dataset.countDuration || '1400', 10), 107 decimals: parseInt(e.target.dataset.countDecimals || '0', 10), 108 prefix: e.target.dataset.prefix || '', 109 suffix: e.target.dataset.suffix || '', 110 }); 111 io.unobserve(e.target); 112 } 113 }); 114 }, { threshold: 0.4 }); 115 items.forEach(el => io.observe(el)); 116} 117 118// ---------- Tabs ---------- 119function initTabs() { 120 $$('.tabs').forEach(group => { 121 const btns = $$('.tabs__btn', group); 122 const panels = $$('.tabs__panel', group); 123 const activate = (id) => { 124 btns.forEach(b => b.setAttribute('aria-selected', String(b.dataset.tab === id))); 125 panels.forEach(p => { 126 if (p.dataset.tab === id) p.setAttribute('data-active', ''); 127 else p.removeAttribute('data-active'); 128 }); 129 }; 130 btns.forEach(b => b.addEventListener('click', () => activate(b.dataset.tab))); 131 const initial = btns.find(b => b.getAttribute('aria-selected') === 'true') || btns[0]; 132 if (initial) activate(initial.dataset.tab); 133 }); 134} 135 136// ---------- Copy buttons ---------- 137function initCopy() { 138 $$('.codeblock').forEach(block => { 139 if (block.querySelector('.codeblock__copy')) return; 140 const btn = document.createElement('button'); 141 btn.type = 'button'; 142 btn.className = 'codeblock__copy'; 143 btn.textContent = 'Copy'; 144 btn.setAttribute('aria-label', 'Copy code'); 145 block.appendChild(btn); 146 btn.addEventListener('click', async () => { 147 const code = block.querySelector('pre, code')?.innerText ?? ''; 148 try { 149 await navigator.clipboard.writeText(code); 150 btn.textContent = 'Copied'; 151 btn.classList.add('is-copied'); 152 setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('is-copied'); }, 1500); 153 } catch { btn.textContent = 'Error'; } 154 }); 155 }); 156} 157 158// ---------- Smooth in-page scroll ---------- 159function initSmoothScroll() { 160 document.addEventListener('click', (e) => { 161 const a = e.target.closest('a[href^="#"]'); 162 if (!a) return; 163 const id = a.getAttribute('href').slice(1); 164 if (!id) return; 165 const target = document.getElementById(id); 166 if (!target) return; 167 e.preventDefault(); 168 target.scrollIntoView({ behavior: prefersReducedMotion ? 'auto' : 'smooth', block: 'start' }); 169 history.replaceState(null, '', '#' + id); 170 }); 171} 172 173// ---------- Marquee duplication ---------- 174function initMarquees() { 175 $$('.marquee__track, .top-ticker__track').forEach(track => { 176 // Duplicate children once for seamless loop 177 if (track.dataset.duplicated) return; 178 const clone = track.cloneNode(true); 179 while (clone.firstChild) track.appendChild(clone.firstChild); 180 track.dataset.duplicated = '1'; 181 }); 182} 183 184// ---------- Wallet filter ---------- 185function initWalletFilter() { 186 const filterEl = $('[data-wallet-filters]'); 187 if (!filterEl) return; 188 const cards = $$('[data-wallet]'); 189 filterEl.addEventListener('click', (e) => { 190 const btn = e.target.closest('[data-filter]'); 191 if (!btn) return;
192 const f = btn.dataset.filter; 193 $$('[data-filter]', filterEl).forEach(b => b.setAttribute('aria-pressed', String(b === btn))); 194 cards.forEach(c => { 195 const types = (c.dataset.wallet || '').split(/\s+/); 196 c.classList.toggle('is-hidden', f !== 'all' && !types.includes(f)); 197 }); 198 }); 199} 200 201// ---------- Native dialog lightbox ---------- 202function initLightbox() { 203 $$('[data-lightbox]').forEach(trigger => { 204 trigger.addEventListener('click', (e) => { 205 e.preventDefault(); 206 const url = trigger.getAttribute('href') || trigger.dataset.lightbox; 207 const dialog = document.createElement('dialog'); 208 dialog.className = 'dialog'; 209 dialog.innerHTML = ` 210 <div class="dialog__inner"> 211 <button type="button" class="dialog__close" aria-label="Close">Ã</button> 212 <div class="aspect-video"> 213 <iframe src="${url}" allow="autoplay; fullscreen" frameborder="0" style="width:100%;height:100%;"></iframe> 214 </div> 215 </div>`; 216 document.body.appendChild(dialog); 217 dialog.showModal(); 218 const close = () => { dialog.close(); dialog.remove(); }; 219 dialog.addEventListener('click', (ev) => { if (ev.target === dialog) close(); }); 220 dialog.querySelector('.dialog__close').addEventListener('click', close); 221 }); 222 }); 223} 224 225// ---------- Language menu ---------- 226function initLangMenu() { 227 $$('.lang-menu').forEach(menu => { 228 const btn = $('.lang-menu__btn', menu); 229 const panel = $('.lang-menu__panel', menu); 230 if (!btn || !panel) return; 231 const close = () => { panel.hidden = true; btn.setAttribute('aria-expanded', 'false'); }; 232 const open = () => { panel.hidden = false; btn.setAttribute('aria-expanded', 'true'); }; 233 btn.addEventListener('click', (e) => { e.stopPropagation(); panel.hidden ? open() : close(); }); 234 document.addEventListener('click', (e) => { if (!menu.contains(e.target)) close(); }); 235 document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); }); 236 }); 237} 238 239// ---------- Year diff helper (legacy compat) ---------- 240function initYearDiff() { 241 const start = new Date('2014-01-10'); 242 const years = Math.floor((Date.now() - start) / (365.25 * 24 * 3600 * 1000)); 243 $$('.year-diff').forEach(el => el.textContent = years); 244} 245
246// ---------- Scroll-to-top ---------- 247function initScrollTop() { 248 let btn = $('.scroll-top'); 249 if (!btn) { 250 btn = document.createElement('button'); 251 btn.className = 'scroll-top'; 252 btn.type = 'button'; 253 btn.setAttribute('aria-label', 'Back to top'); 254 // This site vendors Font Awesome 5 Pro; `fa-solid` is FA6 syntax and won't set the font-family. 255 btn.innerHTML = '<i class="fas fa-arrow-up" aria-hidden="true"></i>'; 256 document.body.appendChild(btn); 257 } 258 const onScroll = () => btn.classList.toggle('is-visible', window.scrollY > 600); 259 onScroll(); 260 window.addEventListener('scroll', onScroll, { passive: true }); 261 btn.addEventListener('click', () => { 262 window.scrollTo({ top: 0, behavior: prefersReducedMotion ? 'auto' : 'smooth' }); 263 }); 264} 265 266// ---------- Partner / contact form ---------- 267// The DigiByte Interest Form (#collaborate) posts to Web3Forms: 268// https://web3forms.com/ (free tier) 269// 270// The access key below is the ONLY place the form is wired to an inbox â 271// there is no server-side secret, no environment variable, no DB. Rotate it 272// like this: 273// 1. Sign in at https://web3forms.com/ with the address that should receive submissions. 274// 2. Regenerate the access key from the dashboard. 275// 3. Replace the string below and open a PR / redeploy. 276// See also README.md â "Maintainer notes · Web3Forms". 277const WEB3FORMS_ACCESS_KEY = '3d4319f1-66fa-4d15-9a53-4058cc60a425'; 278 279function initPartnerForm() { 280 const form = $('#partnerForm'); 281 if (!form) return; 282 const btn = $('[data-partner-submit]', form); 283 const status = $('[data-partner-status]', form); 284 const setStatus = (msg, kind) => { 285 if (!status) return; 286 status.textContent = msg || ''; 287 status.classList.remove('is-ok', 'is-error'); 288 if (kind) status.classList.add(`is-${kind}`); 289 }; 290 291 form.addEventListener('submit', async (e) => { 292 e.preventDefault(); 293 if (form.botcheck && form.botcheck.checked) return; // honeypot tripped 294 if (!form.reportValidity()) return; 295 if (WEB3FORMS_ACCESS_KEY === 'YOUR_WEB3FORMS_ACCESS_KEY_HERE') { 296 setStatus('Form is not configured yet. Add a Web3Forms access key in /js/app.js.', 'error'); 297 return; 298 } 299 300 const data = new FormData(form); 301 data.append('access_key', WEB3FORMS_ACCESS_KEY); 302 data.append('subject', `[DigiByte Interest Form] ${data.get('interest') || 'General'} â ${data.get('name') || ''}`); 303 data.append('from_name', 'DigiByte Interest Form'); 304 data.append('replyto', String(data.get('email') || '')); 305 306 btn.disabled = true; 307 const originalLabel = btn.innerHTML; 308 btn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> <span>Sendingâ¦</span>'; 309 setStatus('', null); 310 311 try { 312 const res = await fetch('https://api.web3forms.com/submit', { 313 method: 'POST', 314 headers: { Accept: 'application/json' }, 315 body: data, 316 }); 317 const json = await res.json().catch(() => ({})); 318 if (res.ok && json.success) { 319 form.reset(); 320 setStatus('Thanks â your message is on its way. A DigiByte community member will reply soon.', 'ok'); 321 } else { 322 setStatus(json.message || 'Something went wrong. Please try again in a moment.', 'error'); 323 } 324 } catch { 325 setStatus('Network error. Please check your connection and try again.', 'error'); 326 } finally { 327 btn.disabled = false; 328 btn.innerHTML = originalLabel; 329 } 330 }); 331} 332 333// ---------- Boot ---------- 334function boot() { 335 initNav(); 336 initReveal(); 337 initCounters(); 338 initTabs(); 339 initCopy(); 340 initSmoothScroll(); 341 initMarquees(); 342 initWalletFilter(); 343 initLightbox(); 344 initLangMenu(); 345 initScrollTop(); 346 initYearDiff(); 347 initPartnerForm(); 348 349 // Lazy-load feature modules on demand 350 if ($('[data-hero-network]')) { 351 const startHero = () => import('./hero-network.js').then(m => m.init?.()).catch(() => {}); 352 if ('requestIdleCallback' in window) requestIdleCallback(startHero, { timeout: 1500 }); 353 else setTimeout(startHero, 250); 354 } 355 if ($('[data-tokenomics-chart]')) { 356 import('./tokenomics-chart.js').then(m => m.init?.()).catch(() => {}); 357 } 358 if ($('[data-chain-dashboard]')) { 359 import('./chain-dashboard.js').then(m => m.init?.()).catch(() => {}); 360 } 361 if ($('[data-github-release]')) { 362 import('./github-release.js').then(m => m.init?.()).catch(() => {}); 363 } 364 365 // Service worker 366 if ('serviceWorker' in navigator) { 367 window.addEventListener('load', () => { 368 navigator.serviceWorker.register('/js/sw.js').catch(() => {}); 369 }); 370 } 371} 372 373if (document.readyState === 'loading') { 374 document.addEventListener('DOMContentLoaded', boot); 375} else { 376 boot(); 377}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.