mirror of
https://github.com/Nezumi-2711/react-training.git
synced 2026-09-22 13:38:51 +00:00
Create hooks and implement login pages
This commit is contained in:
@@ -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 };
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
Reference in New Issue
Block a user