Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into fix/backend/tag-se…
Browse files Browse the repository at this point in the history
…lf-following
  • Loading branch information
mmtftr committed Dec 13, 2024
2 parents 9466705 + b8f4b68 commit 2300c2f
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 27 deletions.
94 changes: 94 additions & 0 deletions frontend/src/components/BookmarkButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {
useBookmarkQuestion,
useGetQuestionDetails,
useRemoveQuestionBookmark,
} from "@/services/api/programmingForumComponents";
import { useState } from "react";
import { Button } from "./ui/button";
// import { ReactComponent as BookmarkIcon } from '../assets/Icon/Nav/Bookmark/Active.svg';

export default function BookmarkButton({
question,
}: {
question: { questionId?: number; bookmarked?: boolean };
}) {
const { isLoading, data, error, refetch } = useGetQuestionDetails(
{
pathParams: {
questionId: question.questionId!,
},
},
{
enabled: typeof question.bookmarked !== "boolean",
},
);

const [optimisticBookmarking, setOptimisticBookmarking] = useState(
null as boolean | null,
);

const { mutateAsync: bookmark } = useBookmarkQuestion({
onSuccess: () => {
refetch().then(() => {
setOptimisticBookmarking(null);
});
},
onError: () => {
setOptimisticBookmarking(null);
},
});
const { mutateAsync: removeBookmark } = useRemoveQuestionBookmark({
onSuccess: () => {
refetch().then(() => {
setOptimisticBookmarking(null);
});
},
onError: () => {
setOptimisticBookmarking(null);
},
});

const bookmarked = optimisticBookmarking ?? data?.data?.bookmarked;

return (
<Button
disabled={!!error || isLoading}
size="icon"
aria-label={bookmarked ? "Remove bookmark" : "Add bookmark"}
variant={bookmarked && !isLoading ? "primary-outline" : "default"}
onClick={() => {
if (bookmarked) {
removeBookmark({
pathParams: {
questionId: question.questionId!,
},
});
setOptimisticBookmarking(false);
} else {
bookmark({
pathParams: {
questionId: question.questionId!,
},
});
setOptimisticBookmarking(true);
}
}}
>
{isLoading ? (
"Loading..."
) : error ? (
"Error"
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill={bookmarked && !isLoading ? "#a51819" : "#fafafa" }
width="20px"
height="20px"
>
<path d="M5 3h14a1 1 0 011 1v16a1 1 0 01-1.496.868L12 15.98l-6.504 4.889A1 1 0 014 20V4a1 1 0 011-1z" />
</svg>
)}
</Button>
);
}
8 changes: 4 additions & 4 deletions frontend/src/components/HighlightedQuestionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ export const HighlightedQuestionCard: React.FC<Partial<QuestionSummary>> = ({
id,
title,
content,
likeCount,
upvoteCount,
difficulty,
commentCount,
answerCount,
author,
}) => {
return (
Expand All @@ -29,11 +29,11 @@ export const HighlightedQuestionCard: React.FC<Partial<QuestionSummary>> = ({
<div className="flex flex-col gap-3 text-xs text-gray-700">
<div className="flex items-center gap-1">
<Star className="h-4 w-4" />
<span>{likeCount} votes</span>
<span>{upvoteCount} votes</span>
</div>
<div className="flex items-center gap-1">
<MessageSquare className="h-4 w-4" />
<span>{commentCount} answers</span>
<span>{answerCount} answers</span>
</div>
{difficulty && (
<div className="flex items-center gap-1">
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/HighlightedQuestionsBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export const HighlightedQuestionsBox: React.FC<{
id={question.id}
title={question.title}
content={question.content}
likeCount={question.likeCount}
commentCount={question.commentCount}
upvoteCount={question.upvoteCount}
answerCount={question.answerCount}
author={question.author}
/>
))}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/routes/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,8 @@ export default function Profile() {
id={question.id}
title={question.title}
content={question.content ?? ""}
votes={question.likeCount}
answerCount={question.commentCount}
votes={question.upvoteCount}
answerCount={question.answerCount}
author={question.author}
/>
))}
Expand Down
43 changes: 40 additions & 3 deletions frontend/src/routes/question.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useGetQuestionDetails } from "@/services/api/programmingForumComponents";
import { useGetQuestionDetails, useSearchTags } from "@/services/api/programmingForumComponents";
import { QuestionDetails } from "@/services/api/programmingForumSchemas";
import useAuthStore from "@/services/auth";
import { testAccessibility } from "@/utils/test-accessibility";
Expand Down Expand Up @@ -40,6 +40,7 @@ const mockQuestionData = vi.hoisted(
createdAt: "2023-01-01T00:00:00Z",
updatedAt: "2023-01-01T00:00:00Z",
dislikeCount: 0,
difficulty: "EASY",
bookmarked: false,
selfVoted: 1,
selfDifficultyVote: "EASY",
Expand All @@ -55,6 +56,12 @@ vi.mock("@/services/api/programmingForumComponents", () => ({
useDeleteQuestion: vi.fn(() => ({
mutateAsync: vi.fn(),
})),
useBookmarkQuestion: vi.fn(() => ({
mutateAsync: vi.fn(),
})),
useRemoveQuestionBookmark: vi.fn(() => ({
mutateAsync: vi.fn(),
})),
useVoteQuestion: vi.fn(() => ({
mutateAsync: vi.fn(),
})),
Expand Down Expand Up @@ -83,6 +90,16 @@ vi.mock("@/services/api/programmingForumComponents", () => ({
},
}),
})),
useSearchTags: vi.fn(() => ({
data: { data: { items: [{ tagId: "1", name: "Tag1" }, { tagId: "2", name: "Tag2" }] } },
isLoading: false,
})),
useUpdateQuestion: vi.fn(() => ({
mutateAsync: vi.fn().mockResolvedValue({
data: { success: true },
}),
isPending: false,
})),
}));

vi.mock("@/services/exercism", () => ({
Expand Down Expand Up @@ -110,9 +127,13 @@ describe("QuestionPage", () => {
isLoading: false,
error: null,
});
(useSearchTags as Mock).mockReturnValue({
data: { data: { items: [{ tagId: "1", name: "Tag1" }, { tagId: "2", name: "Tag2" }] } },
isLoading: false,
});
vi.mocked(useAuthStore).mockReturnValue({
selfProfile: null,
token: null,
selfProfile: { id: mockQuestionData.author.id }, // Ensure the user matches the question's author
token: "mock-token",
});
});

Expand Down Expand Up @@ -196,6 +217,22 @@ describe("QuestionPage", () => {
expect(screen.getByRole("button", { name: /delete/i })).toBeInTheDocument();
});

it("renders bookmark button", () => {
vi.mocked(useAuthStore).mockReturnValue({
selfProfile: { id: 1},
token: "mock-token",
});
render(
<MemoryRouter initialEntries={["/question/1"]}>
<Routes>
<Route path="/question/:questionId" element={<QuestionPage />} />
</Routes>
</MemoryRouter>,
);

expect(screen.getByRole("button", { name: /bookmark/i })).toBeInTheDocument();
});

it("updates difficulty counts when voting", async () => {
// Mock the auth store with a logged-in user
vi.mocked(useAuthStore).mockReturnValue({
Expand Down
Loading

0 comments on commit 2300c2f

Please sign in to comment.