Blitz.js: The Fullstack React Framework Part 2

chapagainashik

Ashik Chapagain

Posted on August 22, 2021

Blitz.js: The Fullstack React Framework Part 2

Welcome Back šŸ‘‹

Hey, Developers, welcome back to the second part of the Blitz.js: The Fullstack React Framework series.

Check part one if you haven't already: https://dev.to/chapagainashik/blitz-js-the-fullstack-react-framework-2kag

In the previous part, we have completed setting up a fresh blitz.js project, added Tailwind CSS to it using a recipe, created a database model, and generated the files required for this project.

Today, we'll start by updating the schema file.

So, let's start.

Index

Update Database Schema

In the previous article, we finished up creating the relationship between project and tasks table, but there we haven't created the field for storing task name and task description. So, first, let's update the scheme.prisma file with required fields.

// file: db/schema.prisma
...

model Project {
  id          Int      @id @default(autoincrement())
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  name        String
  description String
  tasks       Task[]
}

model Task {
  id          Int      @id @default(autoincrement())
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  project     Project  @relation(fields: [projectId], references: [id])
  projectId   Int
  // Add the following new fields
  name        String
  description String?
}

Enter fullscreen mode Exit fullscreen mode

If you haven't noticed what we have changed, check the Task model, where we have added the name field of String type and description of String with nullable ?.

That's it for the schema.

Now run the command blitz prisma migrate dev. And give any name for migration, but since we have updated the tasks table by adding two new fields so, I'll name it update_tasks_table. If you open the Prisma studio using blitz prisma studio, you will see two new fields in the tasks table.

Let's build the logic.

Understanding and updating Logics

We'll understand mutations and queries to alter the data in the database and fetch the data from the database which are generated by code scaffolding from our previous part but since we have added the new field we have to update mutations and logics too.

Logics for Project

First, let's create the CRUD operation for the project.

Open app/projects/mutations/createProject.ts and add the following.

// app/projects/mutations/createProject.ts
import { resolver } from "blitz"
import db from "db"
import { z } from "zod"

const CreateProject = z.object({
  name: z.string(),
  description: z.string(),
})

export default resolver.pipe(
  resolver.zod(CreateProject), // This is a handly utility for using Zod, an awesome input validation library. It takes a zod schema and runs schema.parse on the input data.
  resolver.authorize(), // Require Authentication
  async (input) => {
    // Create the project
    const project = await db.project.create({ data: input })
    // Return created project
    return project
  }
)
Enter fullscreen mode Exit fullscreen mode

Let's split the code and understand each line.

  • import { resolver } from "blitz": Blitz exports a resolver object which contains a few utilities. "Resolver" as used here and for queries and mutations refers to a function that takes some input and "resolves" that into some output or side effect. Click here to know more

  • import db from "db": Here db is a Prisma client enhanced by blitz.

  • import { z } from "zod": Zod is a TypeScript-first schema declaration and validation library. I'm using the term "schema" to broadly refer to any data type, from a simple string to a complex nested object. Click here to know more

  • const CreateProject: CreateProject is an object schema that validates if the given input contains the name field of string type and description field of 'string' type.

  • resolver.pipe: This is a functional pipe that makes it easier and cleaner to write complex resolvers. A pipe automatically pipes the output of one function into the next function. ( Blitz.js Docs )

  • resolver.zod(CreateProject): This is a handy utility for using Zod, an awesome input validation library. It takes a zod schema and runs schema.parse on the input data. ( Blitz.js Docs )

  • resolver.authorize(): Using resolver.authorize in resolver.pipe is a simple way to check whether the user has the authorization to call the query or mutation or not. ( Blitz.js Docs )

  • async (input) => {}: This async function is a callback.

  • db.project.create: Create a new project in the database.

  • return project: Returns the created data.

Now, we have built the logic to create a project.

Let's build the logic to get projects.

// file: app/projects/queries/getProjects.ts
import { paginate, resolver } from "blitz"
import db, { Prisma } from "db"

interface GetProjectsInput
  extends Pick<Prisma.ProjectFindManyArgs, "where" | "orderBy" | "skip" | "take"> {}

export default resolver.pipe(
  resolver.authorize(),
  async ({ where, orderBy, skip = 0, take = 100 }: GetProjectsInput) => {
    // TODO: in multi-tenant app, you must add validation to ensure correct tenant
    const {
      items: projects,
      hasMore,
      nextPage,
      count,
    } = await paginate({
      skip,
      take,
      count: () => db.project.count({ where }),
      query: (paginateArgs) =>
        db.project.findMany({ ...paginateArgs, where, orderBy, include: { tasks: true } }),
    })

    return {
      projects,
      nextPage,
      hasMore,
      count,
    }
  }
)
Enter fullscreen mode Exit fullscreen mode

In this file, made a single change and that is I have added the include option in db.project.findMany().

What this will do is, includes all the tasks that belong to the respective project.

Now let's understand each line of this code. I'll not repeat the one that I have already written while building the create project logic. I'll also skip the imports.

  • interface GetProjectsInput
    extends Pick<Prisma.ProjectFindManyArgs, "where" | "orderBy" | "skip" | "take"> {}
    : What this will do is, create a interface by picking the set of properties (where, orderBy, skip, take) from Prisma.ProjectFindManyArgs. ( TS Docs )

  • Prisma.ProjectFindManyArgs: Prisma generates the types for the model and the arguments. Here we are using ProjectFindManyArgs` which was generated by Prisma.

  • paginate: This is a handy utility for query pagination. ( Blitz.js Docs ).

  • db.project.count({where}): Returns the number of data from the database that follows the conditions we passed in where argument.( Prisma Docs )

  • db.project.findMany(): Get all the data from the projects table. If you compare this with the originally generated one, then we'll know that we have added the include option in this. From with we will get all the tasks that belong to this table.

Now let's look at how to get a single project.
`
// app/projects/queries/getProject.ts
import { resolver, NotFoundError } from "blitz"
import db from "db"
import { z } from "zod"

const GetProject = z.object({
// This accepts type of undefined, but is required at runtime
id: z.number().optional().refine(Boolean, "Required"),
})

export default resolver.pipe(resolver.zod(GetProject), resolver.authorize(), async ({ id }) => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
const project = await db.project.findFirst({ where: { id }, include: { tasks: true } })

if (!project) throw new NotFoundError()

return project
})
`

  • .refine(): (ZOD Docs)

  • db.project.findFirst(): Return the first data that satisfies the given condition. (Prisma Docs)

  • throw new NotFoundError(): Throw 404 error.

Now, let's see the logic to update the project.

`
// app/projects/mutations/updateProject.ts
import { resolver } from "blitz"
import db from "db"
import { z } from "zod"

const UpdateProject = z.object({
id: z.number(),
name: z.string(),
description: z.string(),
})

export default resolver.pipe(
resolver.zod(UpdateProject),
resolver.authorize(),
async ({ id, ...data }) => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
const project = await db.project.update({ where: { id }, data })

return project
Enter fullscreen mode Exit fullscreen mode

}
)

`

  • db.project.update(): Update the data with the given data in the project row with the given id. (Prisma Docs)

Finally, it's time for the logic to delete the project.

`
// app/projects/mutations/deleteProject.ts

import { resolver } from "blitz"
import db from "db"
import { z } from "zod"

const DeleteProject = z.object({
id: z.number(),
})

export default resolver.pipe(resolver.zod(DeleteProject), resolver.authorize(), async ({ id }) => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
const tasks = await db.task.deleteMany({ where: { projectId: id } })
const project = await db.project.deleteMany({ where: { id } })

return project
})

`
If you look there, I have added a new line const tasks = = await db.task.deleteMany({ where: { projectId: id } }). This will first delete all the tasks that belong to that project and only then the actual project got removed.

  • db.project.deleteMany: This will delete the rows from the table which satisfy the given criteria.

Now, The CRUD for the project has been completed, now it's time for CRUD operation of tasks.

Logics for Tasks

Let's update the tasks logic for creating a new task.
`
// app/tasks/mutations/createTask.ts

import { resolver } from "blitz"
import db from "db"
import { z } from "zod"

const CreateTask = z.object({
name: z.string(),
projectId: z.number(),
// This is what we have added
description: z.string().optional(),
})

export default resolver.pipe(resolver.zod(CreateTask), resolver.authorize(), async (input) => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
const task = await db.task.create({ data: input })

return task
})
`

Everything looks familiar, Nah. We have already discussed the syntax used up here before.

After we created tasks, we need to retrieve the tasks, so let getAll the tasks.

`
// app/tasks/queries/getTasks.ts

import { paginate, resolver } from "blitz"
import db, { Prisma } from "db"

interface GetTasksInput
extends Pick {}

export default resolver.pipe(
resolver.authorize(),
async ({ where, orderBy, skip = 0, take = 100 }: GetTasksInput) => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
const {
items: tasks,
hasMore,
nextPage,
count,
} = await paginate({
skip,
take,
count: () => db.task.count({ where }),
query: (paginateArgs) => db.task.findMany({ ...paginateArgs, where, orderBy }),
})

return {
  tasks,
  nextPage,
  hasMore,
  count,
}
Enter fullscreen mode Exit fullscreen mode

}
)

`

Everything is the same up here as generated.

Let's see the mutation to update the task.

`js
// app/tasks/mutations/updateTask.ts

import { resolver } from "blitz"
import db from "db"
import { z } from "zod"

const UpdateTask = z.object({
id: z.number(),
name: z.string(),
// The only thing we have added
description: z.string().optional(),
})

export default resolver.pipe(
resolver.zod(UpdateTask),
resolver.authorize(),
async ({ id, ...data }) => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
const task = await db.task.update({ where: { id }, data })

return task
Enter fullscreen mode Exit fullscreen mode

}
)

`

For the getTask query and delete mutation, leave it as it is.

Now we're done for Logics.

Building UI

We have already installed Tailwind CSS with the blitz recipe in the previous part. ( Read it here ). So, we'll be using the Tailwind CSS library for this project. And we'll create a simple UI using TailwindCSS.

SignUp Page Component

Link: /signup

Open app/auth/pages/signup.tsx. There you will see that they are using the custom component SignupForm for the form. So, open it from app/auth/components/SignupForm.tsx. Then there you will see that they are using the custom Form Component and LabeledTextField components.

So our first work will be to customize Form and LabeledTextFieldComponent.

Open app/core/Form.tsx and add p-5 border rounded classes in the form tag and add text-sm class in alert.

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


{submitError && (

{submitError}

)}
...

...
`

Now, let's customize LabeledTextFieldComponent.

For this, first, we will create a custom component for input with tailwind style classes.

Go to app/core/components and open a file LabeledTextField.tsx and update it with the following code.
`jsx
// app/core/components/LabeledTextField.tsx

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

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

export const LabeledTextField = forwardRef(
({ 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}
      <input
        {...input}
        className="px-1 py-2 border rounded focus:ring focus:outline-none ring-purple-200 block w-full my-2"
        disabled={submitting}
        {...props}
        ref={ref}
      />
    </label>

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

}
)

export default LabeledTextField

`

Always remember that the components that are required for a specific model, we have to create that inside the components folder in that model, for example. if we want a form to create a project then we add that form component inside app/project/components. But if that component is not model specific, then we create those components inside app/core/components.

Let's create a new core Button component to use everywhere on the site.

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

export const Button = ({ children, ...props }) => {
return (

{children}

)
}
`
Now let's use this new Button component in Form.tsx.

In app/core/components/Form.tsx replace

{submitText && (
<button type="submit" disabled={submitting}>
{submitText}
</button>
)}

with

{submitText && (
<Button type="submit" disabled={submitting}>
{submitText}
</Button>
)}

And don't forget to import the Button.

import { Button } from "./Button"

Now, you should have something like this.

image.png

Let's customize this page more.

We'll use a separate layout for the authentication pages. So, go to app/core/layouts and create a new file named AuthLayout.tsx and add the following contents.

`
// app/core/layouts/AuthLayout.tsx

import { ReactNode } from "react"
import { Head } from "blitz"

type LayoutProps = {
title?: string
heading: string
children: ReactNode
}

const AuthLayout = ({ title, heading, children }: LayoutProps) => {
return (
<>


{title || "ProjectManagement"}

  <div className="flex justify-center">
    <div className="w-full md:w-2/3 lg:max-w-2xl mt-5">
      <h2 className="text-xl mb-2">{heading}</h2>
      <div>{children}</div>
    </div>
  </div>
</>
Enter fullscreen mode Exit fullscreen mode

)
}

export default AuthLayout

`

Now go to the SignupForm component and remove the h1 tag. After removing
<h1>Create an Account</h1>
the file should look like.

`
import { useMutation } from "blitz"
import { LabeledTextField } from "app/core/components/LabeledTextField"
import { Form, FORM_ERROR } from "app/core/components/Form"
import signup from "app/auth/mutations/signup"
import { Signup } from "app/auth/validations"

type SignupFormProps = {
onSuccess?: () => void
}

export const SignupForm = (props: SignupFormProps) => {
const [signupMutation] = useMutation(signup)

return (

  <Form
    submitText="Create Account"
    schema={Signup}
    initialValues={{ email: "", password: "" }}
    onSubmit={async (values) => {
      try {
        await signupMutation(values)
        props.onSuccess?.()
      } catch (error) {
        if (error.code === "P2002" && error.meta?.target?.includes("email")) {
          // This error comes from Prisma
          return { email: "This email is already being used" }
        } else {
          return { [FORM_ERROR]: error.toString() }
        }
      }
    }}
  >
    <LabeledTextField name="email" label="Email" placeholder="Email" />
    <LabeledTextField name="password" label="Password" placeholder="Password" type="password" />
  </Form>
</div>

)
}

export default SignupForm
`

Now, we have to tell signup page to use AuthLayout as layout.

For that, go to app/auth/pages/signup.tsx and change the folowing line:

SignupPage.getLayout = (page) => <Layout title="Sign Up">{page}</Layout>

to

SignupPage.getLayout = (page) => <AuthLayout heading="Create an account" title="Sign Up">{page}</AuthLayout>

and import AuthLayout.

import AuthLayout from "app/core/layouts/AuthLayout"

Now, your signup page should look like this.
image.png

āš ļø Ignore that LastPass sign in the input field.

Let's include a link to go to the login page in the signup page.

For this, we'll create our own custom Link component with tailwind style.

Go to /app/core/components and create a new file CustomLink.tsx and add the following.
`
// app/core/components/CustomLink.tsx

import { Link } from "blitz"

export const CustomLink = ({ children, href }: any) => {
return (

{children}

)
}
`

Now, to include the go-to login link you have to add the following line after the Form tag.

`
...

 Already have account? Login
Enter fullscreen mode Exit fullscreen mode

`

After all this, your signup page should look like this.

image.png

Now, since we have already styled many components in the SignUp UI section now, for other pages we won't have to do too much work for other pages.

Login Page

Link : '/login'

For the login page customization replace the following line in login.tsx:
`
// app/auth/pages/login

LoginPage.getLayout = (page) => {page}
`

to


LoginPage.getLayout = (page) => (
<AuthLayout heading="Welcome back, login here" title="Log In">
{page}
</AuthLayout>
)

and import AuthLayout.

import AuthLayout from "app/core/layouts/AuthLayout"

After doing this, your login page should look like this.
image.png

Now, remove <h1>Login</h1> from app/auth/components/LoginForm.tsx.

and also replace the following lines from LoginForm.tsx:
`
// from

Forgot your password?

// to

Forgot your password?

`

and
`
// from
Sign Up

// to
Sign Up
`

After getting up to this, your login page should look like.
image.png

Forgot Password page

Link : '/forgot-password'

As before, change the layout to AuthLayout.

`
// app/auth/pages/forgot-password.tsx

import AuthLayout from "app/core/layouts/AuthLayout"
...

ForgotPasswordPage.getLayout = (page) => (

{page}

)
`

and remove <h1>Forgot your password?</h1> from app/auth/pages/forgot-password.tsx.

Now, the forgot password page is done and it should look like.

image.png

Now, Finally the final page of authentication.

Reset Password page

Link: '/reset-password'

As before, change the layout to AuthLayout.

`
// app/auth/pages/reset-password.tsx

import AuthLayout from "app/core/layouts/AuthLayout"

...

ResetPasswordPage.getLayout = (page) => (

{page}

)
`

and remove <h1>Set a New Password</h1> and it should look like this.

image.png

This much for today guys.

Recap

  • Updated the schema
  • Edited UI for authentication pages using Tailwindcss
  • Created custom components
  • Created AuthLayout and used it
šŸ’– šŸ’Ŗ šŸ™… šŸš©
chapagainashik
Ashik Chapagain

Posted on August 22, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related