/* ── 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;