From 2daf198ce1a48148eaf16d0a88ee01688e1c35dd Mon Sep 17 00:00:00 2001 From: rhys-childs Date: Tue, 23 Jul 2024 12:51:00 -0600 Subject: [PATCH] Add users and subtasks --- src/App.css | 58 +++++++++++++++++++-- src/App.jsx | 26 ++++++++-- src/components/FilterButtons.jsx | 15 ++++-- src/components/TaskForm.jsx | 16 +++++- src/components/TaskItem.jsx | 88 +++++++++++++++++++++++++++----- src/components/TaskItem.test.jsx | 23 +++++++++ src/components/TaskList.jsx | 13 +++-- src/components/UserFilter.jsx | 17 ++++++ 8 files changed, 222 insertions(+), 34 deletions(-) create mode 100644 src/components/TaskItem.test.jsx create mode 100644 src/components/UserFilter.jsx diff --git a/src/App.css b/src/App.css index 6798b71..10e7da8 100644 --- a/src/App.css +++ b/src/App.css @@ -27,6 +27,13 @@ input { margin-right: 10px; } +select { + padding: 10px; + border: 1px solid #ccc; + border-radius: 4px; + margin-right: 10px; +} + button { padding: 10px 20px; border: none; @@ -58,7 +65,6 @@ li { li span { flex: 1; - margin-right: 10px; } li span.completed { @@ -68,16 +74,39 @@ li span.completed { .filter-buttons { display: flex; - justify-content: center; align-items: center; + justify-content: space-between; +} + +.status-filter { + display: flex; gap: 10px; } +.user-filter { + display: flex; + align-items: center; + margin-left: 20px; +} + +.user-filter label { + margin-right: 10px; +} + .task-item { display: flex; - flex-direction: row; - justify-content: center; + flex-direction: column; +} + +.task-item-header { + display: flex; align-items: center; + justify-content: space-between; + cursor: pointer; +} + +.task-item-header .caret { + margin-left: 5px; } .task-item-buttons { @@ -86,3 +115,24 @@ li span.completed { margin-left: auto; align-items: center; } + +.subtasks { + padding-left: 10px; + margin-top: 10px; +} + +.subtask-item { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 5px; + flex-direction: row; +} + +.subtask-input-container { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; + flex-direction: row; +} diff --git a/src/App.jsx b/src/App.jsx index 9144f05..259b681 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -11,35 +11,53 @@ function App() { name: "Complete TPS reports", completed: false, addedBy: { id: 1, name: "Kevin" }, + subtasks: [], }, { name: "Reconcile Dunder Mifflin accounts", completed: false, addedBy: { id: 2, name: "Angela" }, + subtasks: [], }, { name: "Review Peter's timesheet", completed: true, - addedBy: { id: 1, name: "Oscar" }, + addedBy: { id: 3, name: "Oscar" }, + subtasks: [], }, { name: "Party Planning Committee expense reports", completed: false, addedBy: { id: 2, name: "Angela" }, + subtasks: [], }, ]); const [filter, setFilter] = useState("all"); + const [userFilter, setUserFilter] = useState("all"); + + const users = tasks.reduce((users, task) => { + return users.includes(task.addedBy) ? users : [...users, task.addedBy]; + }, []); return (
- +
- - + +
diff --git a/src/components/FilterButtons.jsx b/src/components/FilterButtons.jsx index c69ebaa..365fff6 100644 --- a/src/components/FilterButtons.jsx +++ b/src/components/FilterButtons.jsx @@ -1,11 +1,18 @@ import React from "react"; +import UserFilter from "./UserFilter"; -function FilterButtons({ setFilter }) { +function FilterButtons({ setFilter, setUserFilter, users }) { return (
- - - +
+ + + +
+ setUserFilter(e.target.value)} + users={users} + />
); } diff --git a/src/components/TaskForm.jsx b/src/components/TaskForm.jsx index bde6a86..2c79459 100644 --- a/src/components/TaskForm.jsx +++ b/src/components/TaskForm.jsx @@ -1,19 +1,31 @@ import React, { useState } from "react"; -function TaskForm({ setTasks }) { +function TaskForm({ setTasks, users }) { const [taskName, setTaskName] = useState(""); + const [selectedUser, setSelectedUser] = useState(users[0].id); const handleSubmit = (e) => { e.preventDefault(); + const user = users.find((user) => user.id === selectedUser); setTasks((prevTasks) => [ ...prevTasks, - { name: taskName, completed: false }, + { name: taskName, completed: false, subtasks: [], addedBy: user }, ]); setTaskName(""); }; return (
+ { + const [showSubtasks, setShowSubtasks] = useState(false); + const [newSubtask, setNewSubtask] = useState(""); + const taskItemRef = useRef(null); + + const toggeCompletion = () => { setTasks((prevTasks) => prevTasks.map((t, i) => - i === index ? { ...t, completed: !t.completed } : t + i === index ? { ...t, completed: !t.complted } : t ) ); }; @@ -13,17 +17,75 @@ function TaskItem({ task, index, setTasks }) { setTasks((prevTasks) => prevTasks.filter((_, i) => i !== index)); }; + const addSubtask = () => { + const newSubtaskObject = { + name: newSubtask, + completed: false, + subtasks: [], + addedBy: task.addedBy, + }; + task.subtasks.push(newSubtaskObject); + setTasks((prevTasks) => + prevTasks.map((t, i) => + i === index ? { ...t, subtasks: task.subtasks } : t + ) + ); + setNewSubtask(""); + }; + + const deleteSubtask = (subtaskIndex) => { + const updatedSubtasks = task.subtasks.filter((_, i) => i !== subtaskIndex); + setTasks((prevTasks) => + prevTasks.map((t, i) => + i === index ? { ...t, subtasks: updatedSubtasks } : t + ) + ); + }; + + useEffect(() => { + const currentRef = taskItemRef.current; + currentRef.addEventListener("click", () => setShowSubtasks(!showSubtasks)); + }, []); + return ( -
  • -
    - {task.name} -
    -
    - - -
    +
  • + + + {task.name} (Added by: {task.addedBy.name}) + +
    + + +
    {showSubtasks ? "▼" : "▶"}
    +
    +
    + {showSubtasks && ( +
      +
      + setNewSubtask(e.target.value)} + placeholder="Enter subtask" + onKeyDown={(e) => { + if (e.key === "Enter") { + addSubtask(); + } + }} + /> + +
      + {task.subtasks && + task.subtasks.map((subtask, subIndex) => ( +
    • + {subtask.name} + +
    • + ))} +
    + )}
  • ); } diff --git a/src/components/TaskItem.test.jsx b/src/components/TaskItem.test.jsx new file mode 100644 index 0000000..6db42da --- /dev/null +++ b/src/components/TaskItem.test.jsx @@ -0,0 +1,23 @@ +import React from "react"; +import { render, fireEvent } from "@testing-library/react"; +import TaskItem from "./TaskItem"; + +test("it should add a new subtask", () => { + const { getByPlaceholderText, getByText } = render( + {}} + /> + ); + + const taskItem = getByText("▶"); + fireEvent.click(taskItem); + const input = getByPlaceholderText("Enter subtask"); + fireEvent.change(input, { target: { value: "New Subtask" } }); + fireEvent.keyDown(input, { key: "Enter", code: "Enter" }); +}); diff --git a/src/components/TaskList.jsx b/src/components/TaskList.jsx index 3a9c0ae..29b1c14 100644 --- a/src/components/TaskList.jsx +++ b/src/components/TaskList.jsx @@ -1,13 +1,12 @@ import React from "react"; import TaskItem from "./TaskItem"; -function TaskList({ tasks, filter, setTasks }) { - const filteredTasks = tasks.filter((task) => - filter === "all" - ? true - : filter === "completed" - ? task.completed - : !task.completed +function TaskList({ tasks, filter, userFilter, setTasks }) { + const filteredTasks = tasks.filter( + (task) => + (filter === "all" || + (filter === "completed" ? task.completed : !task.completed)) && + (userFilter === "all" || task.addedBy.id === parseInt(userFilter, 10)) ); return ( diff --git a/src/components/UserFilter.jsx b/src/components/UserFilter.jsx new file mode 100644 index 0000000..d405000 --- /dev/null +++ b/src/components/UserFilter.jsx @@ -0,0 +1,17 @@ +import React from "react"; + +function UserFilter({ setUserFilter, users }) { + return ( +
    + + +
    + ); +} + +export default UserFilter;