Skip to content

Commit

Permalink
Merge pull request #675 from bounswe/frontend/feature/question_edit
Browse files Browse the repository at this point in the history
Implement Question Edit
  • Loading branch information
NazireAta authored Dec 13, 2024
2 parents 0b70bc0 + c0fdd60 commit b8f4b68
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 27 deletions.
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
21 changes: 18 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 Down Expand Up @@ -89,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 @@ -116,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
147 changes: 137 additions & 10 deletions frontend/src/routes/question.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,23 @@ import { FullscreenLoading } from "@/components/FullscreenLoading";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { toast } from "@/components/ui/use-toast";
import { TagDetails } from "@/services/api/programmingForumSchemas";

import {
useDeleteQuestion as useDeleteQuestionById,
useDownvoteQuestion,
useGetQuestionDetails,
useUpvoteQuestion,
useUpdateQuestion,
useSearchTags,
} from "@/services/api/programmingForumComponents";
import { MultiSelect } from "@/components/multi-select";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import useAuthStore from "@/services/auth";
import { convertTagToTrack, useExercismSearch } from "@/services/exercism";
import { Flag, MessageSquare, ThumbsDown, ThumbsUp, Trash } from "lucide-react";
import { useState } from "react";
import { useEffect,useRef, useState } from "react";
import { Link, useParams } from "react-router-dom";

export default function QuestionPage() {
Expand Down Expand Up @@ -85,15 +92,72 @@ export default function QuestionPage() {
},
);

const { data: tagSearchData } = useSearchTags(
{ queryParams: { q: "", pageSize: 1000 } },
{ enabled: true },
);

useEffect(() => {
if (tagSearchData?.data) {
const tagsData = (tagSearchData.data as { items: TagDetails[] }).items;
setAvailableTags(tagsData);
}
}, [tagSearchData]);



const question = data! || {};
const [isEditing, setIsEditing] = useState(false); // To toggle edit mode
const [isPreviewMode, setIsPreviewMode] = useState(false); // Preview toggle for description

const titleRef = useRef<HTMLInputElement>(null);
const contentRef = useRef<HTMLTextAreaElement>(null);

const [tags, setTags] = useState<number[]>(question.tags?.map((tag) => Number(tag.id)) || []); // Tag IDs state
const [availableTags, setAvailableTags] = useState<{ tagId: string; name: string }[]>([]); // Available tags

const { mutateAsync: updateQuestion, isPending } = useUpdateQuestion({
onSuccess: () => {
refetch();
setIsEditing(false);
},
});


const saveChanges = async () => {
try {
await updateQuestion({
pathParams: { questionId: question.id },
body: {
title: titleRef.current?.value || question.title,
content: contentRef.current?.value || question.content,
tags: tags,
},
});
toast({
variant: "default",
title: "Changes saved",
description: "The question has been updated successfully.",
});
setIsEditing(false);
} catch (err) {console.error(
"Failed to save changes",
err
);
toast({
variant: "destructive",
title: "Failed to save changes",
description: "An error occurred while updating the question.",
});
}
};

if (isLoading) {
return <FullscreenLoading overlay />;
}
if (error) {
return <ErrorAlert error={error} />;
}

const question = data! || {};

if (!question) {
return (
<ErrorAlert
Expand All @@ -110,7 +174,15 @@ export default function QuestionPage() {
{/* Left Column: Question and Answers */}
<div className="flex-1">
<div className="mb-4 flex items-center justify-between">
{isEditing ? (
<Input
defaultValue={question.title}
ref={titleRef}
placeholder="Enter question title..." />
) : (
<h1 className="text-3xl font-bold">{question.title}</h1>
)}

<div className="flex gap-2">
<Button
size="icon"
Expand Down Expand Up @@ -213,19 +285,74 @@ export default function QuestionPage() {
<div className="mb-4 grid grid-cols-2 gap-2 py-2">
<span className="flex items-center gap-4 font-semibold">
<Flag className="h-6 w-6" />
{question.tags.map((s) => (
<Link to={`/tag/${s.id}`} key={s.name}>
<Badge>{s.name}</Badge>
</Link>
))}
{isEditing ? (
<MultiSelect
options={availableTags.map((tag) => ({
value: String(tag.tagId),
label: tag.name || "Loading...",
}))}
value={tags.map((tag) => String(tag))}
onValueChange={(selectedIds) =>{
const selectedTags = selectedIds.map((id) => Number(id)); // Convert back to numbers
setTags(selectedTags);
}}
placeholder="Select Tags"
/>
) : (
<div>
{question.tags.map((tag) => (
<Badge key={tag.id}>{tag.name}</Badge>
))}
</div>
)}

</span>
<span className="flex items-center gap-4 font-semibold">
Asked: {new Date(question.createdAt).toLocaleDateString()}
</span>
</div>

{/* Question Content */}
<ContentWithSnippets content={question.content} />
{isEditing ? (
<div>
<div className="flex gap-2">
<Button variant={!isPreviewMode ? "default" : "outline"} onClick={() => setIsPreviewMode(false)}>
Write
</Button>
<Button variant={isPreviewMode ? "default" : "outline"} onClick={() => setIsPreviewMode(true)}>
Preview
</Button>
</div>
{isPreviewMode ? (
<div className="min-h-[200px] rounded-lg border border-gray-300 bg-white p-4">
<ContentWithSnippets content={contentRef.current?.value || ""} />
</div>
) : (
<Textarea ref={contentRef} defaultValue={question.content} placeholder="Enter question content..." />
)}
</div>
) : (
<ContentWithSnippets content={question.content} />
)}

{/* Edit and Save Buttons */}
{isEditing ? (
<div className="flex gap-4">
<Button onClick={saveChanges} disabled={isPending}>
{isPending ? "Saving..." : "Save"}
</Button>
<Button variant="outline" onClick={() => setIsEditing(false)}>
Cancel
</Button>
</div>
) : (
selfProfile?.id === question.author.id && (
<Button variant="default" onClick={() => setIsEditing(true)} >
Edit Question
</Button>
)
)}


{/* Difficulty Bar */}
<DifficultyBar
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/routes/tag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,8 @@ export default function TagPage() {
id={question.id}
title={question.title}
content={question.content ?? ""}
votes={question.likeCount ?? 0}
answerCount={question.commentCount ?? 0}
votes={question.upvoteCount ?? 0}
answerCount={question.answerCount ?? 0}
/>
))}
</div>
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/services/api/programmingForumSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export type NewQuestion = {
export type UpdateQuestion = {
title?: string;
content?: string;
tags?: string[];
tags?: number[];
};

/**
Expand All @@ -143,6 +143,7 @@ export type QuestionDetails = {
tags: TagSummary[];
likeCount: number;
dislikeCount: number;
difficulty: DifficultyLevel;
commentCount: number;
viewCount?: number;
bookmarked: boolean;
Expand Down Expand Up @@ -171,8 +172,9 @@ export type QuestionSummary = {
createdAt: string;
difficulty: DifficultyLevel;
tags: TagSummary[];
likeCount: number;
commentCount: number;
upvoteCount: number;
downvoteCount: number;
answerCount: number;
viewCount?: number;
};

Expand Down
5 changes: 4 additions & 1 deletion swagger/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1393,7 +1393,7 @@ components:
tags:
type: array
items:
type: string
type: number

QuestionDetails:
type: object
Expand All @@ -1407,6 +1407,7 @@ components:
- tags
- likeCount
- dislikeCount
- difficulty
- commentCount
- selfQuestion
- difficulty
Expand Down Expand Up @@ -1440,6 +1441,8 @@ components:
type: integer
dislikeCount:
type: integer
difficulty:
$ref: '#/components/schemas/DifficultyLevel'
commentCount:
type: integer
viewCount:
Expand Down

0 comments on commit b8f4b68

Please sign in to comment.