From e82b06b508bdd059d580626a32fd5c98621ba34f Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 1 Mar 2025 21:32:57 +0300 Subject: [PATCH] redux setup --- cool_todo_manager/package-lock.json | 69 +++++++++ cool_todo_manager/package.json | 4 + cool_todo_manager/src/App.tsx | 10 +- cool_todo_manager/src/api/RTKQuery.ts | 16 ++ .../components/AuthWrapper/AuthWrapper.tsx | 15 ++ .../src/components/CardGroup/CardGroup.tsx | 5 +- .../src/components/MainBoard/MainBoard.tsx | 35 ++++- .../src/pages/auth/LoginPage/LoginPage.tsx | 42 +++-- cool_todo_manager/src/services/mainApi.ts | 145 ++++++++++++++++++ 9 files changed, 320 insertions(+), 21 deletions(-) create mode 100644 cool_todo_manager/src/api/RTKQuery.ts create mode 100644 cool_todo_manager/src/components/AuthWrapper/AuthWrapper.tsx create mode 100644 cool_todo_manager/src/services/mainApi.ts diff --git a/cool_todo_manager/package-lock.json b/cool_todo_manager/package-lock.json index 6d7f662..b47a309 100644 --- a/cool_todo_manager/package-lock.json +++ b/cool_todo_manager/package-lock.json @@ -12,18 +12,22 @@ "@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", @@ -2547,6 +2551,30 @@ } } }, + "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", @@ -3131,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", @@ -4423,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", @@ -4520,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", @@ -5394,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 e51fa4e..c9a7b8f 100644 --- a/cool_todo_manager/package.json +++ b/cool_todo_manager/package.json @@ -14,18 +14,22 @@ "@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 3d02b76..0116187 100644 --- a/cool_todo_manager/src/App.tsx +++ b/cool_todo_manager/src/App.tsx @@ -1,8 +1,10 @@ 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 MyRoutes from './routes/routes'; @@ -10,10 +12,12 @@ 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..7091df8 --- /dev/null +++ b/cool_todo_manager/src/api/RTKQuery.ts @@ -0,0 +1,16 @@ +import { configureStore } from '@reduxjs/toolkit'; +import { setupListeners } from '@reduxjs/toolkit/query'; +import { authApi, mainApi } from '../services/mainApi'; + +export const store = configureStore({ + reducer: { + [mainApi.reducerPath]: mainApi.reducer, + [authApi.reducerPath]: authApi.reducer, + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware() + .concat(mainApi.middleware) + .concat(authApi.middleware), +}); + +setupListeners(store.dispatch); 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..ebafd16 --- /dev/null +++ b/cool_todo_manager/src/components/AuthWrapper/AuthWrapper.tsx @@ -0,0 +1,15 @@ +import { useNavigate } from "react-router-dom" + +export default function AuthWrapper() { + const navigate = useNavigate() + + if(!localStorage.getItem('token')) { + navigate('/login') + } + + return ( +
+ +
+ ) +} diff --git a/cool_todo_manager/src/components/CardGroup/CardGroup.tsx b/cool_todo_manager/src/components/CardGroup/CardGroup.tsx index cdd8109..f41d337 100644 --- a/cool_todo_manager/src/components/CardGroup/CardGroup.tsx +++ b/cool_todo_manager/src/components/CardGroup/CardGroup.tsx @@ -1,5 +1,6 @@ import { Droppable } from '@hello-pangea/dnd'; import { Box } from '@radix-ui/themes'; +import { useState } from 'react'; import TaskCard from '../TaskCard/TaskCard'; const tasks = [ @@ -12,11 +13,13 @@ type TCardGroup = { }; export default function CardGroup(props: TCardGroup) { + const [localTasks, setLocalTasks] = useState(tasks) + return ( {(provided) => ( - {tasks.map((task, i) => ( + {localTasks.map((task, i) => ( { + result; + }; - const dragEndHandle = (result: TDragResult ) => { - result - } + const { data } = useGetTasksQuery(); + + const [create] = useCreateTaskMutation(); + + const onClick = () => { + create({ + title: 'My Task 123', + projectId: 1, + assignedUserId: 2, + deadline: '2025-03-01T12:00:00Z', + }); + }; return ( - - - - + <> + + + + + + ); } diff --git a/cool_todo_manager/src/pages/auth/LoginPage/LoginPage.tsx b/cool_todo_manager/src/pages/auth/LoginPage/LoginPage.tsx index 61064a3..ebb8fa0 100644 --- a/cool_todo_manager/src/pages/auth/LoginPage/LoginPage.tsx +++ b/cool_todo_manager/src/pages/auth/LoginPage/LoginPage.tsx @@ -1,24 +1,42 @@ import { Button, Card, Heading, 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 + + Login + - e.preventDefault()}> + - Email is required - - - - Email is not valid + Username is required - + setUsername(e.target.value)} + > @@ -33,16 +51,22 @@ export default function LoginPage() { setPassword(e.target.value)} > + {isError && ( + {'Unable to login. Please try again.'} + )} + diff --git a/cool_todo_manager/src/services/mainApi.ts b/cool_todo_manager/src/services/mainApi.ts new file mode 100644 index 0000000..cf5f1fe --- /dev/null +++ b/cool_todo_manager/src/services/mainApi.ts @@ -0,0 +1,145 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; + +export const mainApi = createApi({ + reducerPath: 'api', + baseQuery: fetchBaseQuery({ + baseUrl: 'http://109.107.166.17:3000/api/', + prepareHeaders: (headers) => { + headers.set( + 'Authorization', + `Bearer ${localStorage.getItem('token')}`, + ); + return headers; + }, + }), + tagTypes: ['Task', 'Project'], + 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' }], + }), + 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: task, + }), + 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: 'tasks/create', + method: 'POST', + body: newProject, + }), + invalidatesTags: [{ type: 'Project', id: 'LIST' }], + }), + getProjects: builder.query({ + query: () => ({ + url: 'projects', + 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 }], + }), + }), +}); + +export const authApi = createApi({ + reducerPath: 'authApi', + baseQuery: fetchBaseQuery({ + baseUrl: 'http://109.107.166.17:3000/api/auth/', + + }), + endpoints: (builder) => ({ + login: builder.mutation({ + query: (credentials) => ({ + url: 'login', + method: 'POST', + body: credentials, + }), + async onQueryStarted(arg, { queryFulfilled }) { + try { + const response = await queryFulfilled; + if (response.data) { + localStorage.setItem( + 'token', + response.data.access_token, + ); + window.location.href = '/'; + } + } catch (error) {} + }, + }), + register: builder.mutation({ + query: (credentials) => ({ + url: 'register', + method: 'POST', + body: credentials, + }), + }), + }), +}); + +export const { useLoginMutation, useRegisterMutation } = authApi; +export const { useGetTasksQuery, useCreateTaskMutation, useUpdateTaskMutation, useDeleteTaskMutation } = mainApi; +export const { useGetProjectsQuery, useCreateProjectMutation, useUpdateProjectMutation, useDeleteProjectMutation } = mainApi;