From 728baf2bac7ae60fea143407edec3e9754712f9a Mon Sep 17 00:00:00 2001 From: Aniza Date: Sun, 21 Sep 2025 08:12:10 +0300 Subject: [PATCH] completed task15 --- src/App.jsx | 59 +++++++++++++++-- src/components/Navbar.jsx | 11 ++-- src/components/auth/ProtectedRoute.jsx | 11 +++- src/pages/Login.jsx | 78 ++++++++++++++++++++-- src/pages/Register.jsx | 83 ++++++++++++++++++++--- src/store/slices/authSlice.js | 91 ++++++++++++++++++++++++-- 6 files changed, 299 insertions(+), 34 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 129a9e7..541be20 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,9 @@ import { Routes, Route } from "react-router-dom"; +import { useEffect } from "react"; +import ProtectedRoute from "./components/auth/ProtectedRoute"; +import { useDispatch } from "react-redux"; +import { checkAuthStatus } from "./store/slices/authSlice"; +// pages import Login from "./pages/Login"; import Register from "./pages/Register"; import CreateNote from "./pages/CreateNote"; @@ -6,18 +11,58 @@ import ViewNotes from "./pages/ViewNotes"; import Navbar from "./components/Navbar"; const App = () => { + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(checkAuthStatus()); + }, [dispatch]); + return (
- - - } /> - } /> + {/* + } /> + } /> + + } /> + } /> + */} + + + + + } + /> + + + + } + /> - } /> - } /> - + + + + } + /> + + + + } + /> +
); diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 9bd2bc8..904c4b6 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -34,7 +34,8 @@ const Navbar = () => {
- + {isAuthenticated ? ( + <> { Logout - - + + ) : ( + <> { Register - + + )}
diff --git a/src/components/auth/ProtectedRoute.jsx b/src/components/auth/ProtectedRoute.jsx index 0c71b6c..d13248a 100644 --- a/src/components/auth/ProtectedRoute.jsx +++ b/src/components/auth/ProtectedRoute.jsx @@ -2,7 +2,8 @@ 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); + const loading = status === "loading"; // Show loading state while checking authentication if (loading) { @@ -17,10 +18,16 @@ const ProtectedRoute = ({ children, requireAuth }) => { } // TODO: If route requires authentication and user is not authenticated, redirect to login - + + if (requireAuth && !isAuthenticated) { + return ; + } //TODO: If route requires unauthenticated user and user is authenticated, redirect to notes + if (!requireAuth && isAuthenticated) { + return ; + } // Otherwise, render the children return children; diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx index 4f6d4f7..ba4e0fc 100644 --- a/src/pages/Login.jsx +++ b/src/pages/Login.jsx @@ -1,4 +1,48 @@ +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useDispatch, useSelector } from "react-redux"; +import { useEffect } from "react"; +import { Link } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; +import { login, clearError } from "../store/slices/authSlice"; +import { loginSchema } from "../schema/authSchema"; + const Login = () => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + + const { isAuthenticated, loading, error } = useSelector( + (state) => state.auth + ); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(loginSchema), + defaultValues: { + email: "", + password: "", + }, + }); + + const onSubmit = (data) => { + dispatch(login(data)); + }; + + useEffect(() => { + if (isAuthenticated) { + navigate("/notes"); + } + }, [isAuthenticated, navigate]); + + useEffect(() => { + return () => { + dispatch(clearError()); + }; + }, [dispatch]); + return (
@@ -6,7 +50,7 @@ const Login = () => { Login to Your Account -
+
@@ -32,24 +84,36 @@ const Login = () => { + {errors.password && ( +

+ {errors.password.message} +

+ )}
- + {error &&

{error}

}

Don't have an account?{" "} - + Register here - +

diff --git a/src/pages/Register.jsx b/src/pages/Register.jsx index ba6f0bd..29eb58a 100644 --- a/src/pages/Register.jsx +++ b/src/pages/Register.jsx @@ -1,4 +1,46 @@ +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useDispatch, useSelector } from "react-redux"; +import { useNavigate, Link } from "react-router-dom"; +import { useEffect } from "react"; +// import { Link } from "react-router-dom"; +import { registerSchema } from "../schema/authSchema"; +import { + register as registerThunk, + clearError, +} from "../store/slices/authSlice"; + const Register = () => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + + const { isAuthenticated, loading, error } = useSelector( + (state) => state.auth + ); + + const { + register, + handleSubmit, + formState: { errors }, + reset, + } = useForm({ + resolver: zodResolver(registerSchema), + }); + + const onSubmit = async (data) => { + const resultAction = await dispatch(registerThunk(data)); + if (registerThunk.fulfilled.match(resultAction)) { + reset(); + navigate("/notes"); + } + }; + + useEffect(() => { + return () => { + dispatch(clearError()); + }; + }, [dispatch]); + return (
@@ -6,7 +48,7 @@ const Register = () => { Create an Account -
+
@@ -32,9 +82,17 @@ const Register = () => { + {errors.password && ( +

+ {errors.password.message} +

+ )}
@@ -47,24 +105,33 @@ const Register = () => { + {errors.confirmPassword && ( +

+ {errors.confirmPassword.message} +

+ )}
- + {error &&

{error}

}

Already have an account?{" "} - + Login here - +

diff --git a/src/store/slices/authSlice.js b/src/store/slices/authSlice.js index 35778d0..72e9a3f 100644 --- a/src/store/slices/authSlice.js +++ b/src/store/slices/authSlice.js @@ -13,6 +13,12 @@ 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?.error || error.message); + } } ); @@ -24,6 +30,15 @@ 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) { + if (error.response?.data?.error) { + return rejectWithValue(error.response?.data?.error || error.message); + } + throw error; + } } ); @@ -35,6 +50,12 @@ export const register = createAsyncThunk( // 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); + return response.data.user; + } catch (error) { + return rejectWithValue(error.response?.data?.error || error.message); + } } ); @@ -45,13 +66,19 @@ export const logout = createAsyncThunk( // TODO: Implement logout functionality // 1. Make a POST request to /auth/logout // 2. Handle errors appropriately + try { + await axios.post(`${BASE_URL}/auth/logout`); + } catch (error) { + return rejectWithValue(error.response?.data?.error || error.message); + } } ); const initialState = { user: null, isAuthenticated: false, - loading: true, + // loading: true, + status: "idle", error: null, }; @@ -64,11 +91,63 @@ 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(register.pending, (state) => { + state.status = "loading"; + state.error = null; + }) + .addCase(register.fulfilled, (state, action) => { + state.status = "succeeded"; + state.isAuthenticated = true; + state.user = action.payload; + state.error = null; + }) + .addCase(register.rejected, (state, action) => { + state.status = "failed"; + state.error = action.error.message; + }) + // TODO: Add cases for logout + .addCase(logout.pending, (state) => { + state.status = "loading"; + state.error = null; + }) + .addCase(logout.fulfilled, (state) => { + state.status = "idle"; + state.isAuthenticated = false; + state.user = null; + state.error = null; + }); }, });