Activity Application: UI Goals

Moise Lala
8 min readMar 29, 2024

--

This blog article aims to show and capture some crucial architectural decisions about the application without considering the design aspect. I want to show how a goal is listed, created, and updated without going into too much detail about the design but with a fancy user interface.

Pre-requisite

  • Have a back-end in Python Django, as demonstrated in this article.
  • Set up the front-end in React, as demonstrated in this article.

React router dom

React router dom allows us to do client-side rendering. This means that when we navigate to another page on the application, we do not need to send a request to get all the resources again, such as CSS and JavaScript. In other words, it provides a faster user experience.

Now, on the package.json level, open the terminal and run:

npm i react-router-dom

Verify in the package.json that you can see the react-router-dom.

Setting up the routers

We need to set up the routers. A router is a function that maps a URL path to a particular React component. For the activity application, we need routers for goals and activities. However, in this article, I am showing only how to create the goals on the UI as there are many things to cover.

In the App.tsx file, add the following content:

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

export const App: React.FC = () => {
return (
<div>
<nav>
<ul
style={{
display: "flex",
justifyContent: "space-evenly",
}}
>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="goals/">Goals</Link>
</li>
<li>
<Link to="activities/">Activities</Link>
</li>
</ul>
</nav>
<Outlet />
</div>
);
};

Now, create a home folder, and in the home folder, create Home.tsx and paste this content:

import React from "react";

export const Home: React.FC = () => {
return <div>Home page</div>;
};

Our code’s content is straightforward at the moment because we capture the essence of what makes it work. We will then extend this further.

Model

If we look at the server side, we defined the structure of a goal like the content below:

class Goal(models.Model):
title = models.CharField(max_length=200)
description = models.TextField()
start_date = models.DateTimeField()
end_date = models.DateTimeField()
progress = models.IntegerField()
color = models.CharField(max_length=100, default="black")

So, we need to represent this on the UI, too.

Create a folder name models, and in models, create goals.ts file and paste the following content:

export interface GoalDto {
id?: number;
title: string;
description: string;
start_date: string;
end_date: string;
progress: number;
color: string;
}

export interface Goal {
id?: number;
title: string;
description: string;
start: string;
end: string;
progress: number;
color: string;
}

export const toGoal = (dto: GoalDto): Goal => {
return {
id: dto.id,
title: dto.title,
description: dto.description,
start: dto.start_date,
end: dto.end_date,
progress: dto.progress,
color: dto.color,
};
};

export const toGoalDto = (m: Goal): GoalDto => {
return {
id: m.id,
title: m.title,
description: m.description,
start_date: m.start,
end_date: m.end,
progress: m.progress,
color: m.color,
};
};

In the file, we have the models in Dto, which is similar to the backend side representation of the goal, and the plain Goal model, which is the UI representation.

There are also mappers, toGoalDto and toGoal, which map to the different representations.

react-router-dom loader

The loader is a concept in react-router-dom, it is essentially a function passed to the route, and that function provides the data to the component before it gets rendered.

Create a folder named gateway and create a file called goals.ts. Paste the following content in the goals.ts:

import { Params, redirect } from "react-router-dom";
import { APIError } from "../models/error";
import { Goal, GoalDto, toGoal, toGoalDto } from "../models/goals";


export async function goalsLoader() {
const goals: GoalDto[] = await fetch(`${process.env.VITE_REMOTE}goals/`).then(
(data) => {
return data.json();
}
);
return { goals: goals.map(toGoal) };
}

In main.tsx in the router, add a loader entry to the router object: calling the goalsLoader function defined above.

So it will look like this:

const router = createBrowserRouter([
{
path: "/",
element: <App />,
children: [
{
index: true,
element: <Home />,
loader: goalsLoader,
},
],
},
]);

Now in Home.tsx which is in the folder Home, update its return value with:

<div>
<ul>
{goals.map((goal) => (
<li key={goal.id} style={{ display: "flex" }}>
<Link to={`/goals/${goal.id}`}>{goal.title}</Link>
<span style={{ marginLeft: "10px" }}>{goal.progress}</span>
</li>
))}
</ul>
</div>

If we launch the application, we should see this:

As stated before, you will see an error if you click the gym. So, we will add the goals and every associated action, such as creating or updating a goal.

Goals

Create goals folder. And inside the goals folder, create routes.tsx file and paste the following content:

import { RouteObject } from "react-router-dom";


import { Goals } from "./Goals.js";
import { GoalDetails } from "./GoalDetails.js";
import {
createGoalAction,
getGoalLoader,
goalsLoader,
updateGoalAction,
deleteGoalAction,
} from "../gateway/goals.js";
import { CreateGoal } from "./CreateGoal.js";
import { EditGoal } from "./EditGoal.js";
// import { activityRoutes } from "../activities/routes.js";


export const goalsRoutes: RouteObject[] = [
{
path: "goals",
element: <Goals />,
id: "goals",
loader: goalsLoader,
},
{
path: "goals/new",
element: <CreateGoal />,
action: createGoalAction,
},
{
path: "goals/:id",
element: <GoalDetails />,
loader: getGoalLoader,
// children: [activityRoutes[0]],
},
{
path: "goals/:id/edit",
element: <EditGoal />,
loader: getGoalLoader,
action: updateGoalAction,
},
{
path: "goals/:id/destroy",
action: deleteGoalAction,
},
];

Several imports still need to be included for this to run smoothly and make sense.

Goal List

In the goals folder, create a file name Goals.tsx, and add the following content:

import React from "react";
import { Link, useLoaderData } from "react-router-dom";
import { Goal } from "../models/goals.js";


export const Goals: React.FC = () => {
const { goals } = useLoaderData() as { goals: Goal[] };


return (
<div>
<div>
<Link to="/goals/new">New</Link>
</div>
<ul>
{goals.map((goal) => (
<li key={goal.id} style={{ display: "flex" }}>
<Link to={`/goals/${goal.id}`}>{goal.title}</Link>
<span style={{ marginLeft: "10px" }}>{goal.progress}</span>
</li>
))}
</ul>
</div>
);
};

This looks very much like the Home.tsx, but they will be different in the future. At the moment, it is good to capture some aspects of the architecture and see whether there are new ideas.

Goal Details

In the goals folder, create another file call it GoalDetails.tsx and paste the following content:

import { format } from "date-fns";
import React from "react";
import { useLoaderData, Link, Form, Outlet } from "react-router-dom";
import { Goal } from "../models/goals.js";


export const GoalDetails: React.FC = () => {
const { goal } = useLoaderData() as { goal: Goal };


return (
<div>
<h1>{goal.title}</h1>
<div>
{format(new Date(goal?.start ?? new Date()), "yyyy-MM-dd")}-
{format(new Date(goal?.end ?? new Date()), "yyyy-MM-dd")}
</div>
<div>{goal.description}</div>
<div>{goal.progress}</div>
<div style={{ display: "flex" }}>
<button style={{ marginRight: "10px" }}>
<Link to={`/goals/${goal.id}/edit`}>Edit</Link>
</button>
<Form method="post" action="destroy">
<button type="submit">delete</button>
</Form>
</div>
<div>
<Link to={`/goals/${goal.id}/activities`}>activities</Link>
</div>
<Outlet />
</div>
);
};

This shows one goal in full detail. Notice that we repeat the Outlet component from react-router-dom, which renders all child path components. In this case, it will be the activities associated with the goal. This will be shown in the following article.

Create Goal

Create a file named CreateGoal.tsx, and paste the following content:

import React from "react";
import { GoalForm } from "./components/GoalForm";

export const CreateGoal: React.FC = () => {
return (
<div>
<h1>Create Goal</h1>
<GoalForm isNew />
</div>
);
};

We are going to create a folder called components inside the goals folder, and in there, create the GoalForm.tsx file and paste the following content:

import React from "react";
import { Form, useActionData } from "react-router-dom";
import { format } from "date-fns";

import { Goal } from "../../models/goals.js";
import { APIError } from "../../models/error.js";

interface Props {
isNew?: boolean;
goal?: Goal;
}

export const GoalForm: React.FC<Props> = ({ goal, isNew }) => {
const error = useActionData() as APIError | undefined;


return (
<Form method={isNew ? "POST" : "PUT"}>
<p>
<span>Title</span>
<input
placeholder="Title"
aria-label="Title"
type="text"
name="title"
defaultValue={goal?.title}
style={{
...(error?.getFieldError("title") && { border: "1px solid red" }),
}}
/>
</p>
<p>
<span>Description</span>
<textarea
placeholder="Description"
aria-label="Description"
name="description"
defaultValue={goal?.description}
style={{
...(error?.getFieldError("description") && {
border: "1px solid red",
}),
}}
/>
</p>
<p>
<span>Start date</span>
<input
placeholder="start date"
aria-label="Start Date"
type="date"
name="start"
defaultValue={format(
new Date(goal?.start ?? new Date()),
"yyyy-MM-dd"
)}
style={{
...(error?.getFieldError("start") && { border: "1px solid red" }),
}}
/>
</p>
<p>
<span>End date</span>
<input
placeholder="End Date"
aria-label="End Date"
type="date"
name="end"
defaultValue={format(new Date(goal?.end ?? new Date()), "yyyy-MM-dd")}
style={{
...(error?.getFieldError("end") && { border: "1px solid red" }),
}}
/>
</p>
<input
placeholder="Color"
aria-label="Color"
name="color"
defaultValue={goal?.color ?? "black"}
style={{
...(error?.getFieldError("color") && { border: "1px solid red" }),
}}
/>
<input type="hidden" name="id" defaultValue={goal?.id} />
<input
placeholder="Progress"
aria-label="Progress"
type="hidden"
name="progress"
defaultValue={goal?.progress ?? 0}
/>
<p>
<button type="submit">Save</button>
<button type="button">Cancel</button>
</p>
</Form>
);
};

This is the form for the goal and for creating and updating goals.

Edit Goal

Create a file in the goals folder name it EditGoal.tsx, and paste the following content:

import React from "react";
import { useLoaderData } from "react-router-dom";
import { Goal } from "../models/goals.js";
import { GoalForm } from "./components/GoalForm.js";

export const EditGoal: React.FC = () => {
const { goal } = useLoaderData() as { goal: Goal };

return (
<div>
<h1>Edit {goal.title}</h1>
<GoalForm goal={goal} isNew={false} />
</div>
);
};

By this time if you launch the application, you will get errors. As such, We need to add the error file:

In the models file create a file error.ts and paste the following content:

type ErrorKind = "Application" | "Network" | "Authentication";
type Error = Record<string | "details", string[] | undefined>;

export class APIError {
private readonly errorKind: string;
private readonly errors: Error;
constructor(errorKind: ErrorKind, errors: Error) {
this.errorKind = errorKind;
this.errors = errors;
}

getFieldError(field: string): string | undefined {
const fieldErrorMsg = this.errors[field];
return fieldErrorMsg != null ? fieldErrorMsg[0] : undefined;
}

getErrorKind(): string {
return this.errorKind;
}

getSimpleErrors(): Record<string, string> {
return this.errors != null
? Object.keys(this.errors).reduce((rec, k) => {
const f = this.errors[k];
if (f != null) {
rec[k] = f[0];
}
return rec;
}, {} as Record<string, string>)
: {};
}
}

These errors are adapted to the errors we get from the backend side. The backend side in Django, wrapped with the Django rest framework, sets the error so that each field has an array of error messages, which can be set up by looking at the link here.

Running everything

In the terminal at the package.json level, run the following command:

npm i date-fns

This will install the package date-fns for handling date, such as formatting it as you will see eventually.

Now, run:

npm run dev

This starts the UI on localhost:5173 . Make sure to run the backend too and populate it with data if it does not exist.

Viewing the Goal Details

If you navigate to goal detail by clicking one goal, you will see the following:

Thank you for reading!

Happy coding!

--

--