From 97ce631f24a9f63cb6c6a00119847adc1c0a9777 Mon Sep 17 00:00:00 2001 From: farahgarow Date: Sat, 31 May 2025 13:32:50 +0300 Subject: [PATCH] Finished Assignment Week15 --- package-lock.json | 60 ++++++++----- package.json | 2 +- src/App.jsx | 26 ------ src/components/Navbar.jsx | 44 ++++----- src/components/auth/ProtectedRoute.jsx | 10 ++- src/main.jsx | 6 +- src/main.tsx | 2 +- src/pages/Login.jsx | 53 ++++++++++- src/pages/Register.jsx | 81 ++++++++++++++++- src/schema/authSchema.js | 11 +-- src/store/App.jsx | 44 +++++++++ src/store/slices/authSlice.js | 118 +++++++++++++++++++++---- 12 files changed, 356 insertions(+), 101 deletions(-) delete mode 100644 src/App.jsx create mode 100644 src/store/App.jsx diff --git a/package-lock.json b/package-lock.json index b7dad75..b4b82a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "react-dom": "^18.3.1", "react-hook-form": "^7.50.1", "react-redux": "^9.2.0", - "react-router-dom": "^6.22.1", + "react-router-dom": "^7.6.1", "zod": "^3.22.4" }, "devDependencies": { @@ -1108,15 +1108,6 @@ } } }, - "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.40.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", @@ -2142,6 +2133,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3866,35 +3866,41 @@ } }, "node_modules/react-router": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", - "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.1.tgz", + "integrity": "sha512-hPJXXxHJZEsPFNVbtATH7+MMX43UDeOauz+EAU4cgqTn7ojdI9qQORqS8Z0qmDlL1TclO/6jLRYUEtbWidtdHQ==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0" + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" }, "peerDependencies": { - "react": ">=16.8" + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } } }, "node_modules/react-router-dom": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz", - "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==", + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.1.tgz", + "integrity": "sha512-vxU7ei//UfPYQ3iZvHuO1D/5fX3/JOqhNTbRR+WjSBWxf9bIvpWK+ftjmdfJHzPOuMQKe2fiEdG+dZX6E8uUpA==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.0" + "react-router": "7.6.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" }, "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" + "react": ">=18", + "react-dom": ">=18" } }, "node_modules/read-cache": { @@ -4066,6 +4072,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 79c59bb..538d498 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "react-dom": "^18.3.1", "react-hook-form": "^7.50.1", "react-redux": "^9.2.0", - "react-router-dom": "^6.22.1", + "react-router-dom": "^7.6.1", "zod": "^3.22.4" }, "devDependencies": { diff --git a/src/App.jsx b/src/App.jsx deleted file mode 100644 index 129a9e7..0000000 --- a/src/App.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Routes, Route } from "react-router-dom"; -import Login from "./pages/Login"; -import Register from "./pages/Register"; -import CreateNote from "./pages/CreateNote"; -import ViewNotes from "./pages/ViewNotes"; -import Navbar from "./components/Navbar"; - -const App = () => { - return ( -
- -
- - - } /> - } /> - - } /> - } /> - -
-
- ); -}; - -export default App; diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 9bd2bc8..c412a85 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -25,6 +25,8 @@ const Navbar = () => {
diff --git a/src/components/auth/ProtectedRoute.jsx b/src/components/auth/ProtectedRoute.jsx index 0c71b6c..daa7fc3 100644 --- a/src/components/auth/ProtectedRoute.jsx +++ b/src/components/auth/ProtectedRoute.jsx @@ -2,10 +2,10 @@ import { Navigate, useLocation } from "react-router-dom"; import { useSelector } from "react-redux"; const ProtectedRoute = ({ children, requireAuth }) => { - const { isAuthenticated, loading } = useSelector((state) => state.auth); + const { isAuthenticated, status} = useSelector((state) => state.auth); // Show loading state while checking authentication - if (loading) { + if (status === "loading") { return (
@@ -17,9 +17,15 @@ const ProtectedRoute = ({ children, requireAuth }) => { } // TODO: If route requires authentication and user is not authenticated, redirect to login + if (requireAuth && !isAuthenticated){ + + } //TODO: If route requires unauthenticated user and user is authenticated, redirect to notes + if (!requireAuth && isAuthenticated){ + + } // Otherwise, render the children diff --git a/src/main.jsx b/src/main.jsx index 3d23c77..3ca2b7b 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -3,15 +3,17 @@ import { createRoot } from "react-dom/client"; import { Provider } from "react-redux"; import { BrowserRouter } from "react-router-dom"; import store from "./store"; -import App from "./App.jsx"; +import App from "./store/App.jsx"; import "./index.css"; createRoot(document.getElementById("root")).render( - + ); + + diff --git a/src/main.tsx b/src/main.tsx index ea9e363..302d376 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,6 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; +import App from './store/App.jsx'; import './index.css'; createRoot(document.getElementById('root')!).render( diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx index 4f6d4f7..b73b6a8 100644 --- a/src/pages/Login.jsx +++ b/src/pages/Login.jsx @@ -1,12 +1,47 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useDispatch, useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import {login} from "../store/slices/authSlice"; +import {loginSchema} from "../schema/authSchema"; + const Login = () => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { status, error } = useSelector((state) => state.auth); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(loginSchema), + }); + + const onSubmit = async (data) => { + try { + await dispatch(login(data)).unwrap(); + navigate("/notes"); + } catch (err) { + console.error("Failed to login:", err); + } + }; + + return (

Login to Your Account

+ {error && ( +
+ {error} +
+ )} -
+
@@ -32,16 +73,24 @@ const Login = () => { + {errors.password && ( +

+ {errors.password.message} +

+ )}
diff --git a/src/pages/Register.jsx b/src/pages/Register.jsx index ba6f0bd..c89aa2f 100644 --- a/src/pages/Register.jsx +++ b/src/pages/Register.jsx @@ -1,4 +1,35 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { useDispatch, useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { registerUser } from "../store/slices/authSlice"; +import { registerSchema } from "../schema/authSchema"; + + const Register = () => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { status, error } = useSelector((state) => state.auth); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(registerSchema), + }); + + const onSubmit = async (data) => { + try { + await dispatch(registerUser(data)).unwrap(); + navigate("/login"); + } catch (err) { + console.error("Failed to register:", err); + } + }; + return (
@@ -6,7 +37,33 @@ const Register = () => { Create an Account -
+ {error && ( +
+ {error} +
+ )} + + +
+ + + {errors.name && ( +

+ {errors.name.message} +

+ )} +
@@ -32,9 +94,15 @@ const Register = () => { + {errors.password && ( +

+ {errors.password.message} +

+ )}
@@ -47,16 +115,25 @@ const Register = () => { + {errors.confirmPassword && ( +

+ {errors.confirmPassword.message} +

+ )}
diff --git a/src/schema/authSchema.js b/src/schema/authSchema.js index 9a739d6..2fee15f 100644 --- a/src/schema/authSchema.js +++ b/src/schema/authSchema.js @@ -7,14 +7,15 @@ export const loginSchema = z.object({ export const registerSchema = z .object({ + name: z.string().min(1, "Full name is required"), email: z.string().min(1, "Email is required").email("Invalid email format"), password: z .string() - .min(8, "Password must be at least 8 characters") - .regex( - /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, - "Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character" - ), + .min(8, "Password must be at least 8 characters"), + // .regex( + // /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, + // "Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character" + // ), confirmPassword: z.string().min(1, "Please confirm your password"), }) .refine((data) => data.password === data.confirmPassword, { diff --git a/src/store/App.jsx b/src/store/App.jsx new file mode 100644 index 0000000..843102d --- /dev/null +++ b/src/store/App.jsx @@ -0,0 +1,44 @@ +import { Routes, Route } from "react-router-dom"; +import Login from "../pages/Login"; +import Register from "../pages/Register"; +import CreateNote from "../pages/CreateNote"; +import ViewNotes from "../pages/ViewNotes"; +import Navbar from "../components/Navbar"; +import ProtectedRoute from "../components/auth/ProtectedRoute"; + +const App = () => { + return ( +
+ +
+ + + + + + } /> + + + + } /> + + + + + + } /> + + + + } /> + +
+
+ ); +}; + +export default App; diff --git a/src/store/slices/authSlice.js b/src/store/slices/authSlice.js index 35778d0..a9c48e9 100644 --- a/src/store/slices/authSlice.js +++ b/src/store/slices/authSlice.js @@ -13,9 +13,16 @@ export const checkAuthStatus = createAsyncThunk( // 1. Make a GET request to /auth/check // 2. Return the response data // 3. Handle errors appropriately - } -); - + try { + const response = await axios.get(`${BASE_URL}/auth/check`) + return response.data.user; + } + catch (error) { + return rejectWithValue( + error.response?.data?.message + ); + } + }); // TODO: Implement login thunk export const login = createAsyncThunk( "auth/login", @@ -24,19 +31,36 @@ export const login = createAsyncThunk( // 1. Make a POST request to /auth/login with credentials // 2. Return the response data // 3. Handle errors appropriately - } -); + try { + const response = await axios.post(`${BASE_URL}/auth/login`, credentials) + return response.data.user; + } + catch (error) { + return rejectWithValue( + error.response?.data?.message + ); + } + }); // TODO: Implement register thunk -export const register = createAsyncThunk( - "auth/register", +export const registerUser = createAsyncThunk( + "auth/registerUser", async (userData, { rejectWithValue }) => { // TODO: Implement registration functionality // 1. Make a POST request to /auth/register with userData // 2. Return the response data // 3. Handle errors appropriately - } -); + try { + const response = await axios.post(`${BASE_URL}/auth/register`, userData) + console.log(`${BASE_URL}/auth/register`); + return response.data.user; + } + catch (error) { + return rejectWithValue( + error.response?.data?.message + ); + } + }); // TODO: Implement logout thunk export const logout = createAsyncThunk( @@ -45,14 +69,23 @@ export const logout = createAsyncThunk( // TODO: Implement logout functionality // 1. Make a POST request to /auth/logout // 2. Handle errors appropriately - } -); + try { + const response = await axios.post(`${BASE_URL}/auth/logout`) + } + catch (error) { + return rejectWithValue( + error.response?.data?.message + ); + } + }); + const initialState = { user: null, isAuthenticated: false, loading: true, error: null, + status:"idle", }; const authSlice = createSlice({ @@ -64,13 +97,66 @@ const authSlice = createSlice({ }, }, extraReducers: (builder) => { - builder; - // TODO: Add cases for checkAuthStatus - // TODO: Add cases for login - // TODO: Add cases for register - // TODO: Add cases for logout + builder + // TODO: Add cases for checkAuthStatus + + .addCase(checkAuthStatus.pending, (state) => { + state.status = "loading"; + }) + .addCase(checkAuthStatus.fulfilled, (state, action) => { + state.status = "succeeded"; + state.isAuthenticated = true; + state.user = action.payload; + state.error = null; + }) + .addCase(checkAuthStatus.rejected, (state) => { + state.status = "idle"; + state.isAuthenticated = false; + state.user = null; + }) + + // TODO: Add cases for login + .addCase(login.pending, (state) => { + state.status = "loading"; + state.error = null; + }) + .addCase(login.fulfilled, (state, action) => { + state.status = "succeeded"; + state.isAuthenticated = true; + state.user = action.payload; + state.error = null; + }) + .addCase(login.rejected, (state, action) => { + state.status = "failed"; + state.error = action.payload; + }) + + // TODO: Add cases for register + .addCase(registerUser.pending, (state) => { + state.status = "loading"; + state.error = null; + }) + .addCase(registerUser.fulfilled, (state, action) => { + state.status = "succeeded"; + state.isAuthenticated = true; + state.user = action.payload; + state.error = null; + }) + .addCase(registerUser.rejected, (state, action) => { + state.status = "failed"; + state.error = action.payload; + }) + + // TODO: Add cases for logout + .addCase(logout.fulfilled, (state) => { + state.status = "idle"; + state.isAuthenticated = false; + state.user = null; + state.error = null; + }); }, }); + export const { clearError } = authSlice.actions; export default authSlice.reducer;