Create hooks and implement login pages

This commit is contained in:
2023-11-30 11:19:18 +07:00
parent d67dfa685f
commit f09a4bc60d
10 changed files with 290 additions and 27 deletions
+21 -6
View File
@@ -7,12 +7,15 @@ import { useEffect, useMemo, useReducer } from 'react';
// Components // Components
import AppLayout from './components/AppLayout'; import AppLayout from './components/AppLayout';
import Toast from './components/Toast'; import Toast from './components/Toast';
import RouteProtected from '@component/RouteProtected';
import RootLayout from '@component/RootLayout';
// Pages // Pages
import User from './pages/User'; import User from './pages/User';
import Booking from './pages/Booking'; import Booking from './pages/Booking';
import Room from './pages/Room'; import Room from './pages/Room';
import NotFound from './pages/NotFound'; import NotFound from './pages/NotFound';
import Login from '@page/Login';
// Constants // Constants
import * as PATH from './constants/path'; import * as PATH from './constants/path';
@@ -75,13 +78,25 @@ function App() {
<UserRoomAvailableContext.Provider value={store}> <UserRoomAvailableContext.Provider value={store}>
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
<Route element={<AppLayout />}> <Route element={<RootLayout />}>
<Route index element={<Navigate replace to={PATH.BOOKING} />} /> <Route
<Route path={PATH.BOOKING} element={<Booking />} /> element={
<Route path={PATH.USER} element={<User />} /> <RouteProtected>
<Route path={PATH.ROOM} element={<Room />} /> <AppLayout />
</RouteProtected>
}
>
<Route
index
element={<Navigate replace to={PATH.BOOKING} />}
/>
<Route path={PATH.BOOKING} element={<Booking />} />
<Route path={PATH.USER} element={<User />} />
<Route path={PATH.ROOM} element={<Room />} />
</Route>
<Route path={PATH.LOGIN} element={<Login />} />
<Route path={PATH.OTHER_PATH} element={<NotFound />} />
</Route> </Route>
<Route path={PATH.OTHER_PATH} element={<NotFound />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
</UserRoomAvailableContext.Provider> </UserRoomAvailableContext.Provider>
@@ -0,0 +1,20 @@
import { useQuery } from '@tanstack/react-query';
// Services
import { getCurrentAccount } from '@service/authenticationService';
const useAccount = () => {
let isAuthenticated: boolean = false;
const { data: account, isPending } = useQuery({
queryKey: ['account'],
queryFn: getCurrentAccount,
});
if (account) {
isAuthenticated = account.role === 'authenticated';
}
return { isPending, account, isAuthenticated };
};
export { useAccount };
@@ -0,0 +1,32 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate, useSearchParams } from 'react-router-dom';
import toast from 'react-hot-toast';
// Services
import { login as loginFn } from '@service/authenticationService';
// Types
import { ILogin } from '@type/common';
const useLogin = () => {
const queryClient = useQueryClient();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const redirectTo = searchParams.get('redirectTo') || null;
const { mutate: login, isPending } = useMutation({
mutationFn: ({ email, password }: ILogin) => loginFn({ email, password }),
onSuccess: (account) => {
queryClient.setQueryData(['account'], account.user);
redirectTo ? navigate(redirectTo) : navigate('/');
},
onError: (err) => {
console.error(err.message);
toast.error('Email or password not correct!');
},
});
return { login, isPending };
};
export { useLogin };
@@ -0,0 +1,22 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
// Services
import { logout as logoutFn } from '@service/authenticationService';
const useLogout = () => {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { mutate: logout, isPending } = useMutation({
mutationFn: logoutFn,
onSuccess: () => {
queryClient.removeQueries();
navigate('/login', { replace: true });
},
});
return { logout, isPending };
};
export { useLogout };
@@ -0,0 +1,69 @@
import { useForm } from 'react-hook-form';
// Components
import Form from '@component/Form';
// Styled
import { FieldInput, StyledLoginForm } from './styled';
import Button from '@commonStyle/Button';
// Constants
import { REQUIRED_FIELD_ERROR } from '@constant/formValidateMessage';
// Types
import { ILogin } from '@type/common';
// Hooks
import { useLogin } from '@hook/authentication/useLogin';
const LoginForm = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<ILogin>();
const { login, isPending } = useLogin();
const onSubmit = (data: ILogin) => {
login(data);
};
return (
<StyledLoginForm>
<Form onSubmit={handleSubmit(onSubmit)}>
<Form.Row
label="Email:"
direction="vertical"
error={errors.email?.message}
>
<FieldInput
type="text"
{...register('email', {
required: REQUIRED_FIELD_ERROR,
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address',
},
})}
/>
</Form.Row>
<Form.Row
label="Password:"
direction="vertical"
error={errors.password?.message}
>
<FieldInput
type="password"
{...register('password', {
required: REQUIRED_FIELD_ERROR,
})}
/>
</Form.Row>
<Button type="submit" disabled={isPending}>Login</Button>
</Form>
</StyledLoginForm>
);
};
export default LoginForm;
@@ -0,0 +1,32 @@
import { Navigate } from 'react-router-dom';
// Styled
import { LoginLayout, StyledLogo, TitleStyled } from './styled';
// Assets
import logo from '@assets/images/logo.png';
// Components
import LoginForm from './LoginForm';
// Hooks
import { useAccount } from '@hook/authentication/useAccount';
const Login = () => {
const { account } = useAccount();
if (account) {
return <Navigate to={'/booking'} />;
}
return (
<LoginLayout>
<StyledLogo src={logo} />
<TitleStyled>Log In</TitleStyled>
<LoginForm />
</LoginLayout>
);
};
export default Login;
@@ -0,0 +1,43 @@
import styled from 'styled-components';
// Styled
import CommonInput from '@commonStyle/CommonInput';
const LoginLayout = styled.main`
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: var(--background-login);
`;
const StyledLogo = styled.img`
width: 250px;
height: 250px;
`;
const TitleStyled = styled.h1`
font-size: var(--fs-md);
`;
const StyledLoginForm = styled.div`
background-color: var(--form-color);
border-radius: var(--radius-sm);
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 20px;
padding: 30px 50px;
`;
const FieldInput = styled.input`
${CommonInput}
width: 300px;
`
export { LoginLayout, StyledLogo, TitleStyled, StyledLoginForm, FieldInput };
@@ -0,0 +1,43 @@
import { ILogin } from '@type/common';
// Services
import supabase from './supabaseService';
const login = async ({ email, password }: ILogin) => {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
throw new Error(error.message);
}
return data;
};
const getCurrentAccount = async () => {
const { data } = await supabase.auth.getSession();
if (!data.session) {
return null;
}
const { data: accountData, error } = await supabase.auth.getUser();
if (error) {
throw new Error(error.message);
}
return accountData.user;
};
const logout = async () => {
const { error } = await supabase.auth.signOut();
if (error) {
throw new Error(error.message);
}
};
export { login, logout, getCurrentAccount };
+6 -20
View File
@@ -1,6 +1,5 @@
// Types // Types
import { IRoom } from '@type/rooms'; import { IRoom } from '@type/rooms';
import { IDataState } from '@type/common';
// Services // Services
import supabase from './supabaseService'; import supabase from './supabaseService';
@@ -127,28 +126,17 @@ const getRoomById = async (idRoom: string): Promise<IRoom> => {
return data; return data;
}; };
/** /**
* * Update status of room
* @returns * @param id The id of room need to be change status
* @param status The status of room
*/ */
const getRoomsAvailable = async (): Promise<IDataState[]> => {
const { data, error } = await supabase
.from(ROOMS_TABLE)
.select('id, name, status');
if (error) {
console.error(error.message);
throw new Error(ERROR_FETCHING_ROOM);
}
return data;
};
const updateRoomStatus = async ( const updateRoomStatus = async (
id: number, id: number,
status: boolean status: boolean
): Promise<number> => { ): Promise<void> => {
const { error, status: statusConnection } = await supabase const { error} = await supabase
.from(ROOMS_TABLE) .from(ROOMS_TABLE)
.update({ status }) .update({ status })
.eq('id', id); .eq('id', id);
@@ -158,7 +146,6 @@ const updateRoomStatus = async (
throw new Error(ERROR_UPDATE_ROOM); throw new Error(ERROR_UPDATE_ROOM);
} }
return statusConnection;
}; };
export { export {
@@ -166,7 +153,6 @@ export {
updateRoom, updateRoom,
createRoom, createRoom,
deleteRoom, deleteRoom,
getRoomsAvailable,
getRoomById, getRoomById,
updateRoomStatus, updateRoomStatus,
}; };
+2 -1
View File
@@ -32,7 +32,8 @@
"@component/*": ["./src/components/*"], "@component/*": ["./src/components/*"],
"@commonStyle/*": ["./src/commons/styles/*"], "@commonStyle/*": ["./src/commons/styles/*"],
"@type/*": ["./src/types/*"], "@type/*": ["./src/types/*"],
"@page/*": ["./src/pages/*"] "@page/*": ["./src/pages/*"],
"@assets/*": ["./src/assets/*"],
} }
}, },
"include": ["src", "test"], "include": ["src", "test"],