Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Frontend] Fix Bookmarks Page #696

Merged
merged 1 commit into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions frontend/src/components/TagType.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useParams } from "react-router-dom";
import { useEffect, useState } from "react";
import { Loader2 } from "lucide-react";
import ErrorAlert from "@/components/ErrorAlert";
import InfiniteScroll from "@/components/InfiniteScroll";
import { TagCard } from "@/components/TagCard";
import { useSearchTags } from "@/services/api/programmingForumComponents";
import { TagDetails } from "@/services/api/programmingForumSchemas";
import ErrorAlert from "@/components/ErrorAlert";
import InfiniteScroll from "@/components/InfiniteScroll";
import { Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";

export default function SubtypePage() {
const { typeId } = useParams<{ typeId: string }>();
Expand Down Expand Up @@ -151,7 +151,7 @@ export default function SubtypePage() {

{/* Tags in this Category Section */}
<h2 className="mb-4 text-2xl font-semibold text-gray-800">
Tags in This Category:
Tags in Category
</h2>

{/* Infinite Scroll for displaying Related Tags */}
Expand Down
211 changes: 112 additions & 99 deletions frontend/src/routes/bookmarks.test.tsx
Original file line number Diff line number Diff line change
@@ -1,107 +1,120 @@
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[] = [
{
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,
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,
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,
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,
name: "Jane Smith",
username: "user2",
profilePicture: "p",
reputationPoints: 50,
},
];

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} />);
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"],
});

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();
});

await testAccessibility(<RouterProvider router={router} />);
});

it("renders bookmarked questions correctly", () => {
vi.mocked(useGetBookmarkedQuestions).mockReturnValue({
isLoading: false,
error: null,
data: {
data: mockQuestions,
},
} 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();
});
});
});
80 changes: 25 additions & 55 deletions frontend/src/routes/bookmarks.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,14 @@
import { useGetBookmarkedQuestions } from "@/services/api/programmingForumComponents";
import {
QuestionSummary,
} from "@/services/api/programmingForumSchemas";
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 [previousData, setPreviousData] = useState<QuestionSummary[]>([]);

const {
data: resultList,
isLoading,
error,
} = useGetBookmarkedQuestions({});
const { data: resultList, isLoading, error } = useGetBookmarkedQuestions({});

useEffect(() => {
if (resultList?.data && !isLoading) {
Expand All @@ -36,56 +22,40 @@ export const BookmarkedQuestions = () => {

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

const next = () => {
setPageSize(pageSize + 20);
};
const questions = resultListData || [];

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 have ${resultListData.length} bookmarked questions.`
: "You haven't bookmarked any questions."}
</h1>
</div>
{!questions.length && (
<p>Bookmark questions to view them here.</p>
)}
{!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>
{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
}
/>
))}
</div>
{isLoading && (
<div className="col-span-3 flex w-full items-center justify-center">
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/services/temporaryMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const temporaryMocks = {
id: 2,
username: "john_doe",
reputationPoints: 100,
profilePicture: "frontend\src\assets\placeholder_profile.png",
profilePicture: "frontend\\src\\assets\\placeholder_profile.png",
name: "John Doe",
experienceLevel: "INTERMEDIATE",
},
Expand Down Expand Up @@ -62,7 +62,7 @@ export const temporaryMocks = {
id: 1,
username: "jane_doe",
reputationPoints: 150,
profilePicture: "frontend\src\assets\placeholder_profile.png",
profilePicture: "frontend\\src\\assets\\placeholder_profile.png",
name: "Jane Doe",
experienceLevel: "EXPERT",
},
Expand All @@ -83,7 +83,7 @@ export const temporaryMocks = {
id: 2,
username: "john_doe",
reputationPoints: 100,
profilePicture: "frontend\src\assets\placeholder_profile.png",
profilePicture: "frontend\\src\\assets\\placeholder_profile.png",
name: "John Doe",
experienceLevel: "INTERMEDIATE",
},
Expand Down
Loading