From a7c9f58ef3e4f1f09ca609e28c4989850dd1e071 Mon Sep 17 00:00:00 2001 From: Jeff Sieu Date: Mon, 7 Nov 2022 01:51:22 +0800 Subject: [PATCH] [questions][fix] fix upvotes (#521) --- .../components/questions/card/AnswerCard.tsx | 2 +- .../card/question/BaseQuestionCard.tsx | 2 +- .../comments/AnswerCommentListItem.tsx | 28 +++ .../CommentListItem.tsx} | 26 +-- .../comments/QuestionCommentListItem.tsx | 28 +++ .../answer/[answerId]/[answerSlug]/index.tsx | 27 ++- .../[questionId]/[questionSlug]/index.tsx | 60 +++--- .../questions-answer-comment-router.ts | 59 +++--- .../questions/questions-answer-router.ts | 5 +- .../questions-question-comment-router.ts | 4 +- .../questions-question-comment-user-router.ts | 13 +- .../questions/vote/useAnswerCommentVote.ts | 68 +++++++ .../src/utils/questions/vote/useAnswerVote.ts | 98 ++++++++++ .../questions/vote/useQuestionCommentVote.ts | 69 +++++++ .../utils/questions/vote/useQuestionVote.ts | 98 ++++++++++ .../src/utils/questions/{ => vote}/useVote.ts | 174 ++++-------------- 16 files changed, 523 insertions(+), 238 deletions(-) create mode 100644 apps/portal/src/components/questions/comments/AnswerCommentListItem.tsx rename apps/portal/src/components/questions/{AnswerCommentListItem.tsx => comments/CommentListItem.tsx} (67%) create mode 100644 apps/portal/src/components/questions/comments/QuestionCommentListItem.tsx create mode 100644 apps/portal/src/utils/questions/vote/useAnswerCommentVote.ts create mode 100644 apps/portal/src/utils/questions/vote/useAnswerVote.ts create mode 100644 apps/portal/src/utils/questions/vote/useQuestionCommentVote.ts create mode 100644 apps/portal/src/utils/questions/vote/useQuestionVote.ts rename apps/portal/src/utils/questions/{ => vote}/useVote.ts (53%) diff --git a/apps/portal/src/components/questions/card/AnswerCard.tsx b/apps/portal/src/components/questions/card/AnswerCard.tsx index 5407e95e..68e94d17 100644 --- a/apps/portal/src/components/questions/card/AnswerCard.tsx +++ b/apps/portal/src/components/questions/card/AnswerCard.tsx @@ -1,7 +1,7 @@ import { format } from 'date-fns'; import { ChatBubbleLeftRightIcon } from '@heroicons/react/24/outline'; -import { useAnswerVote } from '~/utils/questions/useVote'; +import useAnswerVote from '~/utils/questions/vote/useAnswerVote'; import type { VotingButtonsProps } from '../VotingButtons'; import VotingButtons from '../VotingButtons'; diff --git a/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx b/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx index a917bc87..77b7f5c9 100644 --- a/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx +++ b/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx @@ -10,7 +10,7 @@ import type { QuestionsQuestionType } from '@prisma/client'; import { Button } from '@tih/ui'; import { useProtectedCallback } from '~/utils/questions/useProtectedCallback'; -import { useQuestionVote } from '~/utils/questions/useVote'; +import { useQuestionVote } from '~/utils/questions/vote/useQuestionVote'; import AddToListDropdown from '../../AddToListDropdown'; import type { CreateQuestionEncounterData } from '../../forms/CreateQuestionEncounterForm'; diff --git a/apps/portal/src/components/questions/comments/AnswerCommentListItem.tsx b/apps/portal/src/components/questions/comments/AnswerCommentListItem.tsx new file mode 100644 index 00000000..0a4f76b6 --- /dev/null +++ b/apps/portal/src/components/questions/comments/AnswerCommentListItem.tsx @@ -0,0 +1,28 @@ +import useAnswerCommentVote from '~/utils/questions/vote/useAnswerCommentVote'; + +import type { CommentListItemProps } from './CommentListItem'; +import CommentListItem from './CommentListItem'; + +export type AnswerCommentListItemProps = Omit< + CommentListItemProps, + 'onDownvote' | 'onUpvote' | 'vote' +> & { + answerCommentId: string; +}; + +export default function AnswerCommentListItem({ + answerCommentId, + ...restProps +}: AnswerCommentListItemProps) { + const { handleDownvote, handleUpvote, vote } = + useAnswerCommentVote(answerCommentId); + + return ( + + ); +} diff --git a/apps/portal/src/components/questions/AnswerCommentListItem.tsx b/apps/portal/src/components/questions/comments/CommentListItem.tsx similarity index 67% rename from apps/portal/src/components/questions/AnswerCommentListItem.tsx rename to apps/portal/src/components/questions/comments/CommentListItem.tsx index a51cd291..1039e51c 100644 --- a/apps/portal/src/components/questions/AnswerCommentListItem.tsx +++ b/apps/portal/src/components/questions/comments/CommentListItem.tsx @@ -1,37 +1,37 @@ import { format } from 'date-fns'; -import { useAnswerCommentVote } from '~/utils/questions/useVote'; +import type { BackendVote } from '../VotingButtons'; +import VotingButtons from '../VotingButtons'; -import VotingButtons from './VotingButtons'; - -export type AnswerCommentListItemProps = { - answerCommentId: string; +export type CommentListItemProps = { authorImageUrl: string; authorName: string; content: string; createdAt: Date; + onDownvote: () => void; + onUpvote: () => void; upvoteCount: number; + vote: BackendVote; }; -export default function AnswerCommentListItem({ +export default function CommentListItem({ authorImageUrl, authorName, content, createdAt, upvoteCount, - answerCommentId, -}: AnswerCommentListItemProps) { - const { handleDownvote, handleUpvote, vote } = - useAnswerCommentVote(answerCommentId); - + vote, + onDownvote, + onUpvote, +}: CommentListItemProps) { return (
diff --git a/apps/portal/src/components/questions/comments/QuestionCommentListItem.tsx b/apps/portal/src/components/questions/comments/QuestionCommentListItem.tsx new file mode 100644 index 00000000..0a8cabd6 --- /dev/null +++ b/apps/portal/src/components/questions/comments/QuestionCommentListItem.tsx @@ -0,0 +1,28 @@ +import useQuestionCommentVote from '~/utils/questions/vote/useQuestionCommentVote'; + +import type { CommentListItemProps } from './CommentListItem'; +import CommentListItem from './CommentListItem'; + +export type QuestionCommentListItemProps = Omit< + CommentListItemProps, + 'onDownvote' | 'onUpvote' | 'vote' +> & { + questionCommentId: string; +}; + +export default function QuestionCommentListItem({ + questionCommentId, + ...restProps +}: QuestionCommentListItemProps) { + const { handleDownvote, handleUpvote, vote } = + useQuestionCommentVote(questionCommentId); + + return ( + + ); +} 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 1fa4d2e0..410f03dc 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,8 +5,8 @@ import { useForm } from 'react-hook-form'; import { Button, TextArea } from '@tih/ui'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; -import AnswerCommentListItem from '~/components/questions/AnswerCommentListItem'; import FullAnswerCard from '~/components/questions/card/FullAnswerCard'; +import AnswerCommentListItem from '~/components/questions/comments/AnswerCommentListItem'; import FullScreenSpinner from '~/components/questions/FullScreenSpinner'; import BackButtonLayout from '~/components/questions/layout/BackButtonLayout'; import PaginationLoadMoreButton from '~/components/questions/PaginationLoadMoreButton'; @@ -158,19 +158,18 @@ export default function QuestionPage() {
{/* TODO: Allow to load more pages */} - {(answerCommentsData?.pages ?? []).flatMap( - ({ processedQuestionAnswerCommentsData: comments }) => - comments.map((comment) => ( - - )), + {(answerCommentsData?.pages ?? []).flatMap(({ data: comments }) => + comments.map((comment) => ( + + )), )}
diff --git a/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx b/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx index c4db487d..9fdee125 100644 --- a/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx +++ b/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx @@ -5,9 +5,9 @@ import { useForm } from 'react-hook-form'; import { Button, Collapsible, HorizontalDivider, TextArea } from '@tih/ui'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; -import AnswerCommentListItem from '~/components/questions/AnswerCommentListItem'; import FullQuestionCard from '~/components/questions/card/question/FullQuestionCard'; import QuestionAnswerCard from '~/components/questions/card/QuestionAnswerCard'; +import QuestionCommentListItem from '~/components/questions/comments/QuestionCommentListItem'; import FullScreenSpinner from '~/components/questions/FullScreenSpinner'; import BackButtonLayout from '~/components/questions/layout/BackButtonLayout'; import PaginationLoadMoreButton from '~/components/questions/PaginationLoadMoreButton'; @@ -245,19 +245,18 @@ export default function QuestionPage() { /> - {(commentData?.pages ?? []).flatMap( - ({ processedQuestionCommentsData: comments }) => - comments.map((comment) => ( - - )), + {(commentData?.pages ?? []).flatMap(({ data: comments }) => + comments.map((comment) => ( + + )), )}
{/* TODO: Add button to load more */} - {(answerData?.pages ?? []).flatMap( - ({ processedAnswersData: answers }) => - answers.map((answer) => ( - - )), + {(answerData?.pages ?? []).flatMap(({ data: answers }) => + answers.map((answer) => ( + + )), )} diff --git a/apps/portal/src/server/router/questions/questions-answer-comment-router.ts b/apps/portal/src/server/router/questions/questions-answer-comment-router.ts index 40eb6595..f027ac78 100644 --- a/apps/portal/src/server/router/questions/questions-answer-comment-router.ts +++ b/apps/portal/src/server/router/questions/questions-answer-comment-router.ts @@ -56,35 +56,36 @@ export const questionsAnswerCommentRouter = createRouter().query( answerId, }, }); - const processedQuestionAnswerCommentsData = questionAnswerCommentsData.map((data) => { - const votes: number = data.votes.reduce( - (previousValue: number, currentValue) => { - let result: number = previousValue; + const processedQuestionAnswerCommentsData = + 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, - ); + switch (currentValue.vote) { + case Vote.UPVOTE: + result += 1; + break; + case Vote.DOWNVOTE: + result -= 1; + break; + } + return result; + }, + 0, + ); - const answerComment: AnswerComment = { - content: data.content, - createdAt: data.createdAt, - id: data.id, - numVotes: votes, - updatedAt: data.updatedAt, - user: data.user?.name ?? '', - userImage: data.user?.image ?? '', - }; - return answerComment; - }); + const answerComment: AnswerComment = { + content: data.content, + createdAt: data.createdAt, + id: data.id, + numVotes: votes, + updatedAt: data.updatedAt, + user: data.user?.name ?? '', + userImage: data.user?.image ?? '', + }; + return answerComment; + }); let nextCursor: typeof cursor | undefined = undefined; @@ -98,9 +99,9 @@ export const questionsAnswerCommentRouter = createRouter().query( } return { + data: processedQuestionAnswerCommentsData, nextCursor, - processedQuestionAnswerCommentsData, - } + }; }, }, ); diff --git a/apps/portal/src/server/router/questions/questions-answer-router.ts b/apps/portal/src/server/router/questions/questions-answer-router.ts index e2e11715..a1c51b56 100644 --- a/apps/portal/src/server/router/questions/questions-answer-router.ts +++ b/apps/portal/src/server/router/questions/questions-answer-router.ts @@ -38,7 +38,6 @@ export const questionsAnswerRouter = createRouter() }, ]; - const answersData = await ctx.prisma.questionsAnswer.findMany({ cursor: cursor ? { id: cursor } : undefined, include: { @@ -104,9 +103,9 @@ export const questionsAnswerRouter = createRouter() } return { + data: processedAnswersData, nextCursor, - processedAnswersData, - } + }; }, }) .query('getAnswerById', { diff --git a/apps/portal/src/server/router/questions/questions-question-comment-router.ts b/apps/portal/src/server/router/questions/questions-question-comment-router.ts index 9f42b88e..8ac0ff0b 100644 --- a/apps/portal/src/server/router/questions/questions-question-comment-router.ts +++ b/apps/portal/src/server/router/questions/questions-question-comment-router.ts @@ -97,9 +97,9 @@ export const questionsQuestionCommentRouter = createRouter().query( } return { + data: processedQuestionCommentsData, nextCursor, - processedQuestionCommentsData, - } + }; }, }, ); diff --git a/apps/portal/src/server/router/questions/questions-question-comment-user-router.ts b/apps/portal/src/server/router/questions/questions-question-comment-user-router.ts index f2b9afb9..943423a9 100644 --- a/apps/portal/src/server/router/questions/questions-question-comment-user-router.ts +++ b/apps/portal/src/server/router/questions/questions-question-comment-user-router.ts @@ -256,18 +256,15 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter() } if (vote.vote === Vote.UPVOTE) { - tx.questionsQuestionCommentVote.delete({ - where: { - id: vote.id, - }, - }); - - const createdVote = await tx.questionsQuestionCommentVote.create({ + const updatedVote = await tx.questionsQuestionCommentVote.update({ data: { questionCommentId, userId, vote: Vote.DOWNVOTE, }, + where: { + id: vote.id, + }, }); await tx.questionsQuestionComment.update({ @@ -281,7 +278,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter() }, }); - return createdVote; + return updatedVote; } }); }, diff --git a/apps/portal/src/utils/questions/vote/useAnswerCommentVote.ts b/apps/portal/src/utils/questions/vote/useAnswerCommentVote.ts new file mode 100644 index 00000000..2a7849d4 --- /dev/null +++ b/apps/portal/src/utils/questions/vote/useAnswerCommentVote.ts @@ -0,0 +1,68 @@ +import type { InfiniteData } from 'react-query'; + +import { trpc } from '~/utils/trpc'; + +import useVote from './useVote'; + +import type { AnswerComment } from '~/types/questions'; + +export default function useAnswerCommentVote(id: string) { + const utils = trpc.useContext(); + + return useVote(id, { + idKey: 'answerCommentId', + invalidateKeys: [], + onMutate: async (voteValueChange) => { + // Update answer comment list + const answerCommentQueries = utils.queryClient.getQueriesData([ + 'questions.answers.comments.getAnswerComments', + ]); + + const revertFunctions: Array<() => void> = []; + + if (answerCommentQueries !== undefined) { + for (const [key, query] of answerCommentQueries) { + if (query === undefined) { + continue; + } + + const { pages, ...restQuery } = query as InfiniteData<{ + data: Array; + }>; + + const newQuery = { + pages: pages.map(({ data, ...restPage }) => ({ + data: data.map((answerComment) => { + if (answerComment.id === id) { + const { numVotes, ...restAnswerComment } = answerComment; + return { + numVotes: numVotes + voteValueChange, + ...restAnswerComment, + }; + } + return answerComment; + }), + ...restPage, + })), + ...restQuery, + }; + + utils.queryClient.setQueryData(key, newQuery); + + revertFunctions.push(() => { + utils.queryClient.setQueryData(key, query); + }); + } + } + return () => { + for (const revertFunction of revertFunctions) { + revertFunction(); + } + }; + }, + query: 'questions.answers.comments.user.getVote', + setDownVoteKey: 'questions.answers.comments.user.setDownVote', + setNoVoteKey: 'questions.answers.comments.user.setNoVote', + setUpVoteKey: 'questions.answers.comments.user.setUpVote', + }); +} diff --git a/apps/portal/src/utils/questions/vote/useAnswerVote.ts b/apps/portal/src/utils/questions/vote/useAnswerVote.ts new file mode 100644 index 00000000..1c9ea578 --- /dev/null +++ b/apps/portal/src/utils/questions/vote/useAnswerVote.ts @@ -0,0 +1,98 @@ +import type { InfiniteData } from 'react-query'; + +import { trpc } from '~/utils/trpc'; + +import useVote from './useVote'; + +import type { Answer } from '~/types/questions'; + +export default function useAnswerVote(id: string) { + const utils = trpc.useContext(); + + return useVote(id, { + idKey: 'answerId', + invalidateKeys: [ + // 'questions.answers.getAnswerById', + // 'questions.answers.getAnswers', + ], + onMutate: async (voteValueChange) => { + // Update question answer list + const answerQueries = utils.queryClient.getQueriesData([ + 'questions.answers.getAnswers', + ]); + + const revertFunctions: Array<() => void> = []; + + if (answerQueries !== undefined) { + for (const [key, query] of answerQueries) { + if (query === undefined) { + continue; + } + + const { pages, ...restQuery } = query as InfiniteData<{ + data: Array; + }>; + + const newQuery = { + pages: pages.map(({ data, ...restPage }) => ({ + data: data.map((answer) => { + if (answer.id === id) { + const { numVotes, ...restAnswer } = answer; + return { + numVotes: numVotes + voteValueChange, + ...restAnswer, + }; + } + return answer; + }), + ...restPage, + })), + ...restQuery, + }; + + utils.queryClient.setQueryData(key, newQuery); + + revertFunctions.push(() => { + utils.queryClient.setQueryData(key, query); + }); + } + } + + const prevAnswer = utils.queryClient.getQueryData([ + 'questions.answers.getAnswerById', + { + answerId: id, + }, + ]) as Answer | undefined; + + if (prevAnswer !== undefined) { + const newAnswer = { + ...prevAnswer, + numVotes: prevAnswer.numVotes + voteValueChange, + }; + + utils.queryClient.setQueryData( + ['questions.answers.getAnswerById', { answerId: id }], + newAnswer, + ); + + revertFunctions.push(() => { + utils.queryClient.setQueryData( + ['questions.answers.getAnswerById', { answerId: id }], + prevAnswer, + ); + }); + } + + return () => { + for (const revertFunction of revertFunctions) { + revertFunction(); + } + }; + }, + query: 'questions.answers.user.getVote', + setDownVoteKey: 'questions.answers.user.setDownVote', + setNoVoteKey: 'questions.answers.user.setNoVote', + setUpVoteKey: 'questions.answers.user.setUpVote', + }); +} diff --git a/apps/portal/src/utils/questions/vote/useQuestionCommentVote.ts b/apps/portal/src/utils/questions/vote/useQuestionCommentVote.ts new file mode 100644 index 00000000..e2369f4e --- /dev/null +++ b/apps/portal/src/utils/questions/vote/useQuestionCommentVote.ts @@ -0,0 +1,69 @@ +import type { InfiniteData } from 'react-query'; + +import { trpc } from '~/utils/trpc'; + +import useVote from './useVote'; + +import type { QuestionComment } from '~/types/questions'; + +export default function useQuestionCommentVote(id: string) { + const utils = trpc.useContext(); + + return useVote(id, { + idKey: 'questionCommentId', + invalidateKeys: [], + onMutate: async (voteValueChange) => { + // Update question comment list + const questionCommentQueries = utils.queryClient.getQueriesData([ + 'questions.questions.comments.getQuestionComments', + ]); + + const revertFunctions: Array<() => void> = []; + + if (questionCommentQueries !== undefined) { + for (const [key, query] of questionCommentQueries) { + if (query === undefined) { + continue; + } + + const { pages, ...restQuery } = query as InfiniteData<{ + data: Array; + }>; + + const newQuery = { + pages: pages.map(({ data, ...restPage }) => ({ + data: data.map((questionComment) => { + if (questionComment.id === id) { + const { numVotes, ...restQuestionComment } = questionComment; + return { + numVotes: numVotes + voteValueChange, + ...restQuestionComment, + }; + } + return questionComment; + }), + ...restPage, + })), + ...restQuery, + }; + + utils.queryClient.setQueryData(key, newQuery); + + revertFunctions.push(() => { + utils.queryClient.setQueryData(key, query); + }); + } + } + + return () => { + for (const revertFunction of revertFunctions) { + revertFunction(); + } + }; + }, + query: 'questions.questions.comments.user.getVote', + setDownVoteKey: 'questions.questions.comments.user.setDownVote', + setNoVoteKey: 'questions.questions.comments.user.setNoVote', + setUpVoteKey: 'questions.questions.comments.user.setUpVote', + }); +} diff --git a/apps/portal/src/utils/questions/vote/useQuestionVote.ts b/apps/portal/src/utils/questions/vote/useQuestionVote.ts new file mode 100644 index 00000000..6e2686c8 --- /dev/null +++ b/apps/portal/src/utils/questions/vote/useQuestionVote.ts @@ -0,0 +1,98 @@ +import type { InfiniteData } from 'react-query'; + +import { trpc } from '~/utils/trpc'; + +import useVote from './useVote'; + +import type { Question } from '~/types/questions'; + +export const useQuestionVote = (id: string) => { + const utils = trpc.useContext(); + + return useVote(id, { + idKey: 'questionId', + invalidateKeys: [ + // 'questions.questions.getQuestionById', + // 'questions.questions.getQuestionsByFilterAndContent', + ], + onMutate: async (voteValueChange) => { + // Update question list + const questionQueries = utils.queryClient.getQueriesData([ + 'questions.questions.getQuestionsByFilterAndContent', + ]); + + const revertFunctions: Array<() => void> = []; + + if (questionQueries !== undefined) { + for (const [key, query] of questionQueries) { + if (query === undefined) { + continue; + } + + const { pages, ...restQuery } = query as InfiniteData<{ + data: Array; + }>; + + const newQuery = { + pages: pages.map(({ data, ...restPage }) => ({ + data: data.map((question) => { + if (question.id === id) { + const { numVotes, ...restQuestion } = question; + return { + numVotes: numVotes + voteValueChange, + ...restQuestion, + }; + } + return question; + }), + ...restPage, + })), + ...restQuery, + }; + + utils.queryClient.setQueryData(key, newQuery); + + revertFunctions.push(() => { + utils.queryClient.setQueryData(key, query); + }); + } + } + + const prevQuestion = utils.queryClient.getQueryData([ + 'questions.questions.getQuestionById', + { + id, + }, + ]) as Question | undefined; + + if (prevQuestion !== undefined) { + const newQuestion = { + ...prevQuestion, + numVotes: prevQuestion.numVotes + voteValueChange, + }; + + utils.queryClient.setQueryData( + ['questions.questions.getQuestionById', { id }], + newQuestion, + ); + + revertFunctions.push(() => { + utils.queryClient.setQueryData( + ['questions.questions.getQuestionById', { id }], + prevQuestion, + ); + }); + } + + return () => { + for (const revertFunction of revertFunctions) { + revertFunction(); + } + }; + }, + query: 'questions.questions.user.getVote', + setDownVoteKey: 'questions.questions.user.setDownVote', + setNoVoteKey: 'questions.questions.user.setNoVote', + setUpVoteKey: 'questions.questions.user.setUpVote', + }); +}; diff --git a/apps/portal/src/utils/questions/useVote.ts b/apps/portal/src/utils/questions/vote/useVote.ts similarity index 53% rename from apps/portal/src/utils/questions/useVote.ts rename to apps/portal/src/utils/questions/vote/useVote.ts index f1e8b864..7e3355bc 100644 --- a/apps/portal/src/utils/questions/useVote.ts +++ b/apps/portal/src/utils/questions/vote/useVote.ts @@ -1,11 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { useCallback } from 'react'; -import type { InfiniteData } from 'react-query'; import { Vote } from '@prisma/client'; -import { trpc } from '../trpc'; - -import type { Question } from '~/types/questions'; +import { trpc } from '../../trpc'; type UseVoteOptions = { setDownVote: () => void; @@ -48,133 +45,24 @@ const createVoteCallbacks = ( type MutationKey = Parameters[0]; type QueryKey = Parameters[0][0]; -export const useQuestionVote = (id: string) => { - const utils = trpc.useContext(); - - return useVote(id, { - idKey: 'questionId', - invalidateKeys: [ - // 'questions.questions.getQuestionById', - // 'questions.questions.getQuestionsByFilterAndContent', - ], - onMutate: async (previousVote, currentVote) => { - const questionQueries = utils.queryClient.getQueriesData([ - 'questions.questions.getQuestionsByFilterAndContent', - ]); - - const getVoteValue = (vote: Vote | null) => { - if (vote === Vote.UPVOTE) { - return 1; - } - if (vote === Vote.DOWNVOTE) { - return -1; - } - return 0; - }; - - const voteValueChange = - getVoteValue(currentVote) - getVoteValue(previousVote); - - for (const [key, query] of questionQueries) { - if (query === undefined) { - continue; - } - - const { pages, ...restQuery } = query as InfiniteData<{ - data: Array; - }>; - - const newQuery = { - pages: pages.map(({ data, ...restPage }) => ({ - data: data.map((question) => { - if (question.id === id) { - const { numVotes, ...restQuestion } = question; - return { - numVotes: numVotes + voteValueChange, - ...restQuestion, - }; - } - return question; - }), - ...restPage, - })), - ...restQuery, - }; - - utils.queryClient.setQueryData(key, newQuery); - } - - const prevQuestion = utils.queryClient.getQueryData([ - 'questions.questions.getQuestionById', - { - id, - }, - ]) as Question; - - const newQuestion = { - ...prevQuestion, - numVotes: prevQuestion.numVotes + voteValueChange, - }; - - utils.queryClient.setQueryData( - ['questions.questions.getQuestionById', { id }], - newQuestion, - ); - }, - query: 'questions.questions.user.getVote', - setDownVoteKey: 'questions.questions.user.setDownVote', - setNoVoteKey: 'questions.questions.user.setNoVote', - setUpVoteKey: 'questions.questions.user.setUpVote', - }); -}; - -export const useAnswerVote = (id: string) => { - return useVote(id, { - idKey: 'answerId', - invalidateKeys: [ - 'questions.answers.getAnswerById', - 'questions.answers.getAnswers', - ], - query: 'questions.answers.user.getVote', - setDownVoteKey: 'questions.answers.user.setDownVote', - setNoVoteKey: 'questions.answers.user.setNoVote', - setUpVoteKey: 'questions.answers.user.setUpVote', - }); +const getVoteValue = (vote: Vote | null) => { + if (vote === Vote.UPVOTE) { + return 1; + } + if (vote === Vote.DOWNVOTE) { + return -1; + } + return 0; }; -export const useQuestionCommentVote = (id: string) => { - return useVote(id, { - idKey: 'questionCommentId', - invalidateKeys: ['questions.questions.comments.getQuestionComments'], - query: 'questions.questions.comments.user.getVote', - setDownVoteKey: 'questions.questions.comments.user.setDownVote', - setNoVoteKey: 'questions.questions.comments.user.setNoVote', - setUpVoteKey: 'questions.questions.comments.user.setUpVote', - }); -}; - -export const useAnswerCommentVote = (id: string) => { - return useVote(id, { - idKey: 'answerCommentId', - invalidateKeys: ['questions.answers.comments.getAnswerComments'], - query: 'questions.answers.comments.user.getVote', - setDownVoteKey: 'questions.answers.comments.user.setDownVote', - setNoVoteKey: 'questions.answers.comments.user.setNoVote', - setUpVoteKey: 'questions.answers.comments.user.setUpVote', - }); -}; +type RevertFunction = () => void; -type InvalidateFunction = ( - previousVote: Vote | null, - currentVote: Vote | null, -) => Promise; +type InvalidateFunction = (voteValueChange: number) => Promise; type VoteProps = { idKey: string; invalidateKeys: Array; onMutate?: InvalidateFunction; - - // Invalidate: Partial>; query: VoteQueryKey; setDownVoteKey: MutationKey; setNoVoteKey: MutationKey; @@ -184,12 +72,13 @@ type VoteProps = { type UseVoteMutationContext = { currentData: any; previousData: any; + revert: RevertFunction | undefined; }; -export const useVote = ( +export default function useVote( id: string, opts: VoteProps, -) => { +) { const { idKey, invalidateKeys, @@ -201,7 +90,7 @@ export const useVote = ( } = opts; const utils = trpc.useContext(); - const onVoteUpdate = useCallback(() => { + const onVoteUpdateSettled = useCallback(() => { // TODO: Optimise query invalidation // utils.invalidateQueries([query, { [idKey]: id } as any]); for (const invalidateKey of invalidateKeys) { @@ -229,6 +118,7 @@ export const useVote = ( onError: (_error, _variables, context) => { if (context !== undefined) { utils.setQueryData([query], context.previousData); + context.revert?.(); } }, onMutate: async (vote) => { @@ -252,10 +142,14 @@ export const useVote = ( currentData as any, ); - await onMutate?.(previousData?.vote ?? null, currentData?.vote ?? null); - return { currentData, previousData }; + const voteValueChange = + getVoteValue(currentData?.vote ?? null) - + getVoteValue(previousData?.vote ?? null); + + const revert = await onMutate?.(voteValueChange); + return { currentData, previousData, revert }; }, - onSettled: onVoteUpdate, + onSettled: onVoteUpdateSettled, }, ); const { mutate: setDownVote } = trpc.useMutation( @@ -264,6 +158,7 @@ export const useVote = ( onError: (_error, _variables, context) => { if (context !== undefined) { utils.setQueryData([query], context.previousData); + context.revert?.(); } }, onMutate: async (vote) => { @@ -287,10 +182,14 @@ export const useVote = ( currentData as any, ); - await onMutate?.(previousData?.vote ?? null, currentData?.vote ?? null); - return { currentData, previousData }; + const voteValueChange = + getVoteValue(currentData?.vote ?? null) - + getVoteValue(previousData?.vote ?? null); + + const revert = await onMutate?.(voteValueChange); + return { currentData, previousData, revert }; }, - onSettled: onVoteUpdate, + onSettled: onVoteUpdateSettled, }, ); @@ -300,6 +199,7 @@ export const useVote = ( onError: (_error, _variables, context) => { if (context !== undefined) { utils.setQueryData([query], context.previousData); + context.revert?.(); } }, onMutate: async () => { @@ -319,11 +219,13 @@ export const useVote = ( currentData, ); - await onMutate?.(previousData?.vote ?? null, null); + const voteValueChange = + getVoteValue(null) - getVoteValue(previousData?.vote ?? null); - return { currentData, previousData }; + const revert = await onMutate?.(voteValueChange); + return { currentData, previousData, revert }; }, - onSettled: onVoteUpdate, + onSettled: onVoteUpdateSettled, }, ); @@ -349,4 +251,4 @@ export const useVote = ( ); return { handleDownvote, handleUpvote, vote: backendVote ?? null }; -}; +}