// kb.jsx — «База знаний»: оглавление по 8 кластерам, поиск по названию и «коротко», // страница материала. Тело материала приходит с сервера только при праве доступа; // иначе — заголовок, блок «Коротко», оглавление и замок. function KBIndex({ nav, params }) { const kb = useKB(); const [q, setQ] = React.useState(''); const cluster = params.c || null; const query = q.trim().toLowerCase(); const clusters = (kb && kb.clusters) || CFG().clusters || []; const items = (kb && kb.items) || []; const match = (it) => (!cluster || it.cluster === cluster) && (!query || (it.title + ' ' + (it.summary || '')).toLowerCase().includes(query)); const shown = items.filter(match); const count = (id) => items.filter(i => i.cluster === id).length; const setCluster = (id) => nav('kb', id ? { c: id } : {}); return (
{Icons.search()} setQ(e.target.value)} autoComplete="off" spellCheck="false"/> {q && setQ('')} aria-label="Очистить">×} }/>
База знаний сообщества

Гайды, подборки и кейсы.

Практика основателей и участников: как подключить сервисы, какие модели брать, как внедрять ИИ в работу и команду. Материалы с замком открыты по подписке.

{clusters.map(c => ( ))}
{!kb &&
Загружаем материалы…
} {kb && clusters.filter(c => !cluster || c.id === cluster).map(c => { const list = shown.filter(i => i.cluster === c.id); if (query && !list.length) return null; return (
{c.n}

{c.title}

{c.d}
{list.length ? (
{list.map((it, i) => )}
) : (
Материалы раздела готовятся — появятся в ближайших выпусках.
)}
); })} {kb && query && !shown.length && (
Ничего не нашлось по «{q}». Попробуйте короче или другим словом.
)}
Как читать базу

В каждом материале первым идёт блок «Коротко» — ответ по сути за минуту. Дальше подробности, в конце — частые вопросы и связанные материалы.

); } function KBPage({ nav, slug }) { const kb = useKB(); const [state, setState] = React.useState({ loading: true, item: null, error: null }); React.useEffect(()=>{ let alive = true; setState({ loading: true, item: null, error: null }); fetch('/api/kb/' + encodeURIComponent(slug), { credentials:'same-origin' }) .then(r => r.json().then(j => ({ r, j }))) .then(({ r, j }) => { if (!alive) return; if (r.ok && j && j.ok) setState({ loading: false, item: j.item, error: null }); else setState({ loading: false, item: null, error: r.status === 404 ? 'not_found' : 'server' }); }) .catch(() => alive && setState({ loading: false, item: null, error: 'server' })); return () => { alive = false; }; }, [slug]); const it = state.item; const slugs = React.useMemo(() => new Set(((kb && kb.items) || []).map(i => i.slug)), [kb]); const ctx = { slugs, open: (s) => nav('kb', { m: s }) }; return (
{ e.preventDefault(); nav('kb', it ? { c: it.cluster } : {}); }}> {Icons.back()} Все материалы{it ? ` раздела «${clusterTitle(it.cluster)}»` : ''} {state.loading &&
Открываем материал…
} {state.error === 'not_found' && (

Такого материала нет.

Возможно, ссылка устарела. Загляните в оглавление базы.

)} {state.error === 'server' &&
Сервер не ответил. Обновите страницу через минуту.
} {it && (
{TYPE_LABEL[it.type] || 'Материал'} {it.locked && } {it.fixture && фикстура} {it.updated && обновлено {fmtDate(it.updated)}}

{it.title}

{it.summary}

{it.md ? (
{mdRender(it.md, ctx)}
) : ( <> {it.short && (

Коротко

{mdRender(it.short, ctx)}
)} {it.toc && it.toc.length > 0 && (
Что внутри
    {it.toc.map((t, i) =>
  1. {t}
  2. )}
)} )} {it.related && it.related.length > 0 && (
Связанные материалы
{it.related.map(r => )}
)}
)}
); } function KB({ nav, params = {} }) { // просмотр материала сервер пишет сам (GET /api/kb/:slug) return params.m ? : ; } window.KB = KB;