diff --git a/package.json b/package.json index 4224c57..610ef7b 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@mui/material": "^7.1.1", + "@mui/x-date-pickers": "^8.5.3", + "dayjs": "^1.11.13", "react": "^19.1.0", "react-dom": "^19.1.0", "react-hook-form": "^7.58.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dbeb87c..d000259 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,12 @@ importers: '@mui/material': specifier: ^7.1.1 version: 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/x-date-pickers': + specifier: ^8.5.3 + version: 8.5.3(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + dayjs: + specifier: ^1.11.13 + version: 1.11.13 react: specifier: ^19.1.0 version: 19.1.0 @@ -471,6 +477,50 @@ packages: '@types/react': optional: true + '@mui/x-date-pickers@8.5.3': + resolution: {integrity: sha512-rToJOgIfVN/mG8QPX5RSPcP7QdnNN4yk+37fBztVUnmR7mhiWdUNSHlazGpnRGRqDdr4lXRkJbTjtJLviWkhKg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.15.14 || ^6.0.0 || ^7.0.0 + '@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0 + date-fns: ^2.25.0 || ^3.2.0 || ^4.0.0 + date-fns-jalali: ^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0 + dayjs: ^1.10.7 + luxon: ^3.0.2 + moment: ^2.29.4 + moment-hijri: ^2.1.2 || ^3.0.0 + moment-jalaali: ^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + date-fns: + optional: true + date-fns-jalali: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + moment-hijri: + optional: true + moment-jalaali: + optional: true + + '@mui/x-internals@8.5.3': + resolution: {integrity: sha512-ImCg4E3DT3XoDIZO0pNCbB7iw14N+YCFY3J1V28POwCD7P2f3HSIz4jwzM006oYxI6bqeE6LMfpdPRDW6s6dQw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -833,6 +883,9 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -1222,6 +1275,9 @@ packages: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1746,6 +1802,36 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 + '@mui/x-date-pickers@8.5.3(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@mui/material': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/system': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) + '@mui/utils': 7.1.1(@types/react@19.1.8)(react@19.1.0) + '@mui/x-internals': 8.5.3(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) + '@types/react-transition-group': 4.4.12(@types/react@19.1.8) + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-transition-group: 4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.1.8)(react@19.1.0) + '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) + dayjs: 1.11.13 + transitivePeerDependencies: + - '@types/react' + + '@mui/x-internals@8.5.3(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@mui/system': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) + '@mui/utils': 7.1.1(@types/react@19.1.8)(react@19.1.0) + react: 19.1.0 + reselect: 5.1.1 + transitivePeerDependencies: + - '@types/react' + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2073,6 +2159,8 @@ snapshots: csstype@3.1.3: {} + dayjs@1.11.13: {} + debug@4.4.1: dependencies: ms: 2.1.3 @@ -2453,6 +2541,8 @@ snapshots: react@19.1.0: {} + reselect@5.1.1: {} + resolve-from@4.0.0: {} resolve@1.22.10: diff --git a/src/App.css b/src/App.css index b9d355d..d619ea8 100644 --- a/src/App.css +++ b/src/App.css @@ -1,42 +1,8 @@ #root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; + margin: 0 auto; + text-align: center; } -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; +body { + background-color: #f0f0f0; } diff --git a/src/App.tsx b/src/App.tsx index 153acad..161ed1e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,23 +1,29 @@ -import "./App.css"; +import { LocalizationProvider } from "@mui/x-date-pickers"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { BrowserRouter, Route, Routes } from "react-router-dom"; -import MainPage from "./pages/MainPage"; -import TaskPage from "./pages/TaskPage"; -import NotFound from "./pages/NotFound"; -import { UserContextProvider } from "./contexts/UserContextProvider"; + +import "./App.css"; + import { TaskContextProvider } from "./contexts/TaskContextProvider"; +import { UserContextProvider } from "./contexts/UserContextProvider"; +import MainPage from "./pages/MainPage"; +import NotFound from "./pages/NotFound"; +import TaskPage from "./pages/TaskPage"; function App() { return ( - - - - } /> - } /> - } /> - - - + + + + + } /> + } /> + } /> + + + + ); } diff --git a/src/components/Task.tsx b/src/components/Task.tsx index abd1050..46b5147 100644 --- a/src/components/Task.tsx +++ b/src/components/Task.tsx @@ -3,6 +3,7 @@ import type { Task } from "../utils/types"; import { UserContext } from "../contexts/userContext"; import { useContext } from "react"; import { useNavigate } from "react-router-dom"; +import { Grid, ListItem, Typography } from "@mui/material"; type TaskComponentProps = { task: Task; @@ -22,11 +23,31 @@ const TaskComponent: React.FC = ({ task }) => { } return ( -
navigate(`/task/${task.id}`)}> - {task.text} - Assigned to: {assignedToUser} - -
+ navigate(`/task/${task.id}`)} + > + + + + {task.text} + + + + + Assigned to: {assignedToUser} + + + + + Due: {task.due} + + + + + + + ); }; diff --git a/src/components/TaskForm.tsx b/src/components/TaskForm.tsx index b8ecc1b..eb2a3a2 100644 --- a/src/components/TaskForm.tsx +++ b/src/components/TaskForm.tsx @@ -1,21 +1,34 @@ import React from "react"; -import { useForm } from "react-hook-form"; +import { Controller, useForm } from "react-hook-form"; import type { Task } from "../utils/types"; import { useRef } from "react"; import { useContext } from "react"; import { UserContext } from "../contexts/userContext"; import { TaskContext } from "../contexts/taskContext"; +import { + Autocomplete, + Button, + Card, + CardHeader, + TextField, +} from "@mui/material"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import { validateDueDate } from "../utils/validation"; +import type { Dayjs } from "dayjs"; type FormValues = { text: string; assignedTo: number; + dueDate: Dayjs; }; const TaskForm: React.FC = () => { const { register, handleSubmit, - // formState: { errors: formErrors } + control, + reset, + formState: { errors: formErrors }, } = useForm(); const { users, loading, error } = useContext(UserContext); @@ -26,17 +39,18 @@ const TaskForm: React.FC = () => { function addTask(data: FormValues) { const currentIdMaxId = Math.max(...tasks.map((task) => task.id)) + 1; const newId = Math.max(currentIdMaxId, 1); - console.log("NEW ID: ", newId); const newTask: Task = { id: newId, assignedTo: Number(data.assignedTo), + due: data.dueDate.format("YYYY-MM-DD"), text: data.text, createdAt: creationDate, finished: false, }; setTasks((prev) => [...prev, newTask]); + reset(); } // TODO make a skeleton @@ -46,17 +60,75 @@ const TaskForm: React.FC = () => { if (users == null) return
{error}
; return ( -
- - - -
+ + +
+ + + ( + ({ + label: user.name, + id: user.id, + }))} + getOptionLabel={(option) => option.label} + onChange={(_, data) => field.onChange(data?.id || "")} + renderInput={(params) => ( + + )} + /> + )} + /> + ( + field.onChange(date)} + slotProps={{ + textField: { + fullWidth: true, + error: !!formErrors.dueDate, + helperText: formErrors.dueDate?.message, + }, + }} + /> + )} + /> + + + +
); }; diff --git a/src/components/TaskList.tsx b/src/components/TaskList.tsx index 2146c53..1d0144d 100644 --- a/src/components/TaskList.tsx +++ b/src/components/TaskList.tsx @@ -1,17 +1,26 @@ import React, { useContext } from "react"; import TaskComponent from "./Task"; import { TaskContext } from "../contexts/taskContext"; +import { Card, List } from "@mui/material"; const TaskList: React.FC = () => { const { tasks } = useContext(TaskContext); - if (tasks.length === 0) return
No tasks yet...
; - - return tasks.map((task, index) => ( -
- + return ( +
+ + {tasks.length === 0 ? ( +

No tasks yet...

+ ) : ( + + {tasks.map((task, index) => ( + + ))} + + )} +
- )); + ); }; export default TaskList; diff --git a/src/contexts/UserContextProvider.tsx b/src/contexts/UserContextProvider.tsx index d0cfa46..7a92f95 100644 --- a/src/contexts/UserContextProvider.tsx +++ b/src/contexts/UserContextProvider.tsx @@ -8,6 +8,8 @@ export const UserContextProvider = ({ }: Readonly<{ children: React.ReactNode }>) => { const userProps = useUsers(); + if(userProps.users == null && !userProps.loading) return
Failed to load users
; + return ( {children} diff --git a/src/css/MainPage.css b/src/css/MainPage.css new file mode 100644 index 0000000..5247534 --- /dev/null +++ b/src/css/MainPage.css @@ -0,0 +1,25 @@ +.task-form { + padding: 10px; + display: flex; + flex-direction: row; + gap: 10px; + width: 50vw; + max-width: 1000px; + justify-content: stretch; +} + +.task-form > * { + flex: 1; +} + +.task-list { + margin-top: 20px; +} + +.task-list-item { + cursor: pointer; +} + +.task-list-item:hover { + background-color: #f0f0f0; +} diff --git a/src/hooks/useUsers.ts b/src/hooks/useUsers.ts index e144c67..4c41f4a 100644 --- a/src/hooks/useUsers.ts +++ b/src/hooks/useUsers.ts @@ -23,8 +23,8 @@ export function useUsers() { const data = parseUserData(await response.json()); setUsers(data); setError(null); - } catch(error) { - console.error(error) + } catch (error) { + console.error(error); setError("An error occurred during data fetching."); } setLoading(false); diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index b17f9eb..7d16986 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -1,6 +1,7 @@ import React from "react"; import TaskForm from "../components/TaskForm"; import TaskList from "../components/TaskList"; +import "@/css/MainPage.css"; const MainPage: React.FC = () => { return ( diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx index b40c576..49d6c76 100644 --- a/src/pages/NotFound.tsx +++ b/src/pages/NotFound.tsx @@ -1,7 +1,7 @@ import React from "react"; const NotFound: React.FC = () => { - return
Not found
; + return
Page not found
; }; export default NotFound; diff --git a/src/utils/types.ts b/src/utils/types.ts index 7c5dc37..8825656 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -10,6 +10,7 @@ export type Task = { finished: boolean; assignedTo: number; // User ID createdAt: React.RefObject; + due: string; text: string; }; diff --git a/src/utils/validation.ts b/src/utils/validation.ts new file mode 100644 index 0000000..9f2acdf --- /dev/null +++ b/src/utils/validation.ts @@ -0,0 +1,11 @@ +import type { Dayjs } from "dayjs"; + +export function validateDueDate(value: Dayjs): string | boolean { + const selected = new Date(value.format("YYYY-MM-DD")); + const now = new Date(); + now.setHours(0, 0, 0, 0); + + if (selected < now) return "Due date cannot be in the past"; + + return true; +} diff --git a/tsconfig.app.json b/tsconfig.app.json index c9ccbd4..a311297 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -1,27 +1,27 @@ { - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["src"] + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] } diff --git a/vite.config.ts b/vite.config.ts index 2328e17..38b6de1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,13 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react-swc' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react-swc"; +import path from 'path'; // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], -}) + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +});