[questions][fix] fix upvotes (#521)

pull/522/head
Jeff Sieu 2 years ago committed by GitHub
parent ffbb6a29f2
commit a7c9f58ef3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,7 +1,7 @@
import { format } from 'date-fns'; import { format } from 'date-fns';
import { ChatBubbleLeftRightIcon } from '@heroicons/react/24/outline'; 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 type { VotingButtonsProps } from '../VotingButtons';
import VotingButtons from '../VotingButtons'; import VotingButtons from '../VotingButtons';

@ -10,7 +10,7 @@ import type { QuestionsQuestionType } from '@prisma/client';
import { Button } from '@tih/ui'; import { Button } from '@tih/ui';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback'; import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
import { useQuestionVote } from '~/utils/questions/useVote'; import { useQuestionVote } from '~/utils/questions/vote/useQuestionVote';
import AddToListDropdown from '../../AddToListDropdown'; import AddToListDropdown from '../../AddToListDropdown';
import type { CreateQuestionEncounterData } from '../../forms/CreateQuestionEncounterForm'; import type { CreateQuestionEncounterData } from '../../forms/CreateQuestionEncounterForm';

@ -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 (
<CommentListItem
vote={vote}
onDownvote={handleDownvote}
onUpvote={handleUpvote}
{...restProps}
/>
);
}

@ -1,37 +1,37 @@
import { format } from 'date-fns'; 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 CommentListItemProps = {
export type AnswerCommentListItemProps = {
answerCommentId: string;
authorImageUrl: string; authorImageUrl: string;
authorName: string; authorName: string;
content: string; content: string;
createdAt: Date; createdAt: Date;
onDownvote: () => void;
onUpvote: () => void;
upvoteCount: number; upvoteCount: number;
vote: BackendVote;
}; };
export default function AnswerCommentListItem({ export default function CommentListItem({
authorImageUrl, authorImageUrl,
authorName, authorName,
content, content,
createdAt, createdAt,
upvoteCount, upvoteCount,
answerCommentId, vote,
}: AnswerCommentListItemProps) { onDownvote,
const { handleDownvote, handleUpvote, vote } = onUpvote,
useAnswerCommentVote(answerCommentId); }: CommentListItemProps) {
return ( return (
<div className="flex gap-4 rounded-md border bg-white p-2"> <div className="flex gap-4 rounded-md border bg-white p-2">
<VotingButtons <VotingButtons
size="sm" size="sm"
upvoteCount={upvoteCount} upvoteCount={upvoteCount}
vote={vote} vote={vote}
onDownvote={handleDownvote} onDownvote={onDownvote}
onUpvote={handleUpvote} onUpvote={onUpvote}
/> />
<div className="mt-1 flex flex-col gap-1"> <div className="mt-1 flex flex-col gap-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

@ -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 (
<CommentListItem
vote={vote}
onDownvote={handleDownvote}
onUpvote={handleUpvote}
{...restProps}
/>
);
}

@ -5,8 +5,8 @@ import { useForm } from 'react-hook-form';
import { Button, TextArea } from '@tih/ui'; import { Button, TextArea } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import AnswerCommentListItem from '~/components/questions/AnswerCommentListItem';
import FullAnswerCard from '~/components/questions/card/FullAnswerCard'; import FullAnswerCard from '~/components/questions/card/FullAnswerCard';
import AnswerCommentListItem from '~/components/questions/comments/AnswerCommentListItem';
import FullScreenSpinner from '~/components/questions/FullScreenSpinner'; import FullScreenSpinner from '~/components/questions/FullScreenSpinner';
import BackButtonLayout from '~/components/questions/layout/BackButtonLayout'; import BackButtonLayout from '~/components/questions/layout/BackButtonLayout';
import PaginationLoadMoreButton from '~/components/questions/PaginationLoadMoreButton'; import PaginationLoadMoreButton from '~/components/questions/PaginationLoadMoreButton';
@ -158,19 +158,18 @@ export default function QuestionPage() {
</div> </div>
</div> </div>
{/* TODO: Allow to load more pages */} {/* TODO: Allow to load more pages */}
{(answerCommentsData?.pages ?? []).flatMap( {(answerCommentsData?.pages ?? []).flatMap(({ data: comments }) =>
({ processedQuestionAnswerCommentsData: comments }) => comments.map((comment) => (
comments.map((comment) => ( <AnswerCommentListItem
<AnswerCommentListItem key={comment.id}
key={comment.id} answerCommentId={comment.id}
answerCommentId={comment.id} authorImageUrl={comment.userImage}
authorImageUrl={comment.userImage} authorName={comment.user}
authorName={comment.user} content={comment.content}
content={comment.content} createdAt={comment.createdAt}
createdAt={comment.createdAt} upvoteCount={comment.numVotes}
upvoteCount={comment.numVotes} />
/> )),
)),
)} )}
<PaginationLoadMoreButton query={answerCommentInfiniteQuery} /> <PaginationLoadMoreButton query={answerCommentInfiniteQuery} />
</div> </div>

@ -5,9 +5,9 @@ import { useForm } from 'react-hook-form';
import { Button, Collapsible, HorizontalDivider, TextArea } from '@tih/ui'; import { Button, Collapsible, HorizontalDivider, TextArea } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import AnswerCommentListItem from '~/components/questions/AnswerCommentListItem';
import FullQuestionCard from '~/components/questions/card/question/FullQuestionCard'; import FullQuestionCard from '~/components/questions/card/question/FullQuestionCard';
import QuestionAnswerCard from '~/components/questions/card/QuestionAnswerCard'; import QuestionAnswerCard from '~/components/questions/card/QuestionAnswerCard';
import QuestionCommentListItem from '~/components/questions/comments/QuestionCommentListItem';
import FullScreenSpinner from '~/components/questions/FullScreenSpinner'; import FullScreenSpinner from '~/components/questions/FullScreenSpinner';
import BackButtonLayout from '~/components/questions/layout/BackButtonLayout'; import BackButtonLayout from '~/components/questions/layout/BackButtonLayout';
import PaginationLoadMoreButton from '~/components/questions/PaginationLoadMoreButton'; import PaginationLoadMoreButton from '~/components/questions/PaginationLoadMoreButton';
@ -245,19 +245,18 @@ export default function QuestionPage() {
/> />
</div> </div>
</div> </div>
{(commentData?.pages ?? []).flatMap( {(commentData?.pages ?? []).flatMap(({ data: comments }) =>
({ processedQuestionCommentsData: comments }) => comments.map((comment) => (
comments.map((comment) => ( <QuestionCommentListItem
<AnswerCommentListItem key={comment.id}
key={comment.id} authorImageUrl={comment.userImage}
answerCommentId={comment.id} authorName={comment.user}
authorImageUrl={comment.userImage} content={comment.content}
authorName={comment.user} createdAt={comment.createdAt}
content={comment.content} questionCommentId={comment.id}
createdAt={comment.createdAt} upvoteCount={comment.numVotes}
upvoteCount={comment.numVotes} />
/> )),
)),
)} )}
<PaginationLoadMoreButton query={commentInfiniteQuery} /> <PaginationLoadMoreButton query={commentInfiniteQuery} />
<form <form
@ -326,23 +325,22 @@ export default function QuestionPage() {
</div> </div>
</div> </div>
{/* TODO: Add button to load more */} {/* TODO: Add button to load more */}
{(answerData?.pages ?? []).flatMap( {(answerData?.pages ?? []).flatMap(({ data: answers }) =>
({ processedAnswersData: answers }) => answers.map((answer) => (
answers.map((answer) => ( <QuestionAnswerCard
<QuestionAnswerCard key={answer.id}
key={answer.id} answerId={answer.id}
answerId={answer.id} authorImageUrl={answer.userImage}
authorImageUrl={answer.userImage} authorName={answer.user}
authorName={answer.user} commentCount={answer.numComments}
commentCount={answer.numComments} content={answer.content}
content={answer.content} createdAt={answer.createdAt}
createdAt={answer.createdAt} href={`${router.asPath}/answer/${answer.id}/${createSlug(
href={`${router.asPath}/answer/${answer.id}/${createSlug( answer.content,
answer.content, )}`}
)}`} upvoteCount={answer.numVotes}
upvoteCount={answer.numVotes} />
/> )),
)),
)} )}
<PaginationLoadMoreButton query={answerInfiniteQuery} /> <PaginationLoadMoreButton query={answerInfiniteQuery} />
</div> </div>

@ -56,35 +56,36 @@ export const questionsAnswerCommentRouter = createRouter().query(
answerId, answerId,
}, },
}); });
const processedQuestionAnswerCommentsData = questionAnswerCommentsData.map((data) => { const processedQuestionAnswerCommentsData =
const votes: number = data.votes.reduce( questionAnswerCommentsData.map((data) => {
(previousValue: number, currentValue) => { const votes: number = data.votes.reduce(
let result: number = previousValue; (previousValue: number, currentValue) => {
let result: number = previousValue;
switch (currentValue.vote) { switch (currentValue.vote) {
case Vote.UPVOTE: case Vote.UPVOTE:
result += 1; result += 1;
break; break;
case Vote.DOWNVOTE: case Vote.DOWNVOTE:
result -= 1; result -= 1;
break; break;
} }
return result; return result;
}, },
0, 0,
); );
const answerComment: AnswerComment = { const answerComment: AnswerComment = {
content: data.content, content: data.content,
createdAt: data.createdAt, createdAt: data.createdAt,
id: data.id, id: data.id,
numVotes: votes, numVotes: votes,
updatedAt: data.updatedAt, updatedAt: data.updatedAt,
user: data.user?.name ?? '', user: data.user?.name ?? '',
userImage: data.user?.image ?? '', userImage: data.user?.image ?? '',
}; };
return answerComment; return answerComment;
}); });
let nextCursor: typeof cursor | undefined = undefined; let nextCursor: typeof cursor | undefined = undefined;
@ -98,9 +99,9 @@ export const questionsAnswerCommentRouter = createRouter().query(
} }
return { return {
data: processedQuestionAnswerCommentsData,
nextCursor, nextCursor,
processedQuestionAnswerCommentsData, };
}
}, },
}, },
); );

@ -38,7 +38,6 @@ export const questionsAnswerRouter = createRouter()
}, },
]; ];
const answersData = await ctx.prisma.questionsAnswer.findMany({ const answersData = await ctx.prisma.questionsAnswer.findMany({
cursor: cursor ? { id: cursor } : undefined, cursor: cursor ? { id: cursor } : undefined,
include: { include: {
@ -104,9 +103,9 @@ export const questionsAnswerRouter = createRouter()
} }
return { return {
data: processedAnswersData,
nextCursor, nextCursor,
processedAnswersData, };
}
}, },
}) })
.query('getAnswerById', { .query('getAnswerById', {

@ -97,9 +97,9 @@ export const questionsQuestionCommentRouter = createRouter().query(
} }
return { return {
data: processedQuestionCommentsData,
nextCursor, nextCursor,
processedQuestionCommentsData, };
}
}, },
}, },
); );

@ -256,18 +256,15 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
} }
if (vote.vote === Vote.UPVOTE) { if (vote.vote === Vote.UPVOTE) {
tx.questionsQuestionCommentVote.delete({ const updatedVote = await tx.questionsQuestionCommentVote.update({
where: {
id: vote.id,
},
});
const createdVote = await tx.questionsQuestionCommentVote.create({
data: { data: {
questionCommentId, questionCommentId,
userId, userId,
vote: Vote.DOWNVOTE, vote: Vote.DOWNVOTE,
}, },
where: {
id: vote.id,
},
}); });
await tx.questionsQuestionComment.update({ await tx.questionsQuestionComment.update({
@ -281,7 +278,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
}, },
}); });
return createdVote; return updatedVote;
} }
}); });
}, },

@ -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<AnswerComment>;
}>;
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',
});
}

@ -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<Answer>;
}>;
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',
});
}

@ -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<QuestionComment>;
}>;
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',
});
}

@ -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<Question>;
}>;
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',
});
};

@ -1,11 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { useCallback } from 'react'; import { useCallback } from 'react';
import type { InfiniteData } from 'react-query';
import { Vote } from '@prisma/client'; import { Vote } from '@prisma/client';
import { trpc } from '../trpc'; import { trpc } from '../../trpc';
import type { Question } from '~/types/questions';
type UseVoteOptions = { type UseVoteOptions = {
setDownVote: () => void; setDownVote: () => void;
@ -48,133 +45,24 @@ const createVoteCallbacks = (
type MutationKey = Parameters<typeof trpc.useMutation>[0]; type MutationKey = Parameters<typeof trpc.useMutation>[0];
type QueryKey = Parameters<typeof trpc.useQuery>[0][0]; type QueryKey = Parameters<typeof trpc.useQuery>[0][0];
export const useQuestionVote = (id: string) => { const getVoteValue = (vote: Vote | null) => {
const utils = trpc.useContext(); if (vote === Vote.UPVOTE) {
return 1;
return useVote(id, { }
idKey: 'questionId', if (vote === Vote.DOWNVOTE) {
invalidateKeys: [ return -1;
// 'questions.questions.getQuestionById', }
// 'questions.questions.getQuestionsByFilterAndContent', return 0;
],
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<Question>;
}>;
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',
});
}; };
export const useQuestionCommentVote = (id: string) => { type RevertFunction = () => void;
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 InvalidateFunction = ( type InvalidateFunction = (voteValueChange: number) => Promise<RevertFunction>;
previousVote: Vote | null,
currentVote: Vote | null,
) => Promise<void>;
type VoteProps<VoteQueryKey extends QueryKey = QueryKey> = { type VoteProps<VoteQueryKey extends QueryKey = QueryKey> = {
idKey: string; idKey: string;
invalidateKeys: Array<QueryKey>; invalidateKeys: Array<QueryKey>;
onMutate?: InvalidateFunction; onMutate?: InvalidateFunction;
// Invalidate: Partial<Record<QueryKey, InvalidateFunction | null>>;
query: VoteQueryKey; query: VoteQueryKey;
setDownVoteKey: MutationKey; setDownVoteKey: MutationKey;
setNoVoteKey: MutationKey; setNoVoteKey: MutationKey;
@ -184,12 +72,13 @@ type VoteProps<VoteQueryKey extends QueryKey = QueryKey> = {
type UseVoteMutationContext = { type UseVoteMutationContext = {
currentData: any; currentData: any;
previousData: any; previousData: any;
revert: RevertFunction | undefined;
}; };
export const useVote = <VoteQueryKey extends QueryKey = QueryKey>( export default function useVote<VoteQueryKey extends QueryKey = QueryKey>(
id: string, id: string,
opts: VoteProps<VoteQueryKey>, opts: VoteProps<VoteQueryKey>,
) => { ) {
const { const {
idKey, idKey,
invalidateKeys, invalidateKeys,
@ -201,7 +90,7 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
} = opts; } = opts;
const utils = trpc.useContext(); const utils = trpc.useContext();
const onVoteUpdate = useCallback(() => { const onVoteUpdateSettled = useCallback(() => {
// TODO: Optimise query invalidation // TODO: Optimise query invalidation
// utils.invalidateQueries([query, { [idKey]: id } as any]); // utils.invalidateQueries([query, { [idKey]: id } as any]);
for (const invalidateKey of invalidateKeys) { for (const invalidateKey of invalidateKeys) {
@ -229,6 +118,7 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
onError: (_error, _variables, context) => { onError: (_error, _variables, context) => {
if (context !== undefined) { if (context !== undefined) {
utils.setQueryData([query], context.previousData); utils.setQueryData([query], context.previousData);
context.revert?.();
} }
}, },
onMutate: async (vote) => { onMutate: async (vote) => {
@ -252,10 +142,14 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
currentData as any, currentData as any,
); );
await onMutate?.(previousData?.vote ?? null, currentData?.vote ?? null); const voteValueChange =
return { currentData, previousData }; 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<any, UseVoteMutationContext>( const { mutate: setDownVote } = trpc.useMutation<any, UseVoteMutationContext>(
@ -264,6 +158,7 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
onError: (_error, _variables, context) => { onError: (_error, _variables, context) => {
if (context !== undefined) { if (context !== undefined) {
utils.setQueryData([query], context.previousData); utils.setQueryData([query], context.previousData);
context.revert?.();
} }
}, },
onMutate: async (vote) => { onMutate: async (vote) => {
@ -287,10 +182,14 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
currentData as any, currentData as any,
); );
await onMutate?.(previousData?.vote ?? null, currentData?.vote ?? null); const voteValueChange =
return { currentData, previousData }; 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 = <VoteQueryKey extends QueryKey = QueryKey>(
onError: (_error, _variables, context) => { onError: (_error, _variables, context) => {
if (context !== undefined) { if (context !== undefined) {
utils.setQueryData([query], context.previousData); utils.setQueryData([query], context.previousData);
context.revert?.();
} }
}, },
onMutate: async () => { onMutate: async () => {
@ -319,11 +219,13 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
currentData, 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 = <VoteQueryKey extends QueryKey = QueryKey>(
); );
return { handleDownvote, handleUpvote, vote: backendVote ?? null }; return { handleDownvote, handleUpvote, vote: backendVote ?? null };
}; }
Loading…
Cancel
Save