Skip to content
This repository was archived by the owner on May 4, 2023. It is now read-only.

Commit 60c1eda

Browse files
feat: added upvote/downvote to search result list
1 parent 3cd602c commit 60c1eda

File tree

6 files changed

+196
-11
lines changed

6 files changed

+196
-11
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module.exports = {
1010
'import/no-cycle': 'off',
1111
'@typescript-eslint/no-non-null-assertion': 'off',
1212
'react/require-default-props': [2, { functions: 'defaultArguments' }],
13+
'import/prefer-default-export': 'off',
1314
},
1415
parserOptions: {
1516
ecmaVersion: 2020,

src/renderer/components/SearchResults/SearchResultsListItem.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
import { Flex, Text } from '@chakra-ui/react';
2-
import {
3-
ChartBarsIcon,
4-
DotIcon,
5-
DownVoteIcon,
6-
Logo,
7-
Tags,
8-
UpVoteIcon,
9-
} from '@codiga/codiga-components';
2+
import { ChartBarsIcon, DotIcon, Logo, Tags } from '@codiga/codiga-components';
103
import {
114
AssistantRecipeWithStats,
125
RecipeSummary,
136
} from 'renderer/types/assistantTypes';
147
import FavoriteSnippet from '../Favorite/FavoriteSnippet';
8+
import Votes from './Votes';
159

1610
type SearchResultsListItemProps = {
1711
recipe: AssistantRecipeWithStats;
@@ -60,9 +54,12 @@ export default function SearchResultsListItem({
6054

6155
<Flex alignItems="center" gridGap="space_8">
6256
<Text d="flex" alignItems="center" gridGap="space_4" size="xs">
63-
<UpVoteIcon />
64-
{(recipe?.upvotes || 0) - (recipe?.downvotes || 0)}
65-
<DownVoteIcon />
57+
<Votes
58+
upvotes={recipe.upvotes || 0}
59+
downvotes={recipe.downvotes || 0}
60+
entityType="Recipe"
61+
entityId={recipe.id}
62+
/>
6663
</Text>
6764
<DotIcon h="2px" w="2px" />
6865
<Text d="flex" alignItems="center" gridGap="space_4" size="xs">
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { useMutation, useQuery } from '@apollo/client';
2+
import { Flex, FlexProps, IconButton, Text, Tooltip } from '@chakra-ui/react';
3+
import { DownVoteIcon, UpVoteIcon, useToast } from '@codiga/codiga-components';
4+
import { useUser } from 'renderer/components/UserContext';
5+
import {
6+
AddVoteMutationVariables,
7+
ADD_VOTE,
8+
DeleteVoteMutationVariables,
9+
DELETE_VOTE,
10+
} from 'renderer/graphql/mutations';
11+
import { GET_RECIPE_VOTES_QUERY } from 'renderer/graphql/queries';
12+
import { formatNumber } from 'renderer/utils/formatUtils';
13+
14+
type VotesProps = FlexProps & {
15+
entityId: number;
16+
entityType: 'Recipe';
17+
upvotes: number;
18+
downvotes: number;
19+
};
20+
21+
const Votes = ({
22+
upvotes,
23+
downvotes,
24+
entityId,
25+
entityType = 'Recipe',
26+
...props
27+
}: VotesProps) => {
28+
const toast = useToast();
29+
const { id: userId } = useUser();
30+
31+
const { data, refetch } = useQuery(GET_RECIPE_VOTES_QUERY, {
32+
skip: !userId,
33+
variables: {
34+
recipeId: entityId,
35+
},
36+
});
37+
38+
const isUpVoted = Boolean(data?.votesData.isUpVoted);
39+
const isDownVoted = Boolean(data?.votesData.isDownVoted);
40+
const upVoteCount = Number(data?.votesData.upvotes);
41+
const downVoteCount = Number(data?.votesData.downvotes);
42+
const voteText = data ? upVoteCount - downVoteCount : upvotes - downvotes;
43+
44+
const [addVote] = useMutation<void, AddVoteMutationVariables>(ADD_VOTE);
45+
const [deleteVote] = useMutation<void, DeleteVoteMutationVariables>(
46+
DELETE_VOTE
47+
);
48+
49+
const handleUpVote = async () => {
50+
try {
51+
if (isUpVoted) {
52+
await deleteVote({ variables: { entityId, entityType } });
53+
} else {
54+
await addVote({ variables: { entityId, entityType, isUpvote: true } });
55+
}
56+
refetch();
57+
toast({
58+
status: 'success',
59+
description: 'Snippet upvoted',
60+
});
61+
} catch (err) {
62+
toast({
63+
status: 'error',
64+
description: 'An error occured while upvoting. Please refresh.',
65+
});
66+
}
67+
};
68+
69+
const handleDownVote = async () => {
70+
try {
71+
if (isDownVoted) {
72+
await deleteVote({ variables: { entityId, entityType } });
73+
} else {
74+
await addVote({ variables: { entityId, entityType, isUpvote: false } });
75+
}
76+
refetch();
77+
toast({
78+
status: 'success',
79+
description: 'Snippet downvoted.',
80+
});
81+
} catch (err) {
82+
toast({
83+
status: 'error',
84+
description: 'An error occured while downvoting. Please refresh.',
85+
});
86+
}
87+
};
88+
89+
const countColor = isUpVoted || isDownVoted ? 'rose.50' : undefined;
90+
91+
return (
92+
<Flex gridGap="space_4" alignItems="center" {...props}>
93+
<Tooltip
94+
label="Please log in to upvote"
95+
shouldWrapChildren
96+
isDisabled={!!userId}
97+
>
98+
<IconButton
99+
isDisabled={!userId}
100+
minW="auto"
101+
boxSize="20px"
102+
variant="ghost"
103+
aria-label={`Upvote ${entityType.toLowerCase()}`}
104+
icon={
105+
<UpVoteIcon
106+
boxSize="16px"
107+
color={isUpVoted ? 'base.rose' : 'inherit'}
108+
fillRule={isUpVoted ? 'nonzero' : 'evenodd'}
109+
/>
110+
}
111+
onClick={handleUpVote}
112+
/>
113+
</Tooltip>
114+
<Text lineHeight="20px" color={countColor} _dark={{ color: countColor }}>
115+
{formatNumber(voteText)}
116+
</Text>
117+
<Tooltip
118+
label="Please log in to downvote"
119+
shouldWrapChildren
120+
isDisabled={!!userId}
121+
>
122+
<IconButton
123+
isDisabled={!userId}
124+
minW="auto"
125+
boxSize="20px"
126+
variant="ghost"
127+
aria-label={`Downvote ${entityType.toLowerCase()}`}
128+
icon={
129+
<DownVoteIcon
130+
boxSize="16px"
131+
color={isDownVoted ? 'base.rose' : 'inherit'}
132+
fillRule={isDownVoted ? 'nonzero' : 'evenodd'}
133+
/>
134+
}
135+
onClick={handleDownVote}
136+
/>
137+
</Tooltip>
138+
</Flex>
139+
);
140+
};
141+
142+
export default Votes;

src/renderer/graphql/mutations.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,32 @@ export const UNSUBSCRIBE_TO_RECIPE = gql`
7777
unsubscribeFromRecipe(id: $id)
7878
}
7979
`;
80+
81+
type VotingMutationType = {
82+
entityId: number;
83+
entityType: 'Recipe';
84+
isUpvote: boolean;
85+
};
86+
87+
export type AddVoteMutationVariables = VotingMutationType;
88+
89+
export type DeleteVoteMutationVariables = Pick<
90+
VotingMutationType,
91+
'entityId' | 'entityType'
92+
>;
93+
94+
export const ADD_VOTE = gql`
95+
mutation addVote(
96+
$entityId: Long!
97+
$entityType: VoteEntity!
98+
$isUpvote: Boolean!
99+
) {
100+
addVote(entityId: $entityId, entityType: $entityType, isUpvote: $isUpvote)
101+
}
102+
`;
103+
104+
export const DELETE_VOTE = gql`
105+
mutation deleteVote($entityId: Long!, $entityType: VoteEntity!) {
106+
deleteVote(entityId: $entityId, entityType: $entityType)
107+
}
108+
`;

src/renderer/graphql/queries.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,3 +387,15 @@ export const GET_RECIPES_SEMANTICALLY = gql`
387387
}
388388
}
389389
`;
390+
391+
export const GET_RECIPE_VOTES_QUERY = gql`
392+
query getRecipeVotes($recipeId: Long!) {
393+
votesData: assistantRecipe(id: $recipeId) {
394+
id
395+
isUpVoted
396+
isDownVoted
397+
upvotes
398+
downvotes
399+
}
400+
}
401+
`;

src/renderer/utils/formatUtils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export function formatNumber(num: number) {
2+
if (Math.abs(num) < 1000) return num;
3+
return `${Math.sign(num) * (Math.round(Math.abs(num) / 100) / 10)}k`;
4+
}

0 commit comments

Comments
 (0)