diff --git a/package.json b/package.json index b93934d9c..75ace9966 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "bcrypt": "^5.1.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "cms": "file:", "date-fns": "^3.6.0", "dayjs": "^1.11.10", "discord-oauth2": "^2.11.0", diff --git a/src/app/(main)/(pages)/question/page.tsx b/src/app/(main)/(pages)/question/page.tsx index f23e2bded..24f35e3e9 100644 --- a/src/app/(main)/(pages)/question/page.tsx +++ b/src/app/(main)/(pages)/question/page.tsx @@ -21,9 +21,9 @@ import { getDisabledFeature, getUpdatedUrl, paginationData } from '@/lib/utils'; import db from '@/db'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; -import PostCard from '@/components/posts/PostCard'; import Pagination from '@/components/Pagination'; import { redirect } from 'next/navigation'; +import QuestionHeader from '@/components/questions/QuestionPost'; type QuestionsResponse = { data: ExtendedQuestion[] | null; @@ -48,6 +48,7 @@ const getQuestionsWithQuery = async ( upvotes: true, downvotes: true, totalanswers: true, + content: true, tags: true, slug: true, createdAt: true, @@ -141,28 +142,37 @@ export default async function QuestionsPage({ <>
{/* Header */} -
+

Questions

- - + + New Question + + {/* Next question button */} +
{/* Content Area */}
- {/* Next question button */} - -
-
-
+
+
+ @@ -209,24 +219,13 @@ export default async function QuestionsPage({
- - New Question -
{/* Chat */}
{response?.data?.map((post) => ( -
- - - - - - - - - Most Voted - - - - - Most Down Voted - - - - - Most Recent - - - - - + {answers.length > 0 && ( + + + + + + + + + Most Voted + + + + + Most Down Voted + + + + + Most Recent + + + + + + )}
-
+
{answers.map((post: any) => ( - +
+ +
))}
- + {answers.length > 0 && } + {answers.length === 0 && ( +
+

No Answers

+
+ )}
); }; diff --git a/src/app/questions/[slug]/@question/page.tsx b/src/app/questions/[slug]/@question/page.tsx index a3c103af8..99983d0a2 100644 --- a/src/app/questions/[slug]/@question/page.tsx +++ b/src/app/questions/[slug]/@question/page.tsx @@ -3,8 +3,8 @@ import React from 'react'; import db from '@/db'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; -import PostCard from '@/components/posts/PostCard'; import Link from 'next/link'; +import QuestionPost from '@/components/questions/QuestionPost'; const SingleQuestionPage = async ({ params, @@ -50,18 +50,19 @@ const SingleQuestionPage = async ({ return (
- + Go Back
{question && ( - )}
diff --git a/src/app/questions/page.tsx b/src/app/questions/page.tsx index fbe03aa6e..e0e10e5e3 100644 --- a/src/app/questions/page.tsx +++ b/src/app/questions/page.tsx @@ -21,9 +21,9 @@ import { getDisabledFeature, getUpdatedUrl, paginationData } from '@/lib/utils'; import db from '@/db'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; -import PostCard from '@/components/posts/PostCard'; import Pagination from '@/components/Pagination'; import { redirect } from 'next/navigation'; +import QuestionPost from '@/components/questions/QuestionPost'; type QuestionsResponse = { data: ExtendedQuestion[] | null; @@ -47,6 +47,7 @@ const getQuestionsWithQuery = async ( title: true, upvotes: true, downvotes: true, + content: true, totalanswers: true, tags: true, slug: true, @@ -218,7 +219,7 @@ export default async function Home({
{response?.data?.map((post) => ( - = ({ questionId, answerId }) => { onSuccess: (data) => { toast.success(`${data.message}`); if (questionId) { - router.push('/questions'); + router.push('/question'); } }, onError: (error) => { diff --git a/src/components/questions/AnswerPost.tsx b/src/components/questions/AnswerPost.tsx new file mode 100644 index 000000000..a3a42df0c --- /dev/null +++ b/src/components/questions/AnswerPost.tsx @@ -0,0 +1,168 @@ +'use client'; +import React from 'react'; +import { useState } from 'react'; +import { useTheme } from 'next-themes'; +import { Author, ExtendedAnswer } from '@/actions/question/types'; +import { useAction } from '@/hooks/useAction'; +import { createAnswer } from '@/actions/answer'; +import { toast } from 'sonner'; +import TextSnippet from '../posts/textSnippet'; +import MDEditor from '@uiw/react-md-editor'; +import { Button } from '../ui/button'; +import VoteForm from '../posts/form/form-vote'; +import { Avatar, AvatarFallback } from '../ui/avatar'; +import { FormPostErrors } from '../posts/form/form-errors'; +import { ROLES } from '@/actions/types'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, +} from '../ui/dropdown-menu'; +import { MoreHorizontal } from 'lucide-react'; +import DeleteForm from '../posts/form/form-delete'; + +interface IProps { + post: ExtendedAnswer; + sessionUser: Author | undefined | null; + reply?: boolean; + enableLink?: boolean; + isAnswer?: boolean; + questionId: number; +} + +const AnswerPost: React.FC = ({ + post, + sessionUser, + questionId, + reply = false, + isAnswer = true, +}) => { + const { theme } = useTheme(); + const [markDownValue, setMarkDownValue] = useState(''); + const [enableReply, setEnableReply] = useState(false); + + const handleMarkdownChange = (newValue?: string) => { + if (typeof newValue === 'string') { + setMarkDownValue(newValue); + } + }; + + const { execute, fieldErrors } = useAction(createAnswer, { + onSuccess: () => { + toast.success(`Answer created`); + if (!fieldErrors?.content) { + setEnableReply((prev) => !prev); + setMarkDownValue(''); + } + }, + onError: (error) => { + toast.error(error); + }, + }); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + execute({ + content: markDownValue, + questionId, + parentId: isAnswer ? post?.id : undefined, + }); + }; + + return ( + <> +
+
+ + + {post.author.name?.substring(0, 2).toUpperCase()} + + +
+
+
+

{post.author.name}

+
+

{post.content}

+
+ + + {reply && ( + + )} + + + + + + + {(sessionUser?.role === ROLES.ADMIN || + post?.author?.id === sessionUser?.id) && ( + + )} +
+ {/* + Report spam + */} +
+
+
+ {enableReply && ( +
+
+
+
+
+ + + +
+
+
+ )} +
+ {post.responses && + post.responses.length > 0 && + post.responses.map((answer: ExtendedAnswer) => ( +
+ +
+ ))} +
+
+
+ + ); +}; +export default AnswerPost; diff --git a/src/components/questions/QuestionPost.tsx b/src/components/questions/QuestionPost.tsx new file mode 100644 index 000000000..90162c49f --- /dev/null +++ b/src/components/questions/QuestionPost.tsx @@ -0,0 +1,212 @@ +'use client'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; +dayjs.extend(relativeTime); +import React from 'react'; +import { useState } from 'react'; +import { useTheme } from 'next-themes'; +import { Author, ExtendedQuestion } from '@/actions/question/types'; +import { useAction } from '@/hooks/useAction'; +import { createAnswer } from '@/actions/answer'; +import { toast } from 'sonner'; +import { Answer } from '@prisma/client'; +import TextSnippet from '../posts/textSnippet'; +import Link from 'next/link'; +import MDEditor from '@uiw/react-md-editor'; +import { MessageSquareReply, MoreHorizontal, User } from 'lucide-react'; +import { Button } from '../ui/button'; +import VoteForm from '../posts/form/form-vote'; +import DeleteForm from '../posts/form/form-delete'; +import { FormPostErrors } from '../posts/form/form-errors'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, +} from '../ui/dropdown-menu'; +import { ROLES } from '@/actions/types'; + +interface IProps { + post: ExtendedQuestion; + sessionUser: Author | undefined | null; + reply?: boolean; + enableLink?: boolean; + isAnswer?: boolean; + questionId: number; +} + +const isExtendedQuestion = ( + post: ExtendedQuestion | Answer, +): post is ExtendedQuestion => { + return (post as ExtendedQuestion).slug !== undefined; +}; + +const QuestionPost: React.FC = ({ + post, + sessionUser, + questionId, + reply = true, + isAnswer = true, +}) => { + const { theme } = useTheme(); + const [markDownValue, setMarkDownValue] = useState(''); + const [enableReply, setEnableReply] = useState(false); + + const handleMarkdownChange = (newValue?: string) => { + if (typeof newValue === 'string') { + setMarkDownValue(newValue); + } + }; + + const { execute, fieldErrors } = useAction(createAnswer, { + onSuccess: () => { + toast.success(`Answer created`); + if (!fieldErrors?.content) { + setEnableReply((prev) => !prev); + setMarkDownValue(''); + } + }, + onError: (error) => { + toast.error(error); + }, + }); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + execute({ + content: markDownValue, + questionId, + parentId: isAnswer ? post?.id : undefined, + }); + }; + + return ( + <> +
+
+
+ + + {post?.title} + + + + + + + + {(sessionUser?.role === ROLES.ADMIN || + post?.author?.id === sessionUser?.id) && ( + + )} +
+ {/* + Report spam + */} +
+
+
+
+ 200 ? '...' : '')} + style={{ + whiteSpace: 'pre-wrap', + wordBreak: 'break-word', + overflowWrap: 'break-word', + backgroundColor: 'transparent', + }} + /> +
+
+ {isExtendedQuestion(post) && + post.tags + .filter((v) => v !== '') + .map((v, index) => ( + + {/* */}# {v} + + ))} +
+
+ + + {post.author.name} + +
+ + • Created{' '} + {dayjs(post.createdAt) && dayjs(post.createdAt)?.fromNow()} + + + • Updated{' '} + {dayjs(post.updatedAt) && dayjs(post.updatedAt)?.fromNow()} + +
+
+
+
+ + + {reply && ( + + )} + +

{post.totalanswers}

+
+
+
+
+
+ {enableReply && ( +
+
+
+
+
+ + + +
+
+
+ )} + + ); +}; +export default QuestionPost; diff --git a/src/components/search.tsx b/src/components/search.tsx index 57ac89386..5e20c6977 100644 --- a/src/components/search.tsx +++ b/src/components/search.tsx @@ -18,8 +18,8 @@ const Search = () => { router.push(getUpdatedUrl(path, paramsObj, { search })); }; return ( -
- +
+