diff --git a/cool_todo_manager/package-lock.json b/cool_todo_manager/package-lock.json index 28cdaa3..b47a309 100644 --- a/cool_todo_manager/package-lock.json +++ b/cool_todo_manager/package-lock.json @@ -9,19 +9,25 @@ "version": "0.0.0", "dependencies": { "@hello-pangea/dnd": "^18.0.1", + "@radix-ui/react-form": "^0.1.2", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/themes": "^3.2.0", + "@reduxjs/toolkit": "^2.6.0", "@tailwindcss/vite": "^4.0.6", "@tanstack/react-query": "^5.66.0", "@tanstack/react-query-devtools": "^5.66.0", "axios": "^1.7.9", "jotai": "^2.12.0", + "js-cookie": "^3.0.5", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-redux": "^9.2.0", + "react-router-dom": "^6.26.2", "tailwindcss": "^4.0.6" }, "devDependencies": { "@eslint/js": "^9.19.0", + "@types/js-cookie": "^3.0.6", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "@vitejs/plugin-react": "^4.3.4", @@ -2545,6 +2551,39 @@ } } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.6.0.tgz", + "integrity": "sha512-mWJCYpewLRyTuuzRSEC/IwIBBkYg2dKtQas8mty5MaV2iXzcmicS3gW554FDeOvLnY3x13NIk8MB1e8wHO7rqQ==", + "license": "MIT", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@remix-run/router": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz", + "integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.34.7", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.7.tgz", @@ -3120,6 +3159,13 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "license": "MIT" }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4412,6 +4458,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4509,6 +4565,15 @@ } } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5323,6 +5388,38 @@ } } }, + "node_modules/react-router": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz", + "integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.19.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz", + "integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.19.2", + "react-router": "6.26.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -5351,12 +5448,27 @@ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "license": "MIT" }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "license": "MIT" }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", diff --git a/cool_todo_manager/package.json b/cool_todo_manager/package.json index 091c7b8..c9a7b8f 100644 --- a/cool_todo_manager/package.json +++ b/cool_todo_manager/package.json @@ -11,19 +11,25 @@ }, "dependencies": { "@hello-pangea/dnd": "^18.0.1", + "@radix-ui/react-form": "^0.1.2", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/themes": "^3.2.0", + "@reduxjs/toolkit": "^2.6.0", "@tailwindcss/vite": "^4.0.6", "@tanstack/react-query": "^5.66.0", "@tanstack/react-query-devtools": "^5.66.0", "axios": "^1.7.9", "jotai": "^2.12.0", + "js-cookie": "^3.0.5", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-redux": "^9.2.0", + "react-router-dom": "^6.26.2", "tailwindcss": "^4.0.6" }, "devDependencies": { "@eslint/js": "^9.19.0", + "@types/js-cookie": "^3.0.6", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "@vitejs/plugin-react": "^4.3.4", diff --git a/cool_todo_manager/src/App.tsx b/cool_todo_manager/src/App.tsx index 1cf69e1..0116187 100644 --- a/cool_todo_manager/src/App.tsx +++ b/cool_todo_manager/src/App.tsx @@ -1,14 +1,25 @@ import { Theme } from '@radix-ui/themes'; import '@radix-ui/themes/styles.css'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { Provider } from 'react-redux'; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; +import queryClient from './api/queryClient'; +import { store } from './api/RTKQuery'; import './App.css'; -import MainBoard from './components/MainBoard/MainBoard'; +import MyRoutes from './routes/routes'; + +const router = createBrowserRouter(MyRoutes); function App() { return ( - - - {/* */} - + + + + {/* */} + + + + ); } diff --git a/cool_todo_manager/src/api/RTKQuery.ts b/cool_todo_manager/src/api/RTKQuery.ts new file mode 100644 index 0000000..cd93886 --- /dev/null +++ b/cool_todo_manager/src/api/RTKQuery.ts @@ -0,0 +1,33 @@ +import { configureStore, isRejectedWithValue, Middleware } from '@reduxjs/toolkit'; +import { setupListeners } from '@reduxjs/toolkit/query'; +import { authApi, mainApi } from '../services/mainApi'; + +const loggerMiddleware: Middleware = (_store) => (next) => (action) => { + console.log('dispatching', action); + if(isRejectedWithValue(action)) { + // @ts-ignore + const statusCode = action.payload.status; + if(statusCode === 401) { + console.log('Unauthorized, redirecting to login page'); + localStorage.removeItem('token'); + window.location.href = '/login'; + } + } + let result = next(action); + return result; +}; + +export const store = configureStore({ + reducer: { + [mainApi.reducerPath]: mainApi.reducer, + [authApi.reducerPath]: authApi.reducer, + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware() + .prepend(loggerMiddleware) + .concat(mainApi.middleware) + .concat(authApi.middleware), +}); + +setupListeners(store.dispatch); + diff --git a/cool_todo_manager/src/api/axios.ts b/cool_todo_manager/src/api/axios.ts new file mode 100644 index 0000000..7b0f02d --- /dev/null +++ b/cool_todo_manager/src/api/axios.ts @@ -0,0 +1,14 @@ +import axios from "axios"; + +const BASE_URL = 'http://localhost:4567'; + +export const axiosBase = axios.create({ + baseURL: BASE_URL, +}); + +export const axiosAuth = axios.create({ + baseURL: BASE_URL, + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}` // Maybe we will use cookies + } +}); \ No newline at end of file diff --git a/cool_todo_manager/src/api/queryClient.ts b/cool_todo_manager/src/api/queryClient.ts new file mode 100644 index 0000000..75e4ae8 --- /dev/null +++ b/cool_todo_manager/src/api/queryClient.ts @@ -0,0 +1,11 @@ +import { QueryClient } from "@tanstack/react-query"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 2, + }, + }, +}) + +export default queryClient; \ No newline at end of file diff --git a/cool_todo_manager/src/components/AuthWrapper/AuthWrapper.tsx b/cool_todo_manager/src/components/AuthWrapper/AuthWrapper.tsx new file mode 100644 index 0000000..07d3bac --- /dev/null +++ b/cool_todo_manager/src/components/AuthWrapper/AuthWrapper.tsx @@ -0,0 +1,12 @@ +import { PropsWithChildren } from 'react'; +import { Navigate } from 'react-router-dom'; + +export default function AuthWrapper(props: PropsWithChildren) { + + if (!localStorage.getItem('token')) { + console.log('No token found, redirecting to login'); + return + } + + return
{props.children}
; +} diff --git a/cool_todo_manager/src/components/CardGroup/CardGroup.tsx b/cool_todo_manager/src/components/CardGroup/CardGroup.tsx index daa0ea9..7185d91 100644 --- a/cool_todo_manager/src/components/CardGroup/CardGroup.tsx +++ b/cool_todo_manager/src/components/CardGroup/CardGroup.tsx @@ -1,36 +1,79 @@ import { Droppable } from '@hello-pangea/dnd'; -import { Box } from '@radix-ui/themes'; +import { Box, Button, Flex, Text } from '@radix-ui/themes'; +import { + useCreateTaskMutation, + useDeleteProjectMutation, + useGetTasksForGroupQuery, +} from '../../services/mainApi'; import TaskCard from '../TaskCard/TaskCard'; +import AddUserToProjectDialog from '../dialogs/AddUserToProjectDialog/AddUserToProjectDialog'; +import CreateTaskDialog from '../dialogs/CreateTaskDialog/CreateTaskDialog'; +import DeleteProjectDialog from '../dialogs/DeleteProjectDialog/CreateTaskDialog'; -const tasks = [ - { id: 1, title: 'Task 1', description: 'Description for Task 1' }, - { id: 2, title: 'Task 2', description: 'Description for Task 2' }, -]; +import { PlusIcon } from '@radix-ui/react-icons'; type TCardGroup = { - id: string -} + id: string; + title: string; +}; export default function CardGroup(props: TCardGroup) { + const { data, isLoading } = useGetTasksForGroupQuery(props.id); + + const [createTaskForGroup] = useCreateTaskMutation(); + const [deleteProject] = useDeleteProjectMutation(); + + const createTask = (taskText: string, date: string) => { + createTaskForGroup({ + title: taskText, + projectId: props.id, + assignedUserId: 1, + deadline: date, + }); + }; + + const deleteGroup = () => { + deleteProject(props.id); + }; + return ( - - {(provided) => ( - - {tasks.map((task, i) => ( + + {(provided) => ( + + {props.title} + + {data && + data.map((task, i) => ( ))} - {provided.placeholder} - - )} - + + + + + + + + + + + + + + + )} + ); } diff --git a/cool_todo_manager/src/components/MainBoard/MainBoard.tsx b/cool_todo_manager/src/components/MainBoard/MainBoard.tsx index a51d4c8..41854df 100644 --- a/cool_todo_manager/src/components/MainBoard/MainBoard.tsx +++ b/cool_todo_manager/src/components/MainBoard/MainBoard.tsx @@ -1,17 +1,36 @@ import { DragDropContext } from '@hello-pangea/dnd'; +import { Button, Flex, ScrollArea } from '@radix-ui/themes'; +import { + useCreateProjectMutation, + useGetProjectsQuery +} from '../../services/mainApi'; import CardGroup from '../CardGroup/CardGroup'; - +import CreateProjectDialog from '../dialogs/CreateProjectDialog/CreateProjectDialog'; export default function MainBoard() { + const dragEndHandle = (result: TDragResult) => { + result; + }; - const dragEndHandle = (result: TDragResult ) => { - result - } + const [createProject] = useCreateProjectMutation(); + const { data: cringe, isLoading } = useGetProjectsQuery({}); return ( - - - - + <> + + + + {!isLoading && + (cringe as any[]).map((item: any) => ( + + ))} + + + + + + + + ); } diff --git a/cool_todo_manager/src/components/TaskCard/TaskCard.tsx b/cool_todo_manager/src/components/TaskCard/TaskCard.tsx index 98f44a9..83a99dd 100644 --- a/cool_todo_manager/src/components/TaskCard/TaskCard.tsx +++ b/cool_todo_manager/src/components/TaskCard/TaskCard.tsx @@ -1,35 +1,66 @@ import { Draggable } from '@hello-pangea/dnd'; -import { DragHandleHorizontalIcon } from '@radix-ui/react-icons'; -import { Box, Button, Card, Flex, Text } from '@radix-ui/themes'; +import { Badge, Button, Card, Flex, Text } from '@radix-ui/themes'; +import { useUpdateTaskMutation } from '../../services/mainApi'; type TTaskCard = { title?: string; description?: string; id?: string; index?: number; + status: 'todo' | 'in-progress' | 'completed'; }; +const badgeNames = { + todo: 'To do', + 'in-progress': 'In progress', + completed: 'Completed', +} as const + +const badgeColors = { + todo: 'blue', + 'in-progress': 'orange', + completed: 'green', +} as const + export default function TaskCard(props: TTaskCard) { + const [updateTask] = useUpdateTaskMutation(); + + const updateStatus = (newStatus: 'todo' | 'in-progress' | 'completed') => { + updateTask({ id: props.id, status: newStatus }); + }; + return ( {(provided) => ( - {props.title} - - - - - + + {props.title} + {badgeNames[props.status]} + + + {props.status !== 'todo' && ( + + )} + {props.status !== 'in-progress' && ( + + )} + {props.status !== 'completed' && ( + + )} + )} diff --git a/cool_todo_manager/src/components/dialogs/AddUserToProjectDialog/AddUserToProjectDialog.tsx b/cool_todo_manager/src/components/dialogs/AddUserToProjectDialog/AddUserToProjectDialog.tsx new file mode 100644 index 0000000..65727f8 --- /dev/null +++ b/cool_todo_manager/src/components/dialogs/AddUserToProjectDialog/AddUserToProjectDialog.tsx @@ -0,0 +1,92 @@ +import { Button, Dialog, Flex, Select } from '@radix-ui/themes'; +import { PropsWithChildren, useState } from 'react'; +import { + useAddProjectMemberMutation, + useGetAllUsersQuery, +} from '../../../services/mainApi'; + +type TAddUserToProjectDialog = { + projectId: number; +} & PropsWithChildren; + +export default function AddUserToProjectDialog(props: TAddUserToProjectDialog) { + const { projectId, children } = props; + const { data: users, isLoading, error, refetch } = useGetAllUsersQuery({}); + const [addProjectMember, { isLoading: isAdding }] = + useAddProjectMemberMutation(); + const [selectedUserId, setSelectedUserId] = useState(null); + + const handleAddUser = async () => { + if (selectedUserId) { + try { + await addProjectMember({ + projectId, + memberId: selectedUserId, + }).unwrap(); + setSelectedUserId(null); + } catch (err) { + console.error('Failed to add user:', err); + } + } + }; + + + return ( + refetch()}> + {children} + + + Add User to Project + + + {isLoading ? ( + Loading users... + ) : error ? ( + Error loading users. + ) : ( + + setSelectedUserId(Number(value)) + } + required + > + + {selectedUserId + ? users?.find( + (user: any) => + user.id === selectedUserId, + )?.username + : 'Select a user'} + + + {users?.map((user: any) => ( + + {user.username} + + ))} + + + )} + + + + + + + + + + + + + ); +} diff --git a/cool_todo_manager/src/components/dialogs/CreateProjectDialog/CreateProjectDialog.tsx b/cool_todo_manager/src/components/dialogs/CreateProjectDialog/CreateProjectDialog.tsx new file mode 100644 index 0000000..8d74bf8 --- /dev/null +++ b/cool_todo_manager/src/components/dialogs/CreateProjectDialog/CreateProjectDialog.tsx @@ -0,0 +1,59 @@ +import { Button, Dialog, Flex, TextArea, TextField } from '@radix-ui/themes'; +import { PropsWithChildren, useState } from 'react'; + +type TCreateProjectDialog = { + onCreate: (f: any) => void; +} & PropsWithChildren; + +export default function CreateProjectDialog(props: TCreateProjectDialog) { + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + + const handleCreate = () => { + props.onCreate({title, description}); + setTitle(''); + setDescription(''); + }; + + return ( + + + {props.children} + + + + Create a New Project + + + setTitle(e.target.value)} + required + > +