Files
emaui/libs/auth/src/lib/pages/LoginPage.tsx

193 lines
5.3 KiB
TypeScript

import { useState } from 'react';
import {
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 { 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 } from '../store/auth.slice';
import type { LoginPayload, AuthUser } from '../types/auth.types';
import { useAuthConfig } from '../AuthConfig';
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>;
export function LoginPage() {
const navigate = useNavigate();
const dispatch = useDispatch();
const { appName, loginRedirectPath, enableSignup, enableForgotPassword } =
useAuthConfig();
const [isLoading, setIsLoading] = useState(false);
const [rememberMe, setRememberMe] = useState(true);
const [loginTrigger] = useApiMutation<LoginPayload>();
const [meTrigger] = useApiMutation<AuthUser>();
const [profileCheckTrigger] = useApiMutation<{ total: number; items: Array<{ id: string; user: { id: string }; isComplete: boolean }> }>();
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));
try {
const profiles = await profileCheckTrigger({
url: `/profiles?q=${encodeURIComponent('i=user')}`,
method: 'GET',
}).unwrap();
const userProfile = profiles.items?.find((p) => p.user?.id === me.id);
if (userProfile?.isComplete) {
authStorage.setProfileId(userProfile.id);
} else {
navigate('/profile-setup');
return;
}
} catch {
navigate('/profile-setup');
return;
}
if (me.isPhoneNumberVerified) {
navigate(loginRedirectPath);
} else {
navigate('/otp-verify', {
state: { email: me.email, phoneNumber: me.phoneNumber },
});
}
} catch {
notify.error('Invalid email or password');
} finally {
setIsLoading(false);
}
};
return (
<AuthShell>
<Stack gap="lg">
<div>
<Title order={2} fz={30}>
Welcome to {appName}
</Title>
<Text c="dimmed" mt={6}>
Sign in to access your account.
</Text>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="md">
<TextInput
label="Email or phone"
placeholder="you@example.com"
size="md"
leftSection={<IconMail size={18} />}
error={errors.email?.message}
{...register('email')}
/>
<PasswordInput
label="Password"
placeholder="Your password"
size="md"
leftSection={<IconLock size={18} />}
error={errors.password?.message}
{...register('password')}
/>
<Group justify="space-between">
<Checkbox
label="Remember me"
size="sm"
checked={rememberMe}
onChange={(e) => setRememberMe(e.currentTarget.checked)}
/>
{enableForgotPassword && (
<Anchor
component={Link}
to="/forgot-password"
size="sm"
fw={600}
>
Forgot password?
</Anchor>
)}
</Group>
<Button
type="submit"
loading={isLoading}
fullWidth
size="md"
rightSection={<IconArrowRight size={18} />}
>
Sign in
</Button>
</Stack>
</form>
<Divider label="or" labelPosition="center" />
<Button
variant="default"
fullWidth
size="md"
leftSection={<IconDeviceMobile size={18} />}
onClick={() => notify.info('Phone sign-in is coming soon.')}
>
Sign in with phone number
</Button>
{enableSignup && (
<Text ta="center" size="sm" c="dimmed">
Don&apos;t have an account?{' '}
<Anchor component={Link} to="/signup" fw={700}>
Create one
</Anchor>
</Text>
)}
</Stack>
</AuthShell>
);
}