Peace & Code

Status Manager: First Approuch

Published on

Recommended prior knowledge: React, Typescript

This development was born out of the need to group a set of tasks by state and make it easy to move between them. Anyone who has never moved a task on a Trello or Jira board did not have a childhood πŸ˜†.

First we create the project. This can easily be done in React, but for convenience and habit, I usually do it in Next. To create it, we use the comand provided by Next:

npx create-next-app@latest

When we create the project, it will ask us a series of questions.

What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias (@/*)? No / Yes
What import alias would you like configured? @/*

From these initial configurations we can take away two interesting points to chat about.

  • Typescript VS Javascript: To type or not to type is the question.

  • Tailwind css: To save time or to buy problems?

My opinion about Typescript is that it's better to type everything you can, and for what you can't, we always have our faithful friend any to save us from the fire. A code with a lot of any's is the same as not wanting to use TS, so I recommend to replace them eventually with their proper interface.

Regarding Tailwind css, it happens to me that as I am quite new to CSS, this tool gives me a good scale of colors, padding, font, etc. to tidy me up. I know that my weakest point is CSS and I plan to strengthen it with the CSS for JavaScript Developers course (Go Josh! πŸ˜…).


Note to the reader: Let's take a moment to think about what data models we could create to represent the different parts of the problem (the tasks and the different states for each task).


The data models I can think of are Task/Card and Columns grouped by State:

export interface ICard {
id: string;
text: string;
status: Status;
initStatus: Status;
}

The ICard interface allows us to characterize the text, the state, and the id of the task. The initial state is an optional attribute that I decided to add to be able to restart a task. We could try to abstract the Columns/Status with an interface, but for this first implementation, let's leave it as it is and when we're done, we'll see what else we can move to an interface.

If you look at the types of the status attribute and initStatus, they are not a primitive data type (string, number, etc.). To characterize the status of a task, let's create an enum with the statuses of my dashboard:

export enum Status {
Pending = "PENDINGS",
InProgress = "IN_PROGRESS",
Done = "DONE",
Frozen = "FROZEN",
}

Now let's create the StatusManager component. To create a component in a quick and tidy way, I use the new-component library and run the following command, which I configure in package.json to run with npm:

npm run new-component StatusManager

We have the component. Let's call the StatusManager on the page and pass it a list of test tasks.


Note to the reader: Let's stop and think about what we want to build. Let's ask ourselves these questions. If you had to break the problem into parts, what are they and what would you call them? What are the recurring parts? Take your time, once you have an answer read on.


Now the interesting part begins. It's time to build React components. The components I used are Card and Column, which try to represent the task and the column/state to which we move the tasks.

What responsibility and data should the Card component have? The component should have the text of the task and a function to change the state. To change the state I will use three buttons:

  • An arrow pointing to the right to advance the state.

  • A cross to move to the frozen or stopped task column.

  • A reload button to restart the task.

I leave you the code in case you have any doubts, I strongly recommend that you try your own implementation before reading it 😸 :

Card Component
export default function Card({
card,
processCard,
cancel,
nextState,
}: {
card: ICard;
processCard: any;
cancel?: boolean;
nextState?: string;
}) {
const reload = card.status !== card.initStatus;
return (
<div className={classNames(styles.card, "pr-0")} key={card.id}>
<div className="flex flex-col justify-between h-full">
{cancel && (
<button className="none ">
<X
width={15}
height={12}
onClick={() => processCard(card.id, Status.Frozen)}
className="mr-1"
/>
</button>
)}
{reload && (
<button className="none ">
<RefreshCcw
width={15}
height={12}
onClick={() => processCard(card.id, card.initStatus)}
className="mr-1"
/>
</button>
)}
</div>
<div className="w-full">
<p>{card.text}</p>
</div>
{nextState && (
<button className="none">
<ChevronRight
width={20}
height={20}
onClick={() => processCard(card.id, nextState)}
className="m-auto ml-2 mr-0"
/>
</button>
)}
</div>
);
}

The function responsible for handling this state change I will delegate to its father (StatusManager), receiving it as a parameter. A valid question would be: Why did I make this decision? This leads us to talk about component responsibility.

As we see, the point of contact between Card and Column is StatusManager, it is the one that has the responsibility to manage the tasks, passing to each Card its data and a way to notify it when it changes its status, and giving to Column the list of tasks that it must show.

In the StatusManager we will group the tasks by status to be able to display them in the columns:

const pendingCards = cards.filter((card) => card.status === Status.Pending);
const inProgressCards = cards.filter(
(card) => card.status === Status.InProgress
);
const doneCards = cards.filter((card) => card.status === Status.Done);
const frozenCards = cards.filter((card) => card.status === Status.Frozen);

In the Column component we will have the title and the aesthetics we want to give to each column to differentiate them. After grouping the tasks, we can invoke Column, passing as children the list of cards:

<Column title="Pending" status={Status.Pending}>
{pendingCards?.map((card) => {
return (
<Card
key={card.id}
cancel={true}
card={card}
processCard={processCard}
nextState={Status.InProgress}
/>
);
})}
</Column>

The last thing we need to define is the function that changes the state of a task:

async function processCard(id: string, status: Status) {
const editCard: ICard | undefined = cards.find((c) => c.id === id);
if (!editCard) return;

if (status) {
const newCard: ICard = { ...editCard, id, status };
const newCards: ICard[] | undefined = cards.filter((c) => c.id !== id);
newCards?.unshift(newCard);
setCards(newCards);
}
}

Let's analyze what this function does, first it looks for the task by id and if it doesn't find it, it ends the function with a return. Then it validates that a state has been passed to it. Then it creates a copy of the old task with the new status, then it creates a copy of the list of tasks, removing the old task, adding the new task to the beginning of the copy of the new list, and finally it calls setCards and passes it the new list.

One observation I would make is "If you never filtered by state again, how come the lists you grouped by state updated their tasks? This happens because I stored the tasks inside useState.

export default function StatusManager({ initCards }: { initCards: ICard[] }) {
const [cards, setCards] = useState<ICard[]>(initCards);
...
}

Because of the way React works, when you change a state, it updates its dependencies and re-runs the StatusManager function, causing our const lists to update as well.

Demo:

Frozen

Pending

task 1

task 2

task 3

In Progress

Done

If you have any doubts or get stuck on the implementation, you can check it out in git on the without-framer-motion branch.

Now that we have our first implementation, we can think about adding an interface for the column. In it we could put the name of the column, the background color, and other aspects of the column that you want to add:

export interface IColumn {
id: string;
title: string;
status: Status;
}

I composed the state with the css parts, so the state defines the background and the color of the text.

The advantages of abstracting it in an interface is that we can create an array of IColumn and iterate over it, saving us having to write and filter for each column, thus reducing the amount of repeated code. Also, adding a new column is easier, since we now just need to add our column to the array and define its css.

I recommend that you try this as an exercise to reinforce what you have learned. There is no better way to learn than to get your hands dirty with the code πŸ’ͺ .

As you can see in the demo, the cards instantly change from column to column, which is visually ugly. In the next post we will enter the wonderful world of framer motion animations 😎.

If you want to contact me for feedback, you can write me at (mlpaz.code@gmail.com) .

To find out when there is a new post, follow me on Instagram.

Hope you liked it and see you in the next post.

πŸ–– Mr Peace and code

Note: The component diagram was generated using the React Flow library.