diff --git a/frontend/src/routes/bookmarks.test.tsx b/frontend/src/routes/bookmarks.test.tsx new file mode 100644 index 0000000..2d0917b --- /dev/null +++ b/frontend/src/routes/bookmarks.test.tsx @@ -0,0 +1,107 @@ +import { + GetBookmarkedQuestionsError, + useGetBookmarkedQuestions, + } from "@/services/api/programmingForumComponents"; + import { QuestionDetails } from "@/services/api/programmingForumSchemas"; + import { testAccessibility } from "@/utils/test-accessibility"; + import { QueryObserverSuccessResult } from "@tanstack/react-query"; + import { render, screen } from "@testing-library/react"; + import { + createMemoryRouter, + MemoryRouter, + Route, + RouterProvider, + Routes, + } from "react-router-dom"; + import { beforeEach, describe, expect, it, vi } from "vitest"; + import { routeConfig } from "."; + import { BookmarkedQuestions } from "./bookmarks"; + + // Mock the useGetBookmarkedQuestions hook + vi.mock("@/services/api/programmingForumComponents", () => ({ + useGetBookmarkedQuestions: vi.fn(), + })); + + const mockQuestions: QuestionDetails[] = [ + { + id: 1, + title: "How to implement a binary tree in Python?", + content: "I'm struggling to understand the structure...", + author: { id: 1, name: "John Doe", username: "user1", profilePicture: "p", reputationPoints: 50}, + createdAt: "2024-12-01T12:00:00Z", + updatedAt: "2024-12-01T12:30:00Z", + tags: [{ id: "1", name: "Python" }], + likeCount: 10, + dislikeCount: 2, + commentCount: 4, + viewCount: 50, + bookmarked: true, + selfVoted: 1, + difficulty: "MEDIUM", + selfDifficultyVote: "MEDIUM", + easyCount: 5, + mediumCount: 10, + hardCount: 3, + }, + { + id: 2, + title: "What are closures in JavaScript?", + content: "Can someone explain closures with an example?", + author: { id: 2, name: "Jane Smith", username: "user2", profilePicture: "p", reputationPoints: 50}, + createdAt: "2024-12-02T10:00:00Z", + updatedAt: "2024-12-02T10:20:00Z", + tags: [{ id: "2", name: "JavaScript" }], + likeCount: 15, + dislikeCount: 1, + commentCount: 5, + viewCount: 70, + bookmarked: true, + selfVoted: 0, + difficulty: "EASY", + selfDifficultyVote: "EASY", + easyCount: 8, + mediumCount: 6, + hardCount: 1, + }, + ]; + + describe("BookmarkedQuestions component", () => { + beforeEach(() => { + vi.mocked(useGetBookmarkedQuestions).mockReset(); + }); + + it("should have no accessibility violations", async () => { + const router = createMemoryRouter(routeConfig, { + initialEntries: ["/bookmarks"], + }); + + await testAccessibility(); + }); + + it("renders bookmarked questions correctly", () => { + vi.mocked(useGetBookmarkedQuestions).mockReturnValue({ + isLoading: false, + error: null, + data: { + data: { items: mockQuestions, totalItems: mockQuestions.length }, + }, + } as QueryObserverSuccessResult); + + render( + + + } /> + + , + ); + + expect( + screen.getByText(`You have ${mockQuestions.length} bookmarked questions.`), + ).toBeInTheDocument(); + + mockQuestions.forEach((question) => { + expect(screen.getByText(question.title)).toBeInTheDocument(); + }); + }); + }); + \ No newline at end of file diff --git a/frontend/src/routes/bookmarks.tsx b/frontend/src/routes/bookmarks.tsx new file mode 100644 index 0000000..9be9287 --- /dev/null +++ b/frontend/src/routes/bookmarks.tsx @@ -0,0 +1,101 @@ +import { useGetBookmarkedQuestions } from "@/services/api/programmingForumComponents"; +import { + QuestionSummary, +} from "@/services/api/programmingForumSchemas"; +import { Loader2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import ErrorAlert from "../components/ErrorAlert"; +import InfiniteScroll from "../components/InfiniteScroll"; +import { QuestionCard } from "../components/QuestionCard"; + +export const BookmarkedQuestions = () => { + const [pageSize, setPageSize] = useState(20); + const [previousData, setPreviousData] = useState<{ + items: QuestionSummary[]; + totalItems: number; + }>({ + items: [], + totalItems: 0, + }); + + const { + data: resultList, + isLoading, + error, + } = useGetBookmarkedQuestions({}); + + useEffect(() => { + if (resultList?.data && !isLoading) { + setPreviousData(resultList.data as typeof previousData); + } + }, [resultList, isLoading]); + + if (error) { + return ; + } + + const resultListData = + (resultList?.data as typeof previousData) || previousData; + const questions = resultListData.items || []; + + const next = () => { + setPageSize(pageSize + 20); + }; + + return ( +
+
+

+ {questions.length + ? `You have ${resultListData.totalItems} bookmarked questions.` + : "You haven't bookmarked any questions."} +

+
+ {!questions.length && ( +

Bookmark questions to view them here.

+ )} + +
+
+ pageSize + : false + } + isLoading={isLoading} + > + {questions.map((question) => ( + + ))} + +
+ {isLoading && ( +
+ +
+ )} +
+
+ ); +}; diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index fd77946..cc042e4 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -12,6 +12,7 @@ import QuestionRoute from "./question"; import { Search } from "./search"; import Signup from "./signup"; import TagPage from "./tag"; +import { BookmarkedQuestions } from "@/routes/bookmarks"; export const routes: RouteObject[] = [ { @@ -26,6 +27,10 @@ export const routes: RouteObject[] = [ path: "/login", Component: Login, }, + { + path: "/bookmarks", + Component: BookmarkedQuestions, + }, { path: "/logout", async action() { diff --git a/frontend/src/routes/profile.test.tsx b/frontend/src/routes/profile.test.tsx index fc76cad..82b185e 100644 --- a/frontend/src/routes/profile.test.tsx +++ b/frontend/src/routes/profile.test.tsx @@ -126,7 +126,7 @@ describe("Profile component", () => { render(); - const editButton = screen.getByText("Edit profile"); + const editButton = screen.getByText("Edit Profile"); fireEvent.click(editButton); const bioField = screen.getByPlaceholderText("Bio"); diff --git a/frontend/src/routes/profile.tsx b/frontend/src/routes/profile.tsx index 1eb53a9..bd45753 100644 --- a/frontend/src/routes/profile.tsx +++ b/frontend/src/routes/profile.tsx @@ -153,9 +153,14 @@ export default function Profile() { {isPending ? "Saving..." : "Save"} ) : ( - +
+ + +
) ) : ( data?.data && (