Create a Fullstack Blog App with Next.js, Prisma 2 and Docker- Part III Build the UI
Tags: javascript, fullstack, tutorial, react
Published: 2019-07-12 14:42:35


In the final post of this series, we will build out the UI for this Blog site built with Next.js, Prisma 2 and Docker. Be sure to check out the first two posts to make sure you are caught up first.

#Table of Contents:

**Part I- Set up the Repo and Configure the Backend**

1. Create the base repo
2. Create the Next.js frontend
3. Create the boilerplate Prisma 2 backend
4. Dockerize our web app

   a. Create a docker-compose file

   b. Add Dockerfiles for each container

5. Configure the backend

   a. Switch database from SQLite to MySQL

   b. Remove unused backend boilerplate code

   c. Update backend queries in Prisma Nexus

6. Verify our Docker-Compose cluster works

**Part II- Configure the Frontend**

1.  Add GraphQL fragments, queries and resolvers
2.  Add GraphQL-Codegen for generating React Components
3.  Add Apollo and create HOC for Next.js
4.  Add React-Apollo to project root
5.  Install Antd for a beautiful site layout

**Part III- Build the UI (this post)**

1. Create the Main Layout
2. Create a Users Layout Component
3. Create a Signup User Component
4. Create a Feed Layout Component
5. Create a New Draft Component
6. Create a Publish Draft Button Component
7. Create a Delete Post Button Component

As always, make sure to check out the end for video walkthroughs.

**Part III- Build the UI**

**1. Create the main layout**

Our first step of our UI journey is to create a layout component that we will load on every page. This will contain the header, footer and a passthrough for the rest of the page. Create a main layout file and add the following code:

`frontend/components/main-layout.tsx`

```tsx
import React, { ReactNode, Component } from 'react'
import { Layout } from 'antd'
import Link from 'next/link'
import Head from 'next/head'

const { Footer, Header, Content } = Layout

type Props = {
  title?: string
  children: ReactNode
}

class MainLayout extends Component<Props> {
  render() {
    const { children, title } = this.props
    return (
      <Layout>
        <Head>
          <title>{title}</title>
          <meta charSet="utf-8" />
          <meta
            name="viewport"
            content="initial-scale=1.0, width=device-width"
          />
        </Head>
        <Header>
          <nav>
            <Link href="/">
              <a>Home</a>
            </Link>
          </nav>
        </Header>
        <Content>{children}</Content>
        <Footer>
          <hr />
          <span>I'm here to stay (Footer)</span>
        </Footer>
      </Layout>
    )
  }
}

export default MainLayout
```

Update the index page to add the Layout component. You'll need to add this Layout on every page you make in the future, but in our case we only have the one:

`frontend/pages/index.tsx`

```tsx
import * as React from 'react'
import { NextPage } from 'next'
import Layout from '../components/main-layout'

const IndexPage: NextPage = () => {
  return (
    <Layout title="Blog Layout">
      <h1>Simple Prisma 2 Blog Example</h1>
    </Layout>
  )
}

export default IndexPage
```

When you reload your page, it should have a header, footer and the body should be a gray color:

![Ant Design Layout Theme added.](https://www.codemochi.com/static/0b81ff7f4a7d502a2e44807b37e22fa6/feedf/8a-main-layout.png)

**2. Create a Users Layout Component**

Now that we have Ant Design installed, Apollo configured and our Apollo components autogenerated, it is time to get started making our components. Let's start with a users layout.

`frontend/components/users.tsx`

```tsx
import React from 'react'
import { Table } from 'antd'
import { UsersQueryComponent } from '../generated/apollo-components'

type Props = {}

class UsersList extends React.PureComponent<Props> {
  render() {
    return (
      <UsersQueryComponent>
        {({ loading, error, data }) => {
          if (loading) return <p>Loading...</p>
          if (error) return <p>Error</p>

          if (data && 'users' in data && data.users.length > 0) {
            const feedData = data.users.map(({ name, email }, i) => ({
              key: i,
              name,
              email,
            }))
            const columns = [
              {
                title: 'Name',
                dataIndex: 'name',
                key: 'name',
              },
              {
                title: 'Email',
                dataIndex: 'email',
                key: 'email',
              },
            ]
            return <Table columns={columns} dataSource={feedData} />
          }

          return <p>No users yet.</p>
        }}
      </UsersQueryComponent>
    )
  }
}

export default UsersList
```

We are using an autogenerated component that is called `UsersQueryComponent` that was made by the GraphQL Codegen plugin. It is doing all the hard work of fetching our data. We first check that it isn't loading and there isn't an error and then we pull off a list of users from the `data` object.

Antd has a table component that we can feed it an array of objects and a list of column names and it will create a beautiful table for us. If there aren't any users we just report that back instead.

Now we can import that `UsersList` component into our index.tsx file:

`frontend/pages/index.tsx`

```tsx
import * as React from 'react'
import { NextPage } from 'next'
import Layout from '../components/main-layout'
import UsersList from '../components/users'

const IndexPage: NextPage = () => {
  return (
    <Layout title="Blog Layout">
      <h1>Simple Prisma 2 Blog Example</h1>
      <h3>Users List</h3>
      <UsersList />
    </Layout>
  )
}

export default IndexPage
```

When we look at our webpage now, we should see that it is saying that we don't have any users yet. Let's change that now by creating a signup user component.

![Blog page with user's list](https://www.codemochi.com/static/1fcff96221b2d734d04326d78d651997/feedf/8b-users-layout.png)

**3. Create a Signup User Component**

Let's create a new file called signup-user.tsx and add the following code:

`frontend/components/signup-user.tsx`

```tsx
import React from 'react'
import { Row, Col, Button, Form, Input } from 'antd'
import {
  SignupUserMutationComponent,
  UsersQueryDocument,
} from '../generated/apollo-components'

type Props = {}
const initialState = { name: '', email: '' }
type State = typeof initialState

class SignupUser extends React.Component<Props> {
  state: State = initialState

  handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = event.target
    this.setState({ [name]: value })
  }

  render() {
    return (
      <SignupUserMutationComponent>
        {createUser => (
          <Form
            onSubmit={e => {
              e.preventDefault()
              createUser({
                variables: { ...this.state },
                refetchQueries: [{ query: UsersQueryDocument }],
              }).then(() => {
                this.setState({ name: '', email: '' })
              })
            }}
          >
            <Row>
              <Col span={6}>
                <Form.Item>
                  <Input
                    placeholder="name"
                    name="name"
                    value={this.state.name}
                    onChange={this.handleChange}
                    type="text"
                  />
                </Form.Item>
              </Col>
              <Col span={6}>
                <Form.Item>
                  <Input
                    placeholder="email"
                    name="email"
                    value={this.state.email}
                    onChange={this.handleChange}
                    type="text"
                  />
                </Form.Item>
              </Col>
              <Col span={6}>
                <Form.Item>
                  <Button htmlType="submit">Signup User</Button>
                </Form.Item>
              </Col>
            </Row>
          </Form>
        )}
      </SignupUserMutationComponent>
    )
  }
}

export default SignupUser
```

We are using a different autogenerated component called `SignupUserMutationComponent` to trigger our signupUser mutation. The `SignupUserMutationComponent` yields a method that we call createUser here which allows us to trigger a mutation wherever we'd like. We can pass variables into this method when we call it and they will get added to our request for the backend.

We create an HTML form with several inputs- name and email. We use the `SignupUser` state to save the input as the user types into the two input fields. Since the input fields display the current state of the react component, the user isn't directly typing into the field but instead is triggering an onClick trigger which is updating the state which we then see reflected in the input field.

When the user presses the submit button, we use an onSubmit trigger to fire off the createUser method which we populate with variables from the state.

After the mutation fires off we have a refetchQuery which will re-run the users query to make sure that our `UsersList` will have the new user's entry that was just added. Finally, we clear the state variables name and email which will clear out the input fields. This approach is called controlled components in React and is a pretty standard way of doing things, so if it is unfamiliar to you, check out the [official documentation](https://reactjs.org/docs/forms.html) for more details (or feel free to reach out directly to me for this or other issues for possible future blog posts!).

Now you can add the `SignupUser` component to the index page:

`frontend/pages/index.tsx`

```tsx
import * as React from 'react'
import { NextPage } from 'next'
import Layout from '../components/main-layout'
import UsersList from '../components/users'
import SignupUser from '../components/signup-user'

const IndexPage: NextPage = () => {
  return (
    <Layout title="Blog Layout">
      <h1>Simple Prisma 2 Blog Example</h1>
      <h3>Signup User</h3>
      <SignupUser />
      <h3>Users List</h3>
      <UsersList />
    </Layout>
  )
}

export default IndexPage
```

Now go back to the Next.js website and try adding a new user and email. You should see that it gets added to the users list just like this:

![Blog page with user signup added.](https://www.codemochi.com/static/cd17055f95c39016985992152fbce7a0/feedf/8c-signup.png)

**4. Create a Feed Layout Component**

Our blog page will use the Feed Layout twice:

- all the published blog posts
- all the hidden blog posts

We want to create a `FeedList` component so that we can display either one based on whether the `published` boolean that we pass in is true or false.

Create a feed.tsx file and add the following code:

`frontend/components/feed.tsx`

```tsx
import React from 'react'
import { Table } from 'antd'
import { FeedQueryComponent } from '../generated/apollo-components'

type Props = {
  published: boolean
}

class FeedList extends React.PureComponent<Props> {
  render() {
    const { published } = this.props
    return (
      <FeedQueryComponent variables={{ published }}>
        {({ loading, error, data }) => {
          if (loading) return <p>Loading...</p>
          if (error) return <p>Error</p>

          if (data && 'feed' in data && data.feed.length > 0) {
            const feedData = data.feed.map(({ id, title, content }, i) => ({
              key: i,
              title,
              content,
              id,
            }))
            const columns = [
              {
                title: 'Title',
                dataIndex: 'title',
                key: 'title',
              },
              {
                title: 'Content',
                dataIndex: 'content',
                key: 'content',
              },
              {
                title: 'Action',
                key: 'action',
                render: () => {
                  return <p>Button Group will go here</p>
                },
              },
            ]
            return <Table columns={columns} dataSource={feedData} />
          }

          return <p>No results yet.</p>
        }}
      </FeedQueryComponent>
    )
  }
}

export default FeedList
```

The `FeedList` component looks very similar to the `Users` component that we already created. We are utilizing the `FeedQueryComponent` that is autogenerated just like before and now we are going to create a table with 3 columns- title, content and action. The action group will have our buttons for publishing and deleting the posts but we have just stubbed those out for now.

Now modify the index.tsx file in the pages folder to have two instances of the `FeedList` component- once with the published prop set to true and the second time set to false.

`frontend/pages/index.tsx`

```tsx
import * as React from 'react'
import { NextPage } from 'next'
import Layout from '../components/main-layout'
import FeedList from '../components/feed'
import UsersList from '../components/users'
import SignupUser from '../components/signup-user'

const IndexPage: NextPage = () => {
  return (
    <Layout title="Blog Layout">
      <h1>Simple Prisma 2 Blog Example</h1>
      <h3>Create User</h3>
      <SignupUser />
      <h3>Users</h3>
      <UsersList />

      <h3>Feed</h3>
      <FeedList published={true} />
      <h3>Hidden Feed</h3>
      <FeedList published={false} />
    </Layout>
  )
}

export default IndexPage
```

Now navigate to the Next.js webpage and you should see that it has both Feed components.

![Published and Non-published feeds added.](https://www.codemochi.com/static/5d08de72565e11bb5081b4dd6a095baa/feedf/8d-feeds.png)

**5. Create a New Draft Component**

Now we will create a New Draft Component so we can make new blog posts. This will be very similar to the `SignupUser` component that we have already made. The goal here is that when the draft is created, we will see it show up on the unpublished list.

Create a new-draft.tsx file and add the following code:

`/frontend/components/new-draft.tsx`

```tsx
import React from 'react'
import { Row, Col, Button, Form, Input } from 'antd'
import {
  CreateDraftMutationComponent,
  FeedQueryDocument,
} from '../generated/apollo-components'

type Props = {}
const initialState = { title: '', content: '', authorEmail: '' }
type State = typeof initialState

class NewDraft extends React.Component<Props> {
  state: State = initialState

  handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = event.target
    this.setState({ [name]: value })
  }

  render() {
    return (
      <CreateDraftMutationComponent>
        {createDraft => (
          <Form
            onSubmit={e => {
              e.preventDefault()
              createDraft({
                variables: { ...this.state },
                refetchQueries: [
                  { query: FeedQueryDocument, variables: { published: true } },
                  { query: FeedQueryDocument, variables: { published: false } },
                ],
              }).then(res => {
                console.log(res)
                this.setState({ title: '', content: '', authorEmail: '' })
              })
            }}
          >
            <Row>
              <Col span={6}>
                <Form.Item>
                  <Input
                    placeholder="title"
                    name="title"
                    value={this.state.title}
                    onChange={this.handleChange}
                    type="text"
                  />
                </Form.Item>
              </Col>
              <Col span={6}>
                <Form.Item>
                  <Input
                    placeholder="content"
                    name="content"
                    value={this.state.content}
                    onChange={this.handleChange}
                    type="text"
                  />
                </Form.Item>
              </Col>
              <Col span={6}>
                <Form.Item>
                  <Input
                    placeholder="authorEmail"
                    name="authorEmail"
                    value={this.state.authorEmail}
                    onChange={this.handleChange}
                    type="text"
                  />
                </Form.Item>
              </Col>
              <Col span={6}>
                <Form.Item>
                  <Button htmlType="submit">Create Draft</Button>
                </Form.Item>
              </Col>
            </Row>
          </Form>
        )}
      </CreateDraftMutationComponent>
    )
  }
}

export default NewDraft
```

The code is very similar to the `SignupUser` component- we have inputs for title, content, and author email and typing into these fields stores the value in a state which we use as variables in the createDraft mutation when the submit button is pressed.

Note that we are trusting the user to add their email address in the authorEmail field. You would never do this for a real application. You would likely have a user pass a JSON web token (JWT) which would have the user's email encoded inside so that you'd know that you can trust the email you are given. We won't go through how to use JWTs in this example, but this is something that could be added after our application has been completed.

We need to update our index page for a final time to add this `NewDraft` component.

`frontend/pages/index.tsx`

```tsx
import * as React from 'react'
import { NextPage } from 'next'
import Layout from '../components/main-layout'
import FeedList from '../components/feed'
import NewDraft from '../components/new-draft'
import UsersList from '../components/users'
import SignupUser from '../components/signup-user'

const IndexPage: NextPage = () => {
  return (
    <Layout title="Blog Layout">
      <h1>Simple Prisma 2 Blog Example</h1>
      <h3>Create User</h3>
      <SignupUser />
      <h3>Users</h3>
      <UsersList />

      <h3>Create Draft</h3>
      <NewDraft />
      <h3>Feed</h3>
      <FeedList published={true} />
      <h3>Hidden Feed</h3>
      <FeedList published={false} />
    </Layout>
  )
}

export default IndexPage
```

Now go to the Next.js site and you should see that there is the add draft component. Add a new post, making sure to match the email to the user that you have above and submit it.

![New Draft component added to the blog page- before adding new draft.](https://www.codemochi.com/static/b339a63e1f48d72041696668b71b036b/df66b/8e-new-draft-2-before-add-draft.png)

You should see that it gets added to the drafts feed list.

![New Draft component added to the blog page- after adding new draft.](https://www.codemochi.com/static/4da8444c7fd730c14dc2baf8a1450665/feedf/8e-new-draft-2-after-add-draft.png)

**6. Create a Publish Draft Button Component**

Now that we have a draft, let's publish it! We will create a button that will call a publish mutation with a particular post ID as an input parameter. Our backend will call Prisma 2 which will change that post's `published` field from false to true.

Create a file called publish-draft.tsx and add the following code:

`frontend/components/publish-draft.tsx`

```tsx
import React from 'react'
import { Button } from 'antd'
import {
  PublishMutationComponent,
  FeedQueryDocument,
} from '../generated/apollo-components'

type Props = {
  id: string
}

class PublishDraft extends React.Component<Props> {
  render() {
    const { id } = this.props
    return (
      <PublishMutationComponent>
        {publishDraft => (
          <Button
            onClick={() =>
              publishDraft({
                variables: { id },
                refetchQueries: [
                  { query: FeedQueryDocument, variables: { published: true } },
                  { query: FeedQueryDocument, variables: { published: false } },
                ],
              })
            }
          >
            Publish
          </Button>
        )}
      </PublishMutationComponent>
    )
  }
}

export default PublishDraft
```

We use an auto generated react component just like before and we nest a button inside the `PublishMutationComponent` component. When the button is clicked, we will call the publish mutation. We have a refetch query here to fetch both published and non published results to ensure that when a post gets published that both lists get updated.

Now we need to update the feed.tsx file to add the PublishDraft to the Action block. Make sure to import the `PublishDraft` and `Button` component at the top of the feed.tsx file.

`frontend/components/feed.tsx`

```tsx
import { Table, Button } from 'antd'
import PublishDraft from './publish-draft'
// Lines omitted for brevity
//
{
  title: 'Action',
  key: 'action',
  render: ({ id }: { id: string }) => {
    return (
      <Button.Group>
        {published ? null : <PublishDraft id={id} />}
      </Button.Group>
    );
  }
}
```

Now when you visit your site, you will see that there is a Publish button next to the post that you created.

![Publish button added to each unpublished blog.](https://www.codemochi.com/static/9f2ab54a3ead158d22ded397b01c386c/feedf/8f-add-publish-button.png)

Push the publish button and you'll see that it moves from the unpublished to published table. Sweet!

![After publishing post it shows up in the published feed.](https://www.codemochi.com/static/9b97a9072ba633983ec2ef058735d45d/9424f/8f-after-publish.png)

**7. Create a Delete Post Button Component**

The final component that we have left is to create a delete button for the posts. This button will be very similar to the `PublishDraft` component that we created. First, create a file called delete-post.tsx and add the following code:

`frontend/components/delete-post.tsx`

```tsx
import React from 'react'
import { Button } from 'antd'
import {
  DeleteOnePostComponent,
  FeedQueryDocument,
} from '../generated/apollo-components'

type Props = {
  id: string
}

class DeletePost extends React.Component<Props> {
  render() {
    const { id } = this.props
    return (
      <DeleteOnePostComponent>
        {deleteOnePost => (
          <Button
            type="danger"
            onClick={() =>
              deleteOnePost({
                variables: { id },
                refetchQueries: [
                  { query: FeedQueryDocument, variables: { published: true } },
                  { query: FeedQueryDocument, variables: { published: false } },
                ],
              })
            }
          >
            Delete
          </Button>
        )}
      </DeleteOnePostComponent>
    )
  }
}

export default DeletePost
```

We are calling the `DeleteOnePostComponent` component that is autogenerated and we are calling our mutation when the button is clicked. Now that we have the component, we can use it in our `FeedList` component. Make sure that we import the `DeletePost` at the top of this file as well.

`/frontend/components/feed.tsx`

```tsx
import DeletePost from './delete-post'
// lines omitted for brevity
//
{
  title: 'Action',
  key: 'action',
  render: ({ id }: { id: string }) => {
    return (
      <Button.Group>
        {published ? null : <PublishDraft id={id} />}
        <DeletePost id={id} />
      </Button.Group>
    );
  }
}
```

Now let's go to our website and we'll see that there is now a delete button. If we press the delete button, it should delete the post.

Before:

![Before deleting a blog post entry.](https://www.codemochi.com/static/334982a0ddcfb63dc1c6c8a2c5403a28/10811/8g-before-delete.png)

After:

![After deleting a blog post entry.](https://www.codemochi.com/static/656fd6c15c59cff0a198c34a7f999b92/10811/8g-after-delete.png)

So that's it! We've covered all the basics about building a full stack application using Prisma 2 with Next.js in Docker. This configuration is great for development and you shouldn't hesitate to hack on this and build out your own full stack web applications.

**Productionize all the things!**

For production however, you should make sure to copy your entire code into the docker containers so that you aren't relying on volume mounts for your source code. You'd also likely want to utilize a production ready MySQL instance that has automated backups, scaling, and fault tolerance such as AWS RDS which [I've previously discussed how to hook up to Prisma 2](https://www.codemochi.com/blog/2019-06-26-prisma-2-sneak-peak/). There are numerous other factors to consider when you get to that point though (such as automated deployment with a [CI/CD pipeline](https://www.codemochi.com/blog/2019-05-29-serverless-react-gatsby-aws-amplify/)), but luckily using Docker from the start provides a clear route for productionizing your application in the future.

Video Series for Part III:

_Add Main Layout to Next.js_
{% youtube cYzwSyCkyDI %}

_Add Users Component to Next.js_
{% youtube QXe373DKwPw %}

_Add Signup User to Next.js_
{% youtube r74YdvoMBdo %}

_Add Feed Component to Next.js_
{% youtube oXBBRjpsijk %}

_Add New Blog Draft Component to Next.js_
{% youtube Zkxk4OKioyQ %}

_Add Publish Blog Component to Next.js_
{% youtube M98_twxhjeQ %}

_Add Delete Post Component to Next.js_
{% youtube Mk_Ecansafs %}

**There is more where that came from!**
[Click here to give us your email and we'll let you know when we publish new stuff.](https://gmail.us20.list-manage.com/subscribe?u=37f38485b2c7cff2f3d9935b5&id=e3bc056dde) We respect your email privacy, we will never spam you and you can unsubscribe anytime.

Originally posted at [Code Mochi](https://www.codemochi.com/blog/2019-07-12-prisma-2-nextjs-docker/).