Frontend Serverless with React and GraphQL
Start at the beginning and work your way through this project. The code for each step as well as the finished project can be found in the Github repository.
25. Add a Like Button
Objective: Add a heart button that will trigger a create userLike or delete userLike mutation when clicked or unclicked.
We will create a like button that works by taking a recipeId along with a list of users who have liked that recipe. We will run the userFetchUser
hook to determine if the current user is logged in or not. If they aren't logged in, then we simply display an empty heart. If they are logged in though, we will run a filter to see if they are in the list of users that have liked the current item. If they have liked the item, then we will fill in the heart otherwise the heart will be empty. We can also use whether the user has liked the item or not to determine whether we run a createUserLike query or a deleteUserLike query.
First, let's add @ant-design/icons
to our project. In the latest version of ant-design, they moved icons out into a separate library so we will need to install them separately even though we have already installed the ant-design package.
bash
npm install --save @ant-design/icons
Now we can create the <LikeButton>
component.
components/LikeButton.tsx
import { UserLike } from '../generated/apollo-components';import styled from 'styled-components';import { HeartFilled, HeartTwoTone } from '@ant-design/icons';import { useMutation } from '@apollo/react-hooks';import { createUserLikeGraphQL } from '../graphql/mutations/createUserLike';import { deleteUserLikeGraphQL } from '../graphql/mutations/deleteUserLike';import { useFetchUser } from '../utils/user';import * as _ from 'lodash';import { recipeGraphQL } from '../graphql/queries/recipe';import { userLikesGraphQL } from '../graphql/queries/userLikes';const StyledSpan = styled.span`${({ theme }) => `padding-left: 8px;font-size: ${theme['font-size-xs']} !important;color: ${theme['heart-color']};i {padding-left: 2px}`}`;export const LikeButton = ({userLikes,recipeId,}: {userLikes: UserLike[];recipeId: string;}) => {const { user, loading: isFetchingUser } = useFetchUser();const owner = _.get(user, 'sub');const [createUserLikeMutation,{ loading: createUserLikeLoading },] = useMutation(createUserLikeGraphQL);const [deleteUserLikeMutation,{ loading: deleteUserLikeLoading },] = useMutation(deleteUserLikeGraphQL);const userLike = _.filter(userLikes, { user: owner });const hasUserLiked = userLike.length > 0 ? true : false;const refetchQueries = [{query: recipeGraphQL,variables: { where: { id: recipeId } },},{query: userLikesGraphQL,variables: { where: { user: owner } },},];if (_.isEmpty(owner)) {return (<StyledSpan>{`${userLikes.length}`}<HeartTwoTone twoToneColor="#eb2f96" /></StyledSpan>);}return (<StyledSpan>{`${userLikes.length}`}{hasUserLiked ? (<HeartFilledonClick={() => {deleteUserLikeMutation({refetchQueries,variables: {where: {id: userLike[0].id,},},});}}/>) : (<HeartTwoToneonClick={() => {createUserLikeMutation({refetchQueries,variables: {data: {user: owner,recipe: {connect: { id: recipeId },},},},});}}twoToneColor="#eb2f96"/>)}</StyledSpan>);};
Once we have the working like button, we need to use it both on the <OneRecipe>
and <RecipesListItem>
components so that a user will see the button either on a recipe list type of page or on the recipe page.
components/OneRecipe.tsx
import { Recipe } from '../generated/apollo-components';import { Row, Col, List } from 'antd';import styled from 'styled-components';import GraphImg from 'graphcms-image';import * as _ from 'lodash';import { generateUnit } from '../utils/generateUnit';import { GenerateContent } from './GenerateContent';import { LikeButton } from './LikeButton';const StyledOneRecipe = styled(Col)`${({ theme }) => `margin-top: ${theme['margin-small']};min-height: 320px;border-radius: 8px;box-shadow: 0 0 16px ${theme['border-color']};border: ${theme['border-width']} solid ${theme['border-color']};.graphcms-image-outer-wrapper {border: 0px;.graphcms-image-wrapper {border: 0px;position: relative;float: left;width: 100%;height 400px;background-position: 50% 50%;background-repeat: no-repeat;background-size: cover;img {text-align: center;border-radius: 6px 6px 0px 0px;}}}h1,h2 {padding-top: ${theme['margin-small']};text-align: left;}h3{text-align: left;}`}`;export const OneRecipe = ({ recipe }: { recipe: Recipe }) => {const { image, title, description, content, userLikes, id } = recipe;const ingredients = _.get(recipe, 'ingredients');return (<Row><StyledOneRecipesm={{ span: 20, offset: 2 }}md={{ span: 16, offset: 4 }}lg={{ span: 12, offset: 6 }}><Row><Col span={24}>{image ? <GraphImg image={image} /> : null}</Col></Row><Row><Col span={20} offset={2}><h1>{title}<LikeButton userLikes={userLikes} recipeId={id} /></h1><p>{description}</p></Col></Row><Row><Col span={12} offset={6}><Listheader={<h3>Ingredients:</h3>}bordereddataSource={ingredients || [{ type: 'None added', amount: 0, unit: '' }]}renderItem={({ amount, unit, type }) => {return (<List.Item>{ingredients? `${amount} ${generateUnit(unit, amount)} ${type}`: `${type}`}</List.Item>);}}></List></Col></Row><Row><Col span={20} offset={2}><h2>Directions:</h2><GenerateContent textString={content} /></Col></Row></StyledOneRecipe></Row>);};
components/RecipesListItem.tsx
import { Recipe } from '../generated/apollo-components';import { Col } from 'antd';import styled from 'styled-components';import EllipsisText from 'react-ellipsis-text';import GraphImage from 'graphcms-image';import Link from 'next/link';import { LikeButton } from './LikeButton';const StyledRecipe = styled(Col)`${({ theme }) => `padding: 0px ${theme['padding-small']};.card {cursor: pointer;margin-bottom: ${theme['margin-small']};height: 340px;border-radius: 8px;box-shadow: 0 0 16px ${theme['border-color']};border: ${theme['border-width']} solid ${theme['border-color']};}.graphcms-image-outer-wrapper{border: 0px;.graphcms-image-wrapper {border: 0px;position: relative;float: left;width: 100%;height: 200px;background-position: 50% 50%;background-repeat: no-repeat;background-size: cover;img {text-align: center;border-radius: 6px 6px 0px 6px;}}}p {padding: 0px ${theme['padding-small']};}h3 {padding: 0px ${theme['padding-small']};line-height: 1.5em;}`}`;export const RecipeListItem = ({recipe,parentRoute,}: {recipe: Recipe;parentRoute: string;}) => {const { title, description, image, id, userLikes } = recipe;return (<StyledRecipexs={{ span: 24 }}sm={{ span: 12 }}lg={{ span: 8 }}xl={{ span: 6 }}><div className="card"><Link href={`/${parentRoute}/${id}`}><div>{image ? <GraphImage image={image} /> : null}</div></Link><h3>{title}<LikeButton userLikes={userLikes} recipeId={id} /></h3><p><EllipsisText text={description} length={110} /></p></div></StyledRecipe>);};