DEV Community

Cover image for Blitz.js: The Fullstack React Framework - Part 3
Ashik Chapagain
Ashik Chapagain

Posted on • Originally published at cb-ashik.hashnode.dev

Blitz.js: The Fullstack React Framework - Part 3

πŸ‘‹ Welcome Back,

Hey Developers, welcome back to part 3 of the series "Learn by Building - Blitz.js". 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

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:

// app/core/layouts/layout.tsx import { Header } from "../components/Header" ... </Head>  <Header /> <div className="container mx-auto px-4">{children}</div> ... 
Enter fullscreen mode Exit fullscreen mode

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.

// 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> ) } 
Enter fullscreen mode Exit fullscreen mode

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

When a user is not logged in,
image.png
When a user is authenticated,
image.png

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

  • <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)

  • useCurrentUser(): It is a react hook that returns a authenticated session of a user. (Blitz.js Dccs)

  • 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 )

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 page and create an account. And then you will get redirected to the index page.

Now, replace everything in app/pages/index.tsxwith the given content.

// 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 
Enter fullscreen mode Exit fullscreen mode

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

Click on the Manage projects to see how the projects index page looks like.
image.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, currently it should look like this.
image.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.

// 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 
Enter fullscreen mode Exit fullscreen mode

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

// 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> ) } 
Enter fullscreen mode Exit fullscreen mode

Now /projects/new page should look like.
image.png

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

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

// 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 
Enter fullscreen mode Exit fullscreen mode

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

Now, let's style the index page ' /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

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

// 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 
Enter fullscreen mode Exit fullscreen mode

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.

// app/core/components/Button.tsx export const Button = ({ children, ...props }) => { return ( <button className="... disabled:bg-purple-400 disabled:cursor-not-allowed" > {children} </button> ) } 
Enter fullscreen mode Exit fullscreen mode

Now, projects index page should look like:
image.png

Single Page

Before editing, project single page looks like.
image.png

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

// 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 
Enter fullscreen mode Exit fullscreen mode

Now the page should look like.

image.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.

// From <Link href={Routes.ProjectsPage()}> <a>Projects</a> </Link> // To <CustomLink href={Routes.ProjectsPage()}> Projects </CustomLink> 
Enter fullscreen mode Exit fullscreen mode
// From <h1>Edit Project {project.id}</h1> // To <h1 className="text-lg">Edit Project {project.id}</h1> 
Enter fullscreen mode Exit fullscreen mode

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.

// 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> ) } 
Enter fullscreen mode Exit fullscreen mode

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.

... <ul> {tasks.map((task) => ( <li key={task.id}> <Link href={Routes.ShowTaskPage({ projectId, taskId: task.id })}> <a>{task.name}</a> </Link> </li> ))} </ul> ... 
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)