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.


18. Add OneRecipe component

Objective: Now that we are fetching the data for our Recipe page, let's build a Recipe component to render the view for this page.

First, let's add an npm package called pluralize. We will use this to identify whether an ingredient in our ingredients list needs to be pluralized or not. You could imagine that 1 cup of milk would not be pluralized but 12 peas would. You might think that you could just add the letter s to a word based on the amount, but English is tricky and certain words need an es (lunch/lunches) and others need an ies (party/parties). Let's just use a package so we can get it right.

bash

npm install --save pluralize

Now let's build a function that will generate the unit based on how many there are. This will allow us to say "1 potato" vs "2 potatoes". Provided a unit is not provided, it will just return an empty string:

utils/generateUnit.tsx

import pluralize from 'pluralize';
export const generateUnit = (unit, amount) => {
if (unit !== '-') {
return pluralize(unit, parseInt(amount));
} else {
return '';
}
};

The next helper function that we should create is a component called <GenerateContent>. This is responsible for taking a string and creating paragraphs each time a \n is found in the string.

components/GenerateContent.tsx

import * as React from 'react';
export const GenerateContent = ({ textString }: { textString: string }) => (
<p>
{textString.split('\n').map((item, key) => (
<React.Fragment key={key}>
{item}
<br />
</React.Fragment>
))}
</p>
);

Now that we have those two helper functions created, let's create the <OneRecipe> component:

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';
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 } = recipe;
const ingredients = _.get(recipe, 'ingredients');
console.log(content);
return (
<Row>
<StyledOneRecipe
sm={{ 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}</h1>
<p>{description}</p>
</Col>
</Row>
<Row>
<Col span={12} offset={6}>
<List
header={<h3>Ingredients:</h3>}
bordered
dataSource={
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>
);
};

We should also update the <RecipePage> component to use this new <OneRecipe> component that we just made.

pages/recipe/[id].tsx

import { useQuery } from '@apollo/react-hooks';
import { recipeGraphQL } from '../../graphql/queries/recipe';
import * as _ from 'lodash';
import { Loading } from '../../components/notify/Loading';
import { MainLayout } from '../../components/layout/MainLayout';
import { Error } from '../../components/notify/Error';
import { OneRecipe } from '../../components/OneRecipe';
const Recipe = ({ id }) => {
const { loading, data, error } = useQuery(recipeGraphQL, {
variables: { where: { id } },
});
const title = _.get(data, 'recipe.title');
if (loading) {
return (
<MainLayout title="Recipe Loading">
<Loading />
</MainLayout>
);
}
if (error) {
return (
<MainLayout title="Recipe Loading Error">
<Error errorText={`${error}`} />
</MainLayout>
);
}
if (!title) {
return (
<MainLayout title="Not a valid recipe">
<Error errorText={`Not a valid recipe`} />
</MainLayout>
);
}
return (
<MainLayout title={title}>
<OneRecipe recipe={data.recipe} />
</MainLayout>
);
};
Recipe.getInitialProps = ({ query }) => {
const { id } = query;
return { id };
};
export default Recipe;