mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2026-04-03 02:48:43 +08:00
[questions][fix] fix upvotes (#521)
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 { 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 (
|
||||
<div className="flex gap-4 rounded-md border bg-white p-2">
|
||||
<VotingButtons
|
||||
size="sm"
|
||||
upvoteCount={upvoteCount}
|
||||
vote={vote}
|
||||
onDownvote={handleDownvote}
|
||||
onUpvote={handleUpvote}
|
||||
onDownvote={onDownvote}
|
||||
onUpvote={onUpvote}
|
||||
/>
|
||||
<div className="mt-1 flex flex-col gap-1">
|
||||
<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 { 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() {
|
||||
</div>
|
||||
</div>
|
||||
{/* TODO: Allow to load more pages */}
|
||||
{(answerCommentsData?.pages ?? []).flatMap(
|
||||
({ processedQuestionAnswerCommentsData: comments }) =>
|
||||
comments.map((comment) => (
|
||||
<AnswerCommentListItem
|
||||
key={comment.id}
|
||||
answerCommentId={comment.id}
|
||||
authorImageUrl={comment.userImage}
|
||||
authorName={comment.user}
|
||||
content={comment.content}
|
||||
createdAt={comment.createdAt}
|
||||
upvoteCount={comment.numVotes}
|
||||
/>
|
||||
)),
|
||||
{(answerCommentsData?.pages ?? []).flatMap(({ data: comments }) =>
|
||||
comments.map((comment) => (
|
||||
<AnswerCommentListItem
|
||||
key={comment.id}
|
||||
answerCommentId={comment.id}
|
||||
authorImageUrl={comment.userImage}
|
||||
authorName={comment.user}
|
||||
content={comment.content}
|
||||
createdAt={comment.createdAt}
|
||||
upvoteCount={comment.numVotes}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
<PaginationLoadMoreButton query={answerCommentInfiniteQuery} />
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{(commentData?.pages ?? []).flatMap(
|
||||
({ processedQuestionCommentsData: comments }) =>
|
||||
comments.map((comment) => (
|
||||
<AnswerCommentListItem
|
||||
key={comment.id}
|
||||
answerCommentId={comment.id}
|
||||
authorImageUrl={comment.userImage}
|
||||
authorName={comment.user}
|
||||
content={comment.content}
|
||||
createdAt={comment.createdAt}
|
||||
upvoteCount={comment.numVotes}
|
||||
/>
|
||||
)),
|
||||
{(commentData?.pages ?? []).flatMap(({ data: comments }) =>
|
||||
comments.map((comment) => (
|
||||
<QuestionCommentListItem
|
||||
key={comment.id}
|
||||
authorImageUrl={comment.userImage}
|
||||
authorName={comment.user}
|
||||
content={comment.content}
|
||||
createdAt={comment.createdAt}
|
||||
questionCommentId={comment.id}
|
||||
upvoteCount={comment.numVotes}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
<PaginationLoadMoreButton query={commentInfiniteQuery} />
|
||||
<form
|
||||
@@ -326,23 +325,22 @@ export default function QuestionPage() {
|
||||
</div>
|
||||
</div>
|
||||
{/* TODO: Add button to load more */}
|
||||
{(answerData?.pages ?? []).flatMap(
|
||||
({ processedAnswersData: answers }) =>
|
||||
answers.map((answer) => (
|
||||
<QuestionAnswerCard
|
||||
key={answer.id}
|
||||
answerId={answer.id}
|
||||
authorImageUrl={answer.userImage}
|
||||
authorName={answer.user}
|
||||
commentCount={answer.numComments}
|
||||
content={answer.content}
|
||||
createdAt={answer.createdAt}
|
||||
href={`${router.asPath}/answer/${answer.id}/${createSlug(
|
||||
answer.content,
|
||||
)}`}
|
||||
upvoteCount={answer.numVotes}
|
||||
/>
|
||||
)),
|
||||
{(answerData?.pages ?? []).flatMap(({ data: answers }) =>
|
||||
answers.map((answer) => (
|
||||
<QuestionAnswerCard
|
||||
key={answer.id}
|
||||
answerId={answer.id}
|
||||
authorImageUrl={answer.userImage}
|
||||
authorName={answer.user}
|
||||
commentCount={answer.numComments}
|
||||
content={answer.content}
|
||||
createdAt={answer.createdAt}
|
||||
href={`${router.asPath}/answer/${answer.id}/${createSlug(
|
||||
answer.content,
|
||||
)}`}
|
||||
upvoteCount={answer.numVotes}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
<PaginationLoadMoreButton query={answerInfiniteQuery} />
|
||||
</div>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -97,9 +97,9 @@ export const questionsQuestionCommentRouter = createRouter().query(
|
||||
}
|
||||
|
||||
return {
|
||||
data: processedQuestionCommentsData,
|
||||
nextCursor,
|
||||
processedQuestionCommentsData,
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
68
apps/portal/src/utils/questions/vote/useAnswerCommentVote.ts
Normal file
68
apps/portal/src/utils/questions/vote/useAnswerCommentVote.ts
Normal file
@@ -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',
|
||||
});
|
||||
}
|
||||
98
apps/portal/src/utils/questions/vote/useAnswerVote.ts
Normal file
98
apps/portal/src/utils/questions/vote/useAnswerVote.ts
Normal file
@@ -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',
|
||||
});
|
||||
}
|
||||
98
apps/portal/src/utils/questions/vote/useQuestionVote.ts
Normal file
98
apps/portal/src/utils/questions/vote/useQuestionVote.ts
Normal file
@@ -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 */
|
||||
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<typeof trpc.useMutation>[0];
|
||||
type QueryKey = Parameters<typeof trpc.useQuery>[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<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',
|
||||
});
|
||||
const getVoteValue = (vote: Vote | null) => {
|
||||
if (vote === Vote.UPVOTE) {
|
||||
return 1;
|
||||
}
|
||||
if (vote === Vote.DOWNVOTE) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
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',
|
||||
});
|
||||
};
|
||||
type RevertFunction = () => void;
|
||||
|
||||
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 InvalidateFunction = (
|
||||
previousVote: Vote | null,
|
||||
currentVote: Vote | null,
|
||||
) => Promise<void>;
|
||||
type InvalidateFunction = (voteValueChange: number) => Promise<RevertFunction>;
|
||||
|
||||
type VoteProps<VoteQueryKey extends QueryKey = QueryKey> = {
|
||||
idKey: string;
|
||||
invalidateKeys: Array<QueryKey>;
|
||||
onMutate?: InvalidateFunction;
|
||||
|
||||
// Invalidate: Partial<Record<QueryKey, InvalidateFunction | null>>;
|
||||
query: VoteQueryKey;
|
||||
setDownVoteKey: MutationKey;
|
||||
setNoVoteKey: MutationKey;
|
||||
@@ -184,12 +72,13 @@ type VoteProps<VoteQueryKey extends QueryKey = QueryKey> = {
|
||||
type UseVoteMutationContext = {
|
||||
currentData: any;
|
||||
previousData: any;
|
||||
revert: RevertFunction | undefined;
|
||||
};
|
||||
|
||||
export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
|
||||
export default function useVote<VoteQueryKey extends QueryKey = QueryKey>(
|
||||
id: string,
|
||||
opts: VoteProps<VoteQueryKey>,
|
||||
) => {
|
||||
) {
|
||||
const {
|
||||
idKey,
|
||||
invalidateKeys,
|
||||
@@ -201,7 +90,7 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
|
||||
} = 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 = <VoteQueryKey extends QueryKey = QueryKey>(
|
||||
onError: (_error, _variables, context) => {
|
||||
if (context !== undefined) {
|
||||
utils.setQueryData([query], context.previousData);
|
||||
context.revert?.();
|
||||
}
|
||||
},
|
||||
onMutate: async (vote) => {
|
||||
@@ -252,10 +142,14 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
|
||||
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<any, UseVoteMutationContext>(
|
||||
@@ -264,6 +158,7 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
|
||||
onError: (_error, _variables, context) => {
|
||||
if (context !== undefined) {
|
||||
utils.setQueryData([query], context.previousData);
|
||||
context.revert?.();
|
||||
}
|
||||
},
|
||||
onMutate: async (vote) => {
|
||||
@@ -287,10 +182,14 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
|
||||
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 = <VoteQueryKey extends QueryKey = QueryKey>(
|
||||
onError: (_error, _variables, context) => {
|
||||
if (context !== undefined) {
|
||||
utils.setQueryData([query], context.previousData);
|
||||
context.revert?.();
|
||||
}
|
||||
},
|
||||
onMutate: async () => {
|
||||
@@ -319,11 +219,13 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
|
||||
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 };
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user