From e78160d65484f1c60617cba6f2ce6b06e46120f5 Mon Sep 17 00:00:00 2001 From: Jeff Sieu Date: Mon, 10 Oct 2022 08:17:01 +0800 Subject: [PATCH] [questions][feat] integrate backend (#347) --- apps/portal/prisma/schema.prisma | 4 +- .../questions/ContributeQuestionCard.tsx | 11 +- .../questions/ContributeQuestionDialog.tsx | 10 +- .../questions/ContributeQuestionForm.tsx | 7 +- .../questions/FullScreenSpinner.tsx | 9 + .../questions/card/FullQuestionCard.tsx | 14 +- .../questions/card/QuestionCard.tsx | 4 +- .../questions/filter/FilterSection.tsx | 8 +- .../answer/[answerId]/[answerSlug]/index.tsx | 65 +++++- .../[questionId]/[questionSlug]/index.tsx | 106 ++++++++-- apps/portal/src/pages/questions/index.tsx | 62 +++++- .../router/questions-answer-comment-router.ts | 142 ++++++------- .../server/router/questions-answer-router.ts | 140 ++++++++---- .../questions-question-comment-router.ts | 140 ++++++------ .../router/questions-question-router.ts | 200 +++++++++++++----- apps/portal/src/types/questions.d.ts | 6 + apps/portal/src/utils/questions/constants.ts | 10 +- apps/portal/src/utils/questions/createSlug.ts | 7 + .../src/utils/questions/useSearchFilter.ts | 17 +- 19 files changed, 646 insertions(+), 316 deletions(-) create mode 100644 apps/portal/src/components/questions/FullScreenSpinner.tsx create mode 100644 apps/portal/src/utils/questions/createSlug.ts diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma index 59c53a40..af2ecc0e 100644 --- a/apps/portal/prisma/schema.prisma +++ b/apps/portal/prisma/schema.prisma @@ -204,8 +204,8 @@ model QuestionsQuestionEncounter { userId String? // TODO: sync with models company String @db.Text - location String @db.Text - role String @db.Text + location String? @db.Text + role String? @db.Text seenAt DateTime createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/apps/portal/src/components/questions/ContributeQuestionCard.tsx b/apps/portal/src/components/questions/ContributeQuestionCard.tsx index c647e835..fa11f098 100644 --- a/apps/portal/src/components/questions/ContributeQuestionCard.tsx +++ b/apps/portal/src/components/questions/ContributeQuestionCard.tsx @@ -7,8 +7,16 @@ import { import { TextInput } from '@tih/ui'; import ContributeQuestionDialog from './ContributeQuestionDialog'; +import type { ContributeQuestionFormProps } from './ContributeQuestionForm'; -export default function ContributeQuestionCard() { +export type ContributeQuestionCardProps = Pick< + ContributeQuestionFormProps, + 'onSubmit' +>; + +export default function ContributeQuestionCard({ + onSubmit, +}: ContributeQuestionCardProps) { const [showDraftDialog, setShowDraftDialog] = useState(false); const handleDraftDialogCancel = () => { @@ -68,6 +76,7 @@ export default function ContributeQuestionCard() { ); diff --git a/apps/portal/src/components/questions/ContributeQuestionDialog.tsx b/apps/portal/src/components/questions/ContributeQuestionDialog.tsx index 7406b7c5..e2ae8c8c 100644 --- a/apps/portal/src/components/questions/ContributeQuestionDialog.tsx +++ b/apps/portal/src/components/questions/ContributeQuestionDialog.tsx @@ -3,16 +3,21 @@ import { Dialog, Transition } from '@headlessui/react'; import { HorizontalDivider } from '~/../../../packages/ui/dist'; +import type { ContributeQuestionFormProps } from './ContributeQuestionForm'; import ContributeQuestionForm from './ContributeQuestionForm'; import DiscardDraftDialog from './DiscardDraftDialog'; -export type ContributeQuestionDialogProps = { +export type ContributeQuestionDialogProps = Pick< + ContributeQuestionFormProps, + 'onSubmit' +> & { onCancel: () => void; show: boolean; }; export default function ContributeQuestionDialog({ show, + onSubmit, onCancel, }: ContributeQuestionDialogProps) { const [showDiscardDialog, setShowDiscardDialog] = useState(false); @@ -72,8 +77,7 @@ export default function ContributeQuestionDialog({ setShowDiscardDialog(true)} onSubmit={(data) => { - // eslint-disable-next-line no-console - console.log(data); + onSubmit(data); onCancel(); }} /> diff --git a/apps/portal/src/components/questions/ContributeQuestionForm.tsx b/apps/portal/src/components/questions/ContributeQuestionForm.tsx index 5a0b562b..4215bae2 100644 --- a/apps/portal/src/components/questions/ContributeQuestionForm.tsx +++ b/apps/portal/src/components/questions/ContributeQuestionForm.tsx @@ -5,6 +5,7 @@ import { CalendarDaysIcon, // UserIcon, } from '@heroicons/react/24/outline'; +import type { QuestionsQuestionType } from '@prisma/client'; import { Button, Collapsible, @@ -29,7 +30,7 @@ export type ContributeQuestionData = { location: string; position: string; questionContent: string; - questionType: string; + questionType: QuestionsQuestionType; }; export type ContributeQuestionFormProps = { @@ -87,7 +88,9 @@ export default function ContributeQuestionForm({ required={true} startAddOn={CalendarDaysIcon} startAddOnType="icon" - {...register('date')} + {...register('date', { + valueAsDate: true, + })} /> diff --git a/apps/portal/src/components/questions/FullScreenSpinner.tsx b/apps/portal/src/components/questions/FullScreenSpinner.tsx new file mode 100644 index 00000000..02e6e02f --- /dev/null +++ b/apps/portal/src/components/questions/FullScreenSpinner.tsx @@ -0,0 +1,9 @@ +import { Spinner } from '@tih/ui'; + +export default function FullScreenSpinner() { + return ( +
+ +
+ ); +} diff --git a/apps/portal/src/components/questions/card/FullQuestionCard.tsx b/apps/portal/src/components/questions/card/FullQuestionCard.tsx index f0dc1e4f..bc6f5f30 100644 --- a/apps/portal/src/components/questions/card/FullQuestionCard.tsx +++ b/apps/portal/src/components/questions/card/FullQuestionCard.tsx @@ -19,6 +19,7 @@ export type FullQuestionCardProps = UpvoteProps & { receivedCount: number; role: string; timestamp: string; + type: string; }; export default function FullQuestionCard({ @@ -29,6 +30,7 @@ export default function FullQuestionCard({ timestamp, role, location, + type, }: FullQuestionCardProps) { const altText = company + ' logo'; return ( @@ -41,7 +43,7 @@ export default function FullQuestionCard({
- +

{timestamp} · {location} · {role}

@@ -53,14 +55,4 @@ export default function FullQuestionCard({
); - - // Return href ? ( - // - // {mainCard} - // - // ) : ( - // mainCard - // ); } diff --git a/apps/portal/src/components/questions/card/QuestionCard.tsx b/apps/portal/src/components/questions/card/QuestionCard.tsx index 2c034b9f..75e95dd2 100644 --- a/apps/portal/src/components/questions/card/QuestionCard.tsx +++ b/apps/portal/src/components/questions/card/QuestionCard.tsx @@ -47,12 +47,14 @@ export type QuestionCardProps = ActionButtonProps & receivedCount: number; role: string; timestamp: string; + type: string; }; export default function QuestionCard({ answerCount, content, // ReceivedCount, + type, showVoteButtons, showUserStatistics, showActionButton, @@ -69,7 +71,7 @@ export default function QuestionCard({
- +

{timestamp} · {location} · {role}

diff --git a/apps/portal/src/components/questions/filter/FilterSection.tsx b/apps/portal/src/components/questions/filter/FilterSection.tsx index 9c5f5dae..f7b4f800 100644 --- a/apps/portal/src/components/questions/filter/FilterSection.tsx +++ b/apps/portal/src/components/questions/filter/FilterSection.tsx @@ -4,13 +4,15 @@ import { Collapsible, TextInput } from '@tih/ui'; import Checkbox from '../ui-patch/Checkbox'; import RadioGroup from '../ui-patch/RadioGroup'; -export type FilterOption = { +export type FilterOption = { checked: boolean; label: string; - value: string; + value: V; }; -export type FilterChoices = Array>; +export type FilterChoices = Array< + Omit, 'checked'> +>; type FilterSectionType> = | { diff --git a/apps/portal/src/pages/questions/[questionId]/[questionSlug]/answer/[answerId]/[answerSlug]/index.tsx b/apps/portal/src/pages/questions/[questionId]/[questionSlug]/answer/[answerId]/[answerSlug]/index.tsx index bcbd7f93..aac0a7a2 100644 --- a/apps/portal/src/pages/questions/[questionId]/[questionSlug]/answer/[answerId]/[answerSlug]/index.tsx +++ b/apps/portal/src/pages/questions/[questionId]/[questionSlug]/answer/[answerId]/[answerSlug]/index.tsx @@ -5,13 +5,14 @@ import { Button, Select, TextArea } from '@tih/ui'; import FullAnswerCard from '~/components/questions/card/FullAnswerCard'; import CommentListItem from '~/components/questions/CommentListItem'; +import FullScreenSpinner from '~/components/questions/FullScreenSpinner'; import { SAMPLE_ANSWER, SAMPLE_ANSWER_COMMENT, - SAMPLE_QUESTION, } from '~/utils/questions/constants'; import { useFormRegister } from '~/utils/questions/useFormRegister'; +import { trpc } from '~/utils/trpc'; export type AnswerCommentData = { commentContent: string; @@ -22,22 +23,54 @@ export default function QuestionPage() { const { register: comRegister, + reset: resetComment, handleSubmit: handleCommentSubmit, formState: { isDirty: isCommentDirty, isValid: isCommentValid }, } = useForm({ mode: 'onChange' }); const commentRegister = useFormRegister(comRegister); - const question = SAMPLE_QUESTION; - const comment = SAMPLE_ANSWER_COMMENT; + const { answerId } = router.query; + + const utils = trpc.useContext(); + + const { data: answer } = trpc.useQuery([ + 'questions.answers.getAnswerById', + { answerId: answerId as string }, + ]); + + const { data: comments } = trpc.useQuery([ + 'questions.answers.comments.getAnswerComments', + { answerId: answerId as string }, + ]); + + const { mutate: addComment } = trpc.useMutation( + 'questions.answers.comments.create', + { + onSuccess: () => { + utils.invalidateQuery([ + 'questions.answers.comments.getAnswerComments', + { answerId: answerId as string }, + ]); + }, + }, + ); + const handleBackNavigation = () => { router.back(); }; const handleSubmitComment = (data: AnswerCommentData) => { - // eslint-disable-next-line no-console - console.log(data); + resetComment(); + addComment({ + answerId: answerId as string, + content: data.commentContent, + }); }; + if (!answer) { + return ; + } + return (
@@ -51,7 +84,13 @@ export default function QuestionPage() {
- +
{ // eslint-disable-next-line no-console console.log(value); - }}> + }} + />
diff --git a/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx b/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx index 74253d6a..92149e81 100644 --- a/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx +++ b/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx @@ -6,13 +6,15 @@ import { Button, Collapsible, Select, TextArea } from '@tih/ui'; import AnswerCard from '~/components/questions/card/AnswerCard'; import FullQuestionCard from '~/components/questions/card/FullQuestionCard'; import CommentListItem from '~/components/questions/CommentListItem'; +import FullScreenSpinner from '~/components/questions/FullScreenSpinner'; import { SAMPLE_ANSWER, - SAMPLE_QUESTION, SAMPLE_QUESTION_COMMENT, } from '~/utils/questions/constants'; +import createSlug from '~/utils/questions/createSlug'; import { useFormRegister } from '~/utils/questions/useFormRegister'; +import { trpc } from '~/utils/trpc'; export type AnswerQuestionData = { answerContent: string; @@ -34,26 +36,70 @@ export default function QuestionPage() { const { register: comRegister, handleSubmit: handleCommentSubmit, + reset: resetComment, formState: { isDirty: isCommentDirty, isValid: isCommentValid }, } = useForm({ mode: 'onChange' }); const commentRegister = useFormRegister(comRegister); - const question = SAMPLE_QUESTION; - const comment = SAMPLE_QUESTION_COMMENT; + const { questionId } = router.query; + + const { data: question } = trpc.useQuery([ + 'questions.questions.getQuestionById', + { id: questionId as string }, + ]); + + const utils = trpc.useContext(); + + const { data: comments } = trpc.useQuery([ + 'questions.questions.comments.getQuestionComments', + { questionId: questionId as string }, + ]); + + const { mutate: addComment } = trpc.useMutation( + 'questions.questions.comments.create', + { + onSuccess: () => { + utils.invalidateQueries( + 'questions.questions.comments.getQuestionComments', + ); + }, + }, + ); + + const { data: answers } = trpc.useQuery([ + 'questions.answers.getAnswers', + { questionId: questionId as string }, + ]); + + const { mutate: addAnswer } = trpc.useMutation('questions.answers.create', { + onSuccess: () => { + utils.invalidateQueries('questions.answers.getAnswers'); + }, + }); + const handleBackNavigation = () => { router.back(); }; const handleSubmitAnswer = (data: AnswerQuestionData) => { - // eslint-disable-next-line no-console - console.log(data); + addAnswer({ + content: data.answerContent, + questionId: questionId as string, + }); }; const handleSubmitComment = (data: QuestionCommentData) => { - // eslint-disable-next-line no-console - console.log(data); + addComment({ + content: data.commentContent, + questionId: questionId as string, + }); + resetComment(); }; + if (!question) { + return ; + } + return (
@@ -67,9 +113,15 @@ export default function QuestionPage() {
- +
- +
@@ -106,7 +158,8 @@ export default function QuestionPage() { onChange={(value) => { // eslint-disable-next-line no-console console.log(value); - }}> + }} + />
- - {Array.from({ length: question.answerCount }).map((_, index) => ( + {(answers ?? []).map((answer) => ( ))}
diff --git a/apps/portal/src/pages/questions/index.tsx b/apps/portal/src/pages/questions/index.tsx index 0db97b18..331ef57b 100644 --- a/apps/portal/src/pages/questions/index.tsx +++ b/apps/portal/src/pages/questions/index.tsx @@ -1,5 +1,6 @@ import { useRouter } from 'next/router'; import { useEffect, useMemo, useState } from 'react'; +import type { QuestionsQuestionType } from '@prisma/client'; import QuestionOverviewCard from '~/components/questions/card/QuestionOverviewCard'; import ContributeQuestionCard from '~/components/questions/ContributeQuestionCard'; @@ -13,32 +14,54 @@ import { LOCATIONS, QUESTION_AGES, QUESTION_TYPES, - SAMPLE_QUESTION, } from '~/utils/questions/constants'; +import createSlug from '~/utils/questions/createSlug'; import { useSearchFilter, useSearchFilterSingle, } from '~/utils/questions/useSearchFilter'; +import { trpc } from '~/utils/trpc'; export default function QuestionsHomePage() { const router = useRouter(); const [selectedCompanies, setSelectedCompanies, areCompaniesInitialized] = useSearchFilter('companies'); - const [ selectedQuestionTypes, setSelectedQuestionTypes, areQuestionTypesInitialized, - ] = useSearchFilter('questionTypes'); + ] = useSearchFilter('questionTypes'); const [ selectedQuestionAge, setSelectedQuestionAge, isQuestionAgeInitialized, - ] = useSearchFilterSingle('questionAge', 'all'); + ] = useSearchFilterSingle('questionAge', 'all'); const [selectedLocations, setSelectedLocations, areLocationsInitialized] = useSearchFilter('locations'); + // TODO: Implement filtering + const { data: questions } = trpc.useQuery([ + 'questions.questions.getQuestionsByFilter', + { + // TODO: Update when query accepts multiple question types + questionType: + selectedQuestionTypes.length > 0 + ? (selectedQuestionTypes[0].toUpperCase() as QuestionsQuestionType) + : 'CODING', + }, + ]); + + const utils = trpc.useContext(); + const { mutate: createQuestion } = trpc.useMutation( + 'questions.questions.create', + { + onSuccess: () => { + utils.invalidateQueries('questions.questions.getQuestionsByFilter'); + }, + }, + ); + const [hasLanded, setHasLanded] = useState(false); const [loaded, setLoaded] = useState(false); @@ -74,7 +97,7 @@ export default function QuestionsHomePage() { const { company, location, questionType } = data; setSelectedCompanies([company]); setSelectedLocations([location]); - setSelectedQuestionTypes([questionType]); + setSelectedQuestionTypes([questionType as QuestionsQuestionType]); setHasLanded(true); }; @@ -184,7 +207,17 @@ export default function QuestionsHomePage() {
- + { + createQuestion({ + company: data.company, + content: data.questionContent, + location: data.location, + questionType: data.questionType, + seenAt: data.date, + }); + }} + /> - {Array.from({ length: 10 }).map((_, index) => ( + {(questions ?? []).map((question) => ( ))}
diff --git a/apps/portal/src/server/router/questions-answer-comment-router.ts b/apps/portal/src/server/router/questions-answer-comment-router.ts index b4578cc5..51d17d34 100644 --- a/apps/portal/src/server/router/questions-answer-comment-router.ts +++ b/apps/portal/src/server/router/questions-answer-comment-router.ts @@ -12,57 +12,52 @@ export const questionsAnswerCommentRouter = createProtectedRouter() answerId: z.string(), }), async resolve({ ctx, input }) { - const questionAnswerCommentsData = await ctx.prisma.questionsAnswerComment.findMany({ - include: { - user: { - select: { - name: true, + const questionAnswerCommentsData = + await ctx.prisma.questionsAnswerComment.findMany({ + include: { + user: { + select: { + name: true, + }, + }, + votes: true, }, - }, - votes: true, - }, - orderBy: { - createdAt: 'desc', - }, - where: { - ...input, - }, - }); - return questionAnswerCommentsData.map((data) => { - const votes:number = data.votes.reduce( - (previousValue:number, currentValue) => { - let result:number = previousValue; - - switch(currentValue.vote) { - case Vote.UPVOTE: - result += 1 - break; - case Vote.DOWNVOTE: - result -= 1 - break; - } - return result; - }, - 0 + orderBy: { + createdAt: 'desc', + }, + where: { + ...input, + }, + }); + return questionAnswerCommentsData.map((data) => { + const votes: number = data.votes.reduce( + (previousValue: number, currentValue) => { + let result: number = previousValue; + + switch (currentValue.vote) { + case Vote.UPVOTE: + result += 1; + break; + case Vote.DOWNVOTE: + result -= 1; + break; + } + return result; + }, + 0, ); - let userName = ""; - - if (data.user) { - userName = data.user.name!; - } - - - const answerComment: AnswerComment = { - content: data.content, - id: data.id, - numVotes: votes, - updatedAt: data.updatedAt, - user: userName, - }; - return answerComment; - }); - } + const answerComment: AnswerComment = { + content: data.content, + createdAt: data.createdAt, + id: data.id, + numVotes: votes, + updatedAt: data.updatedAt, + user: data.user?.name ?? '', + }; + return answerComment; + }); + }, }) .mutation('create', { input: z.object({ @@ -88,11 +83,12 @@ export const questionsAnswerCommentRouter = createProtectedRouter() async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - const answerCommentToUpdate = await ctx.prisma.questionsAnswerComment.findUnique({ - where: { - id: input.id, - }, - }); + const answerCommentToUpdate = + await ctx.prisma.questionsAnswerComment.findUnique({ + where: { + id: input.id, + }, + }); if (answerCommentToUpdate?.id !== userId) { throw new TRPCError({ @@ -118,11 +114,12 @@ export const questionsAnswerCommentRouter = createProtectedRouter() async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - const answerCommentToDelete = await ctx.prisma.questionsAnswerComment.findUnique({ - where: { - id: input.id, - }, - }); + const answerCommentToDelete = + await ctx.prisma.questionsAnswerComment.findUnique({ + where: { + id: input.id, + }, + }); if (answerCommentToDelete?.id !== userId) { throw new TRPCError({ @@ -144,11 +141,11 @@ export const questionsAnswerCommentRouter = createProtectedRouter() }), async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - const {answerCommentId} = input + const { answerCommentId } = input; return await ctx.prisma.questionsAnswerCommentVote.findUnique({ where: { - answerCommentId_userId : {answerCommentId,userId }, + answerCommentId_userId: { answerCommentId, userId }, }, }); }, @@ -176,13 +173,14 @@ export const questionsAnswerCommentRouter = createProtectedRouter() }), async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - const {id, vote} = input + const { id, vote } = input; - const voteToUpdate = await ctx.prisma.questionsAnswerCommentVote.findUnique({ - where: { - id: input.id, - }, - }); + const voteToUpdate = + await ctx.prisma.questionsAnswerCommentVote.findUnique({ + where: { + id: input.id, + }, + }); if (voteToUpdate?.id !== userId) { throw new TRPCError({ @@ -208,10 +206,12 @@ export const questionsAnswerCommentRouter = createProtectedRouter() async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - const voteToDelete = await ctx.prisma.questionsAnswerCommentVote.findUnique({ - where: { - id: input.id, - },}); + const voteToDelete = + await ctx.prisma.questionsAnswerCommentVote.findUnique({ + where: { + id: input.id, + }, + }); if (voteToDelete?.id !== userId) { throw new TRPCError({ @@ -226,4 +226,4 @@ export const questionsAnswerCommentRouter = createProtectedRouter() }, }); }, - }); \ No newline at end of file + }); diff --git a/apps/portal/src/server/router/questions-answer-router.ts b/apps/portal/src/server/router/questions-answer-router.ts index 61a5b6c5..21095bf7 100644 --- a/apps/portal/src/server/router/questions-answer-router.ts +++ b/apps/portal/src/server/router/questions-answer-router.ts @@ -14,17 +14,17 @@ export const questionsAnswerRouter = createProtectedRouter() async resolve({ ctx, input }) { const answersData = await ctx.prisma.questionsAnswer.findMany({ include: { - _count: { - select: { - comments: true, + _count: { + select: { + comments: true, + }, }, - }, - user: { - select: { - name: true, + user: { + select: { + name: true, + }, }, - }, - votes: true, + votes: true, }, orderBy: { createdAt: 'desc', @@ -32,43 +32,93 @@ export const questionsAnswerRouter = createProtectedRouter() where: { ...input, }, - }); - return answersData.map((data) => { - const votes:number = data.votes.reduce( - (previousValue:number, currentValue) => { - let result:number = previousValue; - - switch(currentValue.vote) { - case Vote.UPVOTE: - result += 1 - break; - case Vote.DOWNVOTE: - result -= 1 - break; - } - return result; - }, - 0 - ); + }); + return answersData.map((data) => { + const votes: number = data.votes.reduce( + (previousValue: number, currentValue) => { + let result: number = previousValue; - let userName = ""; + switch (currentValue.vote) { + case Vote.UPVOTE: + result += 1; + break; + case Vote.DOWNVOTE: + result -= 1; + break; + } + return result; + }, + 0, + ); - if (data.user) { - userName = data.user.name!; + const answer: Answer = { + content: data.content, + createdAt: data.createdAt, + id: data.id, + numComments: data._count.comments, + numVotes: votes, + user: data.user?.name ?? '', + }; + return answer; + }); + }, + }) + .query('getAnswerById', { + input: z.object({ + answerId: z.string(), + }), + async resolve({ ctx, input }) { + const answerData = await ctx.prisma.questionsAnswer.findUnique({ + include: { + _count: { + select: { + comments: true, + }, + }, + user: { + select: { + name: true, + }, + }, + votes: true, + }, + where: { + id: input.answerId, + }, + }); + if (!answerData) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Answer not found', + }); } + const votes: number = answerData.votes.reduce( + (previousValue: number, currentValue) => { + let result: number = previousValue; + switch (currentValue.vote) { + case Vote.UPVOTE: + result += 1; + break; + case Vote.DOWNVOTE: + result -= 1; + break; + } + return result; + }, + 0, + ); const answer: Answer = { - content: data.content, - createdAt: data.createdAt, - id: data.id, - numComments: data._count.comments, + content: answerData.content, + createdAt: answerData.createdAt, + id: answerData.id, + numComments: answerData._count.comments, numVotes: votes, - user: userName, + user: answerData.user?.name ?? '', }; return answer; - }); - } + }, }) .mutation('create', { input: z.object({ @@ -93,7 +143,7 @@ export const questionsAnswerRouter = createProtectedRouter() }), async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - const {content, id} = input + const { content, id } = input; const answerToUpdate = await ctx.prisma.questionsAnswer.findUnique({ where: { @@ -128,7 +178,8 @@ export const questionsAnswerRouter = createProtectedRouter() const answerToDelete = await ctx.prisma.questionsAnswer.findUnique({ where: { id: input.id, - },}); + }, + }); if (answerToDelete?.id !== userId) { throw new TRPCError({ @@ -150,11 +201,11 @@ export const questionsAnswerRouter = createProtectedRouter() }), async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - const {answerId} = input + const { answerId } = input; return await ctx.prisma.questionsAnswerVote.findUnique({ where: { - answerId_userId : { answerId, userId }, + answerId_userId: { answerId, userId }, }, }); }, @@ -182,7 +233,7 @@ export const questionsAnswerRouter = createProtectedRouter() }), async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - const {id, vote} = input + const { id, vote } = input; const voteToUpdate = await ctx.prisma.questionsAnswerVote.findUnique({ where: { @@ -217,7 +268,8 @@ export const questionsAnswerRouter = createProtectedRouter() const voteToDelete = await ctx.prisma.questionsAnswerVote.findUnique({ where: { id: input.id, - },}); + }, + }); if (voteToDelete?.id !== userId) { throw new TRPCError({ @@ -232,4 +284,4 @@ export const questionsAnswerRouter = createProtectedRouter() }, }); }, - }); \ No newline at end of file + }); diff --git a/apps/portal/src/server/router/questions-question-comment-router.ts b/apps/portal/src/server/router/questions-question-comment-router.ts index 12f2bc21..82345f06 100644 --- a/apps/portal/src/server/router/questions-question-comment-router.ts +++ b/apps/portal/src/server/router/questions-question-comment-router.ts @@ -12,56 +12,51 @@ export const questionsQuestionCommentRouter = createProtectedRouter() questionId: z.string(), }), async resolve({ ctx, input }) { - const questionCommentsData = await ctx.prisma.questionsQuestionComment.findMany({ - include: { - user: { - select: { - name: true, + const questionCommentsData = + await ctx.prisma.questionsQuestionComment.findMany({ + include: { + user: { + select: { + name: true, + }, + }, + votes: true, }, - }, - votes: true, - }, - orderBy: { - createdAt: 'desc', - }, - where: { - ...input, - }, - }); - return questionCommentsData.map((data) => { - const votes:number = data.votes.reduce( - (previousValue:number, currentValue) => { - let result:number = previousValue; - - switch(currentValue.vote) { - case Vote.UPVOTE: - result += 1 - break; - case Vote.DOWNVOTE: - result -= 1 - break; - } - return result; - }, - 0 + orderBy: { + createdAt: 'desc', + }, + where: { + ...input, + }, + }); + return questionCommentsData.map((data) => { + const votes: number = data.votes.reduce( + (previousValue: number, currentValue) => { + let result: number = previousValue; + + switch (currentValue.vote) { + case Vote.UPVOTE: + result += 1; + break; + case Vote.DOWNVOTE: + result -= 1; + break; + } + return result; + }, + 0, ); - let userName = ""; - - if (data.user) { - userName = data.user.name!; - } - - const questionComment: QuestionComment = { - content: data.content, - createdAt: data.createdAt, - id: data.id, - numVotes: votes, - user: userName, - }; - return questionComment; - }); - } + const questionComment: QuestionComment = { + content: data.content, + createdAt: data.createdAt, + id: data.id, + numVotes: votes, + user: data.user?.name ?? '', + }; + return questionComment; + }); + }, }) .mutation('create', { input: z.object({ @@ -87,11 +82,12 @@ export const questionsQuestionCommentRouter = createProtectedRouter() async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - const questionCommentToUpdate = await ctx.prisma.questionsQuestionComment.findUnique({ - where: { - id: input.id, - }, - }); + const questionCommentToUpdate = + await ctx.prisma.questionsQuestionComment.findUnique({ + where: { + id: input.id, + }, + }); if (questionCommentToUpdate?.id !== userId) { throw new TRPCError({ @@ -117,11 +113,12 @@ export const questionsQuestionCommentRouter = createProtectedRouter() async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - const questionCommentToDelete = await ctx.prisma.questionsQuestionComment.findUnique({ - where: { - id: input.id, - }, - }); + const questionCommentToDelete = + await ctx.prisma.questionsQuestionComment.findUnique({ + where: { + id: input.id, + }, + }); if (questionCommentToDelete?.id !== userId) { throw new TRPCError({ @@ -143,11 +140,11 @@ export const questionsQuestionCommentRouter = createProtectedRouter() }), async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - const {questionCommentId} = input + const { questionCommentId } = input; return await ctx.prisma.questionsQuestionCommentVote.findUnique({ where: { - questionCommentId_userId : {questionCommentId,userId }, + questionCommentId_userId: { questionCommentId, userId }, }, }); }, @@ -175,13 +172,14 @@ export const questionsQuestionCommentRouter = createProtectedRouter() }), async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - const {id, vote} = input + const { id, vote } = input; - const voteToUpdate = await ctx.prisma.questionsQuestionCommentVote.findUnique({ - where: { - id: input.id, - }, - }); + const voteToUpdate = + await ctx.prisma.questionsQuestionCommentVote.findUnique({ + where: { + id: input.id, + }, + }); if (voteToUpdate?.id !== userId) { throw new TRPCError({ @@ -207,10 +205,12 @@ export const questionsQuestionCommentRouter = createProtectedRouter() async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - const voteToDelete = await ctx.prisma.questionsQuestionCommentVote.findUnique({ - where: { - id: input.id, - },}); + const voteToDelete = + await ctx.prisma.questionsQuestionCommentVote.findUnique({ + where: { + id: input.id, + }, + }); if (voteToDelete?.id !== userId) { throw new TRPCError({ @@ -225,4 +225,4 @@ export const questionsQuestionCommentRouter = createProtectedRouter() }, }); }, - }); \ No newline at end of file + }); diff --git a/apps/portal/src/server/router/questions-question-router.ts b/apps/portal/src/server/router/questions-question-router.ts index 8918f036..bd65474a 100644 --- a/apps/portal/src/server/router/questions-question-router.ts +++ b/apps/portal/src/server/router/questions-question-router.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import {QuestionsQuestionType, Vote } from '@prisma/client'; +import { QuestionsQuestionType, Vote } from '@prisma/client'; import { TRPCError } from '@trpc/server'; import { createProtectedRouter } from './context'; @@ -17,28 +17,29 @@ export const questionsQuestionRouter = createProtectedRouter() async resolve({ ctx, input }) { const questionsData = await ctx.prisma.questionsQuestion.findMany({ include: { - _count: { - select: { - answers: true, - comments: true, + _count: { + select: { + answers: true, + comments: true, + }, }, - }, - encounters: { - select: { - company: true, - location: true, - role: true, + encounters: { + select: { + company: true, + location: true, + role: true, + seenAt: true, + }, }, - }, - user: { - select: { - name: true, + user: { + select: { + name: true, + }, }, - }, - votes: true, + votes: true, }, orderBy: { - createdAt: 'desc', + createdAt: 'desc', }, where: { questionType: input.questionType, @@ -47,68 +48,159 @@ export const questionsQuestionRouter = createProtectedRouter() return questionsData .filter((data) => { for (let i = 0; i < data.encounters.length; i++) { - const encounter = data.encounters[i] - const matchCompany = (!input.company || (encounter.company === input.company)); - const matchLocation = (!input.location || (encounter.location === input.location)); - const matchRole = (!input.company || (encounter.role === input.role)); - if (matchCompany && matchLocation && matchRole) {return true}; + const encounter = data.encounters[i]; + const matchCompany = + !input.company || encounter.company === input.company; + const matchLocation = + !input.location || encounter.location === input.location; + const matchRole = !input.company || encounter.role === input.role; + if (matchCompany && matchLocation && matchRole) { + return true; + } } return false; }) .map((data) => { - const votes:number = data.votes.reduce( - (previousValue:number, currentValue) => { - let result:number = previousValue; + const votes: number = data.votes.reduce( + (previousValue: number, currentValue) => { + let result: number = previousValue; - switch(currentValue.vote) { - case Vote.UPVOTE: - result += 1 - break; - case Vote.DOWNVOTE: - result -= 1 - break; + switch (currentValue.vote) { + case Vote.UPVOTE: + result += 1; + break; + case Vote.DOWNVOTE: + result -= 1; + break; } return result; }, - 0 - ); - - let userName = ""; - - if (data.user) { - userName = data.user.name!; - } + 0, + ); const question: Question = { - company: "", + company: data.encounters[0].company, content: data.content, id: data.id, - location: "", + location: data.encounters[0].location ?? 'Unknown location', numAnswers: data._count.answers, numComments: data._count.comments, numVotes: votes, - role: "", + role: data.encounters[0].role ?? 'Unknown role', + seenAt: data.encounters[0].seenAt, + type: data.questionType, updatedAt: data.updatedAt, - user: userName, + user: data.user?.name ?? '', }; return question; + }); + }, + }) + .query('getQuestionById', { + input: z.object({ + id: z.string(), + }), + async resolve({ ctx, input }) { + const questionData = await ctx.prisma.questionsQuestion.findUnique({ + include: { + _count: { + select: { + answers: true, + comments: true, + }, + }, + encounters: { + select: { + company: true, + location: true, + role: true, + seenAt: true, + }, + }, + user: { + select: { + name: true, + }, + }, + votes: true, + }, + where: { + id: input.id, + }, }); - } + if (!questionData) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Question not found', + }); + } + const votes: number = questionData.votes.reduce( + (previousValue: number, currentValue) => { + let result: number = previousValue; + + switch (currentValue.vote) { + case Vote.UPVOTE: + result += 1; + break; + case Vote.DOWNVOTE: + result -= 1; + break; + } + return result; + }, + 0, + ); + + const question: Question = { + company: questionData.encounters[0].company, + content: questionData.content, + id: questionData.id, + location: questionData.encounters[0].location ?? 'Unknown location', + numAnswers: questionData._count.answers, + numComments: questionData._count.comments, + numVotes: votes, + role: questionData.encounters[0].role ?? 'Unknown role', + seenAt: questionData.encounters[0].seenAt, + type: questionData.questionType, + updatedAt: questionData.updatedAt, + user: questionData.user?.name ?? '', + }; + return question; + }, }) .mutation('create', { input: z.object({ + company: z.string(), content: z.string(), + location: z.string(), questionType: z.nativeEnum(QuestionsQuestionType), + role: z.string().optional(), + seenAt: z.date(), }), async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - return await ctx.prisma.questionsQuestion.create({ + const question = await ctx.prisma.questionsQuestion.create({ data: { - ...input, + content: input.content, + questionType: input.questionType, userId, }, }); + + // Create question encounter + await ctx.prisma.questionsQuestionEncounter.create({ + data: { + company: input.company, + location: input.location, + questionId: question.id, + role: input.role, + seenAt: input.seenAt, + userId, + }, + }); + + return question; }, }) .mutation('update', { @@ -116,7 +208,6 @@ export const questionsQuestionRouter = createProtectedRouter() content: z.string().optional(), id: z.string(), questionType: z.nativeEnum(QuestionsQuestionType).optional(), - }), async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; @@ -179,11 +270,11 @@ export const questionsQuestionRouter = createProtectedRouter() }), async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - const {questionId} = input + const { questionId } = input; return await ctx.prisma.questionsQuestionVote.findUnique({ where: { - questionId_userId : {questionId,userId } + questionId_userId: { questionId, userId }, }, }); }, @@ -211,7 +302,7 @@ export const questionsQuestionRouter = createProtectedRouter() }), async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - const {id, vote} = input + const { id, vote } = input; const voteToUpdate = await ctx.prisma.questionsQuestionVote.findUnique({ where: { @@ -246,7 +337,8 @@ export const questionsQuestionRouter = createProtectedRouter() const voteToDelete = await ctx.prisma.questionsQuestionVote.findUnique({ where: { id: input.id, - },}); + }, + }); if (voteToDelete?.id !== userId) { throw new TRPCError({ @@ -261,4 +353,4 @@ export const questionsQuestionRouter = createProtectedRouter() }, }); }, - }); \ No newline at end of file + }); diff --git a/apps/portal/src/types/questions.d.ts b/apps/portal/src/types/questions.d.ts index 0d60ca76..3797bf38 100644 --- a/apps/portal/src/types/questions.d.ts +++ b/apps/portal/src/types/questions.d.ts @@ -8,14 +8,19 @@ export type Question = { numComments: number; numVotes: number; role: string; + seenAt: Date; + type: stringl; updatedAt: Date; user: string; }; export type AnswerComment = { content: string; + createdAt: Date; id: string; numVotes: number; + updatedAt: Date; + user: string; }; export type Answer = { @@ -24,6 +29,7 @@ export type Answer = { id: string; numComments: number; numVotes: number; + user: string; }; export type QuestionComment = { diff --git a/apps/portal/src/utils/questions/constants.ts b/apps/portal/src/utils/questions/constants.ts index 966e1a1f..155359d5 100644 --- a/apps/portal/src/utils/questions/constants.ts +++ b/apps/portal/src/utils/questions/constants.ts @@ -1,3 +1,5 @@ +import type { QuestionsQuestionType } from '@prisma/client'; + import type { FilterChoices } from '~/components/questions/filter/FilterSection'; export const COMPANIES: FilterChoices = [ @@ -12,18 +14,18 @@ export const COMPANIES: FilterChoices = [ ]; // Code, design, behavioral -export const QUESTION_TYPES: FilterChoices = [ +export const QUESTION_TYPES: FilterChoices = [ { label: 'Coding', - value: 'coding', + value: 'CODING', }, { label: 'Design', - value: 'design', + value: 'SYSTEM_DESIGN', }, { label: 'Behavioral', - value: 'behavioral', + value: 'BEHAVIORAL', }, ]; diff --git a/apps/portal/src/utils/questions/createSlug.ts b/apps/portal/src/utils/questions/createSlug.ts new file mode 100644 index 00000000..9c8a81ae --- /dev/null +++ b/apps/portal/src/utils/questions/createSlug.ts @@ -0,0 +1,7 @@ +export default function createSlug(content: string) { + return content + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/(^-|-$)+/g, '') + .substring(0, 100); +} diff --git a/apps/portal/src/utils/questions/useSearchFilter.ts b/apps/portal/src/utils/questions/useSearchFilter.ts index 711f5ead..392a69c8 100644 --- a/apps/portal/src/utils/questions/useSearchFilter.ts +++ b/apps/portal/src/utils/questions/useSearchFilter.ts @@ -1,14 +1,14 @@ import { useRouter } from 'next/router'; import { useCallback, useEffect, useState } from 'react'; -export const useSearchFilter = ( +export const useSearchFilter = ( name: string, - defaultValues?: Array, + defaultValues?: Array, ) => { const [isInitialized, setIsInitialized] = useState(false); const router = useRouter(); - const [filters, setFilters] = useState>(defaultValues || []); + const [filters, setFilters] = useState>(defaultValues || []); useEffect(() => { if (router.isReady && !isInitialized) { @@ -16,7 +16,7 @@ export const useSearchFilter = ( const query = router.query[name]; if (query) { const queryValues = Array.isArray(query) ? query : [query]; - setFilters(queryValues); + setFilters(queryValues as Array); } else { // Try to load from local storage const localStorageValue = localStorage.getItem(name); @@ -37,7 +37,7 @@ export const useSearchFilter = ( }, [isInitialized, name, router]); const setFiltersCallback = useCallback( - (newFilters: Array) => { + (newFilters: Array) => { setFilters(newFilters); localStorage.setItem(name, JSON.stringify(newFilters)); router.replace({ @@ -54,14 +54,17 @@ export const useSearchFilter = ( return [filters, setFiltersCallback, isInitialized] as const; }; -export const useSearchFilterSingle = (name: string, defaultValue: string) => { +export const useSearchFilterSingle = ( + name: string, + defaultValue: Value, +) => { const [filters, setFilters, isInitialized] = useSearchFilter(name, [ defaultValue, ]); return [ filters[0], - (value: string) => { + (value: Value) => { setFilters([value]); }, isInitialized,