quiniela-sembradores-frontend/app/register/page.tsx

352 lines
13 KiB
TypeScript

'use client';
import React, {ChangeEvent, SubmitEvent, useState} from 'react';
import Link from 'next/link';
import {useRouter} from 'next/navigation';
import {
Box,
Button,
InputAdornment,
Paper,
Stack,
TextField,
Typography,
Alert,
} from '@mui/material';
import {authClient} from '@/lib/api/auth';
import {ApiError, ApiValidationErrors} from '@/lib/api/client';
import {GuestGuard} from "@/src/auth/GuestGuard";
type FormValues = {
username: string;
firstName: string;
lastName: string;
email: string;
phone: string;
password: string;
confirmPassword: string;
};
type FormErrors = Partial<Record<keyof FormValues, string>>;
const initialValues: FormValues = {
username: '',
firstName: '',
lastName: '',
email: '',
phone: '',
password: '',
confirmPassword: '',
};
const validationFieldMap: Partial<Record<string, keyof FormValues>> = {
username: 'username',
first_name: 'firstName',
last_name: 'lastName',
email: 'email',
phone: 'phone',
password: 'password',
password_confirmation: 'confirmPassword',
};
const mapValidationErrors = (errors?: ApiValidationErrors): FormErrors => {
if (!errors) {
return {};
}
return Object.entries(errors).reduce<FormErrors>((accumulator, [key, messages]) => {
const field = validationFieldMap[key];
if (field && messages.length > 0) {
accumulator[field] = messages[0];
}
return accumulator;
}, {});
};
const Page: React.FC = () => {
const router = useRouter();
const [values, setValues] = useState<FormValues>(initialValues);
const [errors, setErrors] = useState<FormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [serverError, setServerError] = useState('');
const handleChange =
(field: keyof FormValues) => (event: ChangeEvent<HTMLInputElement>) => {
const nextValue =
field === 'phone'
? event.target.value.replace(/\D/g, '').slice(0, 10)
: event.target.value;
setValues((current) => ({
...current,
[field]: nextValue,
}));
setErrors((current) => {
if (!current[field]) {
return current;
}
const nextErrors = {...current};
delete nextErrors[field];
return nextErrors;
});
};
const validateForm = () => {
const nextErrors: FormErrors = {};
if (!values.username.trim()) {
nextErrors.username = 'El nombre de usuario es obligatorio.';
}
if (!values.firstName.trim()) {
nextErrors.firstName = 'El nombre es obligatorio.';
}
if (!values.lastName.trim()) {
nextErrors.lastName = 'El apellido es obligatorio.';
}
if (!values.email.trim()) {
nextErrors.email = 'El correo electrónico es obligatorio.';
} else if (!/\S+@\S+\.\S+/.test(values.email)) {
nextErrors.email = 'Ingresa un correo electrónico válido.';
}
if (!values.phone.trim()) {
nextErrors.phone = 'El número celular es obligatorio.';
} else if (!/^\d{10}$/.test(values.phone)) {
nextErrors.phone = 'Ingresa 10 dígitos numéricos.';
}
if (!values.password) {
nextErrors.password = 'La contraseña es obligatoria.';
} else if (values.password.length < 8) {
nextErrors.password = 'La contraseña debe tener al menos 8 caracteres.';
}
if (!values.confirmPassword) {
nextErrors.confirmPassword = 'Confirma tu contraseña.';
} else if (values.password !== values.confirmPassword) {
nextErrors.confirmPassword = 'Las contraseñas no coinciden.';
}
setErrors(nextErrors);
return Object.keys(nextErrors).length === 0;
};
const handleSubmit = (event: SubmitEvent<HTMLFormElement>) => {
event.preventDefault();
setServerError('');
if (!validateForm()) {
return;
}
setIsSubmitting(true);
authClient
.register({
username: values.username.trim(),
first_name: values.firstName.trim(),
last_name: values.lastName.trim(),
email: values.email.trim(),
phone: values.phone.trim(),
password: values.password,
password_confirmation: values.confirmPassword,
})
.then(() => router.push('/'))
.catch((error: unknown) => {
if (error instanceof ApiError) {
setErrors((current) => ({
...current,
...mapValidationErrors(error.errors),
}));
setServerError(error.message);
return;
}
setServerError('No fue posible crear la cuenta.');
})
.finally(() => {
setIsSubmitting(false);
});
};
return (
<GuestGuard>
<Box
sx={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bgcolor: 'grey.100',
p: 3,
}}
>
<Paper
elevation={4}
sx={{
width: '100%',
maxWidth: 520,
borderRadius: 4,
px: {xs: 3, sm: 4},
py: 4,
}}
>
<Typography variant="h4" fontWeight={700} gutterBottom>
Crea tu cuenta
</Typography>
<Typography variant="body1" color="text.secondary" sx={{mb: 3}}>
Llena todos los campos para crear tu cuenta
</Typography>
<Box component="form" noValidate onSubmit={handleSubmit}>
<Stack spacing={2}>
{serverError ? <Alert severity="error">{serverError}</Alert> : null}
<TextField
label="Apodo"
name="username"
value={values.username}
onChange={handleChange('username')}
error={Boolean(errors.username)}
helperText={errors.username}
required
fullWidth
/>
<Stack direction={{xs: 'column', sm: 'row'}} spacing={2}>
<TextField
label="Nombre(s)"
name="firstName"
value={values.firstName}
onChange={handleChange('firstName')}
error={Boolean(errors.firstName)}
helperText={errors.firstName}
required
fullWidth
/>
<TextField
label="Apellido(s)"
name="lastName"
value={values.lastName}
onChange={handleChange('lastName')}
error={Boolean(errors.lastName)}
helperText={errors.lastName}
required
fullWidth
/>
</Stack>
<TextField
label="Correo electrónico"
name="email"
type="email"
autoComplete="email"
value={values.email}
onChange={handleChange('email')}
error={Boolean(errors.email)}
helperText={errors.email}
required
fullWidth
/>
<TextField
label="Número celular"
name="phone"
type="tel"
autoComplete="tel-national"
value={values.phone}
onChange={handleChange('phone')}
error={Boolean(errors.phone)}
helperText={errors.phone ?? 'Captura 10 dígitos.'}
required
fullWidth
slotProps={{
htmlInput: {
inputMode: 'numeric',
pattern: '[0-9]*',
maxLength: 10,
},
input: {
startAdornment: (
<InputAdornment position="start">+52</InputAdornment>
),
},
}}
/>
<TextField
label="Contraseña"
name="password"
type="password"
autoComplete="new-password"
value={values.password}
onChange={handleChange('password')}
error={Boolean(errors.password)}
helperText={errors.password}
required
fullWidth
/>
<TextField
label="Confirmar contraseña"
name="confirmPassword"
type="password"
autoComplete="new-password"
value={values.confirmPassword}
onChange={handleChange('confirmPassword')}
error={Boolean(errors.confirmPassword)}
helperText={errors.confirmPassword}
required
fullWidth
/>
<Button
type="submit"
variant="contained"
size="large"
fullWidth
disabled={isSubmitting}
sx={{
mt: 1,
py: 1.5,
borderRadius: 2.5,
textTransform: 'none',
fontSize: 16,
fontWeight: 700,
}}
>
{isSubmitting ? 'Creando cuenta...' : 'Registrarme'}
</Button>
<Typography variant="body2" color="text.secondary" textAlign="center">
¿Ya tienes cuenta?{' '}
<Box
component={Link}
href="/login"
sx={{
color: 'primary.main',
textDecoration: 'none',
fontWeight: 600,
}}
>
Inicia sesión
</Box>
</Typography>
</Stack>
</Box>
</Paper>
</Box>
</GuestGuard>
);
};
export default Page;