diff --git a/client/src/AuthenticatedApp.tsx b/client/src/AuthenticatedApp.tsx
index d375c3f..38b65b9 100644
--- a/client/src/AuthenticatedApp.tsx
+++ b/client/src/AuthenticatedApp.tsx
@@ -6,6 +6,7 @@ import { useAppSelector } from "./app/hooks";
import MainPage from "./pages/MainPage/MainPage";
import Forums from "./pages/Forums/Forums";
import Profiles from "./pages/Profiles/Profiles";
+import Profile from "./pages/Profile/Profile";
import NotFoundPage from "./pages/NotFoundPage/NotFoundPage";
import { useNavigate } from "react-router-dom";
@@ -25,6 +26,7 @@ const AuthenticatedApp = () => {
} />
} />
+ } />
} />
} />
diff --git a/client/src/app/store.ts b/client/src/app/store.ts
index b94adb9..d2a17f0 100644
--- a/client/src/app/store.ts
+++ b/client/src/app/store.ts
@@ -1,9 +1,14 @@
import { configureStore } from "@reduxjs/toolkit";
import userReducer from "../features/user/userSlice";
import profilesReducer from "../features/profiles/profilesSlice";
+import userProfileReducer from "../features/userProfile/userProfileSlice";
export const store = configureStore({
- reducer: { user: userReducer, profiles: profilesReducer },
+ reducer: {
+ user: userReducer,
+ profiles: profilesReducer,
+ userProfile: userProfileReducer,
+ },
});
export type AppDispatch = typeof store.dispatch;
diff --git a/client/src/components/Banner/Banner.test.tsx b/client/src/components/Banner/Banner.test.tsx
index 38a2d58..f2ed51a 100644
--- a/client/src/components/Banner/Banner.test.tsx
+++ b/client/src/components/Banner/Banner.test.tsx
@@ -2,13 +2,13 @@ import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom";
import Banner from "./Banner";
-import { useAppDispatch } from "../../app/hooks";
+import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { logout } from "../../features/user/userSlice";
import { useNavigate } from "react-router-dom";
-// Mock the hooks and external dependencies
jest.mock("../../app/hooks", () => ({
useAppDispatch: jest.fn(),
+ useAppSelector: jest.fn(),
}));
jest.mock("../../features/user/userSlice", () => ({
logout: jest.fn(),
@@ -20,10 +20,19 @@ jest.mock("react-router-dom", () => ({
describe("Banner Component", () => {
const mockDispatch = jest.fn();
const mockNavigate = jest.fn();
+ const mockUserData = {
+ id: "123",
+ name: "John Doe",
+ };
beforeEach(() => {
(useAppDispatch as jest.Mock).mockReturnValue(mockDispatch);
(useNavigate as jest.Mock).mockReturnValue(mockNavigate);
+ (useAppSelector as jest.Mock).mockImplementation((selector) =>
+ selector({
+ user: { userData: mockUserData },
+ })
+ );
});
afterEach(() => {
@@ -42,18 +51,44 @@ describe("Banner Component", () => {
expect(title).toBeInTheDocument();
});
- it("renders the logout button and handles logout on click", () => {
+ it("renders the Options button", () => {
render();
- const logoutButton = screen.getByRole("button", { name: "Logout" });
- expect(logoutButton).toBeInTheDocument();
+ const optionsButton = screen.getByRole("button", { name: "Options" });
+ expect(optionsButton).toBeInTheDocument();
+ });
+
+ it("opens the dropdown and shows options when Options button is clicked", () => {
+ render();
+ const optionsButton = screen.getByRole("button", { name: "Options" });
+ fireEvent.click(optionsButton);
- // Simulate a click on the logout button
- fireEvent.click(logoutButton);
+ const profileOption = screen.getByText("Go to Profile");
+ const logoutOption = screen.getByText("Logout");
- // Expect the logout action to be dispatched
- expect(mockDispatch).toHaveBeenCalledWith(logout());
+ expect(profileOption).toBeInTheDocument();
+ expect(logoutOption).toBeInTheDocument();
+ });
+
+ it("handles navigation to Profile on clicking Go to Profile", () => {
+ render();
+ const optionsButton = screen.getByRole("button", { name: "Options" });
+ fireEvent.click(optionsButton);
- // Expect navigation to have been called with the root path
+ const profileOption = screen.getByText("Go to Profile");
+ fireEvent.click(profileOption);
+
+ expect(mockNavigate).toHaveBeenCalledWith("profile");
+ });
+
+ it("handles logout on clicking Logout", () => {
+ render();
+ const optionsButton = screen.getByRole("button", { name: "Options" });
+ fireEvent.click(optionsButton);
+
+ const logoutOption = screen.getByText("Logout");
+ fireEvent.click(logoutOption);
+
+ expect(mockDispatch).toHaveBeenCalledWith(logout());
expect(mockNavigate).toHaveBeenCalledWith("/");
});
});
diff --git a/client/src/components/Banner/Banner.tsx b/client/src/components/Banner/Banner.tsx
index a39e57e..26da12c 100644
--- a/client/src/components/Banner/Banner.tsx
+++ b/client/src/components/Banner/Banner.tsx
@@ -1,32 +1,59 @@
-import React from "react";
+import React, { useState } from "react";
import logo from "../../assets/hammer.png";
-import { useAppDispatch } from "../../app/hooks";
+import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { logout } from "../../features/user/userSlice";
import { useNavigate } from "react-router-dom";
const Banner = (): JSX.Element => {
+ const user = useAppSelector((state) => state.user.userData);
const dispatch = useAppDispatch();
const navigate = useNavigate();
+ const [showDropdown, setShowDropdown] = useState(false);
const handleLogout = () => {
dispatch(logout());
navigate("/");
+ //TODO CLEAR ALL STATE
+ };
+
+ const goToProfile = () => {
+ navigate("profile");
+ setShowDropdown(false);
};
return (
- {" "}
Code Hammers
-
{" "}
+
+
+
+
+ {showDropdown && (
+
+ )}
-
);
};
diff --git a/client/src/features/userProfile/userProfileSlice.ts b/client/src/features/userProfile/userProfileSlice.ts
new file mode 100644
index 0000000..fdd4f6a
--- /dev/null
+++ b/client/src/features/userProfile/userProfileSlice.ts
@@ -0,0 +1,62 @@
+import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
+import axios from "axios";
+import { IProfile } from "../../../types/profile";
+
+interface ProfileState {
+ profile: IProfile | null; //TODO ADD PROPER TYPING ONCE OBJECT IS FINALIZED
+ status: "idle" | "loading" | "failed";
+ error: string | null;
+}
+
+const initialState: ProfileState = {
+ profile: null,
+ status: "idle",
+ error: null,
+};
+
+//TODO REVIEW TYPING
+export const fetchUserProfile = createAsyncThunk(
+ "profile/fetchUserProfile",
+ async (userID: string, thunkAPI) => {
+ try {
+ const response = await axios.get(`/api/profiles/${userID}`);
+ return response.data;
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ return thunkAPI.rejectWithValue(
+ error.response?.data || "Error fetching profiles"
+ );
+ }
+ return thunkAPI.rejectWithValue("An unexpected error occurred");
+ }
+ }
+);
+
+const userProfileSlice = createSlice({
+ name: "profile",
+ initialState,
+ reducers: {
+ resetProfileState(state) {
+ state.profile = null;
+ state.status = "idle";
+ state.error = null;
+ },
+ },
+ extraReducers: (builder) => {
+ builder
+ .addCase(fetchUserProfile.pending, (state) => {
+ state.status = "loading";
+ })
+ .addCase(fetchUserProfile.fulfilled, (state, action) => {
+ state.profile = action.payload;
+ state.status = "idle";
+ })
+ .addCase(fetchUserProfile.rejected, (state, action) => {
+ state.status = "failed";
+ //TODO state.error = action.payload as string; WHAT WOULD PAYLOAD LOOK LIKE HERE?
+ //TODO HOOK UP GOOD ERROR INFO TO ERROR STATE
+ });
+ },
+});
+
+export default userProfileSlice.reducer;
diff --git a/client/src/pages/Profile/Profile.test.tsx b/client/src/pages/Profile/Profile.test.tsx
new file mode 100644
index 0000000..edd9cd3
--- /dev/null
+++ b/client/src/pages/Profile/Profile.test.tsx
@@ -0,0 +1,54 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import Profile from "./Profile";
+import { useAppSelector, useAppDispatch } from "../../app/hooks";
+import { fetchUserProfile } from "../../features/userProfile/userProfileSlice";
+
+jest.mock("../../app/hooks", () => ({
+ useAppDispatch: jest.fn(),
+ useAppSelector: jest.fn(),
+}));
+
+jest.mock("../../features/userProfile/userProfileSlice", () => ({
+ fetchUserProfile: jest.fn(),
+}));
+
+describe("Profile Component", () => {
+ const mockDispatch = jest.fn();
+ const mockUser = {
+ _id: "12345",
+ name: "John Doe",
+ };
+ //TODO MOCK BETTER USERPROFILE DATA
+ beforeEach(() => {
+ (useAppDispatch as jest.Mock).mockReturnValue(mockDispatch);
+ (useAppSelector as jest.Mock).mockImplementation((selector) =>
+ selector({
+ user: { userData: mockUser },
+ userProfile: { profile: null },
+ })
+ );
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("renders the Profile component", () => {
+ render();
+ const profileTitle = screen.getByText("Profile");
+ expect(profileTitle).toBeInTheDocument();
+ });
+
+ it("dispatches fetchUserProfile on component mount", () => {
+ render();
+ expect(mockDispatch).toHaveBeenCalledWith(fetchUserProfile(mockUser._id));
+ });
+
+ it("displays the user's ID", () => {
+ render();
+ const userIdDisplay = screen.getByText(mockUser._id);
+ expect(userIdDisplay).toBeInTheDocument();
+ });
+});
diff --git a/client/src/pages/Profile/Profile.tsx b/client/src/pages/Profile/Profile.tsx
new file mode 100644
index 0000000..d9a8300
--- /dev/null
+++ b/client/src/pages/Profile/Profile.tsx
@@ -0,0 +1,22 @@
+import React, { useEffect } from "react";
+import { useAppSelector, useAppDispatch } from "../../app/hooks";
+import { fetchUserProfile } from "../../features/userProfile/userProfileSlice";
+
+const Profile = (): JSX.Element => {
+ const dispatch = useAppDispatch();
+ const userProfile = useAppSelector((state) => state.userProfile.profile);
+ const user = useAppSelector((state) => state.user.userData);
+
+ useEffect(() => {
+ dispatch(fetchUserProfile(user._id));
+ }, [dispatch]);
+ return (
+
+
Profile
+ {user._id}
+ {userProfile?.bio}
+
+ );
+};
+
+export default Profile;
diff --git a/client/src/pages/Profiles/__snapshots__/Profiles.test.tsx.snap b/client/src/pages/Profiles/__snapshots__/Profiles.test.tsx.snap
new file mode 100644
index 0000000..afd48d2
--- /dev/null
+++ b/client/src/pages/Profiles/__snapshots__/Profiles.test.tsx.snap
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MainPage Component renders correctly 1`] = `
+[
+
+
+ PROFILES
+
+ ,
+
+
+ User1
+
+
+ User2
+
+
,
+]
+`;