Building the Task Detail page

Learn how to build the Task Detail page.

The Task Detail page is the page that shows the details of a specific task. It is a child page of the Tasks page.

As you may know, we can define a dynamic path in Next.js by using square brackets. For example, we can define a dynamic path for the Tasks page by creating a file called app/dashboard/[organization]/tasks/[task]/page.tsx. This will create a dynamic path for the Tasks page that will be available at /dashboard/:organization/tasks/:task.

Building the Task Detail Container

Before we build the page, we define a client component named TaskItemContainer that will be used by the page. This component will be responsible for rendering the form that will be used to update the task.

app/dashboard/[organization]/tasks/components/TaskItemContainer.tsx
'use client'; import { FormEventHandler, useCallback, useTransition } from 'react'; import { ChevronLeftIcon } from '@heroicons/react/24/outline'; import Textarea from '~/core/ui/Textarea'; import Label from '~/core/ui/Label'; import Button from '~/core/ui/Button'; import Heading from '~/core/ui/Heading'; import { TextFieldInput, TextFieldLabel } from '~/core/ui/TextField'; import type Task from '~/lib/tasks/types/task'; import { updateTaskAction } from '~/lib/tasks/actions'; import useCsrfToken from '~/core/hooks/use-csrf-token'; const TaskItemContainer: React.FC<{ task: Task; }> = ({ task }) => { const [isMutating, startTransition] = useTransition(); const csrfToken = useCsrfToken(); const onUpdate: FormEventHandler<HTMLFormElement> = useCallback( (e) => { e.preventDefault(); const data = new FormData(e.currentTarget); const name = data.get('name') as string; const description = data.get('description') as string; startTransition(async () => { await updateTaskAction({ task: { name, description, id: task.id, }, csrfToken, }); }); }, [csrfToken, task.id], ); return ( <form onSubmit={onUpdate}> <div className={'flex flex-col space-y-4 max-w-xl'}> <Heading type={2}>{task.name}</Heading> <TextFieldLabel> Name <TextFieldInput required defaultValue={task.name} name={'name'} /> </TextFieldLabel> <Label> Description <Textarea className={'h-32'} name={'description'} defaultValue={task.description || ''} /> </Label> <div className={'flex space-x-2 justify-between'}> <Button href={'../tasks'} color={'transparent'}> <span className={'flex space-x-2 items-center'}> <ChevronLeftIcon className={'w-4'} /> <span>Back to Tasks</span> </span> </Button> <Button loading={isMutating}>Update Task</Button> </div> </div> </form> ); }; export default TaskItemContainer;

Building the Task Detail page

Now that we have the TaskItemContainer component, we can build the Task Detail page. The page will be responsible for fetching the task data and passing it to the TaskItemContainer component.

First, we define the function loadTaskData - responsible for fetching the task data - and then we use it in the TaskPage component.

If the task does not exist, we redirect the user back to the dashboard page. This is done by using the redirect function from the next/navigation module.

app/dashboard/[organization]/tasks/[task]/page.tsx
async function loadTaskData(taskId: string) { const client = getSupabaseServerClient(); const { data: task } = await getTask(client, Number(taskId)); if (!task) { redirect('/dashboard'); } return { task, }; }

Below is the full code for the Task Detail page.

app/dashboard/[organization]/tasks/[task]/page.tsx
import React, { use } from 'react'; import { redirect } from 'next/navigation'; import ArrowLeftIcon from '@heroicons/react/24/outline/ArrowLeftIcon'; import Button from '~/core/ui/Button'; import getSupabaseServerClient from '~/core/supabase/server-client'; import { getTask } from '~/lib/tasks/queries'; import AppHeader from '~/app/dashboard/[organization]/components/AppHeader'; import AppContainer from '~/app/dashboard/[organization]/components/AppContainer'; import TaskItemContainer from '~/app/dashboard/[organization]/tasks/components/TaskItemContainer'; import { withI18n } from '~/i18n/with-i18n'; interface Context { params: { task: string; }; } export const metadata = { title: `Task`, }; const TaskPage = ({ params }: Context) => { const data = use(loadTaskData(params.task)); const task = data.task; return ( <> <AppHeader> <TaskPageHeading /> </AppHeader> <AppContainer> <TaskItemContainer task={task} /> </AppContainer> </> ); }; function TaskPageHeading() { return ( <div className={'flex items-center space-x-6'}> <span>Task</span> <Button size={'small'} color={'transparent'} href={'../tasks'}> <ArrowLeftIcon className={'mr-2 h-4'} /> Back to Tasks </Button> </div> ); } async function loadTaskData(taskId: string) { const client = getSupabaseServerClient(); const { data: task } = await getTask(client, Number(taskId)); if (!task) { redirect('/dashboard'); } return { task, }; } export default withI18n(TaskPage);

Perfect, our Tasks App is now complete! 🎉

In the next steps, we take a look at some things you should now while keeping working on your app.


Subscribe to our Newsletter
Get the latest updates about React, Remix, Next.js, Firebase, Supabase and Tailwind CSS