From 0c680373cb7c7436f9c8428ace3a79ee4b2061b8 Mon Sep 17 00:00:00 2001 From: lubnaAr Date: Wed, 15 Jan 2025 17:00:44 +0530 Subject: [PATCH 1/2] Shifting the solution to another branch --- src/app/components/CreateTask.tsx | 89 +++++++++++++++ src/app/components/DeleteTask.tsx | 40 +++++++ src/app/components/EditTask.tsx | 78 +++++++++++++ src/app/components/TaskContext.tsx | 45 ++++++++ src/app/components/TaskList.tsx | 173 +++++++++++++++++++++++++++++ src/app/index.tsx | 17 ++- 6 files changed, 438 insertions(+), 4 deletions(-) create mode 100644 src/app/components/CreateTask.tsx create mode 100644 src/app/components/DeleteTask.tsx create mode 100644 src/app/components/EditTask.tsx create mode 100644 src/app/components/TaskContext.tsx create mode 100644 src/app/components/TaskList.tsx diff --git a/src/app/components/CreateTask.tsx b/src/app/components/CreateTask.tsx new file mode 100644 index 0000000..c336fa0 --- /dev/null +++ b/src/app/components/CreateTask.tsx @@ -0,0 +1,89 @@ +import React, { useState, useContext } from 'react'; +import { Button, TextField, Dialog, DialogActions, DialogContent, DialogTitle, Box } from '@mui/material'; +import { Task } from '../types'; +import TaskContext from './TaskContext'; + + + +const CreateTask = () => { + + const { addTask, tasks } = useContext(TaskContext); + const [open, setOpen] = useState(false); + const [newTask, setNewTask] = useState(''); + const [newTaskPrioirty, setNewTaskPriority] = useState<'High'|'Medium'|'Low'>('Low'); + const [error, setError ] = useState(false); + + const handleToggleModal = () => { + if(open){ + setNewTask(''); + setNewTaskPriority('Low'); + setError(false); + } + setOpen(!open); + } + const handleSave = () => { + if (newTask.trim()) { + const task = { + id: Date.now(), + name: newTask, + priority: newTaskPrioirty, + state: 'To Do', + }; + console.log("Adding task to context:", task); + addTask(task); + console.log("Current tasks in context after addition:", tasks); + setNewTask(''); + setNewTaskPriority('Low'); + setOpen(false); + } else { + setError(true); + } + }; + + const handleChange = ( e: React.ChangeEvent) => { + if(error) setError(false); + setNewTask(e.target.value); + } + + return( + + + + Add a new task + + + setNewTaskPriority(e.target.value)} + SelectProps={{ + native: true, + }} + > + + + + + + + + + + + + ) +} + +export default CreateTask; \ No newline at end of file diff --git a/src/app/components/DeleteTask.tsx b/src/app/components/DeleteTask.tsx new file mode 100644 index 0000000..60b1684 --- /dev/null +++ b/src/app/components/DeleteTask.tsx @@ -0,0 +1,40 @@ +import React, { useContext } from 'react'; +import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button } from '@mui/material'; +import TaskContext from './TaskContext'; + +interface DeleteTaskProps { + taskId: number; + taskName: string; + open: boolean; + onClose: () => void; +} + +const DeleteTask: React.FC = ({ taskId, taskName, open, onClose }) => { + const { removeTask } = useContext(TaskContext); + + const handleConfirm = () => { + removeTask(taskId); + onClose(); + }; + + return ( + + Delete Task + + + Are you sure you want to delete the task {taskName}? + + + + + + + + ); +}; + +export default DeleteTask; \ No newline at end of file diff --git a/src/app/components/EditTask.tsx b/src/app/components/EditTask.tsx new file mode 100644 index 0000000..594fbfc --- /dev/null +++ b/src/app/components/EditTask.tsx @@ -0,0 +1,78 @@ +import React, { useState, useContext, useEffect } from 'react'; +import { Dialog, DialogActions, DialogContent, DialogTitle, Button, TextField } from '@mui/material'; +import TaskContext from './TaskContext'; + +interface EditTaskProps { + taskId: number; + taskName: string; + taskPriority: 'High' | 'Medium' | 'Low'; + open: boolean; + onClose: () => void; +} + +const EditTask: React.FC = ({ + taskId, + taskName, + taskPriority, + open, + onClose, +}) => { + const { updateTask } = useContext(TaskContext); + const [name, setName] = useState(taskName); + const [priority, setPriority] = useState(taskPriority); + + useEffect(() => { + setName(taskName); + setPriority(taskPriority); + }, [taskName, taskPriority]); + + const handleSave = () => { + updateTask({ + id: taskId, + name, + priority, + state: 'To Do', + }); + onClose(); + }; + + return ( + + Edit Task + + setName(e.target.value)} + /> + setPriority(e.target.value as 'High' | 'Medium' | 'Low')} + SelectProps={{ + native: true, + }} + > + + + + + + + + + + + ); +}; + +export default EditTask; \ No newline at end of file diff --git a/src/app/components/TaskContext.tsx b/src/app/components/TaskContext.tsx new file mode 100644 index 0000000..ee87b39 --- /dev/null +++ b/src/app/components/TaskContext.tsx @@ -0,0 +1,45 @@ +import React, { createContext, useEffect, useState } from 'react'; +import { Task } from '../types'; + + +const TaskContext = createContext({ + tasks: [], + addTask: (task: Task) => {}, + removeTask: (id: number) => {}, + updateTask: (task: Task) => {} +}); + + +export const TaskProvider = ({ children }) => { + const [tasks, setTasks] = useState(() => { + const savedTasks = localStorage.getItem('tasks'); + return savedTasks ? JSON.parse(savedTasks) : []; + }); + + useEffect(() => { + localStorage.setItem('tasks', JSON.stringify(tasks)); + }, [tasks]); + + const addTask = (task: Task) => { + console.log(" Task added",task); + setTasks([...tasks, task]); + }; + + const removeTask = (id: number) => { + setTasks(tasks.filter(task => task.id !== id)); + }; + + const updateTask = (updatedTask: Task) => { + setTasks((prevTasks) => + prevTasks.map((task) => (task.id === updatedTask.id ? updatedTask : task)) + ); + }; + + return ( + + {children} + + ); +}; + +export default TaskContext; \ No newline at end of file diff --git a/src/app/components/TaskList.tsx b/src/app/components/TaskList.tsx new file mode 100644 index 0000000..8119364 --- /dev/null +++ b/src/app/components/TaskList.tsx @@ -0,0 +1,173 @@ +import React, { useContext, useState } from 'react'; +import { Task } from '../types'; +import { Button, List, ListItem, ListItemText, Box, Typography, FormControl, InputLabel, Select, MenuItem } from '@mui/material'; +import TaskContext from './TaskContext'; +import DeleteTask from './DeleteTask'; +import EditTask from './EditTask'; + +const TaskList = () => { + const { tasks, updateTask, removeTask } = useContext(TaskContext); + + const [stateFilter, setStateFilter] = useState<'All' | 'To Do' | 'In Progress' | 'Done'>('All'); + const [priorityFilter, setPriorityFilter] = useState<'All' | 'High' | 'Medium' | 'Low'>('All'); + + const [deleteTask, setDeleteTask] = useState<{ id: number; name: string } | null>(null); + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const [editTask, setEditTask] = useState<{ id: number; name: string; priority: 'High' | 'Medium' | 'Low' } | null>(null); + const [editModalOpen, setEditModalOpen] = useState(false); + + const getNextState = (currentState: Task['state']): Task['state'] => { + if (currentState === 'To Do') return 'In Progress'; + if (currentState === 'In Progress') return 'Done'; + return 'To Do'; + }; + + const handleStateChange = (task: Task) => { + const nextState = getNextState(task.state); + if (nextState === 'Done') { + removeTask(task.id); + } else { + updateTask({ ...task, state: nextState }); + } + }; + + const handleOpenEditModal = (task: Task) => { + setEditTask(task); + setEditModalOpen(true); + }; + + const handleCloseEditModal = () => { + setEditTask(null); + setEditModalOpen(false); + }; + + const handleOpenDeleteModal = (taskId: number, taskName: string) => { + setDeleteTask({ id: taskId, name: taskName }); + setDeleteModalOpen(true); + }; + + const handleCloseDeleteModal = () => { + setDeleteTask(null); + setDeleteModalOpen(false); + }; + + const filteredAndSortedTasks = [...tasks] + .filter((task) => { + if (stateFilter !== 'All') return task.state === stateFilter; + return task.state !== 'Done'; + }) + .filter((task) => { + if (priorityFilter !== 'All') return task.priority === priorityFilter; + return true; + }) + .sort((a, b) => { + const priorityOrder = { High: 3, Medium: 2, Low: 1 }; + return priorityOrder[b.priority] - priorityOrder[a.priority]; + }); + + if (filteredAndSortedTasks.length === 0) { + return ( + + No tasks available! + + ); + } + + return ( + + + + State + + + + + Priority + + + + + + {filteredAndSortedTasks.map((task) => ( + + + + + + + + + ))} + + + {editTask && ( + + )} + {deleteTask && ( + + )} + + ); +}; + +export default TaskList; \ No newline at end of file diff --git a/src/app/index.tsx b/src/app/index.tsx index 9c7ba6c..e73ee3d 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -1,10 +1,19 @@ -import { Container, Card, CssBaseline, Typography } from '@mui/material'; +import { Container, CssBaseline } from '@mui/material'; import { grey } from '@mui/material/colors'; +import TaskList from './components/TaskList'; +import CreateTask from './components/CreateTask'; +import { TaskProvider } from './components/TaskContext'; + + + function App() { + + return ( <> + - - Evermore Coding Challenge - + < CreateTask /> + + ); } From 69e21009a0e5abf41fd4e7e9279e9cd41e8fb8be Mon Sep 17 00:00:00 2001 From: lubnaAr Date: Wed, 15 Jan 2025 17:54:58 +0530 Subject: [PATCH 2/2] [Refactor] moving filter logic and displaying action buttons into smaller components --- package-lock.json | 57 +++++++++++++----------- package.json | 6 +-- src/app/components/TaskActions.tsx | 47 ++++++++++++++++++++ src/app/components/TaskFilter.tsx | 51 ++++++++++++++++++++++ src/app/components/TaskList.tsx | 69 +++++++----------------------- src/types.ts | 6 +++ 6 files changed, 154 insertions(+), 82 deletions(-) create mode 100644 src/app/components/TaskActions.tsx create mode 100644 src/app/components/TaskFilter.tsx create mode 100644 src/types.ts diff --git a/package-lock.json b/package-lock.json index 4f3911a..2d1e5da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,12 @@ "dependencies": { "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", - "@mui/icons-material": "^5.16.7", + "@mui/icons-material": "^5.16.14", "@mui/material": "^5.16.7", - "@reduxjs/toolkit": "^2.2.7", + "@reduxjs/toolkit": "^2.5.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-redux": "^9.1.2", + "react-redux": "^9.2.0", "react-router-dom": "^6.26.1" }, "devDependencies": { @@ -1035,9 +1035,10 @@ } }, "node_modules/@mui/icons-material": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.7.tgz", - "integrity": "sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q==", + "version": "5.16.14", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.14.tgz", + "integrity": "sha512-heL4S+EawrP61xMXBm59QH6HODsu0gxtZi5JtnXF2r+rghzyU/3Uftlt1ij8rmJh+cFdKTQug1L9KkZB5JgpMQ==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9" }, @@ -1050,8 +1051,8 @@ }, "peerDependencies": { "@mui/material": "^5.0.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -1286,9 +1287,10 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.7.tgz", - "integrity": "sha512-faI3cZbSdFb8yv9dhDTmGwclW0vk0z5o1cia+kf7gCbaCwHI5e+7tP57mJUv22pNcNbeA62GSrPpfrUfdXcQ6g==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz", + "integrity": "sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==", + "license": "MIT", "dependencies": { "immer": "^10.0.3", "redux": "^5.0.1", @@ -1296,7 +1298,7 @@ "reselect": "^5.1.0" }, "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18", + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "peerDependenciesMeta": { @@ -1608,9 +1610,10 @@ } }, "node_modules/@types/use-sync-external-store": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", - "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.2.0", @@ -3255,16 +3258,17 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, "node_modules/react-redux": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", - "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", "dependencies": { - "@types/use-sync-external-store": "^0.0.3", - "use-sync-external-store": "^1.0.0" + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" }, "peerDependencies": { - "@types/react": "^18.2.25", - "react": "^18.0", + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", "redux": "^5.0.0" }, "peerDependenciesMeta": { @@ -3686,11 +3690,12 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", - "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/vite": { diff --git a/package.json b/package.json index 9d47081..5413298 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,12 @@ "dependencies": { "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", - "@mui/icons-material": "^5.16.7", + "@mui/icons-material": "^5.16.14", "@mui/material": "^5.16.7", - "@reduxjs/toolkit": "^2.2.7", + "@reduxjs/toolkit": "^2.5.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-redux": "^9.1.2", + "react-redux": "^9.2.0", "react-router-dom": "^6.26.1" }, "devDependencies": { diff --git a/src/app/components/TaskActions.tsx b/src/app/components/TaskActions.tsx new file mode 100644 index 0000000..3eb57e6 --- /dev/null +++ b/src/app/components/TaskActions.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { Task } from '../../types'; +import { Box, Button } from '@mui/material'; + +interface TaskActionsProps { + task: Task; + onStateChange: (task: Task) => void; + onEdit: (task: Task) => void; + onDelete: (taskId: number, taskName: string) => void; + getNextState: (currentState: Task['state']) => Task['state']; +} + +const TaskActions: React.FC = ({ + task, + onStateChange, + onEdit, + onDelete, + getNextState, +}) => { + return ( + + + + + + ); +}; + +export default TaskActions; \ No newline at end of file diff --git a/src/app/components/TaskFilter.tsx b/src/app/components/TaskFilter.tsx new file mode 100644 index 0000000..286d3c7 --- /dev/null +++ b/src/app/components/TaskFilter.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Box, FormControl, InputLabel, MenuItem, Select } from '@mui/material'; + +interface TaskFilterProps { + stateFilter: 'All' | 'To Do' | 'In Progress' | 'Done'; + priorityFilter: 'All' | 'High' | 'Medium' | 'Low'; + setStateFilter: (value: 'All' | 'To Do' | 'In Progress' | 'Done') => void; + setPriorityFilter: (value: 'All' | 'High' | 'Medium' | 'Low') => void; +} + +const TaskFilters: React.FC = ({ + stateFilter, + priorityFilter, + setStateFilter, + setPriorityFilter, +}) => { + return ( + + + State + + + + + Priority + + + + + ) +} + +export default TaskFilters; \ No newline at end of file diff --git a/src/app/components/TaskList.tsx b/src/app/components/TaskList.tsx index 8119364..875d886 100644 --- a/src/app/components/TaskList.tsx +++ b/src/app/components/TaskList.tsx @@ -1,9 +1,11 @@ import React, { useContext, useState } from 'react'; -import { Task } from '../types'; +import { Task } from '../../types'; import { Button, List, ListItem, ListItemText, Box, Typography, FormControl, InputLabel, Select, MenuItem } from '@mui/material'; import TaskContext from './TaskContext'; import DeleteTask from './DeleteTask'; import EditTask from './EditTask'; +import TaskFilters from './TaskFilter'; +import TaskActions from './TaskActions'; const TaskList = () => { const { tasks, updateTask, removeTask } = useContext(TaskContext); @@ -75,35 +77,12 @@ const TaskList = () => { return ( - - - State - - - - - Priority - - - + {filteredAndSortedTasks.map((task) => ( @@ -122,29 +101,13 @@ const TaskList = () => { primary={task.name} secondary={`Priority: ${task.priority}, State: ${task.state}`} /> - - - - - + < TaskActions + task={task} + onStateChange={handleStateChange} + onEdit={handleOpenEditModal} + onDelete={handleOpenDeleteModal} + getNextState={getNextState} + /> ))} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..20bb7a8 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,6 @@ +export interface Task{ + id: number; + name: string; + priority: 'High' | 'Medium' | 'Low'; + state: 'To Do' | 'In Progress' | 'Done'; +} \ No newline at end of file