From d8d1d283165da28cf34f08ab7d4086a01e57047a Mon Sep 17 00:00:00 2001 From: rushil-bot Date: Fri, 19 Dec 2025 13:31:56 -0800 Subject: [PATCH 1/4] Added Users and updated Front and backend routes and api. Addedd assignees to tasks --- backend/src/app.ts | 2 + backend/src/controllers/task.ts | 6 +- backend/src/controllers/tasks.ts | 2 +- backend/src/controllers/user.ts | 124 +++++++++++++++++++++++++++++++ backend/src/models/task.ts | 2 + backend/src/models/user.ts | 12 +++ backend/src/routes/user.ts | 24 ++++++ backend/src/validators/user.ts | 42 +++++++++++ frontend/src/api/tasks.ts | 6 ++ frontend/src/api/users.ts | 81 ++++++++++++++++++++ 10 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 backend/src/controllers/user.ts create mode 100644 backend/src/models/user.ts create mode 100644 backend/src/routes/user.ts create mode 100644 backend/src/validators/user.ts create mode 100644 frontend/src/api/users.ts diff --git a/backend/src/app.ts b/backend/src/app.ts index 4284170..59a208a 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -8,6 +8,7 @@ import express from "express"; import { isHttpError } from "http-errors"; import taskRoutes from "src/routes/task"; import tasksRoutes from "src/routes/tasks"; +import userRoutes from "src/routes/user"; import type { NextFunction, Request, Response } from "express"; @@ -29,6 +30,7 @@ app.use( app.use("/api/task", taskRoutes); app.use("/api/tasks", tasksRoutes); +app.use("/api/user", userRoutes); /** * 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 8610bbf..179c4c4 100644 --- a/backend/src/controllers/task.ts +++ b/backend/src/controllers/task.ts @@ -31,7 +31,7 @@ export const getTask: RequestHandler = async (req, res, next) => { try { // if the ID doesn't exist, then findById returns null - const task = await TaskModel.findById(id); + const task = await TaskModel.findById(id).populate("assignee"); if (task === null) { throw createHttpError(404, "Task not found."); @@ -77,6 +77,8 @@ export const createTask: RequestHandler = async (req, res, next) => { dateCreated: Date.now(), }); + await task.populate("assignee"); + // 201 means a new resource has been created successfully // the newly created task is sent back to the user res.status(201).json(task); @@ -117,7 +119,7 @@ export const updateTask: RequestHandler = async (req, res, next) => { throw createHttpError(404, "Task not found."); } - const updatedTask = await TaskModel.findById(id); + const updatedTask = await TaskModel.findById(id).populate("assignee"); res.status(200).json(updatedTask); } catch (error) { diff --git a/backend/src/controllers/tasks.ts b/backend/src/controllers/tasks.ts index 0322269..3a4e548 100644 --- a/backend/src/controllers/tasks.ts +++ b/backend/src/controllers/tasks.ts @@ -5,7 +5,7 @@ import type { RequestHandler } from "express"; export const getAllTasks: RequestHandler = async (req, res, next) => { try { // your code here - const sortedTasks = await TaskModel.find().sort({ dateCreated: "desc" }); + const sortedTasks = await TaskModel.find().populate("assignee").sort({ dateCreated: "desc" }); res.status(200).json(sortedTasks); } catch (error) { diff --git a/backend/src/controllers/user.ts b/backend/src/controllers/user.ts new file mode 100644 index 0000000..af548bc --- /dev/null +++ b/backend/src/controllers/user.ts @@ -0,0 +1,124 @@ +/** + * Functions that process task route requests. + */ + +import { validationResult } from "express-validator"; +import createHttpError from "http-errors"; +import UserModel from "src/models/user"; +import validationErrorParser from "src/util/validationErrorParser"; + +import type { RequestHandler } from "express"; + +/** + * This is an example of an Express API request handler. We'll tell Express to + * run this function when our backend receives a request to retrieve a + * particular task. + * + * Request handlers typically have 3 parameters: req, res, and next. + * + * @param req The Request object from Express. This contains all the data from + * the API request. (https://expressjs.com/en/4x/api.html#req) + * @param res The Response object from Express. We use this to generate the API + * response for Express to send back. (https://expressjs.com/en/4x/api.html#res) + * @param next The next function in the chain of middleware. If there's no more + * processing we can do in this handler, but we're not completely done handling + * the request, then we can pass it along by calling next(). For all of the + * handlers defined in `src/controllers`, the next function is the global error + * handler in `src/app.ts`. + */ +export const getUser: RequestHandler = async (req, res, next) => { + const { id } = req.params; + + try { + // if the ID doesn't exist, then findById returns null + const user = await UserModel.findById(id); + + if (user === null) { + throw createHttpError(404, "User not found."); + } + + // Set the status code (200) and body (the task object as JSON) of the response. + // Note that you don't need to return anything, but you can still use a return + // statement to exit the function early. + res.status(200).json(user); + } catch (error) { + // pass errors to the error handler + next(error); + } +}; + +// Define a custom type for the request body so we can have static typing +// for the fields +type CreateUserBody = { + name: string; + profilePictureURL?: string; +}; + +type UpdateUserBody = { + name: string; + profilePictureURL?: string; +}; + +export const createUser: RequestHandler = async (req, res, next) => { + // extract any errors that were found by the validator + const errors = validationResult(req); + const { name, profilePictureURL } = req.body as CreateUserBody; + + try { + // if there are errors, then this function throws an exception + validationErrorParser(errors); + + const user = await UserModel.create({ + name, + profilePictureURL, + }); + + // 201 means a new resource has been created successfully + // the newly created task is sent back to the user + res.status(201).json(user); + } catch (error) { + next(error); + } +}; + +export const removeUser: RequestHandler = async (req, res, next) => { + const { id } = req.params; + + try { + const result = await UserModel.deleteOne({ _id: id }); + + res.status(200).json(result); + } catch (error) { + next(error); + } +}; + +export const updateUser: RequestHandler = async (req, res, next) => { + // extract any errors that were found by the validator + const errors = validationResult(req); + + //CHANGE THIS LINE EVENTUALLY + const { name, _id } = req.body as UpdateUserBody & { _id: string }; + const { id } = req.params; + + try { + // if there are errors, then this function throws an exception + validationErrorParser(errors); + + if (_id && id !== _id) { + throw createHttpError(404, "User ID Mismatch."); + } + + const task = await UserModel.findByIdAndUpdate(id, { name }); + + if (task === null) { + throw createHttpError(404, "Task not found."); + } + + const updatedUser = await UserModel.findById(id); + + res.status(200).json(updatedUser); + } catch (error) { + next(error); + } +}; diff --git a/backend/src/models/task.ts b/backend/src/models/task.ts index 8204067..1016e7b 100644 --- a/backend/src/models/task.ts +++ b/backend/src/models/task.ts @@ -1,3 +1,4 @@ +import { ObjectId } from "mongodb"; import { model, Schema } from "mongoose"; import type { InferSchemaType } from "mongoose"; @@ -12,6 +13,7 @@ const taskSchema = new Schema({ // When we send a Task object in the JSON body of an API response, the date // will automatically get "serialized" into a standard date string. dateCreated: { type: Date, required: true }, + assignee: { type: ObjectId, ref: "User" }, }); type Task = InferSchemaType; diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts new file mode 100644 index 0000000..f0f402a --- /dev/null +++ b/backend/src/models/user.ts @@ -0,0 +1,12 @@ +import { model, Schema } from "mongoose"; + +import type { InferSchemaType } from "mongoose"; + +const userSchema = new Schema({ + name: { type: String, required: true }, + profilePictureURL: { type: String }, +}); + +type User = InferSchemaType; + +export default model("User", userSchema); diff --git a/backend/src/routes/user.ts b/backend/src/routes/user.ts new file mode 100644 index 0000000..a6ba95a --- /dev/null +++ b/backend/src/routes/user.ts @@ -0,0 +1,24 @@ +/** + * User route requests. + */ + +import express from "express"; +import * as UserController from "src/controllers/user"; +import * as UserValidator from "src/validators/user"; + +const router = express.Router(); + +router.get("/:id", UserController.getUser); + +/** + * UserValidator.createUser serves as middleware for this route. This means + * that instead of immediately serving up the route when the request is made, + * Express firsts passes the request to UserValidator.createUser. + * UserValidator.createUser processes the request and determines whether the + * request should be sent through or an error should be thrown. + */ +router.post("/", UserValidator.createUser, UserController.createUser); +router.put("/:id", UserValidator.updateUser, UserController.updateUser); +router.delete("/:id", UserController.removeUser); + +export default router; diff --git a/backend/src/validators/user.ts b/backend/src/validators/user.ts new file mode 100644 index 0000000..259f82d --- /dev/null +++ b/backend/src/validators/user.ts @@ -0,0 +1,42 @@ +import { body } from "express-validator"; + +// more info about validators: +// https://express-validator.github.io/docs/guides/validation-chain +// https://github.com/validatorjs/validator.js#validators + +const makeIDValidator = () => + body("_id") + .exists() + .withMessage("_id is required") + .bail() + .isMongoId() + .withMessage("_id must be a MongoDB object ID"); +const makeNameValidator = () => + body("name") + // name must exist, if not this message will be displayed + .exists() + .withMessage("name is required") + // bail prevents the remainder of the validation chain for this field from being executed if + // there was an error + .bail() + .isString() + .withMessage("name must be a string") + .bail() + .notEmpty() + .withMessage("name cannot be empty"); +const makeProfilePictureURLValidator = () => + body("profilePictureURL") + // order matters for the validation chain - by marking this field as optional, the rest of + // the chain will only be evaluated if it exists + .optional() + .isString() + .withMessage("profilePictureURL must be a string"); + +// establishes a set of rules that the body of the task creation route must follow +export const createUser = [makeNameValidator(), makeProfilePictureURLValidator()]; + +export const updateUser = [ + makeIDValidator(), + makeNameValidator(), + makeProfilePictureURLValidator(), +]; diff --git a/frontend/src/api/tasks.ts b/frontend/src/api/tasks.ts index 223be90..1524b8f 100644 --- a/frontend/src/api/tasks.ts +++ b/frontend/src/api/tasks.ts @@ -1,6 +1,7 @@ import { get, handleAPIError, post, put } from "src/api/requests"; import type { APIResult } from "src/api/requests"; +import type { User } from "src/api/users.ts"; /** * Defines the "shape" of a Task object (what fields are present and their types) for @@ -13,6 +14,7 @@ export type Task = { description?: string; isChecked: boolean; dateCreated: Date; + assignee?: User; }; /** @@ -30,6 +32,7 @@ type TaskJSON = { description?: string; isChecked: boolean; dateCreated: string; + assignee?: User; }; /** @@ -46,6 +49,7 @@ function parseTask(task: TaskJSON): Task { description: task.description, isChecked: task.isChecked, dateCreated: new Date(task.dateCreated), + assignee: task.assignee, }; } @@ -57,6 +61,7 @@ function parseTask(task: TaskJSON): Task { export type CreateTaskRequest = { title: string; description?: string; + assignee?: string; }; /** @@ -69,6 +74,7 @@ export type UpdateTaskRequest = { description?: string; isChecked: boolean; dateCreated: Date; + assignee?: string; }; /** diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts new file mode 100644 index 0000000..e006843 --- /dev/null +++ b/frontend/src/api/users.ts @@ -0,0 +1,81 @@ +import { get, handleAPIError, post, put } from "src/api/requests"; + +import type { APIResult } from "src/api/requests"; + +/** + * Defines the "shape" of a Task object (what fields are present and their types) for + * frontend components to use. This will be the return type of most functions in this + * file. + */ +export type User = { + _id: string; + name: string; + profilePictureURL?: string; +}; + +/** + * The expected inputs when we want to create a new Task object. In the MVP, we only + * need to provide the title and optionally the description, but in the course of + * this tutorial you'll likely want to add more fields here. + */ +export type CreateUserRequest = { + name: string; + profilePictureURL?: string; +}; + +/** + * The expected inputs when we want to update an existing Task object. Similar to + * `CreateTaskRequest`. + */ +export type UpdateUserRequest = { + _id: string; + name: string; + profilePictureURL?: string; +}; + +/** + * The implementations of these API client functions are provided as part of the + * MVP. You can use them as a guide for writing the other client functions. + */ +export async function createUser(user: CreateUserRequest): Promise> { + try { + const response = await post("/api/user", user); + const json = (await response.json()) as User; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +} + +export async function getUser(id: string): Promise> { + try { + const response = await get(`/api/user/${id}`); + const json = (await response.json()) as User; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +} + +export async function getAllTasks(): Promise> { + try { + // your code here + const response = await get(`/api/user`); + + const json = (await response.json()) as User[]; + + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +} + +export async function updateTask(user: UpdateUserRequest): Promise> { + try { + const response = await put(`/api/user/${user._id}`, user); + const json = (await response.json()) as User; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +} From f679aee77846636053f275eae69076fc78dc870c Mon Sep 17 00:00:00 2001 From: rushil-bot Date: Sat, 20 Dec 2025 19:05:41 -0800 Subject: [PATCH 2/4] Added Task details page and linked each task to appropriate page --- frontend/public/userIcon.svg | 10 ++ frontend/src/App.tsx | 3 +- frontend/src/components/TaskItem.module.css | 26 +++++ frontend/src/components/TaskItem.tsx | 7 +- frontend/src/pages/TaskDetail.module.css | 102 +++++++++++++++++++ frontend/src/pages/TaskDetail.tsx | 107 ++++++++++++++++++++ frontend/src/pages/index.ts | 1 + 7 files changed, 253 insertions(+), 3 deletions(-) create mode 100644 frontend/public/userIcon.svg create mode 100644 frontend/src/pages/TaskDetail.module.css create mode 100644 frontend/src/pages/TaskDetail.tsx diff --git a/frontend/public/userIcon.svg b/frontend/public/userIcon.svg new file mode 100644 index 0000000..d1e2a5d --- /dev/null +++ b/frontend/public/userIcon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0c28956..64eed75 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,6 @@ import { ThemeProvider } from "@tritonse/tse-constellation"; import { BrowserRouter, Route, Routes } from "react-router-dom"; -import { About, Home } from "src/pages"; +import { About, Home, TaskDetail } from "src/pages"; import "src/globals.css"; export default function App() { @@ -10,6 +10,7 @@ export default function App() { } /> } /> + } /> diff --git a/frontend/src/components/TaskItem.module.css b/frontend/src/components/TaskItem.module.css index 659a375..59b11b5 100644 --- a/frontend/src/components/TaskItem.module.css +++ b/frontend/src/components/TaskItem.module.css @@ -5,6 +5,15 @@ justify-content: flex-start; align-items: center; column-gap: 0.25rem; + /* Add padding to ensure the background color looks good around the content */ + padding: 0 0.5rem; + /* Smooth transition for the background color change */ + transition: background-color 0.2s; +} + +/* Change background color on hover */ +.item:hover { + background-color: var(--color-surface); } .textContainer { @@ -29,6 +38,19 @@ font: var(--font-label); } +/* Override default link styling */ +.title a { + /* Inherit color from parent (.textContainer), which handles the .checked state */ + color: inherit; + text-decoration: none; +} + +.title a:hover { + text-decoration: underline; + /* Optional: maintain the inherited color on hover, or set a specific hover color if desired */ + color: inherit; +} + .description { font: var(--font-body); } @@ -36,3 +58,7 @@ .checked { color: var(--color-text-secondary); } + +.errorModalText { + color: var(--color-tse-navy-blue); +} diff --git a/frontend/src/components/TaskItem.tsx b/frontend/src/components/TaskItem.tsx index 8699b49..7f9b266 100644 --- a/frontend/src/components/TaskItem.tsx +++ b/frontend/src/components/TaskItem.tsx @@ -1,5 +1,6 @@ import { Dialog } from "@tritonse/tse-constellation"; -import React, { useState } from "react"; // update this line +import React, { useState } from "react"; +import { Link } from "react-router-dom"; import { type Task, updateTask } from "src/api/tasks"; import { CheckButton } from "src/components"; import styles from "src/components/TaskItem.module.css"; @@ -41,7 +42,9 @@ export function TaskItem({ task: initialTask }: TaskItemProps) { disabled={isLoading} />
- {task.title} + + {task.title} + {task.description && {task.description}}
(null); + const [errorModalMessage, setErrorModalMessage] = useState(null); + const { id } = useParams(); + + // 1. Fetch the task data + useEffect(() => { + if (id) { + getTask(id) + .then((result) => { + if (result.success) { + setTask(result.data); + } else { + setErrorModalMessage(result.error); + } + }) + .catch(setErrorModalMessage); + } + }, [id]); + + // 2. Update the document title dynamically when the task loads + useEffect(() => { + if (task) { + document.title = `${task.title} | TSE Todos`; + } + }, [task]); + + if (!task) { + return ( + +

Loading...

+
+ ); + } + + return ( + + {/* Link to home*/} +

+ Back to home +

+ +
+
+ {/*Title of task */} + {task.title} + + {/*Edit button*/} +
+
+
+ +
+ {/*Description*/} +

+ {task.description ? task.description : "(No Description)"} +

+
+ + {/*Assignee information */} +
+ Assignee + User Icon + + User Name Very Long Very Long Very Long Very Long... + +
+ + {/*Status Information */} +
+ Status + {task.isChecked ? "Done" : "Not Done"} +
+ + {/*Date Created Information */} +
+ Date Created + {/* Use the formatter on the task.dateCreated object */} + {dateFormatter.format(task.dateCreated)} +
+
+ + {errorModalMessage}

} + isOpen={errorModalMessage !== null} + onClose={() => setErrorModalMessage(null)} + /> +
+ ); +} diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts index 74e3b9c..74a17b1 100644 --- a/frontend/src/pages/index.ts +++ b/frontend/src/pages/index.ts @@ -2,3 +2,4 @@ // IMO this is fine for an index file export { About } from "./About"; export { Home } from "./Home"; +export { TaskDetail } from "./TaskDetail"; From 5d77405f52ff1bdefbf7becade3c44464aedc457 Mon Sep 17 00:00:00 2001 From: rushil-bot Date: Wed, 31 Dec 2025 12:10:25 -0800 Subject: [PATCH 3/4] Added userTag component and created ability to assign people to tasks --- backend/src/controllers/task.ts | 58 ++++------ frontend/src/components/TaskForm.module.css | 1 + frontend/src/components/TaskForm.test.tsx | 94 ++-------------- frontend/src/components/TaskForm.tsx | 112 +++++++++----------- frontend/src/components/TaskItem.module.css | 36 +++++-- frontend/src/components/TaskItem.tsx | 26 ++--- frontend/src/components/TaskList.module.css | 19 ++++ frontend/src/components/UserTag.module.css | 34 ++++++ frontend/src/components/UserTag.tsx | 24 +++++ frontend/src/components/index.ts | 1 + frontend/src/pages/TaskDetail.module.css | 4 +- frontend/src/pages/TaskDetail.tsx | 46 ++++---- 12 files changed, 224 insertions(+), 231 deletions(-) create mode 100644 frontend/src/components/UserTag.module.css create mode 100644 frontend/src/components/UserTag.tsx diff --git a/backend/src/controllers/task.ts b/backend/src/controllers/task.ts index 179c4c4..3775278 100644 --- a/backend/src/controllers/task.ts +++ b/backend/src/controllers/task.ts @@ -1,7 +1,3 @@ -/** - * Functions that process task route requests. - */ - import { validationResult } from "express-validator"; import createHttpError from "http-errors"; import TaskModel from "src/models/task"; @@ -9,78 +5,55 @@ import validationErrorParser from "src/util/validationErrorParser"; import type { RequestHandler } from "express"; -/** - * This is an example of an Express API request handler. We'll tell Express to - * run this function when our backend receives a request to retrieve a - * particular task. - * - * Request handlers typically have 3 parameters: req, res, and next. - * - * @param req The Request object from Express. This contains all the data from - * the API request. (https://expressjs.com/en/4x/api.html#req) - * @param res The Response object from Express. We use this to generate the API - * response for Express to send back. (https://expressjs.com/en/4x/api.html#res) - * @param next The next function in the chain of middleware. If there's no more - * processing we can do in this handler, but we're not completely done handling - * the request, then we can pass it along by calling next(). For all of the - * handlers defined in `src/controllers`, the next function is the global error - * handler in `src/app.ts`. - */ export const getTask: RequestHandler = async (req, res, next) => { const { id } = req.params; try { - // if the ID doesn't exist, then findById returns null const task = await TaskModel.findById(id).populate("assignee"); if (task === null) { throw createHttpError(404, "Task not found."); } - // Set the status code (200) and body (the task object as JSON) of the response. - // Note that you don't need to return anything, but you can still use a return - // statement to exit the function early. res.status(200).json(task); } catch (error) { - // pass errors to the error handler next(error); } }; -// Define a custom type for the request body so we can have static typing -// for the fields type CreateTaskBody = { title: string; description?: string; isChecked?: boolean; + assignee?: string; // Added assignee }; type UpdateTaskBody = { title: string; description?: string; isChecked?: boolean; + assignee?: string; // Added assignee }; export const createTask: RequestHandler = async (req, res, next) => { - // extract any errors that were found by the validator const errors = validationResult(req); - const { title, description, isChecked } = req.body as CreateTaskBody; + // Extract assignee along with other fields + const { title, description, isChecked, assignee } = req.body as CreateTaskBody; try { - // if there are errors, then this function throws an exception validationErrorParser(errors); const task = await TaskModel.create({ title, description, isChecked, + assignee, // Save the assignee dateCreated: Date.now(), }); + // Populate the assignee so the frontend gets the user object back immediately await task.populate("assignee"); - // 201 means a new resource has been created successfully - // the newly created task is sent back to the user res.status(201).json(task); } catch (error) { next(error); @@ -100,20 +73,31 @@ export const removeTask: RequestHandler = async (req, res, next) => { }; export const updateTask: RequestHandler = async (req, res, next) => { - // extract any errors that were found by the validator const errors = validationResult(req); - const { isChecked, _id } = req.body as UpdateTaskBody & { _id: string }; + // Extract ALL fields: title, description, assignee, isChecked + const { title, description, assignee, isChecked, _id } = req.body as UpdateTaskBody & { + _id: string; + }; const { id } = req.params; try { - // if there are errors, then this function throws an exception validationErrorParser(errors); if (_id && id !== _id) { throw createHttpError(404, "Task ID Mismatch."); } - const task = await TaskModel.findByIdAndUpdate(id, { isChecked }); + // Update all fields in the database + const task = await TaskModel.findByIdAndUpdate( + id, + { + title, + description, + assignee, + isChecked, + }, + { new: true }, // Optional: returns the modified document + ); if (task === null) { throw createHttpError(404, "Task not found."); diff --git a/frontend/src/components/TaskForm.module.css b/frontend/src/components/TaskForm.module.css index 9e84604..f1a21b6 100644 --- a/frontend/src/components/TaskForm.module.css +++ b/frontend/src/components/TaskForm.module.css @@ -15,6 +15,7 @@ justify-content: flex-start; column-gap: 2rem; align-items: flex-end; + margin-bottom: 1rem; /* Added spacing between rows */ } .textField { diff --git a/frontend/src/components/TaskForm.test.tsx b/frontend/src/components/TaskForm.test.tsx index e1291e4..08fa574 100644 --- a/frontend/src/components/TaskForm.test.tsx +++ b/frontend/src/components/TaskForm.test.tsx @@ -1,45 +1,21 @@ import "@testing-library/jest-dom/vitest"; import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { createTask } from "src/api/tasks"; +import { createTask, updateTask } from "src/api/tasks"; import { TaskForm } from "src/components/TaskForm"; import { afterEach, describe, expect, it, vi } from "vitest"; -import type { CreateTaskRequest, Task } from "src/api/tasks"; +import type { CreateTaskRequest, Task, UpdateTaskRequest } from "src/api/tasks"; import type { TaskFormProps } from "src/components/TaskForm"; const TITLE_INPUT_ID = "task-title-input"; const DESCRIPTION_INPUT_ID = "task-description-input"; const SAVE_BUTTON_ID = "task-save-button"; -/** - * The `vi.mock()` function allows us to replace the exports of another module. - * In this case, because `TaskForm` calls `createTask()` directly, we can't pass - * in our mock function as a prop, so we use `vi.mock()` to replace `createTask` - * from `src/api/tasks` with the mock function. Thus, if `TaskForm` is being - * rendered by one of these tests (as opposed to a real browser) and it tries to - * call `createTask()`, it will run the code that we provide below instead. - * - * See https://vitest.dev/guide/mocking.html#modules for more info about mocking - * modules. - */ vi.mock("src/api/tasks", () => ({ - /** - * Some of our tests will verify that `TaskForm` calls the correct functions - * when the user takes certain actions, like clicking the Save button. This mock - * function replaces `createTask` from `src/api/tasks` so we can (1) check - * whether that API function gets called and (2) prevent our test from trying to - * send an actual HTTP request. Because we know how `createTask` is used in the - * `TaskForm`, it's safe to just make this mock return `{ success: true }`--we - * don't need to rewrite a full implementation unless that's required. - * - * See https://vitest.dev/guide/mocking#functions for more info about mock functions. - */ createTask: vi.fn(async (_params: CreateTaskRequest) => Promise.resolve({ success: true })), + updateTask: vi.fn(async (_params: UpdateTaskRequest) => Promise.resolve({ success: true })), })); -/** - * A fake Task object to use in tests - */ const mockTask: Task = { _id: "task123", title: "My task", @@ -48,65 +24,18 @@ const mockTask: Task = { dateCreated: new Date(), }; -/** - * Renders the `TaskForm` component for use in tests - */ function mountComponent(props: TaskFormProps) { render(); } -/** - * The callback below runs after each test (see - * https://vitest.dev/api/#aftereach). Often you'll run `clearAllMocks()` or - * `resetAllMocks()`, which reset the state of all mock functions - * (https://vitest.dev/api/vi.html#vi-clearallmocks). In this case, we only want - * to "clear" because "reset" also removes any mock implementations, which we - * should leave alone for this test suite. - */ afterEach(() => { vi.clearAllMocks(); cleanup(); }); -/** - * A `describe` block helps to group tests together, but is not required. You - * can nest them--for example, we could have something like: - * ```js - * describe("BigComponent", () => { - * describe("functionality 1", () => { - * it("does something", () => { - * // ... - * }) - * // ... - * }); - * describe("functionality 2", () => { - * // ... - * }) - * }) - * ``` - * See https://vitest.dev/api/#describe for more information. - */ describe("taskForm", () => { - /** - * The `it` function defines a single test. The first parameter is a string - * that names the test. You should follow the format below (starts with a - * present-tense verb) so it reads as "it renders create mode", where "it" - * refers to "TaskForm". The second parameter is a function that contains the - * code for the test. - * - * This first test simply renders the component, then checks that the "New - * task" title is present. These kinds of tests are easy to write but do not - * verify any actual behavior/logic, so be sure to write additional tests like - * the third and fourth ones in this file. - * - * `it` is actually an alias for the `test` function. We use `it` so it reads - * like a sentence. - * - * See https://vitest.dev/api/#test for more information. - */ it("renders create mode", () => { mountComponent({ mode: "create" }); - // https://vitest.dev/api/expect.html expect(screen.queryByText("New task")).toBeInTheDocument(); }); @@ -121,8 +50,6 @@ describe("taskForm", () => { }); it("calls submit handler with edited fields", async () => { - // sometimes a test needs to be asynchronous, for example if we need to wait - // for component state updates mountComponent({ mode: "edit", task: mockTask, @@ -133,20 +60,15 @@ describe("taskForm", () => { }); const saveButton = screen.getByTestId(SAVE_BUTTON_ID); fireEvent.click(saveButton); - expect(createTask).toHaveBeenCalledTimes(1); - expect(createTask).toHaveBeenCalledWith({ + expect(updateTask).toHaveBeenCalledTimes(1); + expect(updateTask).toHaveBeenCalledWith({ + _id: mockTask._id, title: "Updated title", description: "Updated description", + isChecked: mockTask.isChecked, + assignee: "", }); await waitFor(() => { - // If the test ends before all state updates and rerenders occur, we'll - // get a warning about updates not being wrapped in an `act(...)` - // function. We resolve it here by waiting for the save button to be - // re-enabled. - // `@testing-library/react` actually uses `act()` in its helper functions, - // like `fireEvent.click()`, so we'll only see that warning when there's - // something missing in our tests. - // More info: https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning expect(saveButton).toBeEnabled(); }); }); diff --git a/frontend/src/components/TaskForm.tsx b/frontend/src/components/TaskForm.tsx index 237498c..6b09b11 100644 --- a/frontend/src/components/TaskForm.tsx +++ b/frontend/src/components/TaskForm.tsx @@ -1,98 +1,78 @@ import { Dialog } from "@tritonse/tse-constellation"; import { useState } from "react"; -import { createTask } from "src/api/tasks"; +import { createTask, type Task, updateTask } from "src/api/tasks"; import { Button, TextField } from "src/components"; import styles from "src/components/TaskForm.module.css"; -import type { Task } from "src/api/tasks"; - export type TaskFormProps = { mode: "create" | "edit"; task?: Task; onSubmit?: (task: Task) => void; }; -/** - * A simple way to handle error states in the form. We'll keep a - * `TaskFormErrors` object in the form's state, initially empty. Before we - * submit a request, we'll check each field for problems. For each invalid - * field, we set the corresponding field in the errors object to true, and the - * corresponding input component will show its error state if the field is true. - * Look at where the `errors` object appears below for demonstration. - * - * In the MVP, the only possible error in this form is that the title is blank, - * so this is slightly overengineered. However, a more complex form would need - * a similar system. - */ type TaskFormErrors = { title?: boolean; }; -/** - * The form that creates or edits a Task object. In the MVP, this is only - * capable of creating Tasks. - * - * @param props.mode Controls how the form renders and submits - * @param props.task Optional initial data to populate the form with (such as - * when we're editing an existing task) - * @param props.onSubmit Optional callback to run after the user submits the - * form and the request succeeds - */ export function TaskForm({ mode, task, onSubmit }: TaskFormProps) { const [title, setTitle] = useState(task?.title || ""); const [description, setDescription] = useState(task?.description || ""); + const [assigneeId, setAssigneeId] = useState(task?.assignee?._id || ""); const [isLoading, setLoading] = useState(false); const [errors, setErrors] = useState({}); - - // This state variable controls the error message that gets displayed to the user in the - // Constellation `Dialog` component. If it's `null`, there's no error, so we don't display the Dialog. - // If it's non-null, there is an error, so we should display that error to the user. const [errorModalMessage, setErrorModalMessage] = useState(null); const handleSubmit = () => { - // first, do any validation that we can on the frontend setErrors({}); if (title.length === 0) { setErrors({ title: true }); return; } setLoading(true); - createTask({ title, description }) - .then((result) => { - if (result.success) { - // clear the form - setTitle(""); - setDescription(""); - // only call onSubmit if it's NOT undefined - if (onSubmit) onSubmit(result.data); - } else { - // You should always clearly inform the user when something goes wrong. - // In this case, we're using the Constellation `Dialog` component to show a popup. - // For errors, you generally want to show some kind of error state or notification - // within your UI. If the problem is with the user's input, then use - // the error states of your smaller components (like the `TextField`s). - // If the problem is something we don't really control, such as network - // issues or an unexpected exception on the server side, then use a - // banner, modal, popup, or similar. - setErrorModalMessage(result.error); - } - setLoading(false); + + if (mode === "create") { + createTask({ title, description, assignee: assigneeId }) + .then((result) => { + if (result.success) { + setTitle(""); + setDescription(""); + setAssigneeId(""); + if (onSubmit) onSubmit(result.data); + } else { + setErrorModalMessage(result.error); + } + setLoading(false); + }) + .catch(setErrorModalMessage); + } else { + updateTask({ + _id: task!._id, + title, + description, + assignee: assigneeId, + dateCreated: task!.dateCreated, + isChecked: task!.isChecked, }) - .catch(setErrorModalMessage); + .then((result) => { + if (result.success) { + if (onSubmit) onSubmit(result.data); + } else { + setErrorModalMessage(result.error); + } + setLoading(false); + }) + .catch(setErrorModalMessage); + } }; const formTitle = mode === "create" ? "New task" : "Edit task"; return (
- {/* we could just use a `
` element because we don't need the special - functionality that browsers give to `` elements, but using `` - is better for accessibility because it's more accurate for this purpose-- - we are making a form, so we should use `` */} {formTitle} + + {/* Row 1: Title & Description - Matches PDF Page 6 Top Row */}
- {/* `data-testid` is used by React Testing Library--see the tests in - `TaskForm.test.tsx` */} setDescription(event.target.value)} /> - {/* set `type="primary"` on the button so the browser doesn't try to - handle it specially (because it's inside a ``) */} +
+ + {/* Row 2: Assignee & Save - Matches PDF Page 6 Bottom Row */} +
+ setAssigneeId(event.target.value)} + />
- {/* Use the Constellation Dialog component to display an error message if there's an error - See the docs (https://tritonse.github.io/TSE-Constellation/?path=/docs/organisms-dialog--documentation) - for a demo and more info on the prop types */} + {errorModalMessage}

} isOpen={errorModalMessage !== null} onClose={() => setErrorModalMessage(null)} diff --git a/frontend/src/components/TaskItem.module.css b/frontend/src/components/TaskItem.module.css index 59b11b5..f31cde1 100644 --- a/frontend/src/components/TaskItem.module.css +++ b/frontend/src/components/TaskItem.module.css @@ -5,25 +5,34 @@ justify-content: flex-start; align-items: center; column-gap: 0.25rem; - /* Add padding to ensure the background color looks good around the content */ padding: 0 0.5rem; - /* Smooth transition for the background color change */ transition: background-color 0.2s; } -/* Change background color on hover */ .item:hover { background-color: var(--color-surface); } -.textContainer { +/* New wrapper that holds text and user tag to ensure border spans both */ +.contentContainer { height: 100%; + flex-grow: 1; + display: flex; + flex-direction: row; + align-items: center; + /* Moved border here */ border-bottom: 1px solid var(--color-text-secondary); - flex-grow: 1; /* Takes up all remaining space in the parent div */ + overflow: hidden; +} + +.textContainer { + height: 100%; + /* Removed border-bottom from here */ + flex-grow: 1; display: flex; - flex-direction: column; /* Lays out children vertically */ - justify-content: center; /* Centers children vertically (main axis) */ - align-items: stretch; /* Stretches children horizontally (cross axis) */ + flex-direction: column; + justify-content: center; + align-items: stretch; overflow: hidden; } @@ -34,20 +43,25 @@ text-overflow: ellipsis; } +.userTagContainer { + width: 12rem; + flex-shrink: 0; + display: flex; + justify-content: flex-end; + align-items: center; +} + .title { font: var(--font-label); } -/* Override default link styling */ .title a { - /* Inherit color from parent (.textContainer), which handles the .checked state */ color: inherit; text-decoration: none; } .title a:hover { text-decoration: underline; - /* Optional: maintain the inherited color on hover, or set a specific hover color if desired */ color: inherit; } diff --git a/frontend/src/components/TaskItem.tsx b/frontend/src/components/TaskItem.tsx index 7f9b266..57082f3 100644 --- a/frontend/src/components/TaskItem.tsx +++ b/frontend/src/components/TaskItem.tsx @@ -2,7 +2,7 @@ import { Dialog } from "@tritonse/tse-constellation"; import React, { useState } from "react"; import { Link } from "react-router-dom"; import { type Task, updateTask } from "src/api/tasks"; -import { CheckButton } from "src/components"; +import { CheckButton, UserTag } from "src/components"; import styles from "src/components/TaskItem.module.css"; export type TaskItemProps = { @@ -35,23 +35,23 @@ export function TaskItem({ task: initialTask }: TaskItemProps) { } return (
- {/* render CheckButton here */} - handleToggleCheck()} - disabled={isLoading} - /> -
- - {task.title} - - {task.description && {task.description}} + + {/* Wrapper to extend the border across both text and user tag */} +
+
+ + {task.title} + + {task.description && {task.description}} +
+
+ +
{errorModalMessage}

} isOpen={errorModalMessage !== null} onClose={() => setErrorModalMessage(null)} diff --git a/frontend/src/components/TaskList.module.css b/frontend/src/components/TaskList.module.css index e7aecdc..28f7782 100644 --- a/frontend/src/components/TaskList.module.css +++ b/frontend/src/components/TaskList.module.css @@ -20,6 +20,25 @@ overflow: hidden; } +.userTagContainer { + width: 12rem; /* Fixed width as suggested in walkthrough */ + flex-shrink: 0; + display: flex; + justify-content: flex-end; /* Align the tag to the right within this container */ +} + +/* Ensure .item stays consistent */ +.item { + height: 3rem; + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + column-gap: 0.25rem; + padding: 0 0.5rem; + transition: background-color 0.2s; +} + /* 4. Error Modal Text: Blue color override */ .errorModalText { color: var(--color-tse-navy-blue); diff --git a/frontend/src/components/UserTag.module.css b/frontend/src/components/UserTag.module.css new file mode 100644 index 0000000..86487b5 --- /dev/null +++ b/frontend/src/components/UserTag.module.css @@ -0,0 +1,34 @@ +.assingeeLabel { + font: var(--font-label); + margin-right: 1rem; +} + +.assingeeName { + font: var(--font-body); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.assingeeIcon { + width: 32px; + height: 32px; + border-radius: 50%; + margin-right: 0.5rem; + flex-shrink: 0; +} + +.assingeeItem { + height: 2rem; + /* Removed margin-top: 2rem; <-- This was causing the misalignment */ + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; +} + +.noUserMessage { + font: var(--font-body); + word-break: break-all; + white-space: normal; +} diff --git a/frontend/src/components/UserTag.tsx b/frontend/src/components/UserTag.tsx new file mode 100644 index 0000000..a561b9c --- /dev/null +++ b/frontend/src/components/UserTag.tsx @@ -0,0 +1,24 @@ +import styles from "src/components/UserTag.module.css"; + +import type { User } from "src/api/users"; + +export type UserTagProps = { + user?: User; +}; + +export function UserTag({ user }: UserTagProps) { + if (!user) { + // Verified against PDF Page 1 : Lowercase 'a' + return Not assigned; + } + let profilePicUrl = "/userIcon.svg"; + if (user?.profilePictureURL) { + profilePicUrl = user.profilePictureURL; + } + return ( +
+ User Icon + {user.name} +
+ ); +} diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index cd1434b..482b2b5 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -8,3 +8,4 @@ export { TaskForm } from "./TaskForm"; export { TaskItem } from "./TaskItem"; export { TaskList } from "./TaskList"; export { TextField } from "./TextField"; +export { UserTag } from "./UserTag"; diff --git a/frontend/src/pages/TaskDetail.module.css b/frontend/src/pages/TaskDetail.module.css index b698b4f..5617cb9 100644 --- a/frontend/src/pages/TaskDetail.module.css +++ b/frontend/src/pages/TaskDetail.module.css @@ -52,8 +52,8 @@ .assingeeIcon { /* Removed margin-left: 2rem */ - width: 28px; /* Set width/height in CSS is often safer */ - height: 28px; + width: 32px; /* Set width/height in CSS is often safer */ + height: 32px; margin-right: 0.5rem; /* Small space between icon and name */ flex-shrink: 0; /* Prevents icon from squishing if name is long */ } diff --git a/frontend/src/pages/TaskDetail.tsx b/frontend/src/pages/TaskDetail.tsx index dcaaf8c..179de63 100644 --- a/frontend/src/pages/TaskDetail.tsx +++ b/frontend/src/pages/TaskDetail.tsx @@ -2,10 +2,9 @@ import { Dialog } from "@tritonse/tse-constellation"; import React, { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { getTask, type Task } from "src/api/tasks"; -import { Button, Page } from "src/components"; +import { Button, Page, TaskForm, UserTag } from "src/components"; import styles from "src/pages/TaskDetail.module.css"; -// Define the date formatter outside the component to avoid recreating it on every render const dateFormatter = new Intl.DateTimeFormat("en-US", { dateStyle: "full", timeStyle: "short", @@ -14,9 +13,9 @@ const dateFormatter = new Intl.DateTimeFormat("en-US", { export function TaskDetail() { const [task, setTask] = useState(null); const [errorModalMessage, setErrorModalMessage] = useState(null); + const [isEditing, setIsEditing] = useState(false); const { id } = useParams(); - // 1. Fetch the task data useEffect(() => { if (id) { getTask(id) @@ -31,7 +30,6 @@ export function TaskDetail() { } }, [id]); - // 2. Update the document title dynamically when the task loads useEffect(() => { if (task) { document.title = `${task.title} | TSE Todos`; @@ -46,51 +44,61 @@ export function TaskDetail() { ); } + // Matches PDF Page 6 logic: Show form when editing + if (isEditing) { + return ( + + { + setTask(updatedTask); + setIsEditing(false); + }} + /> + + ); + } + return ( - {/* Link to home*/}

Back to home

+ {/* Title Row Matches PDF Page 5 */}
- {/*Title of task */} {task.title} - - {/*Edit button*/}
-
- {/*Description*/}

{task.description ? task.description : "(No Description)"}

- {/*Assignee information */}
Assignee - User Icon - - User Name Very Long Very Long Very Long Very Long... - +
- {/*Status Information */}
Status {task.isChecked ? "Done" : "Not Done"}
- {/*Date Created Information */}
Date Created - {/* Use the formatter on the task.dateCreated object */} - {dateFormatter.format(task.dateCreated)} + {dateFormatter.format(new Date(task.dateCreated))}
From 35524de51a644e77c40ffe1a537d338b8504f1f3 Mon Sep 17 00:00:00 2001 From: rushil-bot Date: Wed, 31 Dec 2025 13:28:00 -0800 Subject: [PATCH 4/4] cleaned up extreneous comments --- backend/src/controllers/task.ts | 4 +- backend/src/controllers/tasks.ts | 1 - frontend/src/api/tasks.ts | 1 - frontend/src/api/users.ts | 1 - frontend/src/components/TaskForm.module.css | 2 +- frontend/src/components/TaskForm.tsx | 2 - frontend/src/components/TaskItem.module.css | 3 -- frontend/src/components/TaskItem.tsx | 1 - frontend/src/components/TaskList.module.css | 11 ++---- frontend/src/components/TaskList.tsx | 1 - frontend/src/components/UserTag.module.css | 1 - frontend/src/components/UserTag.tsx | 1 - frontend/src/pages/TaskDetail.module.css | 44 +++++++-------------- frontend/src/pages/TaskDetail.tsx | 1 - 14 files changed, 22 insertions(+), 52 deletions(-) diff --git a/backend/src/controllers/task.ts b/backend/src/controllers/task.ts index 3775278..147a385 100644 --- a/backend/src/controllers/task.ts +++ b/backend/src/controllers/task.ts @@ -25,14 +25,14 @@ type CreateTaskBody = { title: string; description?: string; isChecked?: boolean; - assignee?: string; // Added assignee + assignee?: string; }; type UpdateTaskBody = { title: string; description?: string; isChecked?: boolean; - assignee?: string; // Added assignee + assignee?: string; }; export const createTask: RequestHandler = async (req, res, next) => { diff --git a/backend/src/controllers/tasks.ts b/backend/src/controllers/tasks.ts index 3a4e548..0aac1d3 100644 --- a/backend/src/controllers/tasks.ts +++ b/backend/src/controllers/tasks.ts @@ -4,7 +4,6 @@ import type { RequestHandler } from "express"; export const getAllTasks: RequestHandler = async (req, res, next) => { try { - // your code here const sortedTasks = await TaskModel.find().populate("assignee").sort({ dateCreated: "desc" }); res.status(200).json(sortedTasks); diff --git a/frontend/src/api/tasks.ts b/frontend/src/api/tasks.ts index 1524b8f..5027da4 100644 --- a/frontend/src/api/tasks.ts +++ b/frontend/src/api/tasks.ts @@ -103,7 +103,6 @@ export async function getTask(id: string): Promise> { export async function getAllTasks(): Promise> { try { - // your code here const response = await get(`/api/tasks`); const json = (await response.json()) as TaskJSON[]; diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index e006843..ce5ef60 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -59,7 +59,6 @@ export async function getUser(id: string): Promise> { export async function getAllTasks(): Promise> { try { - // your code here const response = await get(`/api/user`); const json = (await response.json()) as User[]; diff --git a/frontend/src/components/TaskForm.module.css b/frontend/src/components/TaskForm.module.css index f1a21b6..a389ef9 100644 --- a/frontend/src/components/TaskForm.module.css +++ b/frontend/src/components/TaskForm.module.css @@ -15,7 +15,7 @@ justify-content: flex-start; column-gap: 2rem; align-items: flex-end; - margin-bottom: 1rem; /* Added spacing between rows */ + margin-bottom: 1rem; } .textField { diff --git a/frontend/src/components/TaskForm.tsx b/frontend/src/components/TaskForm.tsx index 6b09b11..a6f1a31 100644 --- a/frontend/src/components/TaskForm.tsx +++ b/frontend/src/components/TaskForm.tsx @@ -71,7 +71,6 @@ export function TaskForm({ mode, task, onSubmit }: TaskFormProps) { {formTitle} - {/* Row 1: Title & Description - Matches PDF Page 6 Top Row */}
- {/* Row 2: Assignee & Save - Matches PDF Page 6 Bottom Row */}
- {/* Wrapper to extend the border across both text and user tag */}
diff --git a/frontend/src/components/TaskList.module.css b/frontend/src/components/TaskList.module.css index 28f7782..52739a0 100644 --- a/frontend/src/components/TaskList.module.css +++ b/frontend/src/components/TaskList.module.css @@ -7,27 +7,25 @@ font: var(--font-heading); font-size: 1.5rem; display: block; - margin-bottom: 1rem; /* Added for spacing between title and list */ + margin-bottom: 1rem; } -/* 3. Inner Item Container: Flexbox, column, stretched horizontally */ .itemContainer { width: 100%; display: flex; flex-direction: column; align-items: stretch; - gap: 0.5rem; /* Optional: adds nice spacing between tasks */ + gap: 0.5rem; overflow: hidden; } .userTagContainer { - width: 12rem; /* Fixed width as suggested in walkthrough */ + width: 12rem; flex-shrink: 0; display: flex; - justify-content: flex-end; /* Align the tag to the right within this container */ + justify-content: flex-end; } -/* Ensure .item stays consistent */ .item { height: 3rem; display: flex; @@ -39,7 +37,6 @@ transition: background-color 0.2s; } -/* 4. Error Modal Text: Blue color override */ .errorModalText { color: var(--color-tse-navy-blue); } diff --git a/frontend/src/components/TaskList.tsx b/frontend/src/components/TaskList.tsx index 55b11ff..4a70fd7 100644 --- a/frontend/src/components/TaskList.tsx +++ b/frontend/src/components/TaskList.tsx @@ -44,7 +44,6 @@ export function TaskList({ title }: TaskListProps) { styleVersion="styled" variant="error" title="An error occurred" - // Override the text color so it doesn't show white text on a white background content={

{errorModalMessage}

} isOpen={errorModalMessage !== null} onClose={() => setErrorModalMessage(null)} diff --git a/frontend/src/components/UserTag.module.css b/frontend/src/components/UserTag.module.css index 86487b5..2d38308 100644 --- a/frontend/src/components/UserTag.module.css +++ b/frontend/src/components/UserTag.module.css @@ -20,7 +20,6 @@ .assingeeItem { height: 2rem; - /* Removed margin-top: 2rem; <-- This was causing the misalignment */ display: flex; flex-direction: row; align-items: center; diff --git a/frontend/src/components/UserTag.tsx b/frontend/src/components/UserTag.tsx index a561b9c..0344f1d 100644 --- a/frontend/src/components/UserTag.tsx +++ b/frontend/src/components/UserTag.tsx @@ -8,7 +8,6 @@ export type UserTagProps = { export function UserTag({ user }: UserTagProps) { if (!user) { - // Verified against PDF Page 1 : Lowercase 'a' return Not assigned; } let profilePicUrl = "/userIcon.svg"; diff --git a/frontend/src/pages/TaskDetail.module.css b/frontend/src/pages/TaskDetail.module.css index 5617cb9..51d76f7 100644 --- a/frontend/src/pages/TaskDetail.module.css +++ b/frontend/src/pages/TaskDetail.module.css @@ -4,7 +4,6 @@ .taskTitle { font: var(--font-heading); - /* justify-content is for flex containers, usually not needed on the span itself unless it is flex */ } .item { @@ -22,81 +21,68 @@ .description { font: var(--font-body); - /* margin-top: -1rem; <-- Careful with negative margins, they can cause overlap. - If you want less space, adjust the margin-top of the parent container (.item) instead. */ word-break: break-all; white-space: normal; } .textContainer span { - /* This might conflict with long user names if you want them to wrap. - If you want ellipsis (...) keep this, otherwise remove for full text. */ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -/* Renamed from .assingeeTask to .assingeeLabel for clarity */ .assingeeLabel { font: var(--font-label); - margin-right: 1rem; /* Adjust this to control space between "Assignee" and the Icon */ + margin-right: 1rem; } .assingeeName { font: var(--font-body); - /* Ensure the name truncates if it's too long */ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .assingeeIcon { - /* Removed margin-left: 2rem */ - width: 32px; /* Set width/height in CSS is often safer */ + width: 32px; height: 32px; - margin-right: 0.5rem; /* Small space between icon and name */ - flex-shrink: 0; /* Prevents icon from squishing if name is long */ + margin-right: 0.5rem; + flex-shrink: 0; } .assingeeItem { height: 2rem; margin-top: 2rem; - display: flex; /* Fixes alignment issues */ + display: flex; flex-direction: row; - align-items: center; /* Vertically centers the text and icon */ - justify-content: flex-start; /* Aligns everything to the left */ - /* column-gap can also be used here instead of margins on children */ - /* column-gap: 0.5rem; */ + align-items: center; + justify-content: flex-start; } .statusItem { height: 2rem; margin-top: 1rem; - display: flex; /* Fixes alignment issues */ + display: flex; flex-direction: row; - align-items: center; /* Vertically centers the text and icon */ - justify-content: flex-start; /* Aligns everything to the left */ - /* column-gap can also be used here instead of margins on children */ - /* column-gap: 0.5rem; */ + align-items: center; + justify-content: flex-start; } .statusLabel { font: var(--font-label); - margin-right: 1rem; /* Adjust this to control space between "Assignee" and the Icon */ + margin-right: 1rem; } .dateItem { height: 2rem; margin-top: 1rem; - display: flex; /* Fixes alignment issues */ + display: flex; flex-direction: row; - align-items: center; /* Vertically centers the text and icon */ - justify-content: flex-start; /* Aligns everything to the left */ - /* column-gap can also be used here instead of margins on children */ - /* column-gap: 0.5rem; */ + align-items: center; + justify-content: flex-start; } .dateLabel { font: var(--font-label); - margin-right: 1rem; /* Adjust this to control space between "Assignee" and the Icon */ + margin-right: 1rem; } diff --git a/frontend/src/pages/TaskDetail.tsx b/frontend/src/pages/TaskDetail.tsx index 179de63..c8a4657 100644 --- a/frontend/src/pages/TaskDetail.tsx +++ b/frontend/src/pages/TaskDetail.tsx @@ -44,7 +44,6 @@ export function TaskDetail() { ); } - // Matches PDF Page 6 logic: Show form when editing if (isEditing) { return (