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
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/cool_todo_manager/src/components/dialogs/CreateTaskDialog/CreateTaskDialog.tsx b/cool_todo_manager/src/components/dialogs/CreateTaskDialog/CreateTaskDialog.tsx
new file mode 100644
index 0000000..e9fa374
--- /dev/null
+++ b/cool_todo_manager/src/components/dialogs/CreateTaskDialog/CreateTaskDialog.tsx
@@ -0,0 +1,63 @@
+import { Button, Dialog, Flex, Text, TextField } from '@radix-ui/themes';
+import { PropsWithChildren, useState } from 'react';
+
+type TCreateTaskDialog = {
+ onClose: (text: string, timeString: string) => void;
+} & PropsWithChildren;
+
+export default function CreateTaskDialog(props: TCreateTaskDialog) {
+ const [text, setText] = useState('');
+ const [deadline, setDeadline] = useState('');
+
+ return (
+
+ {props.children}
+
+
+ Task creation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/cool_todo_manager/src/components/dialogs/DeleteProjectDialog/CreateTaskDialog.tsx b/cool_todo_manager/src/components/dialogs/DeleteProjectDialog/CreateTaskDialog.tsx
new file mode 100644
index 0000000..7047c5c
--- /dev/null
+++ b/cool_todo_manager/src/components/dialogs/DeleteProjectDialog/CreateTaskDialog.tsx
@@ -0,0 +1,36 @@
+import { Button, Dialog, Flex } from '@radix-ui/themes';
+import { PropsWithChildren } from 'react';
+
+type TCreateTaskDialog = {
+ onClose: () => void;
+} & PropsWithChildren;
+
+export default function DeleteProjectDialog(props: TCreateTaskDialog) {
+ return (
+
+ {props.children}
+
+
+ Delete project?
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/cool_todo_manager/src/index.css b/cool_todo_manager/src/index.css
index d4b5078..358e675 100644
--- a/cool_todo_manager/src/index.css
+++ b/cool_todo_manager/src/index.css
@@ -1 +1,5 @@
@import 'tailwindcss';
+
+#root {
+ height: 100vh;
+}
\ No newline at end of file
diff --git a/cool_todo_manager/src/main.tsx b/cool_todo_manager/src/main.tsx
index 68176a7..35f6c8f 100644
--- a/cool_todo_manager/src/main.tsx
+++ b/cool_todo_manager/src/main.tsx
@@ -1,5 +1,5 @@
-import { createRoot } from "react-dom/client";
-import App from "./App.tsx";
-import "./index.css";
+import { createRoot } from 'react-dom/client';
+import App from './App.tsx';
+import './index.css';
-createRoot(document.getElementById("root")!).render();
+createRoot(document.getElementById('root')!).render();
diff --git a/cool_todo_manager/src/pages/auth/LoginPage/LoginPage.tsx b/cool_todo_manager/src/pages/auth/LoginPage/LoginPage.tsx
new file mode 100644
index 0000000..a5a6bc5
--- /dev/null
+++ b/cool_todo_manager/src/pages/auth/LoginPage/LoginPage.tsx
@@ -0,0 +1,90 @@
+import {
+ Box,
+ Button,
+ Card,
+ Heading,
+ Link,
+ Text,
+ TextField,
+} from '@radix-ui/themes';
+import { Form } from 'radix-ui';
+import { useState } from 'react';
+import { useLoginMutation } from '../../../services/mainApi';
+
+export default function LoginPage() {
+ const [login, { isError }] = useLoginMutation();
+
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+
+ const submitHandler = (e: React.FormEvent) => {
+ e.preventDefault();
+ login({ username, password });
+ };
+
+ return (
+ <>
+
+
+ Login
+
+
+
+
+
+ Username is required
+
+
+
+ setUsername(e.target.value)}
+ >
+
+
+
+
+
+
+
+ Password is required
+
+
+
+ setPassword(e.target.value)}
+ >
+
+
+
+
+
+ {isError && (
+ {'Unable to login. Please try again.'}
+ )}
+
+
+
+
+
+ Register
+
+
+
+
+ >
+ );
+}
diff --git a/cool_todo_manager/src/pages/auth/RegisterPage/RegisterPage.tsx b/cool_todo_manager/src/pages/auth/RegisterPage/RegisterPage.tsx
new file mode 100644
index 0000000..a23a0fb
--- /dev/null
+++ b/cool_todo_manager/src/pages/auth/RegisterPage/RegisterPage.tsx
@@ -0,0 +1,125 @@
+import {
+ Box,
+ Button,
+ Card,
+ Heading,
+ Link,
+ Text,
+ TextField,
+} from '@radix-ui/themes';
+import { Form } from 'radix-ui';
+import { useState } from 'react';
+import { useRegisterMutation } from '../../../services/mainApi';
+
+export default function RegisterPage() {
+ const [register, { isLoading, error }] = useRegisterMutation();
+ const [formData, setFormData] = useState({
+ username: '',
+ email: '',
+ password: '',
+ });
+
+ const handleChange = (e: React.ChangeEvent) => {
+ setFormData({
+ ...formData,
+ [e.target.name]: e.target.value,
+ });
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ try {
+ await register(formData).unwrap();
+ } catch (err) {
+ console.error('Failed to register:', err);
+ }
+ };
+
+ return (
+
+
+ Register
+
+
+
+
+ Username is required
+
+
+
+
+
+
+
+
+
+
+
+ Email is required
+
+
+
+ Email is not valid
+
+
+
+
+
+
+
+
+
+
+
+ Password is required
+
+
+
+
+
+
+
+
+
+
+
+ {error && (
+
+ Error registering user.
+
+ )}
+
+
+
+ Login
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/cool_todo_manager/src/pages/main/MainPage.tsx b/cool_todo_manager/src/pages/main/MainPage.tsx
new file mode 100644
index 0000000..e03c3e5
--- /dev/null
+++ b/cool_todo_manager/src/pages/main/MainPage.tsx
@@ -0,0 +1,10 @@
+import { Box } from '@radix-ui/themes'
+import { Outlet } from 'react-router-dom'
+
+export default function MainPage() {
+ return (
+
+
+
+ )
+}
diff --git a/cool_todo_manager/src/routes/routes.tsx b/cool_todo_manager/src/routes/routes.tsx
new file mode 100644
index 0000000..3b10cf0
--- /dev/null
+++ b/cool_todo_manager/src/routes/routes.tsx
@@ -0,0 +1,26 @@
+import { createRoutesFromElements, Route } from 'react-router-dom';
+import AuthWrapper from '../components/AuthWrapper/AuthWrapper';
+import MainBoard from '../components/MainBoard/MainBoard';
+import LoginPage from '../pages/auth/LoginPage/LoginPage';
+import RegisterPage from '../pages/auth/RegisterPage/RegisterPage';
+import MainPage from '../pages/main/MainPage';
+
+const MyRoutes = createRoutesFromElements(
+ <>
+
+
+
+ }
+ >
+ } />
+
+
+ } />
+ } />
+ >,
+);
+
+export default MyRoutes;
diff --git a/cool_todo_manager/src/services/mainApi.ts b/cool_todo_manager/src/services/mainApi.ts
new file mode 100644
index 0000000..ddf181c
--- /dev/null
+++ b/cool_todo_manager/src/services/mainApi.ts
@@ -0,0 +1,231 @@
+import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
+
+export const mainApi = createApi({
+ reducerPath: 'api',
+ baseQuery: fetchBaseQuery({
+ baseUrl: 'http://109.107.166.17:5000/api/',
+ prepareHeaders: (headers) => {
+ headers.set(
+ 'Authorization',
+ `Bearer ${localStorage.getItem('token')}`,
+ );
+ return headers;
+ },
+ }),
+ tagTypes: ['Task', 'Project', 'ProjectMembers'],
+ endpoints: (builder) => ({
+ getTasks: builder.query({
+ query: () => ({
+ url: 'tasks',
+ method: 'GET',
+ }),
+ providesTags: (result) =>
+ result
+ ? [
+ ...result.map((task) => ({
+ type: 'Task' as const,
+ id: task,
+ })),
+ { type: 'Task', id: 'LIST' },
+ ]
+ : [{ type: 'Task', id: 'LIST' }],
+ }),
+ getTasksForGroup: builder.query({
+ query: (id: string) => ({
+ url: `tasks/project/${id}`,
+ method: 'GET',
+ }),
+ providesTags: (result) =>
+ result
+ ? [
+ ...result.map((task) => ({
+ type: 'Task' as const,
+ id: task,
+ })),
+ { type: 'Task', id: 'LIST' },
+ ]
+ : [{ type: 'Task', id: 'LIST' }],
+ }),
+ getTask: builder.query({
+ query: (id) => ({
+ url: `tasks/${id}`,
+ method: 'GET',
+ }),
+ providesTags: (result, error, id) => [{ type: 'Task', id }],
+ }),
+ updateTask: builder.mutation({
+ query: (task) => ({
+ url: `tasks/${task.id}`,
+ method: 'PATCH',
+ body: { status: task.status },
+ }),
+ invalidatesTags: (result, error, id) => [{ type: 'Task', id }],
+ }),
+ deleteTask: builder.mutation({
+ query: (id) => ({
+ url: `tasks/${id}`,
+ method: 'DELETE',
+ }),
+ invalidatesTags: (result, error, id) => [{ type: 'Task', id }],
+ }),
+ createTask: builder.mutation({
+ query: (newTask) => ({
+ url: 'tasks/create',
+ method: 'POST',
+ body: newTask,
+ }),
+ invalidatesTags: [{ type: 'Task', id: 'LIST' }],
+ }),
+
+ // PROJECTS
+ createProject: builder.mutation({
+ query: (newProject) => ({
+ url: 'projects/create',
+ method: 'POST',
+ body: newProject,
+ }),
+ invalidatesTags: [{ type: 'Project', id: 'LIST' }],
+ }),
+ getProjects: builder.query({
+ query: () => ({
+ url: 'projects/my',
+ method: 'GET',
+ }),
+ providesTags: (result, error, id) => [
+ { type: 'Project', id: 'LIST' },
+ ],
+ }),
+ getProject: builder.query({
+ query: (id) => ({
+ url: `projects/${id}`,
+ method: 'GET',
+ }),
+ providesTags: (result, error, id) => [{ type: 'Project', id }],
+ }),
+ updateProject: builder.mutation({
+ query: ({ id, project }) => ({
+ url: `projects/${id}`,
+ method: 'PATCH',
+ body: project,
+ }),
+ invalidatesTags: (result, error, id) => [{ type: 'Project', id }],
+ }),
+ deleteProject: builder.mutation({
+ query: (id) => ({
+ url: `projects/${id}`,
+ method: 'DELETE',
+ }),
+ invalidatesTags: (result, error, id) => [
+ { type: 'Project', id: 'LIST' },
+ ],
+ }),
+
+ // PROJECT MEMBERS
+ addProjectMember: builder.mutation({
+ query: ({ projectId, memberId }) => ({
+ url: `projects/${projectId}/members/add`,
+ method: 'POST',
+ body: { userId: memberId, role: '' },
+ }),
+ invalidatesTags: (result, error, args) => [
+ { type: 'ProjectMembers', id: 'LIST' },
+ ],
+ }),
+ getProjectMembers: builder.query({
+ query: (id) => ({
+ url: `projects/${id}/members`,
+ method: 'GET',
+ }),
+ providesTags: (result, error, args) => [
+ { type: 'ProjectMembers', id: 'LIST' },
+ ],
+ }),
+ removeProjectMember: builder.mutation({
+ query: ({ projectId, memberId }) => ({
+ url: `projects/${projectId}/members/remove`,
+ method: 'DELETE',
+ body: { userId: memberId },
+ }),
+ invalidatesTags: (result, error, id) => [
+ { type: 'ProjectMembers', id: 'LIST' },
+ ],
+ }),
+ }),
+});
+
+export const authApi = createApi({
+ reducerPath: 'authApi',
+ baseQuery: fetchBaseQuery({
+ baseUrl: 'http://109.107.166.17:4000/api/',
+ }),
+ endpoints: (builder) => ({
+ login: builder.mutation({
+ query: (credentials) => ({
+ url: '/auth/login',
+ method: 'POST',
+ body: credentials,
+ }),
+ async onQueryStarted(arg, { queryFulfilled }) {
+ try {
+ const response = await queryFulfilled;
+ if (response.data.access_token) {
+ localStorage.setItem(
+ 'token',
+ response.data.access_token,
+ );
+ if (response.meta?.response?.status === 201)
+ window.location.href = '/';
+ }
+ } catch (error) {}
+ },
+ }),
+ register: builder.mutation({
+ query: (credentials) => ({
+ url: '/auth/register',
+ method: 'POST',
+ body: credentials,
+ }),
+ onQueryStarted: async (arg, { queryFulfilled }) => {
+ try {
+ const response = await queryFulfilled;
+ if (response.meta?.response?.status === 201)
+ window.location.href = '/login';
+ } catch (error) {}
+ },
+ }),
+
+ // USERS
+ getAllUsers: builder.query({
+ query: () => ({
+ url: '/users',
+ method: 'GET',
+ }),
+ keepUnusedDataFor: 0,
+
+ }),
+ }),
+});
+
+export const { useLoginMutation, useRegisterMutation, useGetAllUsersQuery } =
+ authApi;
+
+export const {
+ useGetTasksQuery,
+ useCreateTaskMutation,
+ useUpdateTaskMutation,
+ useDeleteTaskMutation,
+ useGetTasksForGroupQuery,
+} = mainApi;
+
+export const {
+ useGetProjectsQuery,
+ useCreateProjectMutation,
+ useUpdateProjectMutation,
+ useDeleteProjectMutation,
+} = mainApi;
+
+export const {
+ useRemoveProjectMemberMutation,
+ useAddProjectMemberMutation,
+ useGetProjectMembersQuery,
+} = mainApi;