1import {
2 Alert,
3 AlertDescription,
4 AlertIcon,
5 Box,
6 Button,
7 chakra,
8 FormControl,
9 HStack,
10 Text,
11 VStack,
12} from '@chakra-ui/react';
13import { ArrowRightStartOnRectangleIcon } from '@heroicons/react/24/outline';
14import { zodResolver } from '@hookform/resolvers/zod';
15import { FC, useEffect, useState } from 'react';
16import { FieldValues, useForm } from 'react-hook-form';
17import { useLocation, useNavigate } from 'react-router-dom';
18import { z } from 'zod';
19import { Footer } from 'components/footer';
20import { Input } from 'components/input';
21import { fetch } from 'service/http';
22import { ReactComponent as Logo } from 'assets/logo.svg';
23import { useTranslation } from 'react-i18next';
24import { Language } from 'components/language';
25import { useAdmin } from 'stores';
26
27const schema = z.object({
28 username: z.string().min(1, 'login.fieldRequired'),
29 password: z.string().min(1, 'login.fieldRequired'),
30});
31
32export const LogoIcon = chakra(Logo, {
33 baseStyle: {
34 strokeWidth: '10px',
35 w: 12,
36 h: 12,
37 },
38});
39
40const LoginIcon = chakra(ArrowRightStartOnRectangleIcon, {
41 baseStyle: {
42 w: 5,
43 h: 5,
44 strokeWidth: '2px',
45 },
46});
47
48export const Login: FC = () => {
49 const [error, setError] = useState('');
50 const [loading, setLoading] = useState(false);
51 const navigate = useNavigate();
52 const { setAuthToken, removeAuthToken } = useAdmin();
53 const { t } = useTranslation();
54 let location = useLocation();
55 const {
56 register,
57 formState: { errors },
58 handleSubmit,
59 } = useForm({
60 resolver: zodResolver(schema),
61 });
62 useEffect(() => {
63 removeAuthToken();
64 if (location.pathname !== '/login') {
65 navigate('/login', { replace: true });
66 }
67 }, []);
68 const login = (values: FieldValues) => {
69 setError('');
70 const formData = new FormData();
71 formData.append('username', values.username);
72 formData.append('password', values.password);
73 formData.append('grant_type', 'password');
74 setLoading(true);
75 fetch('/admins/token', { method: 'post', body: formData })
76 .then(({ access_token: token }) => {
77 setAuthToken(token);
78 navigate('/');
79 })
80 .catch((err) => {
81 setError(err.response._data.detail);
82 })
83 .finally(setLoading.bind(null, false));
84 };
85 return (
86 <VStack justifyContent="space-between" minH="100vh" p="6" w="full">
87 <Box w="full">
88 <HStack justifyContent="end" w="full">
89 <Language />
90 </HStack>
91 <HStack w="full" justifyContent="center" alignItems="center">
92 <Box w="full" maxW="340px" mt="6">
93 <VStack alignItems="center" w="full">
94 <LogoIcon />
95 <Text fontSize="2xl" fontWeight="semibold">
96 {t('login.loginYourAccount')}
97 </Text>
98 <Text color="gray.600" _dark={{ color: 'gray.400' }}>
99 {t('login.welcomeBack')}
100 </Text>
101 </VStack>
102 <Box w="full" maxW="300px" m="auto" pt="4">
103 <form onSubmit={handleSubmit(login)}>
104 <VStack mt={4} rowGap={2}>
105 <FormControl>
106 <Input
107 w="full"
108 placeholder={t('username')}
109 {...register('username')}
110 error={t(errors?.username?.message as string)}
111 />
112 </FormControl>
113 <FormControl>
114 <Input
115 w="full"
116 type="password"
117 placeholder={t('password')}
118 {...register('password')}
119 error={t(errors?.password?.message as string)}
120 />
121 </FormControl>
122 {error && (
123 <Alert status="error" rounded="md">
124 <AlertIcon />
125 <AlertDescription>{error}</AlertDescription>
126 </Alert>
127 )}
128 <Button
129 isLoading={loading}
130 type="submit"
131 w="full"
132 colorScheme="primary"
133 >
134 {<LoginIcon marginRight={1} />}
135 {t('login.login')}
136 </Button>
137 </VStack>
138 </form>
139 </Box>
140 </Box>
141 </HStack>
142 </Box>
143 <Footer bottom={3} />
144 </VStack>
145 );
146};
147
148export default Login;