/* Animated hero with live terminal */ const HERO_ASCII = String.raw` ██╗ ██╗██╗███╗ ██╗███████╗████████╗██╗ █████╗ ██████╗ ██║ ██║██║████╗ ██║██╔════╝╚══██╔══╝██║ ██╔══██╗██╔══██╗ ██║ █╗ ██║██║██╔██╗ ██║█████╗ ██║ ██║ ███████║██████╔╝ ██║███╗██║██║██║╚██╗██║██╔══╝ ██║ ██║ ██╔══██║██╔══██╗ ╚███╔███╔╝██║██║ ╚████║███████╗ ██║ ███████╗██║ ██║██████╔╝ ╚══╝╚══╝ ╚═╝╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═════╝ python · php · docker · nginx · seo ── работаю как разработчик `; const TERMINAL_SCENES = [ { cmd: 'winetlab status', lines: [ { kind: 'out', text: 'available · принимаю небольшие задачи' }, { kind: 'muted', text: 'отвечу в течение нескольких часов' }, ], }, { cmd: 'winetlab fix --form contact.php', lines: [ { kind: 'out', text: 'scan: 4 issues found in /forms/contact.php' }, { kind: 'warn', text: '⚠ PHPMailer creds in plain text · letter goes to spam' }, { kind: 'warn', text: '⚠ no CSRF token · бот-спам через форму' }, { kind: 'ok', text: '✓ applied fixes · отправил тестовое письмо' }, { kind: 'out', text: 'eta: 2h · цена от 3 000 ₽' }, ], }, { cmd: 'winetlab audit https://your-site.ru', lines: [ { kind: 'out', text: 'crawling 312 pages · robots.txt · sitemap.xml' }, { kind: 'err', text: '✗ 17 страниц с дублями canonical' }, { kind: 'err', text: '✗ schema.org Article невалидна (missing author)' }, { kind: 'ok', text: '✓ отчёт + правки готовы · pdf отправил' }, ], }, { cmd: 'winetlab deploy --vps --docker --ssl', lines: [ { kind: 'out', text: 'preparing nginx reverse proxy · letsencrypt · docker-compose' }, { kind: 'ok', text: '✓ app online · https://example.ru · uptime 99.9%' }, { kind: 'muted', text: 'передаю инструкцию по обновлениям' }, ], }, ]; function Terminal() { const [scene, setScene] = React.useState(0); const [phase, setPhase] = React.useState('cmd'); // cmd → out → idle const [history, setHistory] = React.useState([]); // completed scenes (cap at 1 previous) const current = TERMINAL_SCENES[scene]; const cmdText = useTyping(current.cmd, { speed: 28, start: phase === 'cmd', onDone: () => setPhase('out'), }); // Reset typing when scene changes React.useEffect(() => { setPhase('cmd'); }, [scene]); // After output appears, wait then advance React.useEffect(() => { if (phase !== 'out') return; const t = setTimeout(() => setPhase('idle'), 1400 + current.lines.length * 200); return () => clearTimeout(t); }, [phase, scene]); React.useEffect(() => { if (phase !== 'idle') return; const t = setTimeout(() => { // archive current scene into history (cap at last 1 so terminal stays full) setHistory((h) => [current].slice(-1)); setScene((s) => (s + 1) % TERMINAL_SCENES.length); }, 1200); return () => clearTimeout(t); }, [phase]); return (
{HERO_ASCII}