diff --git a/backend/src/app.ts b/backend/src/app.ts
index 75a1fba..4284170 100644
--- a/backend/src/app.ts
+++ b/backend/src/app.ts
@@ -7,6 +7,7 @@ import cors from "cors";
import express from "express";
import { isHttpError } from "http-errors";
import taskRoutes from "src/routes/task";
+import tasksRoutes from "src/routes/tasks";
import type { NextFunction, Request, Response } from "express";
@@ -27,6 +28,7 @@ app.use(
);
app.use("/api/task", taskRoutes);
+app.use("/api/tasks", tasksRoutes);
/**
* Error handler; all errors thrown by server are handled here.
diff --git a/backend/src/controllers/task.ts b/backend/src/controllers/task.ts
index feceb12..de4d887 100644
--- a/backend/src/controllers/task.ts
+++ b/backend/src/controllers/task.ts
@@ -90,3 +90,40 @@ export const removeTask: RequestHandler = async (req, res, next) => {
next(error);
}
};
+
+// Define a custom type for the request body so we can have static typing
+// for the fields
+type UpdateTaskBody = {
+ _id: string;
+ title: string;
+ description?: string;
+ isChecked: boolean;
+ dateCreated: Date;
+};
+
+export const updateTask: RequestHandler = async (req, res, next) => {
+ // extract any errors that were found by the validator
+ const errors = validationResult(req);
+ const { _id, title, description, isChecked, dateCreated } = req.body as UpdateTaskBody;
+ const { id } = req.params;
+ try {
+ // if there are errors, then this function throws an exception
+ validationErrorParser(errors);
+ if (id !== _id) {
+ res.status(400);
+ } else {
+ const task = await TaskModel.findByIdAndUpdate(
+ id,
+ { $set: { title, description, isChecked, dateCreated } },
+ { new: true },
+ );
+
+ if (task === null) {
+ throw createHttpError(404, "Task not found.");
+ }
+ res.status(200).json(task);
+ }
+ } catch (error) {
+ next(error);
+ }
+};
diff --git a/backend/src/controllers/tasks.ts b/backend/src/controllers/tasks.ts
new file mode 100644
index 0000000..ed2d7d5
--- /dev/null
+++ b/backend/src/controllers/tasks.ts
@@ -0,0 +1,12 @@
+import TaskModel from "src/models/task";
+
+import type { RequestHandler } from "express";
+
+export const getAllTasks: RequestHandler = async (req, res, next) => {
+ try {
+ const tasks = await TaskModel.find().sort({ dateCreated: "desc" });
+ res.status(200).json(tasks);
+ } catch (error) {
+ next(error);
+ }
+};
diff --git a/backend/src/routes/task.ts b/backend/src/routes/task.ts
index 69c0a34..17188a4 100644
--- a/backend/src/routes/task.ts
+++ b/backend/src/routes/task.ts
@@ -19,5 +19,6 @@ router.get("/:id", TaskController.getTask);
*/
router.post("/", TaskValidator.createTask, TaskController.createTask);
router.delete("/:id", TaskController.removeTask);
+router.put("/:id", TaskValidator.updateTask, TaskController.updateTask);
export default router;
diff --git a/backend/src/routes/tasks.ts b/backend/src/routes/tasks.ts
new file mode 100644
index 0000000..03b3b9a
--- /dev/null
+++ b/backend/src/routes/tasks.ts
@@ -0,0 +1,8 @@
+import express from "express";
+import * as TasksController from "src/controllers/tasks";
+
+const router = express.Router();
+
+router.get("/", TasksController.getAllTasks);
+
+export default router;
diff --git a/frontend/src/api/tasks.ts b/frontend/src/api/tasks.ts
index f157885..bc5db2f 100644
--- a/frontend/src/api/tasks.ts
+++ b/frontend/src/api/tasks.ts
@@ -1,4 +1,4 @@
-import { get, handleAPIError, post } from "src/api/requests";
+import { get, handleAPIError, post, put } from "src/api/requests";
import type { APIResult } from "src/api/requests";
@@ -94,3 +94,24 @@ export async function getTask(id: string): Promise> {
return handleAPIError(error);
}
}
+
+export async function getAllTasks(): Promise> {
+ try {
+ const response = await get("/api/tasks");
+ const json = (await response.json()) as TaskJSON[];
+ const tasks = json.map((item) => parseTask(item));
+ return { success: true, data: tasks };
+ } catch (error) {
+ return handleAPIError(error);
+ }
+}
+
+export async function updateTask(task: UpdateTaskRequest): Promise> {
+ try {
+ const response = await put(`/api/task/${task._id}`, task);
+ const json = (await response.json()) as TaskJSON;
+ return { success: true, data: parseTask(json) };
+ } catch (error) {
+ return handleAPIError(error);
+ }
+}
diff --git a/frontend/src/components/TaskItem.module.css b/frontend/src/components/TaskItem.module.css
new file mode 100644
index 0000000..46217e8
--- /dev/null
+++ b/frontend/src/components/TaskItem.module.css
@@ -0,0 +1,38 @@
+.item {
+ height: 3rem;
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ column-gap: 0.25rem;
+}
+
+.textContainer {
+ height: 100%;
+ border-bottom: 1px solid var(--color-text-secondary);
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: stretch;
+ overflow: hidden;
+}
+
+.textContainer span {
+ width: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.title {
+ font: var(--font-label);
+}
+
+.description {
+ font: var(--font-body);
+}
+
+.checked {
+ color: var(--color-text-secondary);
+}
diff --git a/frontend/src/components/TaskItem.tsx b/frontend/src/components/TaskItem.tsx
new file mode 100644
index 0000000..6f9c1c9
--- /dev/null
+++ b/frontend/src/components/TaskItem.tsx
@@ -0,0 +1,50 @@
+import { Dialog } from "@tritonse/tse-constellation";
+import React, { useState } from "react";
+import { type Task, updateTask } from "src/api/tasks";
+import { CheckButton } from "src/components";
+import styles from "src/components/TaskItem.module.css";
+
+export type TaskItemProps = {
+ task: Task;
+};
+
+export function TaskItem({ task: initialTask }: TaskItemProps) {
+ const [task, setTask] = useState(initialTask);
+ const [isLoading, setLoading] = useState(false);
+ const [errorModalMessage, setErrorModalMessage] = useState(null);
+
+ const handleToggleCheck = () => {
+ setLoading(true);
+ updateTask({ ...task, isChecked: !task.isChecked })
+ .then((result) => {
+ if (result.success) {
+ setTask(result.data);
+ setLoading(false);
+ }
+ })
+ .catch(setErrorModalMessage);
+ };
+
+ let textContainerClass = styles.textContainer;
+ if (task.isChecked) {
+ textContainerClass += ` ${styles.checked}`;
+ }
+ return (
+
+
+
+ {task.title}
+ {task.description && {task.description}}
+
+
+ );
+}
diff --git a/frontend/src/components/TaskList.module.css b/frontend/src/components/TaskList.module.css
new file mode 100644
index 0000000..c75fff9
--- /dev/null
+++ b/frontend/src/components/TaskList.module.css
@@ -0,0 +1,18 @@
+.listTitle {
+ font: var(--font-heading);
+}
+
+.itemContainer {
+ width: 100%;
+ justify-content: center;
+ flex-direction: column;
+ align-items: stretch;
+}
+
+.listContainer {
+ margin-top: 3rem;
+}
+
+.errorModalText {
+ color: var(--color-tse-navy-blue);
+}
diff --git a/frontend/src/components/TaskList.tsx b/frontend/src/components/TaskList.tsx
new file mode 100644
index 0000000..0782b88
--- /dev/null
+++ b/frontend/src/components/TaskList.tsx
@@ -0,0 +1,46 @@
+import { Dialog } from "@tritonse/tse-constellation";
+import React, { useEffect, useState } from "react";
+import { getAllTasks, type Task } from "src/api/tasks";
+import { TaskItem } from "src/components";
+import styles from "src/components/TaskList.module.css";
+
+export type TaskListProps = {
+ title: string;
+};
+
+export function TaskList({ title }: TaskListProps) {
+ const [tasks, setTasks] = useState([]);
+ const [errorModalMessage, setErrorModalMessage] = useState(null);
+
+ useEffect(() => {
+ getAllTasks()
+ .then((result) => {
+ if (result.success) {
+ setTasks(result.data);
+ }
+ })
+ .catch(setErrorModalMessage);
+ }, []);
+
+ return (
+
+
{title}
+
+ {tasks.length === 0 ? (
+
No tasks yet. Add one above to get started.
+ ) : (
+ tasks.map((task) =>
)
+ )}
+
+
+ );
+}
diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts
index 6a5a32a..cd1434b 100644
--- a/frontend/src/components/index.ts
+++ b/frontend/src/components/index.ts
@@ -5,4 +5,6 @@ export { CheckButton } from "./CheckButton";
export { HeaderBar } from "./HeaderBar";
export { Page } from "./Page";
export { TaskForm } from "./TaskForm";
+export { TaskItem } from "./TaskItem";
+export { TaskList } from "./TaskList";
export { TextField } from "./TextField";
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx
index 0d588d6..6d8de68 100644
--- a/frontend/src/pages/Home.tsx
+++ b/frontend/src/pages/Home.tsx
@@ -1,5 +1,5 @@
import { Link } from "react-router-dom";
-import { Page, TaskForm } from "src/components";
+import { Page, TaskForm, TaskList } from "src/components";
export function Home() {
return (
@@ -12,6 +12,7 @@ export function Home() {
About this app
+
);
}