import { useEffect, useMemo, useState } from 'react'; import { useReveal, Icons, MoonMark, getBootstrap } from './atoms.jsx'; const FIELD_HINTS = { username: 'Используй латиницу и цифры. Регистр символов при входе не имеет значения.', invite_code: 'Регистрация доступна только по действующему инвайт-коду.', }; function firstError(errors, field) { if (!errors || !errors[field]) return null; const value = errors[field]; return Array.isArray(value) ? value[0] : value; } export default function Join() { const ref = useReveal(); const bootstrap = getBootstrap(); const flash = bootstrap.flash || {}; const old = flash.old || {}; const initialErrors = flash.errors || {}; const [form, setForm] = useState({ username: old.username || '', email: old.email || '', invite_code: old.invite_code || '', password: '', password_confirmation: '', terms: false, }); const [touched, setTouched] = useState(() => { const t = {}; Object.keys(initialErrors).forEach((k) => { t[k] = true; }); return t; }); const [serverErrors, setServerErrors] = useState(initialErrors); const [serverMessage, setServerMessage] = useState(flash.error || null); const [submitted, setSubmitted] = useState(Boolean(flash.success)); const [successUsername, setSuccessUsername] = useState( flash.success_username || old.username || '' ); const [submitting, setSubmitting] = useState(false); useEffect(() => { if (!submitted) return; const target = document.getElementById('join'); if (target && (flash.success || flash.error || Object.keys(initialErrors).length)) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }, [submitted]); const set = (k) => (e) => { const v = e.target.type === 'checkbox' ? e.target.checked : e.target.value; setForm((f) => ({ ...f, [k]: v })); if (serverErrors[k]) { setServerErrors((errs) => { const next = { ...errs }; delete next[k]; return next; }); } }; const blur = (k) => () => setTouched((t) => ({ ...t, [k]: true })); const clientErrors = useMemo(() => { const e = {}; if (form.username && !/^[a-zA-Z0-9]{3,32}$/.test(form.username)) { e.username = 'Только латиница и цифры, 3–32 символа.'; } if (form.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) { e.email = 'Похоже на некорректный email.'; } if (form.invite_code && !/^[A-Za-z0-9-]{6,32}$/.test(form.invite_code)) { e.invite_code = 'Код из букв, цифр и тире, 6–32 символа.'; } if (form.password && form.password.length < 8) { e.password = 'Минимум 8 символов.'; } else if (form.password && !/[A-Za-z]/.test(form.password)) { e.password = 'Должна быть хотя бы одна буква.'; } else if (form.password && !/\d/.test(form.password)) { e.password = 'Должна быть хотя бы одна цифра.'; } if (form.password_confirmation && form.password_confirmation !== form.password) { e.password_confirmation = 'Пароли не совпадают.'; } return e; }, [form]); const errorFor = (field) => firstError(serverErrors, field) || (touched[field] && clientErrors[field]) || null; const valid = form.username && form.email && form.invite_code && form.password && form.password_confirmation && form.terms && Object.keys(clientErrors).length === 0; const onSubmit = (e) => { if (!valid) { e.preventDefault(); setTouched({ username: true, email: true, invite_code: true, password: true, password_confirmation: true, terms: true, }); return; } setSubmitting(true); }; if (submitted && flash.success) { return (
Добро пожаловать

Аккаунт создан

{successUsername ? ( <> Логин {successUsername} готов к входу. ) : ( 'Аккаунт готов к входу. ' )} {' '}Открывай лаунчер и жми «Играть».

Войти в личный кабинет {Icons.arrow}
); } const endpoint = bootstrap.register_endpoint || '/register'; const csrfToken = bootstrap.csrf_token || ''; return (
Регистрация

Создай игровой аккаунт

Укажи инвайт-код, логин, почту и пароль — аккаунт будет готов к входу в игру сразу после регистрации.

Закрытый бета-доступ Пока идёт закрытый этап: регистрация только по инвайт-кодам. Код можно получить у друзей-игроков или в нашем Discord.
{serverMessage && (
{serverMessage}
)} setForm((f) => ({ ...f, invite_code: e.target.value.toUpperCase() })) } onBlur={blur('invite_code')} maxLength={32} style={{ letterSpacing: '0.14em', fontFamily: 'var(--f-mono)' }} />
{errorFor('terms') && (
{errorFor('terms')}
)}
); } function Field({ label, hint, error, icon, children }) { return (
{icon && {icon}} {children}
{error ? (
{error}
) : hint ? (
{hint}
) : null}
); }