diff --git a/hotel-management/src/App.tsx b/hotel-management/src/App.tsx
index c0785e7..e6edaa6 100644
--- a/hotel-management/src/App.tsx
+++ b/hotel-management/src/App.tsx
@@ -7,12 +7,15 @@ import { useEffect, useMemo, useReducer } from 'react';
// Components
import AppLayout from './components/AppLayout';
import Toast from './components/Toast';
+import RouteProtected from '@component/RouteProtected';
+import RootLayout from '@component/RootLayout';
// Pages
import User from './pages/User';
import Booking from './pages/Booking';
import Room from './pages/Room';
import NotFound from './pages/NotFound';
+import Login from '@page/Login';
// Constants
import * as PATH from './constants/path';
@@ -75,13 +78,25 @@ function App() {
- }>
- } />
- } />
- } />
- } />
+ }>
+
+
+
+ }
+ >
+ }
+ />
+ } />
+ } />
+ } />
+
+ } />
+ } />
- } />
diff --git a/hotel-management/src/hooks/authentication/useAccount.ts b/hotel-management/src/hooks/authentication/useAccount.ts
new file mode 100644
index 0000000..34c5c86
--- /dev/null
+++ b/hotel-management/src/hooks/authentication/useAccount.ts
@@ -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 };
diff --git a/hotel-management/src/hooks/authentication/useLogin.ts b/hotel-management/src/hooks/authentication/useLogin.ts
new file mode 100644
index 0000000..5529169
--- /dev/null
+++ b/hotel-management/src/hooks/authentication/useLogin.ts
@@ -0,0 +1,34 @@
+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 };
diff --git a/hotel-management/src/hooks/authentication/useLogout.ts b/hotel-management/src/hooks/authentication/useLogout.ts
new file mode 100644
index 0000000..66cc623
--- /dev/null
+++ b/hotel-management/src/hooks/authentication/useLogout.ts
@@ -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 };
diff --git a/hotel-management/src/pages/Login/LoginForm.tsx b/hotel-management/src/pages/Login/LoginForm.tsx
new file mode 100644
index 0000000..9d0f6a2
--- /dev/null
+++ b/hotel-management/src/pages/Login/LoginForm.tsx
@@ -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();
+ const { login, isPending } = useLogin();
+
+ const onSubmit = (data: ILogin) => {
+ login(data);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default LoginForm;
diff --git a/hotel-management/src/pages/Login/index.tsx b/hotel-management/src/pages/Login/index.tsx
new file mode 100644
index 0000000..c443ab2
--- /dev/null
+++ b/hotel-management/src/pages/Login/index.tsx
@@ -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 ;
+ }
+
+ return (
+
+
+ Log In
+
+
+
+ );
+};
+
+export default Login;
diff --git a/hotel-management/src/pages/Login/styled.ts b/hotel-management/src/pages/Login/styled.ts
new file mode 100644
index 0000000..9298f3b
--- /dev/null
+++ b/hotel-management/src/pages/Login/styled.ts
@@ -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 };
diff --git a/hotel-management/src/services/authenticationService.ts b/hotel-management/src/services/authenticationService.ts
new file mode 100644
index 0000000..5a0e5f1
--- /dev/null
+++ b/hotel-management/src/services/authenticationService.ts
@@ -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 };
diff --git a/hotel-management/src/services/roomServices.ts b/hotel-management/src/services/roomServices.ts
index e660376..91c76c4 100644
--- a/hotel-management/src/services/roomServices.ts
+++ b/hotel-management/src/services/roomServices.ts
@@ -1,6 +1,5 @@
// Types
import { IRoom } from '@type/rooms';
-import { IDataState } from '@type/common';
// Services
import supabase from './supabaseService';
@@ -127,28 +126,17 @@ const getRoomById = async (idRoom: string): Promise => {
return data;
};
+
/**
- *
- * @returns
+ * Update status of room
+ * @param id The id of room need to be change status
+ * @param status The status of room
*/
-const getRoomsAvailable = async (): Promise => {
- 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 (
id: number,
status: boolean
-): Promise => {
- const { error, status: statusConnection } = await supabase
+): Promise => {
+ const { error} = await supabase
.from(ROOMS_TABLE)
.update({ status })
.eq('id', id);
@@ -158,7 +146,6 @@ const updateRoomStatus = async (
throw new Error(ERROR_UPDATE_ROOM);
}
- return statusConnection;
};
export {
@@ -166,7 +153,6 @@ export {
updateRoom,
createRoom,
deleteRoom,
- getRoomsAvailable,
getRoomById,
updateRoomStatus,
};
diff --git a/hotel-management/tsconfig.json b/hotel-management/tsconfig.json
index ba79ba5..284f24e 100644
--- a/hotel-management/tsconfig.json
+++ b/hotel-management/tsconfig.json
@@ -32,7 +32,8 @@
"@component/*": ["./src/components/*"],
"@commonStyle/*": ["./src/commons/styles/*"],
"@type/*": ["./src/types/*"],
- "@page/*": ["./src/pages/*"]
+ "@page/*": ["./src/pages/*"],
+ "@assets/*": ["./src/assets/*"],
}
},
"include": ["src", "test"],