68bbbf8895
- Create auth-password feature with login/register form - Create auth-code feature with email+code verification form - Extract getApiErrorMessage to shared lib - Simplify AuthPage to pure UI composer with tabs - Update tests for new component structure - All 40 tests passing
101 lines
2.7 KiB
TypeScript
101 lines
2.7 KiB
TypeScript
import Button from '@mui/material/Button'
|
||
import InputAdornment from '@mui/material/InputAdornment'
|
||
import Stack from '@mui/material/Stack'
|
||
import TextField from '@mui/material/TextField'
|
||
import { useMutation } from '@tanstack/react-query'
|
||
import { Mail } from 'lucide-react'
|
||
import { useForm } from 'react-hook-form'
|
||
import { apiClient } from '@/shared/api/client'
|
||
import { getApiErrorMessage } from '@/shared/lib/get-api-error-message'
|
||
import { tokenSet } from '@/shared/model/auth'
|
||
|
||
type AuthResponse = {
|
||
token: string
|
||
user: {
|
||
id: string
|
||
email: string
|
||
displayName?: string | null
|
||
avatar?: string | null
|
||
avatarStyle?: string | null
|
||
}
|
||
}
|
||
|
||
type FormValues = {
|
||
email: string
|
||
code: string
|
||
}
|
||
|
||
type Props = {
|
||
onSuccess: () => void
|
||
}
|
||
|
||
export function AuthCodeForm({ onSuccess }: Props) {
|
||
const { register, watch } = useForm<FormValues>({
|
||
defaultValues: { email: '', code: '' },
|
||
mode: 'onChange',
|
||
})
|
||
|
||
const email = watch('email')
|
||
const code = watch('code')
|
||
|
||
const requestCodeMutation = useMutation({
|
||
mutationFn: async () => {
|
||
await apiClient.post('auth/request-code', { email })
|
||
},
|
||
})
|
||
|
||
const verifyCodeMutation = useMutation({
|
||
mutationFn: async () => {
|
||
const { data } = await apiClient.post<AuthResponse>('auth/verify-code', { email, code })
|
||
tokenSet(data.token)
|
||
},
|
||
onSuccess,
|
||
})
|
||
|
||
return (
|
||
<Stack spacing={2}>
|
||
<TextField
|
||
label="Email"
|
||
{...register('email')}
|
||
fullWidth
|
||
slotProps={{
|
||
input: {
|
||
startAdornment: (
|
||
<InputAdornment position="start">
|
||
<Mail size={18} />
|
||
</InputAdornment>
|
||
),
|
||
},
|
||
}}
|
||
/>
|
||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
|
||
<Button
|
||
variant="outlined"
|
||
onClick={() => requestCodeMutation.mutate()}
|
||
disabled={!email || requestCodeMutation.isPending}
|
||
sx={{ whiteSpace: 'nowrap' }}
|
||
>
|
||
Отправить код
|
||
</Button>
|
||
<TextField label="Код (6 цифр)" inputMode="numeric" {...register('code')} sx={{ flex: 1 }} />
|
||
<Button
|
||
variant="contained"
|
||
onClick={() => verifyCodeMutation.mutate()}
|
||
disabled={!email || code.length !== 6 || verifyCodeMutation.isPending}
|
||
sx={{ whiteSpace: 'nowrap' }}
|
||
>
|
||
Войти
|
||
</Button>
|
||
</Stack>
|
||
|
||
{(requestCodeMutation.error || verifyCodeMutation.error) && (
|
||
<TextField
|
||
error
|
||
helperText={getApiErrorMessage(requestCodeMutation.error) || getApiErrorMessage(verifyCodeMutation.error)}
|
||
sx={{ display: 'none' }}
|
||
/>
|
||
)}
|
||
</Stack>
|
||
)
|
||
}
|