/* ── Raj Danej Portfolio v4 — Core: Colors, Icons, Shared Components ── */ const C = { purple: '#7c3aed', purpleLight: '#a78bfa', purpleDark: '#6d28d9', purpleGlow: 'rgba(124,58,237,0.25)', purpleSubtle: 'rgba(124,58,237,0.08)', teal: '#2dd4bf', tealDark: '#14b8a6', tealGlow: 'rgba(45,212,191,0.2)', bg: '#0c1222', bgCard: '#131b2e', bgCardHover: '#182240', bgSurface: '#0f172a', border: 'rgba(255,255,255,0.06)', borderHover: 'rgba(255,255,255,0.12)', text: '#e2e8f0', textMuted: '#94a3b8', textDim: '#64748b', white: '#fff', gold: '#f59e0b', green: '#22c55e', red: '#ef4444' }; window.C = C; /* ── SVG Icons ── */ const Icons = { github: (sz = 20, c = C.textMuted) => , linkedin: (sz = 20, c = C.textMuted) => , mail: (sz = 20, c = C.textMuted) => , download: (sz = 18, c = 'currentColor') => , send: (sz = 18, c = 'currentColor') => , external: (sz = 14, c = 'currentColor') => , check: (sz = 18, c = 'currentColor') => , star: (sz = 14, c = C.gold) => , close: (sz = 20, c = C.textMuted) => , arrowUp: (sz = 18, c = 'currentColor') => , chat: (sz = 18, c = 'currentColor') => , target: (sz = 16, c = 'currentColor') => , microscope: (sz = 16, c = 'currentColor') => , chart: (sz = 16, c = 'currentColor') => , briefcase: (sz = 16, c = 'currentColor') => , sparkles: (sz = 16, c = 'currentColor') => , award: (sz = 16, c = 'currentColor') => , book: (sz = 16, c = 'currentColor') => , pkg: (sz = 14, c = 'currentColor') => }; window.Icons = Icons; /* ── Animated Button ── */ function Btn({ children, href, onClick, variant = 'primary', size = 'md', icon, className = '', style: sx, ...rest }) { const cls = { primary: 'btn-primary', outline: 'btn-outline', ghost: 'btn-ghost', social: 'btn-social-icon', danger: 'btn-outline' }[variant] || ''; const sizes = { sm: 'btn-sm', md: 'btn-md', lg: 'btn-lg', icon: 'btn-icon-size' }; const fullCls = `btn-base ${cls} ${sizes[size] || ''} ${className}`.trim(); const track = (ev) => { if (typeof window.plausible === 'function') { if (href && href.includes('github')) window.plausible('GitHub Click'); if (href && href.includes('Resume')) window.plausible('CV Download'); } if (onClick) onClick(ev); }; if (href) { const isMailto = href.startsWith('mailto:'); return {icon}{children}; } return ; } window.Btn = Btn; /* ── Section Title ── */ function SectionTitle({ label, highlight, sub }) { return (

{label} {highlight}

{sub &&

{sub}

}
); } window.SectionTitle = SectionTitle; /* ── Section Divider ── */ function SectionDivider() { return
; } window.SectionDivider = SectionDivider; /* ── Scroll-reveal hook ── */ function useReveal(threshold = 0.15) { const ref = React.useRef(null); const [visible, setVisible] = React.useState(false); React.useEffect(() => { const el = ref.current; if (!el) return; const obs = new IntersectionObserver(([e]) => {if (e.isIntersecting) {setVisible(true);obs.disconnect();}}, { threshold }); obs.observe(el); return () => obs.disconnect(); }, []); return [ref, visible]; } window.useReveal = useReveal; function Reveal({ children, delay = 0, direction = 'up', style: sx }) { const [ref, visible] = useReveal(0.1); const t = { up: 'translateY(40px)', down: 'translateY(-40px)', left: 'translateX(40px)', right: 'translateX(-40px)', none: 'none' }; return (
{children}
); } window.Reveal = Reveal; /* ── Counter ── */ function Counter({ end, duration = 2000, suffix = '', prefix = '' }) { const [ref, visible] = useReveal(0.3); const [val, setVal] = React.useState(0); React.useEffect(() => { if (!visible) return; const start = Date.now(); const tick = () => { const p = Math.min((Date.now() - start) / duration, 1); setVal(Math.floor((1 - Math.pow(1 - p, 3)) * end)); if (p < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); }, [visible, end, duration]); return {prefix}{val.toLocaleString()}{suffix}; } window.Counter = Counter; /* ── Ticker ── */ function Ticker({ items }) { const doubled = [...items, ...items]; return (
{doubled.map((item, i) => {item} )}
); } window.Ticker = Ticker; /* ── Filter Tabs ── */ function FilterTabs({ categories, active, onChange }) { return (
{categories.map((cat) => )}
); } window.FilterTabs = FilterTabs; /* ── GitHub Stats Hook ── */ function useGitHubStats(username) { const [stats, setStats] = React.useState(null); React.useEffect(() => { const CACHE_KEY = `gh_stats_${username}`; const cached = localStorage.getItem(CACHE_KEY); if (cached) { try { const { data, ts } = JSON.parse(cached); if (Date.now() - ts < 3600000) {setStats(data);return;} } catch (e) {} } fetch(`https://api.github.com/users/${username}/repos?sort=updated&per_page=30`). then((r) => {if (!r.ok) throw new Error();return r.json();}). then((repos) => { const langs = {}; let totalStars = 0; repos.forEach((r) => { totalStars += r.stargazers_count || 0; if (r.language) langs[r.language] = (langs[r.language] || 0) + 1; }); const topLang = Object.entries(langs).sort((a, b) => b[1] - a[1])[0]; const lastUpdate = repos[0] ? repos[0].updated_at : null; const data = { repoCount: repos.length, stars: totalStars, topLang: topLang ? topLang[0] : 'Python', lastUpdate }; localStorage.setItem(CACHE_KEY, JSON.stringify({ data, ts: Date.now() })); setStats(data); }). catch(() => setStats({ repoCount: 18, stars: 12, topLang: 'Python', lastUpdate: null })); }, [username]); return stats; } window.useGitHubStats = useGitHubStats; /* ── Analytics helper ── */ function track(event, props) { if (typeof window.plausible === 'function') window.plausible(event, props ? { props } : undefined); } window.track = track;