Files
emaui/libs/auth/src/lib/pages/LoginPage.tsx
2026-07-11 12:41:09 +03:00

223 lines
6.2 KiB
TypeScript

import { useState } from 'react';
import {
Alert,
Anchor,
Button,
Checkbox,
Divider,
Group,
PasswordInput,
Stack,
Text,
TextInput,
Title,
} from '@mantine/core';
import {
IconArrowRight,
IconDeviceMobile,
IconLock,
IconMail,
} from '@tabler/icons-react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useTranslation } from 'react-i18next';
import { useNavigate, Link } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { useApiMutation } from '@ema-platform/api';
import { notify } from '@ema-platform/ui';
import { AuthShell } from '../components/AuthShell';
import { loginSuccess, setUser, setCurrentProfile } from '../store/auth.slice';
import type { LoginPayload, AuthUser, CurrentProfile } from '../types/auth.types';
import { authStorage } from '../utils/auth-storage';
const schema = z.object({
email: z.string().email({ message: 'Enter a valid email' }),
password: z.string().min(5, { message: 'Password must be at least 6 characters' }),
});
type FormValues = z.infer<typeof schema>;
interface LoginPageProps {
appName?: string;
enableSignup?: boolean;
enableForgotPassword?: boolean;
}
export function LoginPage({
appName,
enableSignup = true,
enableForgotPassword = true,
}: LoginPageProps) {
const { t } = useTranslation();
const navigate = useNavigate();
const dispatch = useDispatch();
const [isLoading, setIsLoading] = useState(false);
const [rememberMe, setRememberMe] = useState(true);
const [serverError, setServerError] = useState<string | null>(null);
const [loginTrigger] = useApiMutation<LoginPayload>();
const [meTrigger] = useApiMutation<AuthUser>();
const [profileCheckTrigger] = useApiMutation<{ total: number; items: CurrentProfile[] }>();
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
});
const onSubmit = async (values: FormValues) => {
setIsLoading(true);
try {
const data = await loginTrigger({
url: '/auth/login',
method: 'POST',
body: values,
}).unwrap();
dispatch(loginSuccess(data));
const me = await meTrigger({
url: '/auth/me',
method: 'GET',
}).unwrap();
dispatch(setUser(me));
let hasProfile = false;
try {
const q = `w=user_id:=:${me.id}&i=user,address,profession`;
const result = await profileCheckTrigger({
url: `/profiles?q=${encodeURIComponent(q)}`,
method: 'GET',
}).unwrap();
if (result.total > 0 && result.items.length > 0) {
const profile = result.items[0];
authStorage.setProfileId(profile.id);
dispatch(setCurrentProfile(profile));
hasProfile = true;
}
} catch {
// profile not found — redirect to setup
}
if (!me.isPhoneNumberVerified) {
navigate('/otp-verify', {
state: {
email: me.email,
phoneNumber: me.phoneNumber,
needsProfile: !hasProfile,
},
});
return;
}
if (!hasProfile) {
navigate('/profile-setup');
return;
}
navigate('/dashboard');
} catch (err: unknown) {
const msg =
(err as { data?: { message?: string } })?.data?.message ??
(err instanceof Error ? err.message : t('common.somethingWentWrong'));
setServerError(msg);
notify.error(msg);
} finally {
setIsLoading(false);
}
};
return (
<AuthShell>
<Stack gap="lg">
<div>
<Title order={2} fz={30}>
{t('auth.welcomeTo', { appName: appName ?? t('app.name') })}
</Title>
<Text c="dimmed" mt={6}>
{t('auth.signInToAccess')}
</Text>
</div>
{serverError && (
<Alert variant="light" color="red" withCloseButton onClose={() => setServerError(null)}>
{serverError}
</Alert>
)}
<form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="md">
<TextInput
label={t('auth.emailOrPhone')}
placeholder={t('auth.emailPlaceholder')}
size="md"
leftSection={<IconMail size={18} />}
error={errors.email ? t('auth.validation.emailInvalid') : undefined}
{...register('email')}
/>
<PasswordInput
label={t('auth.password')}
placeholder={t('auth.passwordPlaceholder')}
size="md"
leftSection={<IconLock size={18} />}
error={errors.password ? t('auth.validation.passwordMin', { min: 6 }) : undefined}
{...register('password')}
/>
<Group justify="space-between">
<Checkbox
label={t('auth.rememberMe')}
size="sm"
checked={rememberMe}
onChange={(e) => setRememberMe(e.currentTarget.checked)}
/>
{enableForgotPassword && (
<Anchor
component={Link}
to="/forgot-password"
size="sm"
fw={600}
>
{t('auth.forgotPassword')}
</Anchor>
)}
</Group>
<Button
type="submit"
loading={isLoading}
fullWidth
size="md"
rightSection={<IconArrowRight size={18} />}
>
{t('auth.signIn')}
</Button>
</Stack>
</form>
<Divider label={t('common.or')} labelPosition="center" />
<Button
variant="default"
fullWidth
size="md"
leftSection={<IconDeviceMobile size={18} />}
onClick={() => notify.info('Phone sign-in is coming soon.')}
>
{t('auth.signInWithPhone')}
</Button>
{enableSignup && (
<Text ta="center" size="sm" c="dimmed">
{t('auth.noAccount')}{' '}
<Anchor component={Link} to="/signup" fw={700}>
{t('auth.createOne')}
</Anchor>
</Text>
)}
</Stack>
</AuthShell>
);
}