Skip to content

Commit

Permalink
feat: Add OpenClassrooms-Student-Center#3 (Logout) + refactor NavBar …
Browse files Browse the repository at this point in the history
…+ add Redux (store + api thunk + userSlice), implement PrivateRoute
  • Loading branch information
reffinger committed Feb 13, 2024
1 parent 559fabe commit 6d72d50
Show file tree
Hide file tree
Showing 11 changed files with 215 additions and 29 deletions.
99 changes: 95 additions & 4 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
"preview": "vite preview"
},
"dependencies": {
"@reduxjs/toolkit": "^2.1.0",
"proptype": "^1.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^9.1.0",
"react-router-dom": "^6.22.0"
},
"devDependencies": {
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Navbar from './layout/NavBar';
import Home from './pages/Home';
import Login from './pages/Login';
import Profile from './pages/Profile';
import PrivateRoute from './components/PrivateRoute';

const router = createBrowserRouter([
{
Expand All @@ -19,7 +20,7 @@ const router = createBrowserRouter([
children: [
{ path: '/', element: <Home /> },
{ path: '/login', element: <Login /> },
{ path: '/profile', element: <Profile /> },
{ path: '/profile', element: <PrivateRoute component={Profile}/> },
// { path: '/transactions', element: <Transactions /> },
],
},
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/components/PrivateRoute.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useSelector } from 'react-redux';
import { Navigate } from 'react-router-dom';
import PropTypes from 'prop-types';

// PrivateRoute component used to protect routes that require authentication
const PrivateRoute = ({ component: Component }) => {
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);

return isAuthenticated ? <Component /> : <Navigate to='/login' />;
};

PrivateRoute.propTypes = {
component: PropTypes.elementType.isRequired,
};

export default PrivateRoute;
20 changes: 18 additions & 2 deletions frontend/src/layout/NavBar.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { NavLink } from 'react-router-dom';
import argBankLogo from '../assets/img/argentBankLogo.png';
import { useDispatch, useSelector } from 'react-redux';
import { authenticate } from '../service/user/userApi';

const Navbar = () => {
const dispatch = useDispatch();
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);

const handleAuth = () => {
if (isAuthenticated) {
localStorage.removeItem('token'); // Remove the token from local storage
dispatch(authenticate.rejected()); // Dispatch the authSlice action
}
};

return (
<nav className='main-nav'>
<NavLink className='main-nav-logo' to='/'>
Expand All @@ -13,9 +25,13 @@ const Navbar = () => {
<h1 className='sr-only'>Argent Bank</h1>
</NavLink>
<div>
<NavLink className='main-nav-item' to='/login'>
<NavLink
className='main-nav-item'
to={isAuthenticated ? '/' : '/login'}
onClick={handleAuth}
>
<i className='fa fa-user-circle'></i>
Sign In
{isAuthenticated ? 'Sign Out' : 'Sign In'}
</NavLink>
</div>
</nav>
Expand Down
16 changes: 10 additions & 6 deletions frontend/src/main.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import { Provider } from 'react-redux';
import { store } from './service/store.js';

ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
25 changes: 10 additions & 15 deletions frontend/src/pages/Login.jsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { authenticate } from '../service/user/userApi';

const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const dispatch = useDispatch();
const navigate = useNavigate();
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);

const handleSubmit = async (e) => {
e.preventDefault();
dispatch(authenticate({ email, password }));
};

const response = await fetch('http://localhost:3001/api/v1/user/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});

const data = await response.json();

if (response.ok) {
localStorage.setItem('token', data.body.token);
console.log(data.message);
useEffect(() => {
if (isAuthenticated) {
navigate('/profile');
} else {
window.alert(data.message);
}
};
}, [isAuthenticated, navigate]);

return (
<main className='main bg-dark'>
Expand Down
1 change: 0 additions & 1 deletion frontend/src/pages/Profile.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const Profile = () => {
// const { user } = useAuth();
return (
<main className='main bg-dark'>
<div className='header'>
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/service/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { configureStore } from '@reduxjs/toolkit';
import authSlice from '../service/user/userSlice';

// The value of isAuthenticated is determined by
// whether there is a 'token' item in the local storage.
const preloadedState = {
auth: {
isAuthenticated: !!localStorage.getItem('token'),
},
};

export const store = configureStore({
reducer: {
auth: authSlice,
},
preloadedState,
});
24 changes: 24 additions & 0 deletions frontend/src/service/user/userApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createAsyncThunk } from '@reduxjs/toolkit';

// This async thunk dispatches actions to authenticate a user.
export const authenticate = createAsyncThunk(
'user/authenticate',
async ({ email, password }) => {
const response = await fetch('http://localhost:3001/api/v1/user/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});

const data = await response.json();

if (response.ok) {
localStorage.setItem('token', data.body.token);
console.log(data.message);
return data;
} else {
window.alert(data.message);
throw new Error(data.message);
}
}
);
21 changes: 21 additions & 0 deletions frontend/src/service/user/userSlice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createSlice } from '@reduxjs/toolkit';
import { authenticate } from './userApi';

// The authSlice reducer manages the state of the user's authentication status.
const authSlice = createSlice({
name: 'auth',
initialState: { isAuthenticated: false },
reducers: {},
// The authenticate.fulfilled action changes the value of isAuthenticated to true.
extraReducers: (builder) => {
builder
.addCase(authenticate.fulfilled, (state) => {
state.isAuthenticated = true;
})
.addCase(authenticate.rejected, (state) => {
state.isAuthenticated = false;
});
},
});

export default authSlice.reducer;

0 comments on commit 6d72d50

Please sign in to comment.