From the last post, we added the back-end code. So now we need to add front-end code. Let’s add a separate component to UpvoteSection.tsx
. In this component, we want to show up and down arrow buttons and current points that the post has. Also want to show that the status of our response here from different colours. If we upvoted it need to show green colour and downvoted need to show in the red colour.
Before adding that change, we are changing both vote
and post
methods to match with this. In thevote
method we first check that we are upvoting or downvoting multiple times. If so nothing happens. If we are changing the vote we are updating the values accordingly. Replace the code in the vote
from the below code form checking on userId
.
const { userId } = req.session;
const upvote = await Upvote.findOne({ where: { postId, userId } });
if (upvote && upvote.value !== realValue) {
await getConnection().transaction(async (tm) => {
await tm.query(
` update upvote
set value = $1
where "postId" = $2 and "userId" = $3`,
[realValue, postId, userId]
);
await tm.query(
` update post
set points = points + $1
where id = $2`,
[2 * realValue, postId]
);
});
} else if (!upvote) {
// has never voted before
await getConnection().transaction(async (tm) => {
await tm.query(
` insert into upvote ("userId", "postId", value)
values ($1, $2, $3)`,
[userId, postId, realValue]
);
await tm.query(
` update post
set points = points + $1
where id = $2`,
[realValue, postId]
);
});
}
Then add the new field to Post
entity. This is not a column. This will only use for keep the voteStatus
for each post for current logged user.
@Field(() => Int, { nullable: true })
voteStatus: number | null;
Also, need to change the posts
query to get the data with voteStatus
of the current user. To do that, replace the code with the below code.
// In the posts method add context as parameter
@Ctx() { req }: RedditDbContext
// add the values to replacement array by conditionaly
if (req.session.userId) {
replacement.push(req.session.userId);
}
let cursorIdx = 3;
if (cursor) {
replacement.push(new Date(parseInt(cursor)));
cursorIdx = replacement.length;
}
// change the query to below query
SELECT p.*,
json_build_object(
'id', u.id,
'username', u.username,
'email', u.email
) creator,
${
req.session.userId
? '(SELECT value FROM upvote WHERE "userId" = $2 AND "postId" = p.id) "voteStatus"'
: 'null AS "voteStatus"'
}
FROM post p
INNER JOIN public.user u ON u.id = p."creatorId"
${cursor ? ` WHERE p."createdAt" < $${cursorIdx}` : ""}
ORDER BY p."createdAt" DESC
LIMIT $1
We can get post related data to this component. Because in future there can need some other details from the post that we need to show in here or some other data to make desitions.
So we are moving post related data to a new fragment.
fragment PostSnippet on Post {
id
createdAt
updatedAt
title
textSnippet
points
voteStatus
creator {
id
username
}
}
To match with this change, make a change to Posts
query in the front-end.
query Posts($limit: Int!, $cursor: String) {
posts(cursor: $cursor, limit: $limit) {
hasMore
posts{
...PostSnippet
}
}
}
Now is time to add the UpvoteSection
. Here is the code for related that section.
interface UpvoteSectionProps {
post: PostSnippetFragment;
}
export const UpvoteSection: React.FC<UpvoteSectionProps> = ({ post }) => {
const [loadingState, setLoadingState] =
(useState < "upvote-loading") |
"downvote-loading" |
("not-loading" > "not-loading");
const [, vote] = useVoteMutation();
return (
<Flex direction="column" justifyContent="center" alignItems="center" mr={4}>
<IconButton
onClick={async () => {
setLoadingState("upvote-loading");
await vote({
postId: post.id,
value: 1,
});
setLoadingState("not-loading");
}}
isLoading={loadingState === "upvote-loading"}
aria-label="upvote post"
colorScheme={post.voteStatus === 1 ? "green" : undefined}
icon={<ChevronUpIcon />}
/>
{post.points}
<IconButton
onClick={async () => {
setLoadingState("downvote-loading");
await vote({
postId: post.id,
value: -1,
});
setLoadingState("not-loading");
}}
isLoading={loadingState === "downvote-loading"}
colorScheme={post.voteStatus === -1 ? "red" : undefined}
aria-label="downvote post"
icon={<ChevronDownIcon />}
/>
</Flex>
);
};
We can add this component to index.tsx
file.
<Flex key={p.id} p={5} shadow="md" borderWidth="1px">
<UpvoteSection post={p} />
<Box>
// add this before post title.
<Heading fontSize="xl">{p.title}</Heading>
<Text>posted by {p.creator.username}</Text>
<Text mt={4}>{p.textSnippet}</Text>
</Box>
</Flex>
Now the most important part. Once we are voted we are updating the current vote count. To do that we are using readFragment
and writeFragment
. What happened here, once we voted, we will request the new data from the graphql
server by, passing postId
. Once we receive the new values we are updating them. The main benefit is we are not requesting a full data set, but part of it. Here is the relevant vote
mutation in createUrqlClient
method. Also to do these we need to add grapgql-tag
.
import gql from "graphql-tag";
vote: (_result, args, cache, info) => {
const { postId, value } = args as VoteMutationVariables;
const data = cache.readFragment(
gql`
fragment _ on Post {
id
points
voteStatus
}
`,
{
id: postId,
} as any
);
if (data) {
if (data.voteStatus === value) {
return;
}
const newPoints =
(data.points as number) + (!data.voteStatus ? 1 : 2) * value;
cache.writeFragment(
gql`
fragment __ on Post {
points
voteStatus
}
`,
{ id: postId, points: newPoints, voteStatus: value } as any
);
}
},
Thanks for reading this. If you have anything to ask regarding this please leave a comment here. Also, I wrote this according to my understanding. So if any point is wrong, don’t hesitate to correct me. I really appreciate you. That’s for today friends. See you soon. Thank you.
References:
This article series based on the Ben Award - Fullstack React GraphQL TypeScript Tutorial. This is amazing tutorial and I highly recommend you to check that out.
Main image credit