PageSourceSearch

https://www.digibyte.org/js/app.js

js digibyte.org collected 2026-10-03 20:26:18 UTC 12,965 bytes, 377 lines download raw bytes

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.