/* global React, ReactDOM */
const { useState, useEffect, useRef } = React;
// ====== ICONS ======
const Icon = {
Python: () => (
),
Django: () => (
),
JS: () => (
),
Flutter: () => (
),
Android: () => (
),
Algo: () => (
),
Grad: () => (
),
Clock: () => (
),
Moon: () => (
),
Arrow: () => (
),
Menu: () => (
),
Close: () => (
),
Pin: () => (
),
};
window.Icon = Icon;
// ====== REVEAL ======
function Reveal({ children, stagger = false, className = '', as: As = 'div', ...rest }) {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver(
(entries) => entries.forEach(e => {
if (e.isIntersecting) { el.classList.add('in'); io.disconnect(); }
}),
{ threshold: 0.15 }
);
io.observe(el);
return () => io.disconnect();
}, []);
return {children};
}
window.Reveal = Reveal;
// ====== PARTICLES ======
function Particles({ count = 24 }) {
const [items] = useState(() => {
const syms = ['>', '{}', '[]', '0x', '#', '//', '=>', '&&', '||', '::', '()', '<>'];
return Array.from({ length: count }, (_, i) => ({
id: i,
sym: syms[Math.floor(Math.random() * syms.length)],
left: Math.random() * 100,
delay: Math.random() * 20,
dur: 15 + Math.random() * 20,
size: 10 + Math.random() * 10,
color: ['', 'cyan', 'magenta'][Math.floor(Math.random() * 3)],
top: 100 + Math.random() * 50,
}));
});
return (
{items.map(p => (
{p.sym}
))}
);
}
window.Particles = Particles;
// ====== TYPING ======
function Typing({ text, speed = 45 }) {
const [out, setOut] = useState('');
useEffect(() => {
let i = 0;
setOut('');
const t = setInterval(() => {
i++;
setOut(text.slice(0, i));
if (i >= text.length) clearInterval(t);
}, speed);
return () => clearInterval(t);
}, [text, speed]);
return {out};
}
window.Typing = Typing;
// ====== MATRIX RAIN ======
function MatrixRain() {
const CHARSET = '01{}[]>#0xABCDEFabcdefKODAIFROADS+*=!?&';
const COLS = 18;
const LINES = 30;
// hidden easter egg phrase — drops rarely, in a random column
const SECRET = 'ACHEI O BRINDE';
const randCh = () => CHARSET[Math.floor(Math.random() * CHARSET.length)];
const randomColumnText = () => {
let s = '';
for (let j = 0; j < LINES; j++) s += randCh() + '\n';
return s;
};
const [cols, setCols] = useState(() =>
Array.from({ length: COLS }, (_, i) => ({
id: i,
left: (i / COLS) * 100,
delay: Math.random() * 5,
dur: 6 + Math.random() * 6,
text: randomColumnText(),
opacity: 0.2 + Math.random() * 0.5,
}))
);
// Easter egg cycle — every 60-120s embed SECRET into a random column,
// then restore the column to random chars after one animation cycle.
useEffect(() => {
let cancelled = false;
let timer;
const inject = () => {
if (cancelled) return;
const target = Math.floor(Math.random() * COLS);
// letters of SECRET as a vertical sequence, padded by random chars
const cleanLetters = SECRET.split('').map(ch => (ch === ' ' ? '·' : ch));
const padTop = 5 + Math.floor(Math.random() * (LINES - cleanLetters.length - 6));
let liveDur = 0;
setCols(prev => prev.map((c, idx) => {
if (idx !== target) return c;
const lines = [];
for (let j = 0; j < LINES; j++) {
const k = j - padTop;
lines.push(k >= 0 && k < cleanLetters.length ? cleanLetters[k] : randCh());
}
liveDur = c.dur;
return { ...c, text: lines.join('\n') };
}));
// After one full fall, scramble the column back to noise
setTimeout(() => {
if (cancelled) return;
setCols(prev => prev.map((c, idx) =>
idx === target ? { ...c, text: randomColumnText() } : c
));
}, (liveDur + 1) * 1000);
// schedule next injection (60-120s)
timer = setTimeout(inject, 60000 + Math.random() * 60000);
};
// first injection: 45-90s after mount (so it isn't visible immediately)
timer = setTimeout(inject, 45000 + Math.random() * 45000);
return () => { cancelled = true; clearTimeout(timer); };
}, []);
return (
{cols.map(c => (
{c.text}
))}
);
}
window.MatrixRain = MatrixRain;
Object.assign(window, { Icon, Reveal, Particles, Typing, MatrixRain });