diff --git a/client/src/App.tsx b/client/src/App.tsx index dcb35e75b78dc174e79d6937110815a5c77dc0e0..d2e8cce382665acdc8dfe1b68534959e231b581a 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,22 +1,25 @@ +import { Outlet } from 'react-router-dom'; +import AuthProvider from './providers/AuthProvider'; import Header from './components/Header/Header'; import Footer from './components/Footer/Footer'; -import { Outlet } from 'react-router-dom'; import './App.scss'; function App() { return ( <> - <div className='wrapper'> - <Header></Header> - <div className='main'> - <Outlet></Outlet> - </div> - <div className='footer-wrapper'> - <Footer></Footer> + <AuthProvider> + <div className='wrapper'> + <Header></Header> + <div className='main'> + <Outlet></Outlet> + </div> + <div className='footer-wrapper'> + <Footer></Footer> + </div> </div> - </div> + </AuthProvider> </> ); } -export default App; \ No newline at end of file +export default App; diff --git a/client/src/components/CustomerDashboard/CustomerDashboard.tsx b/client/src/components/CustomerDashboard/CustomerDashboard.tsx index c89e523df8c3def1d5f8e90f5ddbe32c982b7149..c6a58d2ba59969521c599ff844725d869a5c753c 100644 --- a/client/src/components/CustomerDashboard/CustomerDashboard.tsx +++ b/client/src/components/CustomerDashboard/CustomerDashboard.tsx @@ -97,7 +97,7 @@ function CustomerDashboard() { </div> <div className='flights'> - <div> + <div className='flex-row'> <span className='flights-title'>Upcoming Flights</span> <button type='submit' className='view_more_button'>View more</button> </div> @@ -111,7 +111,7 @@ function CustomerDashboard() { </div> <div className='flights'> - <div> + <div className='flex-row'> <span className='flights-title'>Flights History</span> <button type='submit' className='view_more_button'>View more</button> </div> diff --git a/client/src/components/Header/Header.tsx b/client/src/components/Header/Header.tsx index 14ad345398acf5c53f8014be51a7dfd5985ff3aa..cd7ac92ff6dbd3560c4eca45c8a404be2f385178 100644 --- a/client/src/components/Header/Header.tsx +++ b/client/src/components/Header/Header.tsx @@ -1,7 +1,10 @@ import { Link } from 'react-router-dom'; +import { useAuth } from '../../hooks/useAuth'; import './Header.scss'; function Header() { + const { isAuth } = useAuth(); + return ( <> <div className='header'> @@ -10,7 +13,16 @@ function Header() { <nav className='nav'> <Link to={'/'} className='nav-item'>Home</Link> - <Link to={'booking/query'} className='nav-item'>Book a Flight</Link> + + {isAuth ? + <div> + <Link to={'booking/query'} className='nav-item'>Book a Flight</Link> + <Link to={'logout'} className='nav-item'>Logout</Link> + </div> : + <div> + <Link to={'login'} className='nav-item'>Login</Link> + <Link to={'register'} className='nav-item'>Register</Link> + </div>} </nav> </div> </div> diff --git a/client/src/components/Login/Login.tsx b/client/src/components/Login/Login.tsx index 3c3ecf232ce68c4ff92b11173b38d45b9b9b36af..ab0c224276d94fcc1cf647a418c7f1d74152050d 100644 --- a/client/src/components/Login/Login.tsx +++ b/client/src/components/Login/Login.tsx @@ -1,5 +1,7 @@ -import { useForm } from 'react-hook-form'; import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useForm } from 'react-hook-form'; +import { useAuth } from '../../hooks/useAuth'; import './Login.scss'; interface ILogin { @@ -9,11 +11,15 @@ interface ILogin { export function Login() { const [error, setError] = useState(''); + const { giveAuth } = useAuth(); + const navigate = useNavigate(); const { register, handleSubmit } = useForm<ILogin>({mode: 'onChange'}); const onSubmit = (formValue: ILogin) => { setError('TODO: remove me once actual errors are implemented'); console.log('ready to make login api call', formValue); + giveAuth('testToken'); + navigate('/'); }; return ( diff --git a/client/src/components/Logout/Logout.scss b/client/src/components/Logout/Logout.scss new file mode 100644 index 0000000000000000000000000000000000000000..bca2b7caf89c564678178a42befcf91055678704 --- /dev/null +++ b/client/src/components/Logout/Logout.scss @@ -0,0 +1,30 @@ +.logout { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} + +.logout-card { + width: 20vw; + min-width: 350px; +} + +.full-main { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} + +.logout-content { + display: flex; + flex-direction: column; + gap: 2rem; + align-items: center; + justify-content: center; + font-size: 2rem; + text-align: center; +} diff --git a/client/src/components/Logout/Logout.tsx b/client/src/components/Logout/Logout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..34cb705dd4650dd4d09e16434ca697cfc02f9cb9 --- /dev/null +++ b/client/src/components/Logout/Logout.tsx @@ -0,0 +1,40 @@ +import { useEffect, useState } from 'react'; +import { useAuth } from '../../hooks/useAuth'; +import Spinner from '../Spinner/Spinner'; +import './Logout.scss'; + +function Logout() { + const [loading, setLoading] = useState(true); + const { isAuth, removeAuth } = useAuth(); + + useEffect(() => { + // TODO: do logout api call + if (isAuth) { + removeAuth(); + setLoading(false); + } else { + setLoading(false); + } + }, []); + + return ( + <> + { + loading ? + <div className='full-main'> + <Spinner></Spinner> + </div> : + <div className='logout'> + <div className='card logout-card'> + <div className='logout-content'> + <span>Sucessfully logged out!</span> + <span>Use the Header to navigate!</span> + </div> + </div> + </div> + } + </> + ); +} + +export default Logout; diff --git a/client/src/components/ProtectedRoute/ProtectedRoute.tsx b/client/src/components/ProtectedRoute/ProtectedRoute.tsx new file mode 100644 index 0000000000000000000000000000000000000000..95b1b4b7b708e29e89c5ced6f310fd4b243ae162 --- /dev/null +++ b/client/src/components/ProtectedRoute/ProtectedRoute.tsx @@ -0,0 +1,14 @@ +import { Navigate, Outlet } from 'react-router-dom'; +import { useAuth } from '../../hooks/useAuth'; + +function ProtectedRoute() { + const { isAuth } = useAuth(); + + if (isAuth) { + return <Outlet></Outlet> + } + + return <Navigate to={'login'}></Navigate> +} + +export default ProtectedRoute; diff --git a/client/src/components/Spinner/Spinner.scss b/client/src/components/Spinner/Spinner.scss new file mode 100644 index 0000000000000000000000000000000000000000..35d68e36cb0c7bd71b7fb6e0cd2e207d6428faa3 --- /dev/null +++ b/client/src/components/Spinner/Spinner.scss @@ -0,0 +1,13 @@ +.spinner { + border: 16px solid #f3f3f3; + border-radius: 50%; + border-top: 16px solid #3498db; + width: 30px; + height: 30px; + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/client/src/components/Spinner/Spinner.tsx b/client/src/components/Spinner/Spinner.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c6b609f452d23d570225c81b8c813b8eadd7455b --- /dev/null +++ b/client/src/components/Spinner/Spinner.tsx @@ -0,0 +1,11 @@ +import './Spinner.scss'; + +function Spinner() { + return ( + <> + <div className='spinner'></div> + </> + ); +} + +export default Spinner; diff --git a/client/src/contexts/AuthContext.ts b/client/src/contexts/AuthContext.ts new file mode 100644 index 0000000000000000000000000000000000000000..16e7a00bec82321a64a569273713d39ecfde3683 --- /dev/null +++ b/client/src/contexts/AuthContext.ts @@ -0,0 +1,13 @@ +import { createContext } from 'react'; + +interface IAuthContext { + isAuth: boolean; + giveAuth: (token: string) => void; + removeAuth: () => void; +} + +export const AuthContext = createContext<IAuthContext>({ + isAuth: false, + giveAuth: () => console.error('no give auth function'), + removeAuth: () => console.error('no remove auth function') +}); diff --git a/client/src/helpers/SearchParams.ts b/client/src/helpers/SearchParams.ts index 8e3cc93c243f449c80fda365a5171cf850507daf..260ab89293a4e3e4a89764272400224905e4f597 100644 --- a/client/src/helpers/SearchParams.ts +++ b/client/src/helpers/SearchParams.ts @@ -1,4 +1,4 @@ export function getSearchParam(requestURL: string, param: string): string { const url = new URL(requestURL); return url.searchParams.get(param) ?? ''; -} \ No newline at end of file +} diff --git a/client/src/hooks/useAuth.ts b/client/src/hooks/useAuth.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a098ba96e07fb577fd54147881c6b24c6ddf02d --- /dev/null +++ b/client/src/hooks/useAuth.ts @@ -0,0 +1,6 @@ +import { useContext } from 'react'; +import { AuthContext } from '../contexts/AuthContext'; + +export const useAuth = () => { + return useContext(AuthContext); +}; diff --git a/client/src/index.scss b/client/src/index.scss index 384cc903defb9f26f3d36ee47d032e9ce79a9861..0f7974e7a6d96776691e3a1b1304d819323fba6d 100644 --- a/client/src/index.scss +++ b/client/src/index.scss @@ -82,4 +82,18 @@ body { align-items: center; justify-content: center; gap: 1rem; -} \ No newline at end of file +} + +.flex-row { + display: flex; + flex-direction: row; + align-items: center; +} + +.full { + display: flex; + align-items: center; + justify-content: center; + width: 100vw; + height: 100vh; +} diff --git a/client/src/main.tsx b/client/src/main.tsx index 758311d76d271ce84f1815ac7795c9d09e6a44fa..64d8bf3af88b7ddf69928fa91eecc49067778b7b 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -4,12 +4,14 @@ import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import App from './App.tsx'; import Login from './components/Login/Login.tsx'; import Register from './components/Register/Register.tsx'; +import Logout from './components/Logout/Logout.tsx'; +import ProtectedRoute from './components/ProtectedRoute/ProtectedRoute.tsx'; import CustomerDashboard from './components/CustomerDashboard/CustomerDashboard.tsx'; import BookingQuery from './components/BookingQuery/BookingQuery.tsx'; import BookingList from './components/BookingList/BookingList.tsx'; import { GetCustomerDashboardData } from './services/CustomerDashboard/CustomerDashboard.ts'; -import './index.scss'; import { GetBookingList } from './services/BookingList/BookingList.ts'; +import './index.scss'; const router = createBrowserRouter([ { @@ -25,18 +27,27 @@ const router = createBrowserRouter([ element: <Register></Register> }, { - path: 'customer-dashboard', - loader: GetCustomerDashboardData, - element: <CustomerDashboard></CustomerDashboard> - }, - { - path: 'booking/query', - element: <BookingQuery></BookingQuery> + path: 'logout', + element: <Logout></Logout> }, { - path: 'booking/list', - loader: GetBookingList, - element: <BookingList></BookingList> + element: <ProtectedRoute></ProtectedRoute>, + children: [ + { + path: 'customer-dashboard', + loader: GetCustomerDashboardData, + element: <CustomerDashboard></CustomerDashboard> + }, + { + path: 'booking/query', + element: <BookingQuery></BookingQuery> + }, + { + path: 'booking/list', + loader: GetBookingList, + element: <BookingList></BookingList> + } + ] } ] } diff --git a/client/src/providers/AuthProvider.tsx b/client/src/providers/AuthProvider.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ee85b039739ff1ad755adb7829591331d34b0d2f --- /dev/null +++ b/client/src/providers/AuthProvider.tsx @@ -0,0 +1,42 @@ +import { ReactNode, useEffect, useState } from 'react'; +import { AuthContext } from '../contexts/AuthContext'; +import Spinner from '../components/Spinner/Spinner'; + +function AuthProvider({ children }: { children: ReactNode }) { + const [loading, setLoading] = useState(true); + const [token, setToken] = useState(localStorage.getItem('token') ?? ''); + + useEffect(() => { + if (token) { + // validate token + } + + setTimeout(() => setLoading(false), 500); // Fake api timer + }, []); + + const giveAuth = (token: string) => { + setToken(token); + localStorage.setItem('token', token); + }; + + const removeAuth = () => { + setToken(''); + localStorage.removeItem('token'); + }; + + if (loading) { + return ( + <div className='full'> + <Spinner></Spinner> + </div> + ); + } + + return ( + <AuthContext.Provider value={{ isAuth: !!token, giveAuth, removeAuth }}> + {!loading && children} + </AuthContext.Provider> + ); +} + +export default AuthProvider;