352 lines
13 KiB
TypeScript
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;
|