🎬
@@ -2537,7 +3097,7 @@
-
+
📊
@@ -2608,7 +3168,7 @@
-
+
🛡
@@ -2684,7 +3244,7 @@
-
+
💳
@@ -2755,7 +3315,7 @@
-
+
👥
@@ -2817,7 +3377,7 @@
-
+
🇨🇦
@@ -3851,6 +4411,275 @@
}
};
+ /* ===========================================
+ BETA MODAL
+ =========================================== */
+ const BetaModal = {
+ init() {
+ const pill = document.getElementById('beta-pill');
+ const backdrop = document.getElementById('beta-modal-backdrop');
+ const closeBtn = document.getElementById('beta-modal-close');
+ if (!pill || !backdrop || !closeBtn) return;
+ pill.addEventListener('click', () => backdrop.classList.add('open'));
+ closeBtn.addEventListener('click', () => backdrop.classList.remove('open'));
+ backdrop.addEventListener('click', (e) => { if (e.target === backdrop) backdrop.classList.remove('open'); });
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && backdrop.classList.contains('open')) backdrop.classList.remove('open'); });
+ }
+ };
+
+ /* ===========================================
+ TERMINAL COPY — clipboard for install cmd
+ =========================================== */
+ const TerminalCopy = {
+ init() {
+ const btn = document.getElementById('hero-copy-btn');
+ if (!btn) return;
+ btn.addEventListener('click', () => {
+ const cmd = 'curl -fsSL gitea.bnkops.com/admin/changemaker.lite/raw/branch/main/scripts/install.sh | bash';
+ navigator.clipboard.writeText(cmd).then(() => {
+ btn.classList.add('copied');
+ btn.querySelector('.copy-label').textContent = 'Copied!';
+ setTimeout(() => {
+ btn.classList.remove('copied');
+ btn.querySelector('.copy-label').textContent = 'Copy';
+ }, 2000);
+ });
+ });
+ }
+ };
+
+ /* ===========================================
+ TYPEWRITER — rotating hero words
+ =========================================== */
+ const Typewriter = {
+ words: ['campaigns', 'canvassing', 'fundraising', 'newsletters', 'media library', 'volunteer shifts', 'team chat', 'events'],
+ el: null,
+ wordIdx: 0,
+ charIdx: 0,
+ isDeleting: false,
+ timeout: null,
+
+ init() {
+ this.el = document.getElementById('tw-word');
+ if (!this.el) return;
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
+ this.el.textContent = this.words[0];
+ return;
+ }
+ this.tick();
+ },
+
+ tick() {
+ const word = this.words[this.wordIdx];
+
+ if (this.isDeleting) {
+ this.charIdx--;
+ } else {
+ this.charIdx++;
+ }
+
+ this.el.textContent = word.substring(0, this.charIdx);
+
+ let delay = this.isDeleting ? 40 : 80;
+
+ if (!this.isDeleting && this.charIdx === word.length) {
+ delay = 2200;
+ this.isDeleting = true;
+ } else if (this.isDeleting && this.charIdx === 0) {
+ this.isDeleting = false;
+ this.wordIdx = (this.wordIdx + 1) % this.words.length;
+ delay = 400;
+ }
+
+ this.timeout = setTimeout(() => this.tick(), delay);
+ }
+ };
+
+ /* ===========================================
+ FEATURE PILLS — staggered entrance
+ =========================================== */
+ const FeaturePills = {
+ init() {
+ const pills = document.querySelectorAll('.hero-pill');
+ if (!pills.length) return;
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
+ pills.forEach(p => p.classList.add('visible'));
+ return;
+ }
+ pills.forEach((pill, i) => {
+ setTimeout(() => pill.classList.add('visible'), 600 + i * 120);
+ });
+ }
+ };
+
+ /* ===========================================
+ FEATURE SHOWCASE — rotating cards
+ =========================================== */
+ const FeatureShowcase = {
+ current: 0,
+ cards: [],
+ dots: [],
+ interval: null,
+ DURATION: 5000,
+
+ init() {
+ this.cards = document.querySelectorAll('.showcase-card');
+ this.dots = document.querySelectorAll('.showcase-dot');
+ if (this.cards.length < 2) return;
+
+ this.dots.forEach(dot => {
+ dot.addEventListener('click', () => {
+ const idx = parseInt(dot.getAttribute('data-idx'), 10);
+ if (idx !== this.current) this.goTo(idx);
+ });
+ });
+
+ this.startAutoplay();
+ },
+
+ goTo(idx) {
+ const prev = this.cards[this.current];
+ prev.classList.remove('active');
+ prev.classList.add('exiting');
+ setTimeout(() => prev.classList.remove('exiting'), 600);
+
+ this.current = idx;
+ this.cards[this.current].classList.add('active');
+
+ this.dots.forEach((d, i) => d.classList.toggle('active', i === idx));
+ this.restartAutoplay();
+ },
+
+ next() {
+ this.goTo((this.current + 1) % this.cards.length);
+ },
+
+ startAutoplay() {
+ this.interval = setInterval(() => this.next(), this.DURATION);
+ },
+
+ restartAutoplay() {
+ clearInterval(this.interval);
+ this.startAutoplay();
+ }
+ };
+
+ /* ===========================================
+ COUNT-UP STATS — animated number counters
+ =========================================== */
+ const CountUpStats = {
+ init() {
+ const stats = document.querySelectorAll('.hero-stat-value[data-count]');
+ if (!stats.length) return;
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
+ stats.forEach(el => {
+ const target = parseInt(el.getAttribute('data-count'), 10);
+ const prefix = el.getAttribute('data-prefix') || '';
+ const suffix = el.getAttribute('data-suffix') || '';
+ el.textContent = prefix + target + suffix;
+ });
+ return;
+ }
+
+ const obs = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ this.animate(entry.target);
+ obs.unobserve(entry.target);
+ }
+ });
+ }, { threshold: 0.5 });
+
+ stats.forEach(el => obs.observe(el));
+ },
+
+ animate(el) {
+ const target = parseInt(el.getAttribute('data-count'), 10);
+ const prefix = el.getAttribute('data-prefix') || '';
+ const suffix = el.getAttribute('data-suffix') || '';
+ const duration = 1400;
+ const start = performance.now();
+
+ const step = (now) => {
+ const progress = Math.min((now - start) / duration, 1);
+ const eased = 1 - Math.pow(1 - progress, 3);
+ const current = Math.round(eased * target);
+ el.textContent = prefix + current + suffix;
+ if (progress < 1) requestAnimationFrame(step);
+ };
+ requestAnimationFrame(step);
+ }
+ };
+
+ /* ===========================================
+ PARTICLE DRIFT — floating background dots
+ =========================================== */
+ const ParticleDrift = {
+ canvas: null,
+ ctx: null,
+ particles: [],
+ raf: null,
+ COUNT: 35,
+
+ init() {
+ this.canvas = document.getElementById('hero-particles');
+ if (!this.canvas) return;
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
+ // Fewer particles on mobile
+ if (window.innerWidth < 768) this.COUNT = 15;
+
+ this.ctx = this.canvas.getContext('2d');
+ this.resize();
+ this.createParticles();
+ this.loop();
+ window.addEventListener('resize', () => this.resize());
+ },
+
+ resize() {
+ const hero = this.canvas.parentElement;
+ this.canvas.width = hero.offsetWidth;
+ this.canvas.height = hero.offsetHeight;
+ },
+
+ createParticles() {
+ this.particles = [];
+ for (let i = 0; i < this.COUNT; i++) {
+ this.particles.push({
+ x: Math.random() * this.canvas.width,
+ y: Math.random() * this.canvas.height,
+ r: Math.random() * 2 + 0.5,
+ vx: (Math.random() - 0.5) * 0.3,
+ vy: -(Math.random() * 0.4 + 0.1),
+ alpha: Math.random() * 0.3 + 0.1,
+ });
+ }
+ },
+
+ loop() {
+ const { ctx, canvas, particles } = this;
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ const isDark = document.documentElement.getAttribute('data-theme') !== 'light';
+ const baseColor = isDark ? '139, 92, 246' : '111, 66, 193';
+
+ particles.forEach(p => {
+ p.x += p.vx;
+ p.y += p.vy;
+ // Wrap around
+ if (p.y < -5) { p.y = canvas.height + 5; p.x = Math.random() * canvas.width; }
+ if (p.x < -5) p.x = canvas.width + 5;
+ if (p.x > canvas.width + 5) p.x = -5;
+
+ ctx.beginPath();
+ ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
+ ctx.fillStyle = `rgba(${baseColor}, ${p.alpha})`;
+ ctx.fill();
+ });
+
+ this.raf = requestAnimationFrame(() => this.loop());
+ }
+ };
+
/* ===========================================
BOOT
=========================================== */
@@ -3864,7 +4693,14 @@
FloatingElements.init();
SmoothScroll.init();
FreeModal.init();
+ BetaModal.init();
initSearch();
+ TerminalCopy.init();
+ Typewriter.init();
+ FeaturePills.init();
+ FeatureShowcase.init();
+ CountUpStats.init();
+ ParticleDrift.init();
});
})();
+
+
+
+