diff --git a/package-lock.json b/package-lock.json
index 8232f10..df92756 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,8 @@
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
+ "axios": "^1.6.7",
+ "js-cookie": "^3.0.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^9.1.0",
@@ -5508,6 +5510,29 @@
"node": ">=4"
}
},
+ "node_modules/axios": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
+ "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
+ "dependencies": {
+ "follow-redirects": "^1.15.4",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/axios/node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/axobject-query": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
@@ -12486,6 +12511,14 @@
"jiti": "bin/jiti.js"
}
},
+ "node_modules/js-cookie": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+ "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -15705,6 +15738,11 @@
"node": ">= 0.10"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
diff --git a/package.json b/package.json
index 4c692b4..96aa1aa 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,8 @@
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
+ "axios": "^1.6.7",
+ "js-cookie": "^3.0.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^9.1.0",
diff --git a/src/App.js b/src/App.js
index 9d9431d..5a40662 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,24 +1,34 @@
import { Route, Routes } from 'react-router-dom';
import './App.css';
-import Doctors from './pages/Doctors';
-import Navbar from './components/navbar/Navbar';
import MyAppointments from './pages/MyAppointments';
import AddDoctor from './pages/AddDoctor';
+import Doctors from './pages/Doctors';
import BookAppointment from './pages/BookAppointment';
import DeleteDoctor from './pages/DeleteDoctor';
import DoctorDetails from './pages/DoctorDetails';
+import SplashScreen from './components/splashScreen/SplashScreen';
+import Login from './components/login/login';
+import Register from './components/register/Register';
+import ProtectedRoute from './components/ProtectedRoute';
+import Layout from './components/layout';
function App() {
return (
-
- } />
- } />
- } />
- } />
- } />
- } />
+ } />
+ } />
+ } />
+ }>
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
);
diff --git a/src/assets/logo.png b/src/assets/logo.png
new file mode 100644
index 0000000..7e601b3
Binary files /dev/null and b/src/assets/logo.png differ
diff --git a/src/components/ProtectedRoute.jsx b/src/components/ProtectedRoute.jsx
new file mode 100644
index 0000000..e677017
--- /dev/null
+++ b/src/components/ProtectedRoute.jsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import { Outlet, Navigate } from 'react-router-dom';
+import Cookies from 'js-cookie';
+
+const ProtectedRoute = () => {
+ const token = Cookies.get('jwt_token');
+ return token ? : ;
+};
+
+export default ProtectedRoute;
diff --git a/src/components/layout.js b/src/components/layout.js
new file mode 100644
index 0000000..24e852d
--- /dev/null
+++ b/src/components/layout.js
@@ -0,0 +1,16 @@
+import { Outlet } from 'react-router-dom';
+import Navbar from './navbar/Navbar';
+import layout from './layout.module.css';
+
+const Layout = () => (
+
+);
+
+export default Layout;
diff --git a/src/components/layout.module.css b/src/components/layout.module.css
new file mode 100644
index 0000000..abde53a
--- /dev/null
+++ b/src/components/layout.module.css
@@ -0,0 +1,3 @@
+.container {
+ display: flex;
+}
diff --git a/src/components/login/login.js b/src/components/login/login.js
new file mode 100644
index 0000000..3fc4360
--- /dev/null
+++ b/src/components/login/login.js
@@ -0,0 +1,68 @@
+import React, { useRef } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { NavLink, useNavigate } from 'react-router-dom';
+import { loginAsync } from '../../redux/user/userSlice';
+import login from './login.module.css';
+
+const Login = () => {
+ const { loginError, success } = useSelector((store) => store.user);
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const formRef = useRef();
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ const formData = new FormData(formRef.current);
+ const email = formData.get('email');
+ const password = formData.get('password');
+
+ const data = {
+ user: { email, password },
+ };
+
+ try {
+ await dispatch(loginAsync(data));
+ navigate('/doctors');
+ e.target.reset();
+ } catch (error) {
+ throw new Error(error);
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default Login;
diff --git a/src/components/login/login.module.css b/src/components/login/login.module.css
new file mode 100644
index 0000000..962686f
--- /dev/null
+++ b/src/components/login/login.module.css
@@ -0,0 +1,7 @@
+.container {
+ width: 100%;
+ height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
diff --git a/src/components/navbar/Navbar.jsx b/src/components/navbar/Navbar.jsx
index 068f3e4..af365dd 100644
--- a/src/components/navbar/Navbar.jsx
+++ b/src/components/navbar/Navbar.jsx
@@ -1,21 +1,58 @@
-import { NavLink } from 'react-router-dom';
-import './navbar.css';
+import { NavLink, useLocation, useNavigate } from 'react-router-dom';
+import { useDispatch, useSelector } from 'react-redux';
+import Cookies from 'js-cookie';
+import { logout } from '../../redux/user/userSlice';
+import logo from '../../assets/logo.png';
+import navbar from './navbar.module.css';
-const Navbar = () => (
-
-);
+const Navbar = () => {
+ const { userData } = useSelector((store) => store.user);
+ const location = useLocation();
+
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const logoutUser = () => {
+ dispatch(logout());
+ Cookies.remove('jwt_token');
+ Cookies.remove('user_info');
+ navigate('/');
+ };
+ return (
+
+ );
+};
export default Navbar;
diff --git a/src/components/navbar/navbar.css b/src/components/navbar/navbar.css
deleted file mode 100644
index 9787c13..0000000
--- a/src/components/navbar/navbar.css
+++ /dev/null
@@ -1,36 +0,0 @@
-.nav-container {
- display: flex;
- flex-direction: column;
- width: 20%;
- height: 100vh;
- padding: 0 0 0 12px;
- border-right: 1px solid var(--text-color-secondary);
-}
-
-.profile {
- margin-bottom: 120px;
-}
-
-.nav-links {
- position: relative;
- display: flex;
- margin-left: 12px;
- flex-direction: column;
-}
-
-.nav-links > a {
- text-decoration: none;
- color: #000;
- padding: 12px 0;
- text-transform: uppercase;
- font-weight: 700;
-}
-
-.nav-links a.active {
- background-color: var(--bg-button-color) !important;
- color: #fff !important;
-}
-
-.logout {
- margin-top: auto;
-}
diff --git a/src/components/navbar/navbar.module.css b/src/components/navbar/navbar.module.css
new file mode 100644
index 0000000..3a2e3b8
--- /dev/null
+++ b/src/components/navbar/navbar.module.css
@@ -0,0 +1,63 @@
+.nav_container {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ border-right: 1px solid var(--text-color-secondary);
+}
+
+.logo {
+ width: 140px;
+ margin-bottom: 20px;
+ padding: 0 20px;
+}
+
+.profile {
+ margin: 30px 0 40px 0;
+ padding: 0 20px;
+}
+
+.profile p {
+ font-weight: bold;
+}
+
+.nav_links {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+}
+
+.nav_links > a,
+.logout {
+ font-size: 14px;
+ text-decoration: none;
+ color: #000;
+ padding: 15px 20px;
+ border: none;
+ border-bottom: 1px solid var(--text-color-secondary);
+ text-transform: uppercase;
+ font-weight: 700;
+}
+
+.active {
+ background-color: var(--bg-button-color) !important;
+ color: #fff !important;
+}
+
+.logout {
+ background: none;
+ text-align: left;
+ margin-bottom: 30px;
+}
+
+.social_links {
+ list-style-type: none;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-around;
+ padding-left: 0;
+}
+
+.copyright {
+ color: grey;
+ padding: 0 20px;
+}
diff --git a/src/components/register/Register.js b/src/components/register/Register.js
new file mode 100644
index 0000000..9008cd1
--- /dev/null
+++ b/src/components/register/Register.js
@@ -0,0 +1,106 @@
+import React, { useRef, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { NavLink, useNavigate } from 'react-router-dom';
+import { signUpAsync } from '../../redux/user/userSlice';
+import register from './register.module.css';
+
+const Register = () => {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const MINIMUM_PASSWORD_LENGTH = 6;
+
+ const { signError, success } = useSelector((store) => store.user);
+ const [errors, setErrors] = useState('');
+ const [mismatch, setMismatch] = useState('');
+ const formRef = useRef();
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+
+ const formData = new FormData(formRef.current);
+
+ const name = formData.get('name');
+ const email = formData.get('email');
+ const password = formData.get('password');
+ const passwordConfirmation = formData.get('password_confirmation');
+
+ if (password.length < MINIMUM_PASSWORD_LENGTH) {
+ setErrors(`Password must be at least ${MINIMUM_PASSWORD_LENGTH} characters!`);
+ return;
+ }
+
+ if (password !== passwordConfirmation) {
+ setMismatch('Passwords do not match');
+ return;
+ }
+
+ setErrors('');
+ setMismatch('');
+
+ const data = {
+ user: {
+ name,
+ email,
+ password,
+ password_confirmation: passwordConfirmation,
+ },
+ };
+
+ dispatch(signUpAsync(data)).then((result) => {
+ if (result && result.error) return;
+ navigate('/login');
+ });
+ };
+
+ return (
+
+ {signError && {signError}}
+ {errors && {errors}}
+
+
+ );
+};
+
+export default Register;
diff --git a/src/components/register/register.module.css b/src/components/register/register.module.css
new file mode 100644
index 0000000..962686f
--- /dev/null
+++ b/src/components/register/register.module.css
@@ -0,0 +1,7 @@
+.container {
+ width: 100%;
+ height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
diff --git a/src/components/splashScreen/Splash.module.css b/src/components/splashScreen/Splash.module.css
new file mode 100644
index 0000000..962686f
--- /dev/null
+++ b/src/components/splashScreen/Splash.module.css
@@ -0,0 +1,7 @@
+.container {
+ width: 100%;
+ height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
diff --git a/src/components/splashScreen/SplashScreen.jsx b/src/components/splashScreen/SplashScreen.jsx
new file mode 100644
index 0000000..9612adf
--- /dev/null
+++ b/src/components/splashScreen/SplashScreen.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { NavLink } from 'react-router-dom';
+import styles from './Splash.module.css';
+
+const SplashScreen = () => (
+
+
+
Healthcare App
+
+
+
+
+
+
+
+
+
+
+);
+
+export default SplashScreen;
diff --git a/src/index.js b/src/index.js
index 556b8d2..16d286e 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,13 +2,17 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import { BrowserRouter } from 'react-router-dom';
+import { Provider } from 'react-redux';
+import store from './redux/store';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
-
+
+
+
,
);
diff --git a/src/redux/store.js b/src/redux/store.js
new file mode 100644
index 0000000..21e35d3
--- /dev/null
+++ b/src/redux/store.js
@@ -0,0 +1,10 @@
+import { configureStore } from '@reduxjs/toolkit';
+import userReducer from './user/userSlice';
+
+const store = configureStore({
+ reducer: {
+ user: userReducer,
+ },
+});
+
+export default store;
diff --git a/src/redux/user/userSlice.js b/src/redux/user/userSlice.js
new file mode 100644
index 0000000..109d1da
--- /dev/null
+++ b/src/redux/user/userSlice.js
@@ -0,0 +1,93 @@
+import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
+import axios from 'axios';
+import Cookies from 'js-cookie';
+
+const initialState = {
+ userData: null,
+ isAuthenticated: false,
+ success: false,
+ signError: null,
+ loginError: null,
+};
+
+const url = 'http://localhost:3001';
+
+export const signUpAsync = createAsyncThunk(
+ 'signup/Async',
+ async (FormData) => {
+ try {
+ const res = await axios.post(`${url}/signup`, FormData, {
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ },
+ });
+ const { token } = res.data;
+ const expirationTimeInMinutes = 10;
+ Cookies.set('jwt_token', token, { expires: expirationTimeInMinutes });
+ return res.data;
+ } catch (error) {
+ if (error.response && error.response.data && error.response.data.error) {
+ throw new Error(error.response.data.error);
+ }
+ throw new Error('Unknow action error!');
+ }
+ },
+);
+
+export const loginAsync = createAsyncThunk(
+ 'login/Async',
+ async (formData) => {
+ try {
+ const res = await axios.post(`${url}/login`, formData, {
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ },
+ });
+ const { token } = res.data;
+ const userInfo = res.data.user;
+ const expirationTimeInMinutes = 5;
+ Cookies.set('jwt_token', token, { expires: expirationTimeInMinutes / (24 * 60) });
+ Cookies.set('user_info', JSON.stringify(userInfo), { expires: expirationTimeInMinutes / (24 * 60) });
+ return res.data;
+ } catch (error) {
+ if (error.response && error.response.data && error.response.data.error) {
+ throw new Error(error.response.data.error);
+ }
+ throw new Error('Unknown action error!');
+ }
+ },
+);
+
+const userSlice = createSlice({
+ name: 'auth',
+ initialState,
+ reducers: {
+ logout: (state) => ({
+ ...state,
+ userData: null,
+ isAuthenticated: false,
+ }),
+ },
+ extraReducers(builder) {
+ builder.addCase(signUpAsync.fulfilled, (state, action) => ({
+ ...state,
+ userData: action.payload,
+ })).addCase(signUpAsync.rejected, (state, action) => ({
+ ...state,
+ isAuthenticated: false,
+ signError: action.error.message,
+ })).addCase(loginAsync.fulfilled, (state, action) => ({
+ ...state,
+ userData: action.payload,
+ isAuthenticated: true,
+ })).addCase(loginAsync.rejected, (state, action) => ({
+ ...state,
+ isAuthenticated: false,
+ loginError: action.error.message,
+ }));
+ },
+});
+export const { logout } = userSlice.actions;
+export default userSlice.reducer;