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.


16. Add RecipeListItem component

Objective: We will create the recipe card that we will use on the recipe list type of pages.

Click to download the mock recipes

Now that we have the <RecipeList> working, it is time to create the <RecipeListItem> component. First, let's add two packages that we will need. The first is graphcms-image and this will handle asynchronously loading the images from GraphCMS and displaying them in a progressive manner. That means we will first see a blurry image, which then will progressively get filled in as the rest of the image gets downloaded. Next we will install the react-ellipsis-text component which will allow us to ensure that if a title or description is particuarly long that we can cut it off with a certain point and add an ellipsis so that it won't wreck the flow of our cards.

bash

npm install --save react-ellipsis-text graphcms-image

Now let's create our component

components/RecipeListItem.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';
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 } = recipe;
return (
<StyledRecipe
xs={{ 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}</h3>
<p>
<EllipsisText text={description} length={110} />
</p>
</div>
</StyledRecipe>
);
};

Now, lets update our <RecipeList> component to add our fancy new list item component to it. The final file will look like the following:

components/RecipeList.tsx

import { QueryHookOptions, useQuery } from '@apollo/react-hooks';
import { recipesGraphQL } from '../graphql/queries/recipes';
import { userLikesGraphQL } from '../graphql/queries/userLikes';
import * as _ from 'lodash';
import { Row } from 'antd';
import { Recipe } from '../generated/apollo-components';
import { Loading } from './notify/Loading';
import { Warning } from './notify/Warning';
import { Error } from './notify/Error';
import { RecipeListItem } from './RecipeListItem';
export enum queryEnum {
userLikes = 'userLikes',
recipes = 'recipes',
}
type RecipesListProps = {
options?: QueryHookOptions;
parentRoute: string;
queryType: queryEnum;
};
export const RecipesList = ({
options,
parentRoute,
queryType,
}: RecipesListProps) => {
const query =
queryType === queryEnum.recipes ? recipesGraphQL : userLikesGraphQL;
const { loading, data, error } = useQuery(query, options);
const parentArray = _.get(data, queryType);
const recipesList = _.map(parentArray, (value) =>
_.get(value, 'recipe', value),
);
if (loading) return <Loading />;
if (error) return <Error errorText={`${error}`} />;
if (recipesList.length === 0)
return (
<Warning
warningHeader="No Recipes"
warningText="No Recipes are present. Why not add one?"
/>
);
return (
<Row>
{recipesList.map((recipe: Recipe) => (
<RecipeListItem
recipe={recipe}
parentRoute={parentRoute}
key={`${recipe.id}-${queryType}`}
/>
))}
</Row>
);
};