Blitz.js: The Fullstack React Framework - Part 3
Tags: blitzjs, blitz, fullstack, nextjs
Published: 2021-08-23 18:40:01

# 👋 Welcome Back,

Hey Developers, welcome back to part 3 of the series  ["Learn by Building - Blitz.js"](https://dev.to/chapagainashik/series/14193). Today we'll create the UI for projects and tasks models. And also add the functionalities in the UI to CRUD from the database.

# Index 
- [Recap of the previous part](#recap-of-the-previous-part)
- [Today's objectives 🎯](#todays-objectives)
- [Layout](#layout)
     - [Header](#header)
- [Index page](#index-page)
- [Project](#project)
     - [Create Page](#create-page)
     - [Index Page](#index-page)
     - [Single Page](#single-page)
     - [Edit](#edit)
- [Tasks](#tasks)
     - [Create and Update](#create-and-update)
     - [Index](#index)
- [Conclusion](#conclusion)


# Recap of the previous part

In the previous part of this series, we updated the database schema and updated and understand the logic for the CRUD operation of projects and tasks. And also build the UI for the authentication pages.

By looking at the above line, it looks like that the previous article doesn't include much information, but it crosses more than 2900 words. 🤯

# Today's objectives 🎯

In today's articles, we'll create the UI for the CRUD operations of projects and tasks model and connect the UI with the logic. And we'll also learn to add the search functionalities for both projects and tasks.

Today, we'll start by editing the `Layout Component`. We used `AuthLayout` for the authentication pages, and now we'll use `Layout Component` for other pages.

# Layout

Open `app/core/layouts/layout.tsx` and add the `<Header/>` tag after `<Head/>` tag like below and also wrap `{children}` with `div` of class `container mx-auto px-4`:

```jsx
// app/core/layouts/layout.tsx

import { Header } from "../components/Header"
...
      </Head>
      <Header />
      <div className="container mx-auto px-4">{children}</div>
...
```

In the `<Layout />` component, we have used the `<Header />` component, so let's build it.

## Header

Create a new file at `app/core/components/Header.tsx` and add the following code.

```jsx
// app/core/components/Header.tsx
import logout from "app/auth/mutations/logout"
import { Link, Routes, useMutation } from "blitz"
import { Suspense } from "react"
import { useCurrentUser } from "../hooks/useCurrentUser"
import { Button } from "./Button"

const NavLink = ({ href, children }) => {
  return (
    <Link href={href}>
      <a className="bg-purple-600 text-white py-2 px-3 rounded hover:bg-purple-800 block">
        {children}
      </a>
    </Link>
  )
}

const Nav = () => {
  const currentUser = useCurrentUser()
  const [logoutMutation] = useMutation(logout)

  return (
    <nav>
      {!currentUser ? (
        <ul className="flex gap-8">
          <li>
            <NavLink href={Routes.LoginPage()}>Login</NavLink>
          </li>
          <li>
            <NavLink href={Routes.SignupPage()}>Register</NavLink>
          </li>
        </ul>
      ) : (
        <ul className="">
          <li>
            <Button
              onClick={async () => {
                await logoutMutation()
              }}
            >
              Logout
            </Button>
          </li>
        </ul>
      )}
    </nav>
  )
}

export const Header = () => {
  return (
    <header className="flex sticky top-0 z-30 bg-white justify-end h-20 items-center px-6 border-b">
      <Suspense fallback="Loading...">
        <Nav />
      </Suspense>
    </header>
  )
}
```

With this, you'll get the header like as shown below:

When a user is not logged in,
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629733163382/Eld-WVJ9O.png)
When a user is authenticated,
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629733218577/CrhrbAdLL.png)

In the header component, there are some lines that you might not understand. So, let's know what they really do.

- `<Suspense>...</Suspense>`: <Suspense> component that lets you “wait” for some code to load and declaratively specify a loading state (like a spinner) while we’re waiting. ( [React Docs](https://reactjs.org/docs/concurrent-mode-suspense.html))

- `useCurrentUser()`: It is a react hook that returns a authenticated session of a user. ([Blitz.js Dccs](https://blitzjs.com/docs/authorization#displaying-different-content-based-on-user-role))

- `useMutation(logout)`: Logout task is a mutation and to run the mutation we use the powerful hook `useMutation` provided by Blitz.js.( [Blitz.js Docs](https://blitzjs.com/docs/use-mutation) )

If you look over the `onClick` event listener in the `Logout` button. There we are using async/await, because `mutations` return promises.

Now, let's display the `User Email` on the index page and add a link to go to the projects index page.

# Index page

If you guys, remembered we have removed the content from the `index.tsx` page. Now, we'll display the email of the authenticated user and make that page accessible only by the logged-in user.

To work with the index page, first, go to the [signup](http://localhost:3000/signup)  page and create an account. And then you will get redirected to the `index` page.

Now, replace everything in `app/pages/index.tsx`with the given content.

```jsx
// app/pages/index.tsx

import { Suspense } from "react"
import { Image, Link, BlitzPage, useMutation, Routes } from "blitz"
import Layout from "app/core/layouts/Layout"
import { useCurrentUser } from "app/core/hooks/useCurrentUser"
import logout from "app/auth/mutations/logout"
import logo from "public/logo.png"
import { CustomLink } from "app/core/components/CustomLink"

/*
 * This file is just for a pleasant getting started page for your new app.
 * You can delete everything in here and start from scratch if you like.
 */

const UserInfo = () => {
  const user = useCurrentUser()
  return (
    <div className="flex justify-center my-4">{user && <div>Logged in as {user.email}.</div>}</div>
  )
}

const Home: BlitzPage = () => {
  return (
    <>
      <Suspense fallback="Loading User Info...">
        <UserInfo />
      </Suspense>
      <div className="flex justify-center">
        <CustomLink href="/projects">Manage Projects</CustomLink>
      </div>
    </>
  )
}

Home.suppressFirstRenderFlicker = true
Home.getLayout = (page) => <Layout title="Home">{page}</Layout>
Home.authenticate = true

export default Home
```

If you see the third last line in the code, `Home.authenticate = true`, this means, this page requires a user to be authenticated to access this page.

Now, the index page should look like this:

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629735290888/dIiFXjlrL.png)
 
Click on the `Manage projects` to see how the `projects` index page looks like.
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629735473048/_snCWs6iE.png)

 Now, let's customize the project creation page. We are not editing the index page, first because we need to show the data on the `index` page.

# Project

## Create Page
If you go to  [`/projects/new`](http://localhost:3000/projects/new), currently it should look like this.
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629735715386/wKwAIoCAL.png)

Now, let's customize this page.

In our schema, we have a `description` field for the `projects` model. So, let's add the text area for the description field.

We also need a text area for the task model too. So, we'll create a new component for the text area. For this I have created a new file `/app/core/components/LabeledTextAreaField.tsx` and copied the content of `LabeledTextField` and customized it for `textarea`.

```jsx
// app/core/components/LabeledTextAreaField

import { forwardRef, PropsWithoutRef } from "react"
import { Field, useField } from "react-final-form"

export interface LabeledTextFieldProps extends PropsWithoutRef<JSX.IntrinsicElements["input"]> {
  /** Field name. */
  name: string
  /** Field label. */
  label: string
  /** Field type. Doesn't include radio buttons and checkboxes */
  type?: "text" | "password" | "email" | "number"
  outerProps?: PropsWithoutRef<JSX.IntrinsicElements["div"]>
}

export const LabeledTextAreaField = forwardRef<HTMLInputElement, LabeledTextFieldProps>(
  ({ name, label, outerProps, ...props }, ref) => {
    const {
      input,
      meta: { touched, error, submitError, submitting },
    } = useField(name, {
      parse: props.type === "number" ? Number : undefined,
    })

    const normalizedError = Array.isArray(error) ? error.join(", ") : error || submitError

    return (
      <div {...outerProps}>
        <label className="flex flex-col items-start">
          {label}
          <Field
            component={"textarea"}
            className="px-1 py-2 border rounded focus:ring focus:outline-none ring-purple-200 block w-full my-2"
            {...props}
            {...input}
            disabled={submitting}
          ></Field>
        </label>

        {touched && normalizedError && (
          <div role="alert" className="text-sm" style={{ color: "red" }}>
            {normalizedError}
          </div>
        )}
      </div>
    )
  }
)

export default LabeledTextAreaField
```

After doing this, now you can use it in the `/app/projects/components/ProjectForm.tsx`. 

```jsx
// app/projects/components/ProjectForm.tsx

import { Form, FormProps } from "app/core/components/Form"
import { LabeledTextField } from "app/core/components/LabeledTextField"
import { LabeledTextAreaField } from "app/core/components/LabeledTextAreaField"
import { z } from "zod"
export { FORM_ERROR } from "app/core/components/Form"

export function ProjectForm<S extends z.ZodType<any, any>>(props: FormProps<S>) {
  return (
    <Form<S> {...props}>
      <LabeledTextField name="name" label="Name" placeholder="Name" />
      <LabeledTextAreaField name="description" label="Description" placeholder="Description" />
    </Form>
  )
}

```

Now `/projects/new` page should look like.
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629737903874/ZglXrX3CY.png)

Now, you can use that form to create a project.

But, there is still many thing to customize in this page.

```js
// app/pages/projects/new

import { Link, useRouter, useMutation, BlitzPage, Routes } from "blitz"
import Layout from "app/core/layouts/Layout"
import createProject from "app/projects/mutations/createProject"
import { ProjectForm, FORM_ERROR } from "app/projects/components/ProjectForm"
import { CustomLink } from "app/core/components/CustomLink"

const NewProjectPage: BlitzPage = () => {
  const router = useRouter()
  const [createProjectMutation] = useMutation(createProject)

  return (
    <div className="mt-4"> 
      <h1 className="text-xl mb-4">Create New Project</h1>

      <ProjectForm
        ....
      />

      <p className="mt-4">

        <CustomLink href={Routes.ProjectsPage()}>
          <a>Projects</a>
        </CustomLink>

      </p>
    </div>
  )
}

NewProjectPage.authenticate = true
NewProjectPage.getLayout = (page) => <Layout title={"Create New Project"}>{page}</Layout>

export default NewProjectPage

```

Here, I have added some class in `h1` and `divs` and replace `Link` tag with our `CustomLink` component.

After this, the page will look like this.
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629738496341/F07U2mOLl.png)

Now, let's style the index page ' [/projects](http://localhost:3000/projects) '.

## Index Page

Before styling the index page, add some of the projects to play with.

After adding them. You'll get redirected to `single` project page. Go to `/projects`.

This is what your page will look like.
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629739978169/3Zoy8bRGY.png)

Now, paste the following content in `app/pages/projects/index.tsx`.
```jsx
// app/pages/projects/index.tsx

import { Suspense } from "react"
import { Head, Link, usePaginatedQuery, useRouter, BlitzPage, Routes } from "blitz"
import Layout from "app/core/layouts/Layout"
import getProjects from "app/projects/queries/getProjects"
import { CustomLink } from "app/core/components/CustomLink"
import { Button } from "app/core/components/Button"

const ITEMS_PER_PAGE = 100

export const ProjectsList = () => {
  const router = useRouter()
  const page = Number(router.query.page) || 0
  const [{ projects, hasMore }] = usePaginatedQuery(getProjects, {
    orderBy: { id: "asc" },
    skip: ITEMS_PER_PAGE * page,
    take: ITEMS_PER_PAGE,
  })

  const goToPreviousPage = () => router.push({ query: { page: page - 1 } })
  const goToNextPage = () => router.push({ query: { page: page + 1 } })

  return (
    <div className="mt-4">
      <h2>Your projects</h2>
      <ul className="mb-4 mt-3 flex flex-col gap-4">
        {projects.map((project) => (
          <li key={project.id}>
            <CustomLink href={Routes.ShowProjectPage({ projectId: project.id })}>
              <a>{project.name}</a>
            </CustomLink>
          </li>
        ))}
      </ul>

      <div className="flex gap-2">
        <Button disabled={page === 0} onClick={goToPreviousPage}>
          Previous
        </Button>
        <Button disabled={!hasMore} onClick={goToNextPage}>
          Next
        </Button>
      </div>
    </div>
  )
}

const ProjectsPage: BlitzPage = () => {
  return (
    <>
      <Head>
        <title>Projects</title>
      </Head>

      <div>
        <p>
          <CustomLink href={Routes.NewProjectPage()}>Create Project</CustomLink>
        </p>

        <Suspense fallback={<div>Loading...</div>}>
          <ProjectsList />
        </Suspense>
      </div>
    </>
  )
}

ProjectsPage.authenticate = true
ProjectsPage.getLayout = (page) => <Layout>{page}</Layout>

export default ProjectsPage

```

And let's make the `Button` component looks unclickable when it is disabled.

For that, you can add `disabled:bg-purple-400 disabled:cursor-not-allowed` class in `Button` component.

```jsx
// app/core/components/Button.tsx

export const Button = ({ children, ...props }) => {
  return (
    <button
      className="... disabled:bg-purple-400 disabled:cursor-not-allowed"
    >
      {children}
    </button>
  )
}

```

Now, projects index page should look like:
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629740470487/0YDWlUbOU.png)

## Single Page
Before editing, project single page looks like.
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629740540304/Z8w-rw2dZ7.png)

Now, replace the code of `app/pages/projects/[projectId].tsx` with following.

```jsx
// app/pages/projects/[projectId].tsx

import { Suspense } from "react"
import { Head, Link, useRouter, useQuery, useParam, BlitzPage, useMutation, Routes } from "blitz"
import Layout from "app/core/layouts/Layout"
import getProject from "app/projects/queries/getProject"
import deleteProject from "app/projects/mutations/deleteProject"
import { CustomLink } from "app/core/components/CustomLink"
import { Button } from "app/core/components/Button"

export const Project = () => {
  const router = useRouter()
  const projectId = useParam("projectId", "number")
  const [deleteProjectMutation] = useMutation(deleteProject)
  const [project] = useQuery(getProject, { id: projectId })

  return (
    <>
      <Head>
        <title>Project {project.id}</title>
      </Head>

      <div>
        <h1>Project {project.id}</h1>
        <pre>{JSON.stringify(project, null, 2)}</pre>

        <CustomLink href={Routes.EditProjectPage({ projectId: project.id })}>Edit</CustomLink>

        <Button
          type="button"
          onClick={async () => {
            if (window.confirm("This will be deleted")) {
              await deleteProjectMutation({ id: project.id })
              router.push(Routes.ProjectsPage())
            }
          }}
          style={{ marginLeft: "0.5rem", marginRight: "0.5rem" }}
        >
          Delete
        </Button>
        <CustomLink href={Routes.TasksPage({ projectId: project.id })}>Tasks</CustomLink>
      </div>
    </>
  )
}

const ShowProjectPage: BlitzPage = () => {
  return (
    <div className="mt-2">
      <p className="mb-2">
        <CustomLink href={Routes.ProjectsPage()}>Projects</CustomLink>
      </p>

      <Suspense fallback={<div>Loading...</div>}>
        <Project />
      </Suspense>
    </div>
  )
}

ShowProjectPage.authenticate = true
ShowProjectPage.getLayout = (page) => <Layout>{page}</Layout>

export default ShowProjectPage

```

Now the page should look like.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1629741502951/ru4JpFljP.png)

## Edit

We'll do a decent style on the edit page. We'll just replace `<Link>` tag with `<CustomLink>` component and add `text-lg` class to `h1`.

```jsx
// From
<Link href={Routes.ProjectsPage()}>
          <a>Projects</a>
</Link>

// To
<CustomLink href={Routes.ProjectsPage()}>
         Projects
</CustomLink>
```

```
// From
<h1>Edit Project {project.id}</h1>

// To
<h1 className="text-lg">Edit Project {project.id}</h1>
```

Now, it's time to edit the Tasks pages.

# Tasks

## Create and Update

We have added description field in the schema, so let's add textarea for description in the form.
Both create and update use the same form, we don't have to customize them seperately.
```jsx
// app/tasks/components/TaskForm.tsx

import { Form, FormProps } from "app/core/components/Form"
import LabeledTextAreaField from "app/core/components/LabeledTextAreaField"
import { LabeledTextField } from "app/core/components/LabeledTextField"
import { z } from "zod"
export { FORM_ERROR } from "app/core/components/Form"

export function TaskForm<S extends z.ZodType<any, any>>(props: FormProps<S>) {
  return (
    <Form<S> {...props}>
      <LabeledTextField name="name" label="Name" placeholder="Name" />
      <LabeledTextAreaField name="description" label="Description" placeholder="Description" />
    </Form>
  )
}

```

I have already written on how to customize the pages for projects so, you can follow the same to customize tasks pages. So, now I'll not style any pages.

## Index
In the index page, you need to add `projectId` in query param.
```jsx
...
      <ul>
        {tasks.map((task) => (
          <li key={task.id}>
            <Link href={Routes.ShowTaskPage({ projectId, taskId: task.id })}>
              <a>{task.name}</a>
            </Link>
          </li>
        ))}
      </ul>
          
...
```

# Conclusion

Now, all the functionalities works fine. So this much for today guys, In next article. We'll see how to deploy this app. In that I will show you the complete guide to deploy in multiple platform.