Public Access
186 lines
7.9 KiB
React
186 lines
7.9 KiB
React
import {useState} from 'react';
|
|
import {Link, useNavigate} from 'react-router-dom';
|
|
import {useTranslation} from 'react-i18next';
|
|
import {useForm} from 'react-hook-form';
|
|
import {zodResolver} from '@hookform/resolvers/zod';
|
|
|
|
import {registrationSchema} from '../utils/validation.js';
|
|
|
|
const BACKEND_ERROR_KEY = {
|
|
EMAIL_ALREADY_IN_USE: 'REGISTRATION.ERROR_EMAIL_IN_USE',
|
|
INVALID_INVITATION_CODE: 'REGISTRATION.ERROR_INVALID_INVITE_CODE',
|
|
STEAM_ID_ALREADY_IN_USE: 'REGISTRATION.ERROR_STEAM_ID_IN_USE',
|
|
};
|
|
|
|
const inputClass =
|
|
'w-full px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:outline-none';
|
|
const inputErrorClass =
|
|
'w-full px-4 py-2 border border-red-500 dark:border-red-500 rounded-lg bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-red-500 focus:outline-none';
|
|
|
|
export default function RegistrationPage() {
|
|
const {t} = useTranslation();
|
|
const navigate = useNavigate();
|
|
const [feedback, setFeedback] = useState({message: '', isError: false});
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
formState: {errors, isSubmitting},
|
|
} = useForm({
|
|
resolver: zodResolver(registrationSchema),
|
|
defaultValues: {
|
|
email: '',
|
|
password: '',
|
|
passwordConfirmation: '',
|
|
inviteCode: '',
|
|
steamId: '',
|
|
},
|
|
});
|
|
|
|
const onSubmit = async (values) => {
|
|
setFeedback({message: '', isError: false});
|
|
try {
|
|
const apiUrl = `${import.meta.env.VITE_API_URL}/api/registration/new`;
|
|
const response = await fetch(apiUrl, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
email: values.email,
|
|
password: values.password,
|
|
inviteCode: values.inviteCode,
|
|
steamId: values.steamId,
|
|
}),
|
|
});
|
|
|
|
if (response.ok) {
|
|
setFeedback({message: t('REGISTRATION.SUCCESS'), isError: false});
|
|
setTimeout(() => navigate('/login'), 800);
|
|
return;
|
|
}
|
|
|
|
const data = await response.json().catch(() => ({}));
|
|
const mappedKey = BACKEND_ERROR_KEY[data.code];
|
|
setFeedback({
|
|
message: mappedKey ? t(mappedKey) : data.message || t('REGISTRATION.ERROR_GENERIC'),
|
|
isError: true,
|
|
});
|
|
} catch (err) {
|
|
console.error('Registration API error:', err);
|
|
setFeedback({message: t('INFO.ERROR.NETWORK_FAILURE'), isError: true});
|
|
}
|
|
};
|
|
|
|
const renderError = (field) =>
|
|
errors[field] && (
|
|
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{t(errors[field].message)}</p>
|
|
);
|
|
|
|
return (
|
|
<div className="flex min-h-screen items-center justify-center bg-slate-50 dark:bg-slate-900 px-4 py-8">
|
|
<div className="w-full max-w-md bg-white dark:bg-slate-800 rounded-xl shadow-lg p-8">
|
|
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-6 text-center">
|
|
{t('REGISTRATION.TITLE')}
|
|
</h2>
|
|
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4" noValidate>
|
|
<div>
|
|
<label htmlFor="email" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
|
{t('REGISTRATION.EMAIL')}
|
|
</label>
|
|
<input
|
|
id="email"
|
|
type="email"
|
|
autoComplete="email"
|
|
className={errors.email ? inputErrorClass : inputClass}
|
|
{...register('email')}
|
|
/>
|
|
{renderError('email')}
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="password" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
|
{t('REGISTRATION.PASSWORD')}
|
|
</label>
|
|
<input
|
|
id="password"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
className={errors.password ? inputErrorClass : inputClass}
|
|
{...register('password')}
|
|
/>
|
|
{renderError('password')}
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="passwordConfirmation" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
|
{t('REGISTRATION.PASSWORD_CONFIRMATION')}
|
|
</label>
|
|
<input
|
|
id="passwordConfirmation"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
className={errors.passwordConfirmation ? inputErrorClass : inputClass}
|
|
{...register('passwordConfirmation')}
|
|
/>
|
|
{renderError('passwordConfirmation')}
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="inviteCode" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
|
{t('REGISTRATION.INVITE_CODE')}
|
|
</label>
|
|
<input
|
|
id="inviteCode"
|
|
type="text"
|
|
className={errors.inviteCode ? inputErrorClass : inputClass}
|
|
{...register('inviteCode')}
|
|
/>
|
|
{renderError('inviteCode')}
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="steamId" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
|
{t('REGISTRATION.STEAM_ID')}
|
|
</label>
|
|
<input
|
|
id="steamId"
|
|
type="text"
|
|
inputMode="numeric"
|
|
className={errors.steamId ? inputErrorClass : inputClass}
|
|
{...register('steamId')}
|
|
/>
|
|
{renderError('steamId')}
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
className="w-full py-2 px-4 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-semibold rounded-lg transition-colors duration-200"
|
|
>
|
|
{isSubmitting ? '...' : t('REGISTRATION.SUBMIT')}
|
|
</button>
|
|
</form>
|
|
|
|
{feedback.message && (
|
|
<div
|
|
className={`mt-6 p-4 rounded-lg text-sm text-center ${
|
|
feedback.isError
|
|
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
|
: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
|
}`}
|
|
>
|
|
{feedback.message}
|
|
</div>
|
|
)}
|
|
|
|
<p className="mt-6 text-sm text-center text-slate-600 dark:text-slate-400">
|
|
{t('REGISTRATION.HAVE_ACCOUNT')}{' '}
|
|
<Link to="/login" className="text-blue-600 hover:text-blue-500 font-semibold">
|
|
{t('MAIN.LOGIN')}
|
|
</Link>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|