Skip to content

Latest commit

 

History

History
444 lines (378 loc) · 16.3 KB

File metadata and controls

444 lines (378 loc) · 16.3 KB

In this lesson, we will add the capability to create a task under a project.

Let's open src/pages/project_details/ProjectDetails.tsx in VS Code.

Adding a button for new task

We will add a button wrapped in Link to navigate to tasks/new URL so that we can render a modal window. The button should have newTaskBtn as it's id.

import React, { useEffect } from "react";
import { Link, useParams } from "react-router-dom";

import { useProjectsState } from "../../context/projects/context";

const ProjectDetails = () => {
  const projectState = useProjectsState();
  let { projectID } = useParams();

  const selectedProject = projectState?.projects.filter(
    (project) => `${project.id}` === projectID
  )?.[0];

  if (!selectedProject) {
    return <>No such Project!</>;
  }

  return (
    <>
      <div className="flex justify-between">
        <h2 className="text-2xl font-medium tracking-tight text-slate-700">
          {selectedProject.name}
        </h2>
        <Link to={`tasks/new`}>
          <button
            id="newTaskBtn"
            className="rounded-md bg-blue-600 px-4 py-2 m-2 text-sm font-medium text-white hover:bg-opacity-95 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
          >
            New Task
          </button>
        </Link>
      </div>
    </>
  );
};

export default ProjectDetails;

Save the file.

Now, if you visit the project details page, you will see a New Task button. If you click on it, it will take you to /accounts/project/:projectID/tasks/new route.

Render a modal window for the new task

Next, we will render a modal window, which will accept title, description, and dueDate from the user and then create a task.

We have to first create and set up a context, actions and reducer like we did while creating a project.

Let's create a folder task in src/context. We will next create empty files actions.ts, context.tsx, reducer.ts, and types.ts.

Open src/context/task/types.ts and add actions for the API request. We will also create a TaskListState to hold the loading status of API requests and a TasksDispatch type to be used with context. We will use enum to store the list of actions available.

export interface TaskListState {
  isLoading: boolean;
  isError: boolean;
  errorMessage: string;
}

// Actions that are available
export enum TaskListAvailableAction {
  CREATE_TASK_REQUEST = "CREATE_TASK_REQUEST",
  CREATE_TASK_SUCCESS = "CREATE_TASK_SUCCESS",
  CREATE_TASK_FAILURE = "CREATE_TASK_FAILURE",
}

// Create a type to hold list of actions that can be dispatched
export type TaskActions =
  | { type: TaskListAvailableAction.CREATE_TASK_REQUEST }
  | { type: TaskListAvailableAction.CREATE_TASK_SUCCESS }
  | { type: TaskListAvailableAction.CREATE_TASK_FAILURE; payload: string };

// A type to hold dispatch actions in a context.
export type TasksDispatch = React.Dispatch<TaskActions>;

Save the file.

Next, we need to create a reducer. Open src/context/task/reducer.ts. Here, we will update the state based on the action that is dispatched. We will toggle the isLoading to true when the request is initiated. Then, we will turn it to false when the request succeeds, or update the state with an error message.

import { Reducer } from "react";
import { TaskListAvailableAction, TaskListState, TaskActions } from "./types";
// Define the initial state
export const initialState = {
  isLoading: false,
  isError: false,
  errorMessage: "",
};
export const taskReducer: Reducer<TaskListState, TaskActions> = (
  state = initialState,
  action
) => {
  switch (action.type) {
    // Toggle the `isLoading` to true when request is initiated.
    case TaskListAvailableAction.CREATE_TASK_REQUEST:
      return { ...state, isLoading: true };

    // Toggle the `isLoading` to false when request is succesfull or errored.
    case TaskListAvailableAction.CREATE_TASK_SUCCESS:
      return { ...state, isLoading: false };
    case TaskListAvailableAction.CREATE_TASK_FAILURE:
      return {
        ...state,
        isLoading: false,
        isError: true,
        errorMessage: action.payload,
      };
    default:
      return state;
  }
};

Now we have our reducer ready.

Add API corresponding to new task

Next, we need to add the actual API call that needs to be invoked to create a task.

Let's open src/context/task/actions.ts and update it as per the following code.

// Import required type annotations
import { API_ENDPOINT } from "../../config/constants";
import {
  TaskDetailsPayload,
  TaskListAvailableAction,
  TasksDispatch,
} from "./types";

// The function will take a dispatch as first argument, which can be used to send an action to `reducer` and update the state accordingly
export const addTask = async (
  dispatch: TasksDispatch,
  projectID: string,
  task: TaskDetailsPayload
) => {
  const token = localStorage.getItem("authToken") ?? "";
  try {
    // The following action will toggle `isLoading` to `true`
    dispatch({ type: TaskListAvailableAction.CREATE_TASK_REQUEST });

    // Invoke the backend server with POST request and create a task.
    const response = await fetch(
      `${API_ENDPOINT}/projects/${projectID}/tasks/`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${token}`,
        },
        body: JSON.stringify(task),
      }
    );

    if (!response.ok) {
      throw new Error("Failed to create task");
    }
    // Turn `isLoading` to `false`
    dispatch({ type: TaskListAvailableAction.CREATE_TASK_SUCCESS });
  } catch (error) {
    console.error("Operation failed:", error);
    // Update error status in the state.
    dispatch({
      type: TaskListAvailableAction.CREATE_TASK_FAILURE,
      payload: "Unable to create task",
    });
  }
};

Save the file.

Here, we don't have TaskDetailsPayload type yet, so let's define it in types.ts

export type TaskDetailsPayload = {
  title: string;
  description: string;
  dueDate: string;
};

Switch back to actions.ts.

In this file, we provide a dispatch, projectID, and task to create a new task. We are sending a POST request to {API_ENDPOINT}/projects/{projectID}/tasks/ as mentioned in Create Task API doc

Next, we need to create a context so that we can pass around the state and actions to components. Open src/context/task/context.tsx and create TasksStateContext and TasksDispatchContext similar to how we added ProjectsStateContext and ProjectsDispatchContext. We will also create useTasksState and useTasksDispatch hooks to make the context easier to use in components.

import React, { createContext, useContext, useReducer } from "react";
import { taskReducer, initialState } from "./reducer";
import { TaskListState, TasksDispatch } from "./types";
const TasksStateContext = createContext<TaskListState>(initialState);
const TasksDispatchContext = createContext<TasksDispatch>(() => {});
export const TasksProvider: React.FC<React.PropsWithChildren> = ({
  children,
}) => {
  // Create a state and dispatch with `useReducer` passing in the `taskReducer` and an initial state. Pass these as values to our contexts.
  const [state, dispatch] = useReducer(taskReducer, initialState);
  return (
    <TasksStateContext.Provider value={state}>
      <TasksDispatchContext.Provider value={dispatch}>
        {children}
      </TasksDispatchContext.Provider>
    </TasksStateContext.Provider>
  );
};

// Create helper hooks to extract the `state` and `dispacth` out of the context.
export const useTasksState = () => useContext(TasksStateContext);
export const useTasksDispatch = () => useContext(TasksDispatchContext);

Next, we will use this context to pass the list of tasks to the ProjectDetail component.

Open index.tsx file from src/pages/project_details folder in VS Code and import the newly created context in it.

import { TasksProvider } from "../../context/task/context";

We will wrap the ProjectDetails component with TasksProvider, so that values passed in the context are available in ProjectDetails component.

const ProjectDetailsIndex: React.FC = () => {
  return (
    <TasksProvider>
      <ProjectDetails />
      <Outlet />
    </TasksProvider>
  );
};

Creating the modal window component

Now, we have to create the modal window component. We will use something similar to the window used to create a new project.

Let's create a folder named tasks in src/pages. Inside this folder, let's create a file named NewTask.tsx with the following content. We will use react-hook-form to manage the form. We will register three elements with react-hook-form. i.e., title, description and dueDate.

import { Dialog, Transition } from "@headlessui/react";
import { Fragment, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { useForm, SubmitHandler } from "react-hook-form";
import { useProjectsState } from "../../context/projects/context";
import { useTasksDispatch } from "../../context/task/context";
import { addTask } from "../../context/task/actions";
import { TaskDetailsPayload } from "../../context/task/types";

const NewTask = () => {
  let [isOpen, setIsOpen] = useState(true);

  let { projectID } = useParams();
  let navigate = useNavigate();

  // Use react-hook-form to create form submission handler and state.
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<TaskDetailsPayload>();
  const projectState = useProjectsState();
  const taskDispatch = useTasksDispatch();

  // We do some sanity checks to make sure the `projectID` passed is a valid one
  const selectedProject = projectState?.projects.filter(
    (project) => `${project.id}` === projectID
  )?.[0];
  if (!selectedProject) {
    return <>No such Project!</>;
  }
  function closeModal() {
    setIsOpen(false);
    navigate("../../");
  }
  const onSubmit: SubmitHandler<TaskDetailsPayload> = async (data) => {
    try {
      // Invoke the actual API and create a task.
      addTask(taskDispatch, projectID ?? "", data);
      closeModal();
    } catch (error) {
      console.error("Operation failed:", error);
    }
  };
  return (
    <>
      <Transition appear show={isOpen} as={Fragment}>
        <Dialog as="div" className="relative z-10" onClose={closeModal}>
          <Transition.Child
            as={Fragment}
            enter="ease-out duration-300"
            enterFrom="opacity-0"
            enterTo="opacity-100"
            leave="ease-in duration-200"
            leaveFrom="opacity-100"
            leaveTo="opacity-0"
          >
            <div className="fixed inset-0 bg-black bg-opacity-25" />
          </Transition.Child>
          <div className="fixed inset-0 overflow-y-auto">
            <div className="flex min-h-full items-center justify-center p-4 text-center">
              <Transition.Child
                as={Fragment}
                enter="ease-out duration-300"
                enterFrom="opacity-0 scale-95"
                enterTo="opacity-100 scale-100"
                leave="ease-in duration-200"
                leaveFrom="opacity-100 scale-100"
                leaveTo="opacity-0 scale-95"
              >
                <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
                  <Dialog.Title
                    as="h3"
                    className="text-lg font-medium leading-6 text-gray-900"
                  >
                    Create new Task
                  </Dialog.Title>
                  <div className="mt-2">
                    <form onSubmit={handleSubmit(onSubmit)}>
                      <input
                        type="text"
                        required
                        placeholder="Enter title"
                        autoFocus
                        id="title"
                        // Register the title field
                        {...register("title", { required: true })}
                        className="w-full border rounded-md py-2 px-3 my-4 text-gray-700 leading-tight focus:outline-none focus:border-blue-500 focus:shadow-outline-blue"
                      />
                      <input
                        type="text"
                        required
                        placeholder="Enter description"
                        autoFocus
                        id="description"
                        // register the description field
                        {...register("description", { required: true })}
                        className="w-full border rounded-md py-2 px-3 my-4 text-gray-700 leading-tight focus:outline-none focus:border-blue-500 focus:shadow-outline-blue"
                      />
                      <input
                        type="date"
                        required
                        placeholder="Enter due date"
                        autoFocus
                        id="dueDate"
                        // register due date field
                        {...register("dueDate", { required: true })}
                        className="w-full border rounded-md py-2 px-3 my-4 text-gray-700 leading-tight focus:outline-none focus:border-blue-500 focus:shadow-outline-blue"
                      />
                      <button
                        type="submit"
                        // Set an id for the submit button
                        id="newTaskSubmitBtn"
                        className="inline-flex justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 mr-2 text-sm font-medium text-white hover:bg-blue-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
                      >
                        Submit
                      </button>
                      <button
                        onClick={closeModal}
                        className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
                      >
                        Cancel
                      </button>
                    </form>
                  </div>
                </Dialog.Panel>
              </Transition.Child>
            </div>
          </div>
        </Dialog>
      </Transition>
    </>
  );
};
export default NewTask;

Save the file.

This is very similar to the modal window used to create a new project. Here, we use the navigate hook from react-router-dom to control the redirection of the browser after creating a task or when the modal window is closed.

To create a task, we send the API request when the form is submitted. In this component also, we do some sanity checks like, whether the project id in the URL is valid or not.

We use context to pass down an already fetched project list to the NewTask component.

Next, we need to update the src/routes/index.tsx to render this component for a new task route.

Open src/routes/index.tsx in VS Code.

Import the NewTask component.

import NewTask from "../pages/tasks/NewTask";

Next, we will ask the router to render this component for the new task URL.

{
  path: "projects",
  element: <ProjectContainer />,
  children: [
    { index: true, element: <Projects /> },
    {
      path: ":projectID",
      element: <ProjectDetails />,
      children: [
        { index: true, element: <></> },
        {
          path: "tasks",
          children: [
            { index: true, element: <Navigate to="../" /> },
            {
              path: "new",
              // Render `NewTask` component
              element: <NewTask />,
            },
            {
              path: ":taskID",
              children: [{ index: true, element: <>Show Task Details</> }],
            },
          ],
        },
      ],
    },
  ],
}

Save the file. Now if you click on the New Task button in project details page, it will show a modal where you can provide a title, description and dueDate. On submitting the form, it will also invoke the API and create a task. You can verify it by opening the Network tab of the developer console of your browser.

We currently don't display the tasks associated with the project. Let's do that in the next lesson.