/* runtime.jsx — shared React runtime Exports to window: useI18n, I18nProvider useToasts, ToastStack Boot, RouteLoader, CommandBar, MatrixOverlay useClock, useUptime, useVisitorN useStageSequence PAGES, PAGE_LABEL, PAGE_FILE */ const { useState, useEffect, useRef, useCallback, useMemo, useContext, createContext } = React; /* ============================================ PAGE MAP ============================================ */ const PAGES = ['home', 'about', 'education', 'skills', 'experience', 'projects', 'contact']; const PAGE_LABEL = { home: 'home', about: 'about', education: 'education', skills: 'skills', experience: 'experience', projects: 'projects', contact: 'contact', }; const PAGE_FILE = { home: 'home.sh', about: 'about.md', education: 'education/', skills: 'skills.json', experience: 'experience.log', projects: 'projects/', contact: 'contact.sh', }; const PAGE_PROMPT_PATH = { home: '~', about: '~/about', education: '~/education', skills: '~/skills', experience: '~/career', projects: '~/projects', contact: '~', }; /* ============================================ ROUTING (folder-based, no hash) - Production: / → home, /about/ → about, etc. - Preview: /projects// → home /projects//about/ → about ============================================ */ function getPathSegs() { const segs = window.location.pathname.split('/').filter(Boolean); if (segs[segs.length - 1] === 'index.html') segs.pop(); return segs; } function pageFromPath() { const segs = getPathSegs(); const last = segs[segs.length - 1] || 'home'; return PAGES.includes(last) ? last : 'home'; } function getBasePath() { const segs = getPathSegs(); if (segs.length && PAGES.includes(segs[segs.length - 1])) segs.pop(); return '/' + (segs.length ? segs.join('/') + '/' : ''); } function pageToPath(page) { const base = getBasePath(); return page === 'home' ? base : base + page + '/'; } const NavContext = createContext(null); function useNavigate() { return useContext(NavContext); } function Link({ to, children, className, ...rest }) { const navigate = useNavigate(); function onClick(e) { if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; e.preventDefault(); navigate(to); } return ( {children} ); } /* ============================================ I18N ============================================ */ const I18nContext = createContext(null); function I18nProvider({ children }) { const [lang, setLangState] = useState(() => localStorage.getItem('portfolio.lang') || 'es'); const setLang = useCallback((l) => { if (l !== 'es' && l !== 'en') return; setLangState(l); localStorage.setItem('portfolio.lang', l); }, []); const t = useCallback((key) => { const dict = window.I18N?.[lang] || {}; return dict[key] ?? window.I18N?.es?.[key] ?? key; }, [lang]); useEffect(() => { document.documentElement.lang = lang; }, [lang]); const value = useMemo(() => ({ lang, t, setLang }), [lang, t, setLang]); return {children}; } function useI18n() { return useContext(I18nContext); } /* ============================================ CLOCK + UPTIME + VISITOR ============================================ */ function useClock() { const [time, setTime] = useState(() => formatTime(new Date())); useEffect(() => { const id = setInterval(() => setTime(formatTime(new Date())), 1000); return () => clearInterval(id); }, []); return time; } function formatTime(d) { return [d.getHours(), d.getMinutes(), d.getSeconds()].map(x => String(x).padStart(2, '0')).join(':'); } function useUptime() { // Reset on every page load — measures time spent on this view session. const startRef = useRef(Date.now()); const [val, setVal] = useState('0m 00s'); useEffect(() => { function tick() { const s = Math.floor((Date.now() - startRef.current) / 1000); const m = Math.floor(s / 60); setVal(`${m}m ${String(s % 60).padStart(2, '0')}s`); } tick(); const id = setInterval(tick, 1000); return () => clearInterval(id); }, []); return val; } function useVisitorN() { // Global cross-visitor counter — calls your own backend endpoint. // Each browser may increment exactly once (localStorage flag). // Returning browsers do a GET to read the current count without incrementing. // Expected response shape: { count: } const COUNTER_ENDPOINT = '/api/counter'; const COUNTED_KEY = 'portfolio.visits.counted'; // permanent per-browser flag const cached = +sessionStorage.getItem('portfolio.visits.global.session') || 0; const [n, setN] = useState(cached); useEffect(() => { if (cached) return; // already loaded this tab session let cancelled = false; const alreadyCounted = localStorage.getItem(COUNTED_KEY) === '1'; async function load() { try { const ctrl = new AbortController(); setTimeout(() => ctrl.abort(), 3500); const method = alreadyCounted ? 'GET' : 'POST'; const r = await fetch(COUNTER_ENDPOINT, { method, signal: ctrl.signal }); if (!r.ok) throw new Error('http ' + r.status); const d = await r.json(); const v = d?.count ?? d?.value ?? null; if (!cancelled && Number.isFinite(v)) { if (!alreadyCounted) localStorage.setItem(COUNTED_KEY, '1'); setN(v); sessionStorage.setItem('portfolio.visits.global.session', v); localStorage.setItem('portfolio.visits.global', v); return; } throw new Error('bad response'); } catch (e) { // fallback: last known global count from cache const last = +localStorage.getItem('portfolio.visits.global') || 0; if (!cancelled && last > 0) { setN(last); sessionStorage.setItem('portfolio.visits.global.session', last); } else if (!cancelled) { // no cache yet — use a local per-browser tally so something shows let local = +localStorage.getItem('portfolio.visits') || 0; if (!alreadyCounted) { local += 1; localStorage.setItem('portfolio.visits', local); localStorage.setItem(COUNTED_KEY, '1'); } setN(local || 1); sessionStorage.setItem('portfolio.visits.global.session', local || 1); } } } load(); return () => { cancelled = true; }; }, []); return n; } /* ============================================ TOAST SYSTEM ============================================ */ const ToastContext = createContext(null); function useToasts() { return useContext(ToastContext); } function ToastProvider({ children }) { const [toasts, setToasts] = useState([]); const push = useCallback((html, kind = '', ms = 4200) => { const id = Math.random().toString(36).slice(2); setToasts(prev => [...prev, { id, html, kind, fading: false }]); setTimeout(() => { setToasts(prev => prev.map(t => t.id === id ? { ...t, fading: true } : t)); setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 300); }, ms); }, []); const clear = useCallback(() => setToasts([]), []); const value = useMemo(() => ({ push, clear }), [push, clear]); return ( {children} ); } function ToastStack({ toasts }) { return (
{toasts.map(t => (
))}
); } /* ============================================ BOOT OVERLAY ============================================ */ function Boot() { const [show, setShow] = useState(() => !sessionStorage.getItem('booted')); const [gone, setGone] = useState(false); useEffect(() => { if (!show) return; sessionStorage.setItem('booted', '1'); const lines = document.querySelectorAll('.boot .b-line'); lines.forEach((l, i) => { l.style.animationDelay = `${i * 80}ms`; }); const t1 = setTimeout(() => setGone(true), Math.max(1400, lines.length * 80 + 600)); const t2 = setTimeout(() => setShow(false), Math.max(1400, lines.length * 80 + 600) + 500); return () => { clearTimeout(t1); clearTimeout(t2); }; }, [show]); if (!show) return null; return (
[ portfolio.os v1.0.0 / kernel: david-5.18.2 ]
BIOS POST ........................................... [OK]
mounting /home/david ................................ [OK]
loading bash 5.2 / zsh-5.9 .......................... [OK]
checking projects ................................... [OK]
scanning skills ..................................... [OK]
connecting github.com/DavidFB-creator ............... [OK]
starting portfolio.service .......................... [OK]
tip: type 'help' in the command bar at the bottom.
> login as david_
); } /* ============================================ ROUTE LOADER SPLASH ============================================ */ function RouteLoader({ target }) { if (!target) return null; const file = PAGE_FILE[target] || target; return (
david@fontanet:~ $ cd ~/{target}
LOADmounting {file} ...
route: /{target}[ OK ]
); } /* ============================================ MATRIX OVERLAY ============================================ */ function MatrixOverlay({ on, onExit }) { const canvasRef = useRef(null); useEffect(() => { if (!on) return; const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); function size() { canvas.width = innerWidth; canvas.height = innerHeight; } size(); window.addEventListener('resize', size); const chars = 'アァイィウヴエカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤヨラリルレロワヲン0123456789{}[]<>=#$&*+'; const fontSize = 16; const cols = Math.floor(innerWidth / fontSize); const drops = Array.from({ length: cols }, () => Math.random() * -50); let raf; function draw() { ctx.fillStyle = 'rgba(5, 8, 13, 0.08)'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.font = fontSize + 'px JetBrains Mono, monospace'; for (let i = 0; i < drops.length; i++) { const c = chars[Math.floor(Math.random() * chars.length)]; const x = i * fontSize; const y = drops[i] * fontSize; ctx.fillStyle = y < fontSize * 2 ? '#e6edf7' : '#00d4ff'; ctx.fillText(c, x, y); if (y > canvas.height && Math.random() > 0.975) drops[i] = 0; drops[i] += 0.6 + Math.random() * 0.4; } raf = requestAnimationFrame(draw); } draw(); function onKey(e) { if (e.key === 'Escape') onExit(); } document.addEventListener('keydown', onKey); return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', size); document.removeEventListener('keydown', onKey); }; }, [on, onExit]); return ( <> {on &&
press Esc to exit
} ); } /* ============================================ TYPEWRITER + STAGE SEQUENCE ============================================ */ function typewriter(el, opts = {}) { const speed = opts.speed ?? 16; const jitter = opts.jitter ?? 10; const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, { acceptNode(n) { const p = n.parentElement; if (!p || p.closest('script, style')) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } }); const nodes = []; let n; while ((n = walker.nextNode())) { nodes.push({ node: n, full: n.textContent }); n.textContent = ''; } el.classList.add('tw-active'); let aborted = false; function abort() { if (aborted) return; aborted = true; nodes.forEach(({ node, full }) => { node.textContent = full; }); el.classList.remove('tw-active'); el.classList.add('tw-done'); } const promise = new Promise(resolve => { let i = 0, ci = 0; function tick() { if (aborted) { resolve(); return; } if (i >= nodes.length) { el.classList.remove('tw-active'); el.classList.add('tw-done'); resolve(); return; } const { node, full } = nodes[i]; if (ci < full.length) { node.textContent += full[ci]; const ch = full[ci]; ci++; let d = speed + (Math.random() * jitter - jitter / 2); if (ch === ' ') d *= 0.55; setTimeout(tick, Math.max(5, d)); } else { i++; ci = 0; setTimeout(tick, speed); } } tick(); }); return { promise, abort }; } function useStageSequence(ref, key) { useEffect(() => { const wrapper = ref.current; if (!wrapper) return; if (matchMedia('(prefers-reduced-motion: reduce)').matches) return; // The ref wraps the page component; the actual stage is .content-inner const stage = wrapper.querySelector('.content-inner') || wrapper; stage.classList.add('tw-stage'); const children = [...stage.children]; children.forEach(c => c.classList.add('tw-hidden')); let aborted = false; const aborts = []; function revealAll() { stage.querySelectorAll('.tw-hidden').forEach(el => { el.classList.remove('tw-hidden'); el.classList.add('tw-shown'); }); aborts.forEach(fn => fn()); } function abortSeq() { if (aborted) return; aborted = true; revealAll(); } function onSkip(e) { if (e.type === 'keydown') { const ae = document.activeElement; if (ae && ['INPUT', 'TEXTAREA'].includes(ae.tagName)) return; if (['Control', 'Meta', 'Shift', 'Alt'].includes(e.key)) return; } abortSeq(); } document.addEventListener('keydown', onSkip, true); stage.addEventListener('click', onSkip); (async () => { await new Promise(r => setTimeout(r, 60)); for (const el of children) { if (aborted) break; el.classList.remove('tw-hidden'); if (el.classList.contains('prompt-line')) { const tw = typewriter(el, { speed: 14, jitter: 8 }); aborts.push(tw.abort); await tw.promise; await new Promise(r => setTimeout(r, 110)); } else if (el.classList.contains('comment')) { const tw = typewriter(el, { speed: 12, jitter: 6 }); aborts.push(tw.abort); await tw.promise; await new Promise(r => setTimeout(r, 140)); } else if (el.classList.contains('ascii-name')) { el.classList.add('tw-shown', 'tw-scan'); await new Promise(r => setTimeout(r, 700)); } else if (el.tagName === 'H1') { el.classList.add('tw-shown'); await new Promise(r => setTimeout(r, 180)); } else { el.classList.add('tw-shown'); await new Promise(r => setTimeout(r, 70)); } } })(); return () => { abortSeq(); document.removeEventListener('keydown', onSkip, true); stage.removeEventListener('click', onSkip); }; }, [key]); } /* ============================================ COMMAND BAR ============================================ */ const HELP_CMDS = [ ['help', 'esta lista / show this list'], ['ls', 'listar páginas disponibles'], ['cd <page>', 'navegar — p.ej. cd about'], ['whoami', '¿quién eres?'], ['lang es|en', 'cambiar idioma'], ['clear', 'limpiar toasts'], ['matrix · hire · sudo · vim · coffee', '...try them'], ]; function CommandBar({ pathLabel, onNavigate, onMatrix }) { const { t, lang, setLang } = useI18n(); const toasts = useToasts(); const inputRef = useRef(null); const histRef = useRef(JSON.parse(localStorage.getItem('cmd.history') || '[]')); const idxRef = useRef(histRef.current.length); const [value, setValue] = useState(''); // tab-completion state const [suggestions, setSuggestions] = useState([]); // array of strings const cycleRef = useRef(0); const cyclingRef = useRef(false); // true while user is Tab-ing through matches const cycleModeRef = useRef('cd'); // 'cd' | 'cmd' function run(raw) { const cmd = raw.trim(); if (!cmd) return; const lc = cmd.toLowerCase(); const parts = lc.split(/\s+/); const [head, ...rest] = parts; if (head === 'cd' || PAGES.includes(head) || head === 'exp' || head === 'proj' || head === 'edu' || head === 'work' || head === 'stack') { const aliasMap = { exp: 'experience', work: 'experience', proj: 'projects', edu: 'education', stack: 'skills' }; const target = head === 'cd' ? rest[0] : (aliasMap[head] || head); if (target === '..' || target === '/' || target === '~') { onNavigate('home'); return; } if (PAGES.includes(target)) { onNavigate(target); return; } const mapped = aliasMap[target]; if (mapped) { onNavigate(mapped); return; } toasts.push(`cd: ${target || ''}: no such directory`, 'error'); return; } if (head === 'ls') { const list = PAGES; toasts.push(`$ ls
${list.map(x => `${x}/`).join(' ')}`); return; } if (head === 'help') { const rows = HELP_CMDS.map(([k, v]) => `
${k}${v}
` ).join(''); toasts.push(`
${t('ee.help.title')}
${rows}
`, '', 8000); return; } if (head === 'whoami') { const n = +sessionStorage.getItem('portfolio.visits.global.session') || +localStorage.getItem('portfolio.visits.global') || 1; toasts.push(`${t('ee.whoami')}${n}`); return; } if (head === 'date') { toasts.push(`${t('ee.date')} ${new Date().toString()}`); return; } if (head === 'uname') { toasts.push(`${t('ee.uname')}`); return; } if (head === 'fortune') { const arr = t('ee.fortune') || []; const pick = arr[Math.floor(Math.random() * arr.length)]; toasts.push(`~ ${pick}`); return; } if (head === 'lang') { const target = rest[0]; if (target === 'es' || target === 'en') { setLang(target); toasts.push(`lang → ${target}`, 'success', 1800); return; } toasts.push(`usage: lang es|en`); return; } if (head === 'sudo') { toasts.push(`[sudo] ${t('ee.sudo')}`, 'warn'); return; } if (lc.startsWith('rm -rf')) { const lines = ['rm: removing /bin...', 'rm: removing /etc...', 'rm: removing /home/david...', 'rm: PANIC: filesystem corrupted']; lines.forEach((l, i) => setTimeout(() => toasts.push(`${l}`, 'error', 1400), i * 320)); setTimeout(() => toasts.push(`${t('ee.rm')}`, 'success', 4200), lines.length * 320 + 200); return; } if (head === 'vim' || head === 'vi' || head === 'nvim') { toasts.push(`${t('ee.vim')}`, 'warn'); return; } if (head === 'coffee') { toasts.push(`${t('ee.coffee')}`, 'warn'); return; } if (head === 'hire' || head === 'hireme' || head === 'hire-me') { toasts.push(`${t('ee.hire')}`, 'success', 5200); setTimeout(() => onNavigate('contact'), 1800); return; } if (head === 'matrix') { toasts.push(`${t('ee.matrix')}`, '', 2000); setTimeout(onMatrix, 600); return; } if (head === 'clear' || head === 'cls') { toasts.clear(); return; } if (head === 'theme' || head === 'glitch') { document.documentElement.classList.add('glitch'); setTimeout(() => document.documentElement.classList.remove('glitch'), 1800); toasts.push(`// reticulating splines...`); return; } if (head === 'echo') { toasts.push(`${rest.join(' ') || ''}`); return; } if (head === 'exit' || head === 'logout') { toasts.push(`nice try — there is no exit. (this is the web)`); return; } toasts.push(`${cmd}: ${t('ee.unknown')}`, 'error'); } function onKeyDown(e) { if (e.key === 'Enter') { const v = value.trim(); if (!v) return; const h = histRef.current; h.push(v); if (h.length > 50) h.shift(); localStorage.setItem('cmd.history', JSON.stringify(h)); idxRef.current = h.length; run(v); setValue(''); setSuggestions([]); } else if (e.key === 'ArrowUp') { const h = histRef.current; if (idxRef.current > 0) { idxRef.current--; setValue(h[idxRef.current] || ''); } setSuggestions([]); e.preventDefault(); } else if (e.key === 'ArrowDown') { const h = histRef.current; if (idxRef.current < h.length - 1) { idxRef.current++; setValue(h[idxRef.current] || ''); } else { idxRef.current = h.length; setValue(''); } setSuggestions([]); e.preventDefault(); } else if (e.key === 'Tab') { e.preventDefault(); const raw = value; // Already cycling? Just advance and re-fill. if (cyclingRef.current && suggestions.length) { cycleRef.current = (cycleRef.current + 1) % suggestions.length; const pick = suggestions[cycleRef.current]; setValue(cycleModeRef.current === 'cd' ? 'cd ' + pick : pick + ' '); setSuggestions([...suggestions]); // force re-render of highlight return; } // Start a new completion. const cdMatch = raw.match(/^cd(\s+(.*))?$/i); if (cdMatch) { const stub = (cdMatch[2] || '').toLowerCase(); const matches = PAGES.filter(p => p.startsWith(stub)); if (matches.length === 0) { setSuggestions([]); cyclingRef.current = false; return; } if (matches.length === 1) { setValue('cd ' + matches[0] + ' '); setSuggestions([]); cyclingRef.current = false; return; } cycleModeRef.current = 'cd'; cycleRef.current = 0; cyclingRef.current = true; setSuggestions(matches); setValue('cd ' + matches[0]); return; } const all = ['help', 'cd', 'ls', 'whoami', 'date', 'uname', 'fortune', 'lang', 'matrix', 'hire', 'sudo', 'vim', 'coffee', 'clear', 'theme', 'echo']; const stub = raw.toLowerCase(); const matches = all.filter(k => k.startsWith(stub)); if (matches.length === 1) { setValue(matches[0] + ' '); setSuggestions([]); cyclingRef.current = false; } else if (matches.length > 1) { cycleModeRef.current = 'cmd'; cycleRef.current = 0; cyclingRef.current = true; setSuggestions(matches); setValue(matches[0] + ' '); } } else if (e.key === 'Escape') { setSuggestions([]); cyclingRef.current = false; } else { // any other key breaks the cycle (next Tab will recompute fresh) cyclingRef.current = false; } } function onChange(e) { setValue(e.target.value); if (suggestions.length) setSuggestions([]); cyclingRef.current = false; } // Ctrl+K global focus useEffect(() => { function onKey(e) { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); inputRef.current?.focus(); } } document.addEventListener('keydown', onKey); return () => document.removeEventListener('keydown', onKey); }, []); return (
{suggestions.length > 0 && (
{suggestions.map((s, i) => ( {s}/ ))}
)} {pathLabel} $
); } /* ============================================ KONAMI HOOK ============================================ */ function useKonami(onTrigger) { useEffect(() => { const seq = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a']; let pos = 0; function onKey(e) { const ae = document.activeElement; if (ae && ['INPUT', 'TEXTAREA'].includes(ae.tagName)) return; if (e.key.toLowerCase() === seq[pos].toLowerCase()) { pos++; if (pos === seq.length) { pos = 0; onTrigger(); } } else pos = 0; } document.addEventListener('keydown', onKey); return () => document.removeEventListener('keydown', onKey); }, [onTrigger]); } /* exports */ Object.assign(window, { useI18n, I18nProvider, useToasts, ToastProvider, Boot, RouteLoader, CommandBar, MatrixOverlay, useClock, useUptime, useVisitorN, useStageSequence, useKonami, Link, NavContext, useNavigate, pageFromPath, pageToPath, getBasePath, PAGES, PAGE_LABEL, PAGE_FILE, PAGE_PROMPT_PATH, });