From 29d5a6ac82c673751a6ebfe61099b764b8cb458a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atakan=20Ya=C5=9Far?= Date: Mon, 16 Dec 2024 19:13:17 +0300 Subject: [PATCH 1/8] feat(mobile): sort questions by recommended sorting option --- mobile/app/tags/[tagId].tsx | 15 +++++++++++---- mobile/components/Feed.tsx | 2 +- mobile/components/QuestionsList.tsx | 10 ++++++---- mobile/services/api/programmingForumComponents.ts | 6 ++++++ mobile/services/api/programmingForumSchemas.ts | 5 +++-- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/mobile/app/tags/[tagId].tsx b/mobile/app/tags/[tagId].tsx index 48d99f4d..287c2cc7 100644 --- a/mobile/app/tags/[tagId].tsx +++ b/mobile/app/tags/[tagId].tsx @@ -47,7 +47,7 @@ export default function TagPage() { const tag = data?.data; const token = useAuthStore((s) => s.token); - const [tab, setTab] = useState<"top-rated" | "recent">("top-rated"); + const [tab, setTab] = useState<"top_rated" | "recent" | "recommended">("top_rated"); const [difficultyFilter, setDifficultyFilter] = useState(); if (isLoading) { @@ -126,8 +126,8 @@ export default function TagPage() { @@ -137,6 +137,12 @@ export default function TagPage() { > Recent + - + + + + + + + { Tags - Latest Questions + Recommended Questions ); From 01bb3f17846fc7086d45aab57884aec5529ef732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atakan=20Ya=C5=9Far?= Date: Mon, 16 Dec 2024 19:52:38 +0300 Subject: [PATCH 3/8] feat(mobile): add content snippets --- mobile/app/question/[questionId].tsx | 3 +- mobile/app/question/[questionId]/answer.tsx | 64 +++++++++++++++++---- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/mobile/app/question/[questionId].tsx b/mobile/app/question/[questionId].tsx index 56efd795..b24ca997 100644 --- a/mobile/app/question/[questionId].tsx +++ b/mobile/app/question/[questionId].tsx @@ -38,6 +38,7 @@ import { import { useEffect, useState } from "react"; import { ScrollView, View } from "react-native"; import placeholderProfile from "@/assets/images/placeholder_profile.png"; +import { ContentWithSnippets } from "@/components/ContentWithSnippets"; export default function QuestionPage() { const { questionId } = useLocalSearchParams(); @@ -329,7 +330,7 @@ export default function QuestionPage() { - {question.content} + Answers diff --git a/mobile/app/question/[questionId]/answer.tsx b/mobile/app/question/[questionId]/answer.tsx index 204d5e5d..7afe6c56 100644 --- a/mobile/app/question/[questionId]/answer.tsx +++ b/mobile/app/question/[questionId]/answer.tsx @@ -15,8 +15,17 @@ import { Icon, Textarea, TextareaInput, + Popover, + PopoverBackdrop, + PopoverContent, + PopoverArrow, + PopoverHeader, + PopoverCloseButton, + PopoverBody, + PopoverFooter, } from "@/components/ui"; -import { X } from "lucide-react-native"; +import { X, InfoIcon } from "lucide-react-native"; +import { ContentWithSnippets } from "@/components/ContentWithSnippets"; export default function NewAnswerPage() { const { questionId } = useLocalSearchParams<{ questionId: string }>(); @@ -24,6 +33,7 @@ export default function NewAnswerPage() { const router = useRouter(); const [content, setContent] = useState(""); + const [preview, setPreview] = useState(false); const contentLength = content.length; const token = useAuthStore((state) => state.token); @@ -68,15 +78,49 @@ export default function NewAnswerPage() { - Write your answer - + + Write your answer + { + return ( + + ) + }} + onOpen={() => console.log("Popover opened")} + > + + + + + + + + + + + + + + + + {preview ? ( + + ) : ( + + ) + } + {contentLength} / 1000 + )} @@ -294,6 +316,7 @@ export default function QuestionPage() { value: String(tag.tagId), label: tag.name || "Loading...", }))} + defaultValue={tags.map((tag) => String(tag))} value={tags.map((tag) => String(tag))} onValueChange={(selectedIds) => { const selectedTags = selectedIds.map((id) => Number(id)); // Convert back to numbers @@ -318,8 +341,8 @@ export default function QuestionPage() { {/* Question Content */} {isEditing ? ( -
-
+ <> +
{isPreviewMode ? ( -
+
) : ( + onOpen={() => console.log("Popover opened")} + > + + + + + + + + + + + + { preview ? ( + + ) : ( + + )} + {contentLength} characters diff --git a/mobile/components/PostingGuide.tsx b/mobile/components/PostingGuide.tsx new file mode 100644 index 00000000..30178086 --- /dev/null +++ b/mobile/components/PostingGuide.tsx @@ -0,0 +1,55 @@ +import { + Text, + View, + ScrollView, +} from "@/components/ui"; +import { openURL } from "expo-linking"; + +export const PostingGuide = () => { + return ( + + + {/* Writing Answers Section */} + Writing Answers + + We use Markdown for formatting answers. You can use standard Markdown + syntax for headers, lists, links, etc. For a basic reference, you can + check{" "} + openURL("https://commonmark.org/help/")} + > + CommonMark + + . + + + {/* Code Execution Section */} + Code Execution + + To create executable code blocks, use triple backticks with + language-exec: + + + + {`\`\`\`javascript-exec\nconsole.log("Hello, world!, This is executable!");\`\`\``} + + + + {/* Linking Section */} + Linking + + Link to tags using:{" "} + + [tag name](#tag-123) + + {"\n"} + Link to questions using:{" "} + + [question title](#q-456) + + + + + ); +}; From 42dd7a1acb8b3c02a2046367f68dc746935b52b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Efe=20Ak=C3=A7a?= Date: Mon, 16 Dec 2024 20:52:44 +0300 Subject: [PATCH 8/8] fix: accessibility issue and add delete answer --- frontend/src/components/AnswerCard.tsx | 5 +- frontend/src/components/AnswerItem.tsx | 65 ++++++++++++++++-------- frontend/src/components/Answers.test.tsx | 3 ++ frontend/src/components/Answers.tsx | 2 +- frontend/src/index.css | 2 +- frontend/src/routes/question.tsx | 1 - 6 files changed, 52 insertions(+), 26 deletions(-) diff --git a/frontend/src/components/AnswerCard.tsx b/frontend/src/components/AnswerCard.tsx index 2d70f055..450832f4 100644 --- a/frontend/src/components/AnswerCard.tsx +++ b/frontend/src/components/AnswerCard.tsx @@ -1,8 +1,8 @@ +import placeholderProfile from "@/assets/placeholder_profile.png"; import { Card } from "@/components/ui/card"; import { ArrowRight, CornerDownRight, Star } from "lucide-react"; import React from "react"; import { Link } from "react-router-dom"; -import placeholderProfile from "@/assets/placeholder_profile.png"; interface AnswerCardProps { id: number; @@ -49,8 +49,7 @@ export const AnswerCard: React.FC = ({
{"Profile diff --git a/frontend/src/components/AnswerItem.tsx b/frontend/src/components/AnswerItem.tsx index f750d95d..3d03411c 100644 --- a/frontend/src/components/AnswerItem.tsx +++ b/frontend/src/components/AnswerItem.tsx @@ -1,26 +1,31 @@ +import placeholderProfile from "@/assets/placeholder_profile.png"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; +import { useDeleteAnswer } from "@/services/api/programmingForumComponents"; import { AnswerDetails } from "@/services/api/programmingForumSchemas"; import useAuthStore from "@/services/auth"; -import { ThumbsDown, ThumbsUp } from "lucide-react"; +import { ThumbsDown, ThumbsUp, Trash2 } from "lucide-react"; import React from "react"; import { Link } from "react-router-dom"; import { ContentWithSnippets } from "./ContentWithSnippets"; -import placeholderProfile from "@/assets/placeholder_profile.png"; interface AnswerItemProps { answer: AnswerDetails; onUpvote: () => void; onDownvote: () => void; + onDelete: () => void; } export const AnswerItem: React.FC = ({ answer, onUpvote, onDownvote, + onDelete, }) => { - const { token } = useAuthStore(); + const { token, selfProfile } = useAuthStore(); + const isSelfAnswer = answer.author?.id === selfProfile?.id; + const { mutateAsync: deleteAnswer } = useDeleteAnswer(); return (
@@ -55,23 +60,43 @@ export const AnswerItem: React.FC = ({
)}
-
- - {"Profile - {answer.author?.name} - - - Answered: {new Date(answer.createdAt || "").toLocaleDateString()} - +
+
+ + {"Profile + + {answer.author?.name} + + + + Answered:{" "} + {new Date(answer.createdAt || "").toLocaleDateString()} + +
+ {isSelfAnswer && ( + + )}
diff --git a/frontend/src/components/Answers.test.tsx b/frontend/src/components/Answers.test.tsx index 99247c3c..f9ef1a6a 100644 --- a/frontend/src/components/Answers.test.tsx +++ b/frontend/src/components/Answers.test.tsx @@ -21,6 +21,9 @@ vi.mock("@/services/api/programmingForumComponents", () => ({ useGetQuestionAnswers: vi.fn(), useUpvoteAnswer: vi.fn(), useDownvoteAnswer: vi.fn(), + useDeleteAnswer: vi.fn().mockReturnValue({ + mutateAsync: vi.fn(), + }), })); // Mock the auth store diff --git a/frontend/src/components/Answers.tsx b/frontend/src/components/Answers.tsx index 5c490211..a9f09af2 100644 --- a/frontend/src/components/Answers.tsx +++ b/frontend/src/components/Answers.tsx @@ -67,11 +67,11 @@ export function Answers({ questionId }: AnswersProps) { answer={answer} onUpvote={() => handleVote(answer.id, 1)} onDownvote={() => handleVote(answer.id, -1)} + onDelete={() => refetch()} /> ))} {answers.length === 0 && ( - {" "} This question doesn't have an answer yet. Contribute to the discussion by answering this question. diff --git a/frontend/src/index.css b/frontend/src/index.css index 8b146400..ef7de356 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -104,7 +104,7 @@ blockquote { } code { - @apply relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold; + @apply relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold; } @layer base { diff --git a/frontend/src/routes/question.tsx b/frontend/src/routes/question.tsx index 926ffc95..2f9d796c 100644 --- a/frontend/src/routes/question.tsx +++ b/frontend/src/routes/question.tsx @@ -123,7 +123,6 @@ export default function QuestionPage() { useEffect(() => { if (!isEditing && question.tags) { - console.log("set tags", question.tags); setTags(question.tags.map((t) => Number(t.id))); } }, [question?.tags, isEditing]);