// onboarding.jsx — анкета первого входа (CJM Насти, п. 3.2, на «ты»): имя и фамилия, сфера, // компания, страна и город, о себе (до 300 знаков), цель (7 из data/goals.json + свой вариант). // Сохраняется в app.profiles (PUT /api/profile). В ответ — 3 материала под цель и промокод // на Хакку-коворк, если для участника есть свободный код. Экран «Спасибо!» — тут же. const ERR_TEXT_PROFILE = { bad_first_name: 'Напиши имя.', bad_last_name: 'Напиши фамилию.', bad_sphere: 'Выбери, чем ты занимаешься.', bad_country: 'Укажи страну.', bad_city: 'Укажи город.', about_too_long: 'Расскажи о себе короче — до 300 знаков.', bad_goal: 'Выбери, чего хочешь достичь с помощью ИИ.', goal_custom_required: 'Опиши свою цель в паре слов.', }; const ABOUT_MAX = 300; const ABOUT_EXAMPLE = 'Я маркетолог, веду соцсети небольшого бренда. С ИИ пишу тексты и придумываю идеи. Хочу собрать ассистента для отчётов.'; // Имя из Hakku ID часто = начало почты (OTP-вход отдаёт пустое имя) — такое не предзаполняем function nameParts(user) { const n = String((user && user.name) || '').trim(); const local = String((user && user.email) || '').split('@')[0]; if (!n || n === local) return ['', '']; const p = n.split(/\s+/); return [p[0] || '', p.slice(1).join(' ')]; } function CoworkingCode({ c }) { if (!c) return null; return (
{Icons.gift()}
Держи промокод на Хакку-коворк
{c.code} track('cowork:copy')}/>
Личный и одноразовый{c.expires ? `, действует до ${fmtDateShort(c.expires)}` : ''}. Промокод останется в профиле.
); } window.CoworkingCode = CoworkingCode; function Thanks({ nav, goalId, goalCustom, rec, coworking }) { const g = goalOf(goalId); const title = goalId === 'other' ? goalCustom : g && g.title; const proHint = g && g.pro_hint && ME().member.tier !== 'pro'; return (
Анкета сохранена

Спасибо!

Твоя цель — {title}. Николай уже написал тебе совет по ней, он на главной. Ниже три материала, с которых стоит начать.

{rec.map(r => )}
Подключи Telegram
Уведомления о сообщениях и эфирах от бота hakku.ai. Без бота пришлём на почту.
track('tg:connect')} style={{ textDecoration:'none' }}>Подключить Telegram ↗
{proHint && (
Под твою цель хорошо подходит PRO
Q&A с основателями, гость с сетапом, мастермайнд раз в месяц и офлайн-встречи. {ME().member.tier === 'enthusiast' ? ' Для участников Enthusiast — 4 500 ₽/мес вместо 4 900 ₽.' : ''}
)}
); } function Onboarding({ nav }) { const cfg = CFG(); const prev = ME().profile; const [fn0, ln0] = nameParts(ME().user); const [firstName_, setFirst] = React.useState((prev && prev.first_name) || fn0); const [lastName, setLast] = React.useState((prev && prev.last_name) || ln0); const [sphere, setSphere] = React.useState((prev && prev.sphere) || ''); const [sphereCustom, setSphereCustom] = React.useState((prev && prev.sphere_custom) || ''); const [company, setCompany] = React.useState((prev && prev.company) || ''); const [country, setCountry] = React.useState((prev && prev.country) || ''); const [city, setCity] = React.useState((prev && prev.city) || ''); const [about, setAbout] = React.useState((prev && prev.about) || ''); // старая цель (до 10.2026) подставляется её новым аналогом const prevGoal = prev ? (prev.goal === 'other' ? 'other' : ((goalOf(prev.goal) || {}).id || '')) : ''; const [goal, setGoal] = React.useState(prevGoal); const [goalCustom, setGoalCustom] = React.useState((prev && prev.goal_custom) || ''); const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(''); const [done, setDone] = React.useState(null); const clr = (f) => (e) => { f(e.target.value); setErr(''); }; const submit = async (e) => { e.preventDefault(); if (busy) return; const E = ERR_TEXT_PROFILE; if (!firstName_.trim()) return setErr(E.bad_first_name); if (!lastName.trim()) return setErr(E.bad_last_name); if (!sphere) return setErr(E.bad_sphere); if (!country.trim()) return setErr(E.bad_country); if (!city.trim()) return setErr(E.bad_city); if (about.trim().length > ABOUT_MAX) return setErr(E.about_too_long); if (!goal) return setErr(E.bad_goal); if (goal === 'other' && !goalCustom.trim()) return setErr(E.goal_custom_required); setBusy(true); setErr(''); try { const r = await fetch('/api/profile', { method:'PUT', credentials:'same-origin', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify({ first_name: firstName_, last_name: lastName, sphere, sphere_custom: sphereCustom, company, country, city, about, goal, goal_custom: goalCustom }) }); const j = await r.json().catch(()=>null); if (r.ok && j && j.ok) { window.__ME = { ...ME(), profile: j.profile }; setDone({ rec: j.recommended || [], coworking: j.coworking || null }); window.scrollTo(0, 0); } else setErr(ERR_TEXT_PROFILE[j && j.error] || 'Не удалось сохранить, попробуй ещё раз.'); } catch (e2) { setErr('Сервер не ответил, попробуй через минуту.'); } setBusy(false); }; const skip = () => { try { sessionStorage.setItem('cm_onboarding_skip', '1'); } catch (e) {} nav('home'); }; if (done) return ; const Opt = ({ children }) => {children}; return (
{prev ? 'Анкета участника' : 'Первый вход · 2 минуты'}

Давай познакомимся

По ответам подберём материалы, познакомим с участниками рядом и подскажем, с чего начать.

1Как тебя зовут?
2Чем ты занимаешься?
{(cfg.spheres || []).map(s => ( ))}
{sphere === 'other' && ( )}
3Где ты работаешь? по желанию
4Откуда ты?
Так мы знакомим с участниками рядом и зовём на офлайн-встречи
5Расскажи о себе в трёх предложениях по желанию