Skip to content

Commit

Permalink
Merge pull request #684 from bounswe/frontend/feature/594/bookmark-page
Browse files Browse the repository at this point in the history
[Frontend] Implement Bookmark Page
  • Loading branch information
ozdentarikcan authored Dec 15, 2024
2 parents 686ef30 + 7794988 commit 9e0cf7a
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 4 deletions.
107 changes: 107 additions & 0 deletions frontend/src/routes/bookmarks.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<RouterProvider router={router} />);
});

it("renders bookmarked questions correctly", () => {
vi.mocked(useGetBookmarkedQuestions).mockReturnValue({
isLoading: false,
error: null,
data: {
data: { items: mockQuestions, totalItems: mockQuestions.length },
},
} as QueryObserverSuccessResult<unknown, GetBookmarkedQuestionsError>);

render(
<MemoryRouter initialEntries={["/bookmarks"]}>
<Routes>
<Route path="/bookmarks" element={<BookmarkedQuestions />} />
</Routes>
</MemoryRouter>,
);

expect(
screen.getByText(`You have ${mockQuestions.length} bookmarked questions.`),
).toBeInTheDocument();

mockQuestions.forEach((question) => {
expect(screen.getByText(question.title)).toBeInTheDocument();
});
});
});

101 changes: 101 additions & 0 deletions frontend/src/routes/bookmarks.tsx
Original file line number Diff line number Diff line change
@@ -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 <ErrorAlert error={error} />;
}

const resultListData =
(resultList?.data as typeof previousData) || previousData;
const questions = resultListData.items || [];

const next = () => {
setPageSize(pageSize + 20);
};

return (
<div className="container flex flex-col gap-2 py-8">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold ">
{questions.length
? `You have ${resultListData.totalItems} bookmarked questions.`
: "You haven't bookmarked any questions."}
</h1>
</div>
{!questions.length && (
<p>Bookmark questions to view them here.</p>
)}

<div className="mt-4">
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
<InfiniteScroll
next={next}
hasMore={
resultListData.totalItems
? resultListData.totalItems > pageSize
: false
}
isLoading={isLoading}
>
{questions.map((question) => (
<QuestionCard
difficulty={question.difficulty}
key={question.id}
id={question.id}
title={question.title}
content={question.content ?? ""}
votes={
((question as unknown as { upvoteCount: number })
.upvoteCount ?? 0) -
((question as unknown as { downvoteCount: number })
.downvoteCount ?? 0)
}
answerCount={
(question as unknown as { answerCount: number })
.answerCount ?? 0
}
/>
))}
</InfiniteScroll>
</div>
{isLoading && (
<div className="col-span-3 flex w-full items-center justify-center">
<Loader2
aria-label="Loading"
className="h-16 w-16 animate-spin text-primary"
/>
</div>
)}
</div>
</div>
);
};
5 changes: 5 additions & 0 deletions frontend/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
{
Expand All @@ -26,6 +27,10 @@ export const routes: RouteObject[] = [
path: "/login",
Component: Login,
},
{
path: "/bookmarks",
Component: BookmarkedQuestions,
},
{
path: "/logout",
async action() {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/routes/profile.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ describe("Profile component", () => {

render(<RouterProvider router={router} />);

const editButton = screen.getByText("Edit profile");
const editButton = screen.getByText("Edit Profile");
fireEvent.click(editButton);

const bioField = screen.getByPlaceholderText("Bio");
Expand Down
11 changes: 8 additions & 3 deletions frontend/src/routes/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,14 @@ export default function Profile() {
{isPending ? "Saving..." : "Save"}
</Button>
) : (
<Button onClick={() => setEditing(true)} variant="outline">
Edit profile
</Button>
<div className="flex gap-4 justify-center items-center">
<Button onClick={() => setEditing(true)} variant="outline">
Edit Profile
</Button>
<Button asChild variant="outline">
<Link to="/bookmarks">Bookmarks</Link>
</Button>
</div>
)
) : (
data?.data && (
Expand Down

0 comments on commit 9e0cf7a

Please sign in to comment.