// ════════════════════════════════════════════════════════════════ // CorpusPage — реестр 3068 стартапов // Hero + warm stat cards + filter bar + virtualized rows // Layer B integration: hero micro-bars, row CategoryPills + SimDot + DateDot // ════════════════════════════════════════════════════════════════ const { useState: useStateCorpus, useMemo: useMemoCorpus, useRef: useRefCorpus, useEffect: useEffectCorpus } = React; function CorpusPage() { const D = window.SDH_DATA; const [niche, setNiche] = useStateCorpus('all'); const [source, setSource] = useStateCorpus('all'); const [q, setQ] = useStateCorpus(''); const [sort, setSort] = useStateCorpus('date'); // date | sim | name const [openStartup, setOpenStartup] = useStateCorpus(null); // close on Esc useEffectCorpus(() => { function onKey(e) { if (e.key === 'Escape') setOpenStartup(null); } window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, []); const filtered = useMemoCorpus(() => { let arr = D.corpus; if (niche !== 'all') arr = arr.filter(s => s.niche === niche); if (source !== 'all') arr = arr.filter(s => s.source === source); if (q.trim()) { const qq = q.trim().toLowerCase(); arr = arr.filter(s => s.name.toLowerCase().includes(qq) || s.desc.toLowerCase().includes(qq)); } arr = arr.slice(); if (sort === 'date') arr.sort((a, b) => a.daysAgo - b.daysAgo); else if (sort === 'sim') arr.sort((a, b) => b.similarity - a.similarity); else arr.sort((a, b) => a.name.localeCompare(b.name)); return arr; }, [niche, source, q, sort, D.corpus]); // Stat cards (hero) const heroStats = [ { key: 'total', metric: 'startups', label: 'всего стартапов', value: D.corpus.length, hue: 'info', series: D.series.startups, delta: 12.4 }, { key: 'week', metric: 'weekNew', label: 'за последние 7 дней', value: countNew(D.corpus, 7), hue: 'emerald', series: D.series.startups.slice(-7), delta: 8.1 }, { key: 'class', metric: 'classified', label: 'классифицировано', value: D.series.classified[D.series.classified.length-1].value, hue: 'amber', series: D.series.classified, delta: 4.6 }, { key: 'srcs', metric: 'sources', label: 'источников активно', value: D.series.sources[D.series.sources.length-1].value, hue: 'purple', series: D.series.sources, delta: 2.0 }, ]; return (
{/* HERO */}
реестр · корпус данных

Полный реестр стартапов

Один источник данных для исследований Стартапзавода. Сегодня в базе{' '} {D.corpus.length.toLocaleString('ru-RU')} {' '} компаний из {D.SOURCES.length} источников. Обновление ежедневно в 10:00 по МСК.

{/* 4 mini stat cards */}
{heroStats.map(s => { const m = window.METRIC_DICT[s.metric]; return (
{s.label}
{s.value.toLocaleString('ru-RU')} = 0 ? 'var(--success)' : 'var(--danger)', }}>{s.delta >= 0 ? '+' : ''}{s.delta}%
); })}
{/* FILTER BAR */} фильтр {D.NICHES.map(n => { const count = D.corpus.filter(s => s.niche === n.id).length; return ( ); })} } right={ <>
setQ(e.target.value)} placeholder="поиск по названию или описанию" style={{ background: 'var(--surface-elevated)', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-md)', padding: '7px 12px 7px 30px', fontSize: 13, color: 'var(--text-primary)', width: 240, outline: 'none', }} />
} /> {/* RESULTS HEAD */}
{filtered.length.toLocaleString('ru-RU')} {' '}результатов
горячие клавиши: / поиск · j/k навигация
{/* VIRTUALIZED LIST */}
{openStartup && setOpenStartup(null)} />}
); } function countNew(corpus, days) { return corpus.filter(s => s.daysAgo <= days).length; } // ── VirtualRows ────────────────────────────────────────────── function VirtualRows({ items, rowH = 56, height = 640, onOpen }) { const [scroll, setScroll] = useStateCorpus(0); const ref = useRefCorpus(null); const totalH = items.length * rowH; const visibleCount = Math.ceil(height / rowH) + 6; const startIdx = Math.max(0, Math.floor(scroll / rowH) - 3); const endIdx = Math.min(items.length, startIdx + visibleCount); return (
{/* table header */}
стартап
ниша
источник
появился
близость
гео
setScroll(e.target.scrollTop)} style={{ height, overflowY: 'auto', position: 'relative' }} >
{items.slice(startIdx, endIdx).map((s, i) => ( ))}
); } function CorpusRow({ startup, y, h, onOpen }) { const s = startup; return (
onOpen && onOpen(s)} style={{ position: 'absolute', top: y, left: 0, right: 0, height: h, display: 'grid', gridTemplateColumns: '1fr 130px 130px 110px 80px 70px', gap: 12, padding: '0 16px', alignItems: 'center', borderBottom: '1px solid var(--border-subtle)', cursor: 'pointer', transition: 'background .15s', }} onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-sunken)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
{s.name}
{s.desc}
{s.nicheLabel}
{s.sourceLabel}
{s.daysAgo === 0 ? 'сегодня' : `${s.daysAgo}д`}
{s.geo}
); } window.CorpusPage = CorpusPage;