/* global React */ const { useState: useStateF, useEffect: useEffectF, useRef: useRefF, useCallback } = React; /* ============== SCROLL REVEAL ============== */ function useReveal() { useEffectF(() => { const els = document.querySelectorAll('.reveal'); const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } }); }, { threshold: 0.12 }); els.forEach(el => io.observe(el)); return () => io.disconnect(); }, []); } /* ============== CUSTOM CURSOR ============== */ function CustomCursor() { useEffectF(() => { if (window.matchMedia('(pointer: coarse)').matches) return; const dot = document.createElement('div'); dot.className = 'cursor-dot'; const ring = document.createElement('div'); ring.className = 'cursor-ring'; document.body.append(dot, ring); let mx = 0, my = 0, rx = 0, ry = 0; const onMove = (e) => { mx = e.clientX; my = e.clientY; dot.style.transform = `translate(${mx}px, ${my}px)`; }; const tick = () => { rx += (mx - rx) * 0.18; ry += (my - ry) * 0.18; ring.style.transform = `translate(${rx}px, ${ry}px)`; requestAnimationFrame(tick); }; window.addEventListener('mousemove', onMove); tick(); const onOver = (e) => { if (e.target.closest('a, button, .pain-tab, .phase-pill, .mentor-card, .faq-q, .magnet')) ring.classList.add('hover'); else ring.classList.remove('hover'); }; window.addEventListener('mouseover', onOver); return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseover', onOver); dot.remove(); ring.remove(); }; }, []); return null; } /* ============== SCROLL PROGRESS ============== */ function ScrollProgress() { const ref = useRefF(null); useEffectF(() => { const onScroll = () => { const h = document.documentElement.scrollHeight - window.innerHeight; const p = h > 0 ? window.scrollY / h : 0; if (ref.current) ref.current.style.transform = `scaleX(${p})`; }; window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); return () => window.removeEventListener('scroll', onScroll); }, []); return
; } /* ============== HERO CANVAS — ANIMATED CONSTELLATION ============== */ function HeroCanvas() { const ref = useRefF(null); useEffectF(() => { const canvas = ref.current; if (!canvas) return; const ctx = canvas.getContext('2d'); let w, h, dpr; const resize = () => { dpr = Math.min(window.devicePixelRatio || 1, 2); const rect = canvas.getBoundingClientRect(); w = rect.width; h = rect.height; canvas.width = w * dpr; canvas.height = h * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); }; resize(); window.addEventListener('resize', resize); const N = 70; const nodes = Array.from({length: N}, () => ({ x: Math.random() * w, y: Math.random() * h, vx: (Math.random() - 0.5) * 0.25, vy: (Math.random() - 0.5) * 0.25, r: Math.random() * 1.4 + 0.4, gold: Math.random() < 0.18 })); let mx = w/2, my = h/2; const onMouse = (e) => { const r = canvas.getBoundingClientRect(); mx = e.clientX - r.left; my = e.clientY - r.top; }; canvas.parentElement.addEventListener('mousemove', onMouse); let raf; const tick = () => { ctx.clearRect(0, 0, w, h); for (let i = 0; i < N; i++) { const a = nodes[i]; for (let j = i+1; j < N; j++) { const b = nodes[j]; const dx = a.x - b.x, dy = a.y - b.y; const d2 = dx*dx + dy*dy; if (d2 < 14000) { const op = (1 - d2/14000) * 0.18; ctx.strokeStyle = `rgba(232,176,74,${op})`; ctx.lineWidth = 0.6; ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke(); } } const mdx = mx - a.x, mdy = my - a.y; const md2 = mdx*mdx + mdy*mdy; if (md2 < 28000 && md2 > 100) { const f = 0.04 / Math.sqrt(md2); a.vx += mdx * f; a.vy += mdy * f; } a.vx *= 0.985; a.vy *= 0.985; const sp = Math.sqrt(a.vx*a.vx + a.vy*a.vy); if (sp > 1.2) { a.vx = (a.vx/sp)*1.2; a.vy = (a.vy/sp)*1.2; } a.x += a.vx; a.y += a.vy; if (a.x < 0) a.x = w; if (a.x > w) a.x = 0; if (a.y < 0) a.y = h; if (a.y > h) a.y = 0; ctx.fillStyle = a.gold ? 'rgba(232,176,74,0.9)' : 'rgba(212,232,223,0.6)'; ctx.beginPath(); ctx.arc(a.x, a.y, a.r, 0, Math.PI*2); ctx.fill(); } raf = requestAnimationFrame(tick); }; tick(); return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', resize); canvas.parentElement.removeEventListener('mousemove', onMouse); }; }, []); return ; } /* ============== MAGNETIC BUTTON ============== */ function Magnet({ children, className = '', strength = 0.25, ...rest }) { const ref = useRefF(null); useEffectF(() => { if (window.matchMedia('(pointer: coarse)').matches) return; const el = ref.current; if (!el) return; const onMove = (e) => { const r = el.getBoundingClientRect(); const x = e.clientX - (r.left + r.width/2); const y = e.clientY - (r.top + r.height/2); el.style.transform = `translate(${x*strength}px, ${y*strength}px)`; }; const onLeave = () => { el.style.transform = ''; }; el.addEventListener('mousemove', onMove); el.addEventListener('mouseleave', onLeave); return () => { el.removeEventListener('mousemove', onMove); el.removeEventListener('mouseleave', onLeave); }; }, [strength]); return {children}; } /* ============== ANIMATED COUNTER ============== */ function Counter({ from = 0, to, suffix = '', duration = 1600 }) { const [val, setVal] = useStateF(from); const ref = useRefF(null); useEffectF(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { const start = performance.now(); const tick = (now) => { const t = Math.min(1, (now - start) / duration); const eased = 1 - Math.pow(1 - t, 3); setVal(Math.round(from + (to - from) * eased)); if (t < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); io.disconnect(); } }, { threshold: 0.5 }); io.observe(el); return () => io.disconnect(); }, [to, from, duration]); return {val}{suffix}; } /* ============== TYPED TERMINAL ============== */ function HeroTerminal() { const cmd = 'majelisjihad --status'; const [typed, setTyped] = useStateF(''); const [stage, setStage] = useStateF(0); useEffectF(() => { let i = 0; const id = setInterval(() => { i++; setTyped(cmd.slice(0, i)); if (i >= cmd.length) { clearInterval(id); setTimeout(() => setStage(1), 400); } }, 60); return () => clearInterval(id); }, []); return (
~/ukm-ldk-majelis-jihad — bash
$ {typed}{stage === 0 && }
{stage >= 1 && (
→ unit: UKM LDK Majelis Jihad · Universitas Dipa Makassar
→ nama lengkap: Majelis Kajian Mahasiswa Dipanegara
→ status: KADERISASI_TERBUKA
)}
); } /* ============== HERO SIDE WIDGETS ============== */ function HeroWidgets() { return ( ); } /* ============== CODE RAIN ============== */ function CodeRain() { const chars = 'ilmu إيمان taqwa tahsin ukhuwah kajian دعوة akhlak amal dakwah tarbiyah صبر istiqamah'; const cols = 12; return ( ); } /* ============== MARQUEE ============== */ function Marquee() { const items = [ 'Majelis Kajian Mahasiswa Dipanegara', 'Lembaga Dakwah Kampus', 'Kajian rutin', 'Tahsin Al-Quran', 'Kaderisasi', 'Syiar & media', 'Pengabdian masyarakat', 'Universitas Dipa Makassar' ]; const seq = [...items, ...items]; return ( ); } Object.assign(window, { useReveal, CustomCursor, ScrollProgress, HeroCanvas, Magnet, Counter, HeroTerminal, HeroWidgets, CodeRain, Marquee });