Files
moonwell-web/resources/js/moonwell/Join.jsx
T
2026-04-26 16:09:08 +04:00

326 lines
14 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (
<section id="join" data-screen-label="04 Join">
<div className="wrap">
<div className="join-card join-success reveal is-visible">
<div className="join-success-mark">
<MoonMark size={56} />
</div>
<span className="eyebrow" style={{ justifyContent: 'center' }}>Добро пожаловать</span>
<h3 className="h-display" style={{ fontSize: 36, marginTop: 16 }}>
Аккаунт создан
</h3>
<p className="lead" style={{ marginTop: 14 }}>
{successUsername ? (
<>
Логин <span style={{ color: 'var(--moon)', fontStyle: 'normal' }}>{successUsername}</span> готов к входу.
</>
) : (
'Аккаунт готов к входу. '
)}
{' '}Открывай лаунчер и жми «Играть».
</p>
<div className="hero-cta" style={{ justifyContent: 'center', marginTop: 28 }}>
<a
href={bootstrap.auth?.cabinet_url || bootstrap.auth?.login_url || '#'}
className="btn btn-primary"
>
Войти в личный кабинет {Icons.arrow}
</a>
</div>
</div>
</div>
</section>
);
}
const endpoint = bootstrap.register_endpoint || '/register';
const csrfToken = bootstrap.csrf_token || '';
return (
<section id="join" data-screen-label="04 Join">
<div className="wrap">
<div className="join-grid reveal" ref={ref}>
<div className="join-intro">
<span className="eyebrow">Регистрация</span>
<div style={{ height: 20 }} />
<h2 className="h-display" style={{ fontSize: 'clamp(36px, 5vw, 56px)' }}>
Создай игровой аккаунт
</h2>
<div className="divider-ornament" style={{ justifyContent: 'flex-start', margin: '22px 0' }}>
<span style={{ opacity: 0.6 }}></span>
</div>
<p className="lead" style={{ fontStyle: 'italic' }}>
Укажи инвайт-код, логин, почту и пароль аккаунт будет готов к входу в игру сразу после
регистрации.
</p>
<div className="join-note">
<span className="join-note-k">Закрытый бета-доступ</span>
<span className="join-note-v">
Пока идёт закрытый этап: регистрация только по инвайт-кодам. Код можно получить у
друзей-игроков или в нашем Discord.
</span>
</div>
</div>
<form
className="join-card"
method="POST"
action={endpoint}
onSubmit={onSubmit}
noValidate
>
<input type="hidden" name="_token" value={csrfToken} />
<div className="join-card-corner tl" aria-hidden="true"></div>
<div className="join-card-corner tr" aria-hidden="true"></div>
<div className="join-card-corner bl" aria-hidden="true"></div>
<div className="join-card-corner br" aria-hidden="true"></div>
{serverMessage && (
<div className="join-error" style={{ marginBottom: 14 }}>{serverMessage}</div>
)}
<Field
label="Логин"
hint={FIELD_HINTS.username}
error={errorFor('username')}
>
<input
type="text"
name="username"
autoComplete="username"
placeholder="Moonwalker"
value={form.username}
onChange={set('username')}
onBlur={blur('username')}
maxLength={32}
/>
</Field>
<Field label="Email" error={errorFor('email')}>
<input
type="email"
name="email"
autoComplete="email"
placeholder="player@example.com"
value={form.email}
onChange={set('email')}
onBlur={blur('email')}
/>
</Field>
<Field
label="Инвайт-код"
hint={FIELD_HINTS.invite_code}
error={errorFor('invite_code')}
icon={Icons.mask}
>
<input
type="text"
name="invite_code"
placeholder="ABCD-EFGH-IJKL"
value={form.invite_code}
onChange={(e) =>
setForm((f) => ({ ...f, invite_code: e.target.value.toUpperCase() }))
}
onBlur={blur('invite_code')}
maxLength={32}
style={{ letterSpacing: '0.14em', fontFamily: 'var(--f-mono)' }}
/>
</Field>
<div className="join-row">
<Field label="Пароль" error={errorFor('password')}>
<input
type="password"
name="password"
autoComplete="new-password"
placeholder="Минимум 8 символов"
value={form.password}
onChange={set('password')}
onBlur={blur('password')}
maxLength={32}
/>
</Field>
<Field
label="Повтор пароля"
error={errorFor('password_confirmation')}
>
<input
type="password"
name="password_confirmation"
autoComplete="new-password"
placeholder="Повтори пароль"
value={form.password_confirmation}
onChange={set('password_confirmation')}
onBlur={blur('password_confirmation')}
maxLength={32}
/>
</Field>
</div>
<label className="join-check">
<input
type="checkbox"
name="terms"
value="1"
checked={form.terms}
onChange={set('terms')}
/>
<span className="box" aria-hidden="true"></span>
<span>
Я принимаю <a href="#">правила сервера</a> и согласен на обработку данных для создания игрового
аккаунта.
</span>
</label>
{errorFor('terms') && (
<div className="join-error">{errorFor('terms')}</div>
)}
<button
type="submit"
className={`btn btn-primary join-submit ${(!valid || submitting) ? 'is-disabled' : ''}`}
disabled={!valid || submitting}
>
{submitting ? 'Создаём аккаунт…' : 'Зарегистрировать аккаунт'} {Icons.arrow}
</button>
</form>
</div>
</div>
</section>
);
}
function Field({ label, hint, error, icon, children }) {
return (
<div className={`join-field ${error ? 'has-error' : ''}`}>
<label className="join-label">{label}</label>
<div className="join-input">
{icon && <span className="join-input-icon">{icon}</span>}
{children}
</div>
{error ? (
<div className="join-error">{error}</div>
) : hint ? (
<div className="join-hint">{hint}</div>
) : null}
</div>
);
}