uchill/front_minimal/src/sections/auth/jwt/jwt-sign-up-view.jsx

324 lines
10 KiB
JavaScript
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 { z as zod } from 'zod';
import { useState, useCallback, useEffect, useRef } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import Link from '@mui/material/Link';
import Alert from '@mui/material/Alert';
import Stack from '@mui/material/Stack';
import MenuItem from '@mui/material/MenuItem';
import Checkbox from '@mui/material/Checkbox';
import IconButton from '@mui/material/IconButton';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import Autocomplete from '@mui/material/Autocomplete';
import FormControlLabel from '@mui/material/FormControlLabel';
import CircularProgress from '@mui/material/CircularProgress';
import LoadingButton from '@mui/lab/LoadingButton';
import InputAdornment from '@mui/material/InputAdornment';
import { paths } from 'src/routes/paths';
import { useRouter } from 'src/routes/hooks';
import { RouterLink } from 'src/routes/components';
import { useBoolean } from 'src/hooks/use-boolean';
import { Iconify } from 'src/components/iconify';
import { Form, Field } from 'src/components/hook-form';
import { signUp } from 'src/auth/context/jwt';
import { useAuthContext } from 'src/auth/hooks';
import { searchCities } from 'src/utils/profile-api';
// ----------------------------------------------------------------------
export const SignUpSchema = zod
.object({
firstName: zod.string().min(1, { message: 'Введите имя!' }),
lastName: zod.string().min(1, { message: 'Введите фамилию!' }),
email: zod
.string()
.min(1, { message: 'Введите email!' })
.email({ message: 'Введите корректный email!' }),
role: zod.enum(['mentor', 'client', 'parent'], { message: 'Выберите роль!' }),
city: zod.string().min(1, { message: 'Введите город!' }),
password: zod
.string()
.min(1, { message: 'Введите пароль!' })
.min(8, { message: 'Пароль должен содержать минимум 8 символов!' }),
passwordConfirm: zod.string().min(1, { message: 'Подтвердите пароль!' }),
})
.refine((data) => data.password === data.passwordConfirm, {
message: 'Пароли не совпадают!',
path: ['passwordConfirm'],
});
// ----------------------------------------------------------------------
function CityAutocomplete({ value, onChange, error, helperText }) {
const [inputValue, setInputValue] = useState(value || '');
const [options, setOptions] = useState([]);
const [loading, setLoading] = useState(false);
const timerRef = useRef(null);
const fetch = useCallback(async (query) => {
if (!query || query.length < 2) { setOptions([]); return; }
setLoading(true);
try {
const res = await searchCities(query, 30);
setOptions(res.map((c) => (typeof c === 'string' ? c : c.name || c.city || String(c))));
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => fetch(inputValue), 350);
return () => clearTimeout(timerRef.current);
}, [inputValue, fetch]);
return (
<Autocomplete
freeSolo
options={options}
loading={loading}
inputValue={inputValue}
onInputChange={(_, val) => {
setInputValue(val);
onChange(val);
}}
onChange={(_, val) => {
if (val) { setInputValue(val); onChange(val); }
}}
noOptionsText="Города не найдены"
loadingText="Поиск..."
renderInput={(params) => (
<TextField
{...params}
label="Город (для часового пояса)"
error={error}
helperText={helperText}
InputLabelProps={{ shrink: true }}
InputProps={{
...params.InputProps,
endAdornment: (
<>
{loading ? <CircularProgress size={18} /> : null}
{params.InputProps.endAdornment}
</>
),
}}
/>
)}
/>
);
}
// ----------------------------------------------------------------------
export function JwtSignUpView() {
const { checkUserSession } = useAuthContext();
const router = useRouter();
const password = useBoolean();
const passwordConfirm = useBoolean();
const [errorMsg, setErrorMsg] = useState('');
const [successMsg, setSuccessMsg] = useState('');
const [consent, setConsent] = useState(false);
const defaultValues = {
firstName: '',
lastName: '',
email: '',
role: 'client',
city: '',
password: '',
passwordConfirm: '',
};
const methods = useForm({
resolver: zodResolver(SignUpSchema),
defaultValues,
});
const {
control,
handleSubmit,
formState: { isSubmitting },
} = methods;
const onSubmit = handleSubmit(async (data) => {
if (!consent) {
setErrorMsg('Необходимо согласиться с условиями использования и политикой конфиденциальности.');
return;
}
try {
const result = await signUp({
email: data.email,
password: data.password,
passwordConfirm: data.passwordConfirm,
firstName: data.firstName,
lastName: data.lastName,
role: data.role,
city: data.city,
});
if (result?.requiresVerification) {
setSuccessMsg(
`Письмо с подтверждением отправлено на ${data.email}. Пройдите по ссылке в письме для активации аккаунта.`
);
return;
}
await checkUserSession?.();
router.replace(paths.dashboard.root);
} catch (error) {
console.error(error);
const msg = error?.response?.data?.error?.message || error?.response?.data?.message || error?.response?.data?.detail || (error instanceof Error ? error.message : 'Ошибка регистрации. Проверьте введённые данные.');
setErrorMsg(msg);
}
});
const renderHead = (
<Stack spacing={1.5} sx={{ mb: 5 }}>
<Typography variant="h5">Создать аккаунт</Typography>
<Stack direction="row" spacing={0.5}>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Уже есть аккаунт?
</Typography>
<Link component={RouterLink} href={paths.auth.jwt.signIn} variant="subtitle2">
Войти
</Link>
</Stack>
</Stack>
);
if (successMsg) {
return (
<>
{renderHead}
<Alert severity="success">{successMsg}</Alert>
<Stack sx={{ mt: 3 }}>
<Link component={RouterLink} href={paths.auth.jwt.signIn} variant="subtitle2">
Вернуться ко входу
</Link>
</Stack>
</>
);
}
const renderForm = (
<Stack spacing={3}>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<Field.Text name="firstName" label="Имя" InputLabelProps={{ shrink: true }} />
<Field.Text name="lastName" label="Фамилия" InputLabelProps={{ shrink: true }} />
</Stack>
<Field.Text name="email" label="Email" InputLabelProps={{ shrink: true }} />
<Field.Select name="role" label="Роль" InputLabelProps={{ shrink: true }}>
<MenuItem value="client">Ученик</MenuItem>
<MenuItem value="mentor">Ментор</MenuItem>
<MenuItem value="parent">Родитель</MenuItem>
</Field.Select>
<Controller
name="city"
control={control}
render={({ field, fieldState }) => (
<CityAutocomplete
value={field.value}
onChange={field.onChange}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
<Field.Text
name="password"
label="Пароль"
placeholder="Минимум 8 символов"
type={password.value ? 'text' : 'password'}
InputLabelProps={{ shrink: true }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={password.onToggle} edge="end">
<Iconify icon={password.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
</IconButton>
</InputAdornment>
),
}}
/>
<Field.Text
name="passwordConfirm"
label="Подтвердите пароль"
placeholder="Минимум 8 символов"
type={passwordConfirm.value ? 'text' : 'password'}
InputLabelProps={{ shrink: true }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={passwordConfirm.onToggle} edge="end">
<Iconify icon={passwordConfirm.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
</IconButton>
</InputAdornment>
),
}}
/>
<FormControlLabel
control={<Checkbox checked={consent} onChange={(e) => setConsent(e.target.checked)} />}
label={
<Typography variant="body2">
Я соглашаюсь с{' '}
<Link underline="always" color="text.primary">
Условиями использования
</Link>{' '}
и{' '}
<Link underline="always" color="text.primary">
Политикой конфиденциальности
</Link>
</Typography>
}
/>
<LoadingButton
fullWidth
color="inherit"
size="large"
type="submit"
variant="contained"
loading={isSubmitting}
loadingIndicator="Создание аккаунта..."
>
Зарегистрироваться
</LoadingButton>
</Stack>
);
return (
<>
{renderHead}
{!!errorMsg && (
<Alert severity="error" sx={{ mb: 3 }}>
{errorMsg}
</Alert>
)}
<Form methods={methods} onSubmit={onSubmit}>
{renderForm}
</Form>
</>
);
}