Public Access
Added registration and settings page
This commit is contained in:
1 parent
b34234caa1
commit
2b5f77f0b9
12 files changed
+905
-142
No files matched your search
@@ -0,0 +1,185 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user