231 lines
7.1 KiB
JavaScript
231 lines
7.1 KiB
JavaScript
/**
|
|
* ArcaneNeko Website - Main Initialization
|
|
*
|
|
* Handles: mobile menu, scroll reveal, stat counters,
|
|
* back-to-top, scroll progress, smooth scroll
|
|
*/
|
|
|
|
// ----------------------
|
|
// Mobile Hamburger Menu
|
|
// ----------------------
|
|
function initMobileMenu() {
|
|
const hamburger = document.getElementById('hamburger');
|
|
const mobileMenu = document.getElementById('mobileMenu');
|
|
if (!hamburger || !mobileMenu) return;
|
|
|
|
hamburger.addEventListener('click', () => {
|
|
const open = hamburger.classList.toggle('open');
|
|
mobileMenu.classList.toggle('open', open);
|
|
hamburger.setAttribute('aria-expanded', String(open));
|
|
});
|
|
|
|
mobileMenu.querySelectorAll('a').forEach(link => {
|
|
link.addEventListener('click', () => {
|
|
hamburger.classList.remove('open');
|
|
mobileMenu.classList.remove('open');
|
|
hamburger.setAttribute('aria-expanded', 'false');
|
|
});
|
|
});
|
|
}
|
|
|
|
// ----------------------
|
|
// Scroll Reveal (cards)
|
|
// ----------------------
|
|
function initScrollReveal() {
|
|
const observer = new IntersectionObserver(entries => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('visible');
|
|
observer.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, { threshold: 0.1 });
|
|
|
|
document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
|
|
}
|
|
|
|
// ----------------------
|
|
// Stat Counter Animation
|
|
// ----------------------
|
|
function animateCount(el, target, suffix, duration) {
|
|
const start = performance.now();
|
|
const from = 0;
|
|
|
|
function step(now) {
|
|
const elapsed = now - start;
|
|
const progress = Math.min(elapsed / duration, 1);
|
|
const eased = 1 - Math.pow(1 - progress, 3);
|
|
const current = Math.round(from + (target - from) * eased);
|
|
el.textContent = current + suffix;
|
|
if (progress < 1) requestAnimationFrame(step);
|
|
}
|
|
requestAnimationFrame(step);
|
|
}
|
|
|
|
function initStatCounters() {
|
|
const statsBar = document.querySelector('.stats-bar');
|
|
if (!statsBar) return;
|
|
|
|
const observer = new IntersectionObserver(entries => {
|
|
if (!entries[0].isIntersecting) return;
|
|
observer.disconnect();
|
|
|
|
document.querySelectorAll('.stat').forEach(el => el.classList.add('animated'));
|
|
|
|
document.querySelectorAll('.stat-value[data-count]').forEach(el => {
|
|
const target = parseFloat(el.dataset.count);
|
|
const suffix = el.dataset.suffix || '';
|
|
const duration = 1400;
|
|
animateCount(el, target, suffix, duration);
|
|
});
|
|
|
|
}, { threshold: 0.5 });
|
|
|
|
observer.observe(statsBar);
|
|
}
|
|
|
|
// ----------------------
|
|
// Back to Top Button
|
|
// ----------------------
|
|
function initBackToTop() {
|
|
const btn = document.getElementById('backToTop');
|
|
if (!btn) return;
|
|
|
|
window.addEventListener('scroll', () => {
|
|
btn.classList.toggle('visible', window.scrollY > 450);
|
|
}, { passive: true });
|
|
|
|
btn.addEventListener('click', () => {
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
});
|
|
}
|
|
|
|
// ----------------------
|
|
// Scroll Progress Bar (legal pages)
|
|
// ----------------------
|
|
function initScrollProgress() {
|
|
const bar = document.getElementById('scrollProgress');
|
|
if (!bar) return;
|
|
|
|
const updateProgress = () => {
|
|
const total = document.documentElement.scrollHeight - window.innerHeight;
|
|
const progress = total > 0 ? (window.scrollY / total) * 100 : 0;
|
|
const rounded = Math.round(progress);
|
|
bar.style.width = progress + '%';
|
|
bar.setAttribute('aria-valuenow', String(rounded));
|
|
};
|
|
|
|
updateProgress();
|
|
window.addEventListener('scroll', updateProgress, { passive: true });
|
|
window.addEventListener('resize', updateProgress);
|
|
}
|
|
|
|
// ----------------------
|
|
// Navbar scroll shadow
|
|
// ----------------------
|
|
function initNavShadow() {
|
|
const navbar = document.querySelector('.navbar');
|
|
if (!navbar) return;
|
|
window.addEventListener('scroll', () => {
|
|
navbar.style.boxShadow = window.scrollY > 10
|
|
? '0 4px 24px rgba(0,0,0,0.3)'
|
|
: 'none';
|
|
}, { passive: true });
|
|
}
|
|
|
|
// ----------------------
|
|
// Smooth scroll (hash links)
|
|
// ----------------------
|
|
function initSmoothScroll() {
|
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
anchor.addEventListener('click', e => {
|
|
const href = anchor.getAttribute('href');
|
|
if (href === '#') return;
|
|
const target = document.querySelector(href);
|
|
if (target) {
|
|
e.preventDefault();
|
|
const offset = 80;
|
|
window.scrollTo({
|
|
top: target.getBoundingClientRect().top + window.scrollY - offset,
|
|
behavior: 'smooth'
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// ----------------------
|
|
// Legal page TOC highlight
|
|
// ----------------------
|
|
function initTocHighlight() {
|
|
const sections = document.querySelectorAll('.legal-section');
|
|
const tocLinks = document.querySelectorAll('.legal-toc a');
|
|
if (!sections.length || !tocLinks.length) return;
|
|
|
|
const observer = new IntersectionObserver(entries => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
tocLinks.forEach(l => l.classList.remove('active'));
|
|
const active = document.querySelector(`.legal-toc a[href="#${entry.target.id}"]`);
|
|
if (active) active.classList.add('active');
|
|
}
|
|
});
|
|
}, { rootMargin: '-20% 0px -70% 0px' });
|
|
|
|
sections.forEach(s => observer.observe(s));
|
|
}
|
|
|
|
// ----------------------
|
|
// Legal page mobile TOC
|
|
// ----------------------
|
|
function initLegalTocToggle() {
|
|
document.querySelectorAll('.legal-toc').forEach(toc => {
|
|
const button = toc.querySelector('.legal-toc-toggle');
|
|
const panel = toc.querySelector('.legal-toc-panel');
|
|
if (!button || !panel) return;
|
|
|
|
button.addEventListener('click', () => {
|
|
const open = toc.classList.toggle('open');
|
|
button.setAttribute('aria-expanded', String(open));
|
|
});
|
|
|
|
panel.querySelectorAll('a').forEach(link => {
|
|
link.addEventListener('click', () => {
|
|
if (window.innerWidth <= 768) {
|
|
toc.classList.remove('open');
|
|
button.setAttribute('aria-expanded', 'false');
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// ----------------------
|
|
// Init on DOM ready
|
|
// ----------------------
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Apply saved theme before anything renders
|
|
if (window.ThemeUtils) {
|
|
window.ThemeUtils.applyTheme(window.ThemeUtils.getCurrentTheme());
|
|
}
|
|
|
|
// Theme toggle
|
|
document.querySelectorAll('.theme-toggle').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
if (window.ThemeUtils) {
|
|
window.ThemeUtils.applyTheme(window.ThemeUtils.nextTheme());
|
|
}
|
|
});
|
|
});
|
|
|
|
initMobileMenu();
|
|
initScrollReveal();
|
|
initStatCounters();
|
|
initBackToTop();
|
|
initScrollProgress();
|
|
initNavShadow();
|
|
initSmoothScroll();
|
|
initTocHighlight();
|
|
initLegalTocToggle();
|
|
});
|