feature/SPA #10

Merged
tarasovne merged 6 commits from feature/SPA into develop 2025-03-22 18:31:24 +00:00
11 changed files with 274 additions and 60 deletions
Showing only changes of commit cabb8a9b27 - Show all commits

View File

@ -1,7 +1,22 @@
import { configureStore } from '@reduxjs/toolkit';
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,
@ -9,8 +24,10 @@ export const store = configureStore({
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
.prepend(loggerMiddleware)
.concat(mainApi.middleware)
.concat(authApi.middleware),
});
setupListeners(store.dispatch);

View File

@ -1,15 +1,12 @@
import { useNavigate } from "react-router-dom"
import { PropsWithChildren } from 'react';
import { Navigate } from 'react-router-dom';
export default function AuthWrapper() {
const navigate = useNavigate()
export default function AuthWrapper(props: PropsWithChildren) {
if(!localStorage.getItem('token')) {
navigate('/login')
}
if (!localStorage.getItem('token')) {
console.log('No token found, redirecting to login');
return <Navigate to={'/login'} />
}
return (
<div>
</div>
)
return <div>{props.children}</div>;
}

View File

@ -1,34 +1,69 @@
import { Droppable } from '@hello-pangea/dnd';
import { Box } from '@radix-ui/themes';
import { useState } from 'react';
import { Box, Button, Flex, Text } from '@radix-ui/themes';
import {
useCreateTaskMutation,
useDeleteProjectMutation,
useGetTasksForGroupQuery,
} from '../../services/mainApi';
import TaskCard from '../TaskCard/TaskCard';
const tasks = [
{ id: 1, title: 'Task 1', description: 'Description for Task 1' },
{ id: 2, title: 'Task 2', description: 'Description for Task 2' },
];
import CreateTaskDialog from './CreateTaskDialog/CreateTaskDialog';
import DeleteProjectDialog from './DeleteProjectDialog/CreateTaskDialog';
type TCardGroup = {
id: string;
title: string;
};
export default function CardGroup(props: TCardGroup) {
const [localTasks, setLocalTasks] = useState(tasks)
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 (
<Droppable droppableId={props.id}>
<Droppable droppableId={props.id.toString()}>
{(provided) => (
<Box ref={provided.innerRef} {...provided.droppableProps}>
{localTasks.map((task, i) => (
<TaskCard
key={task.id}
id={task.id.toString() + props.id}
title={task.title}
description={task.description}
index={i}
/>
))}
{provided.placeholder}
<Box className="flex flex-col text-center rounded-lg p-4 bg-gray-200 min-w-fit">
<Text className="mb-2">{props.title}</Text>
<Box
ref={provided.innerRef}
className="bg-gray-200 min-w-full rounded-lg !flex flex-col gap-2"
>
{data &&
data.map((task, i) => (
<TaskCard
key={task.id}
id={task.id.toString() + props.id}
title={task.title}
description={task.status}
index={i}
status={task.status}
/>
))}
</Box>
<Flex gap={'2'} className="mx-auto w-full mt-2">
<CreateTaskDialog onClose={createTask}>
<Button>Add Task</Button>
</CreateTaskDialog>
<DeleteProjectDialog onClose={deleteGroup}>
<Button color="red" onClick={deleteGroup}>
Delete Project
</Button>
</DeleteProjectDialog>
</Flex>
</Box>
)}
</Droppable>

View File

@ -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 (
<Dialog.Root>
<Dialog.Trigger>{props.children}</Dialog.Trigger>
<Dialog.Content maxWidth="450px">
<Dialog.Title>Task creation</Dialog.Title>
<Flex direction="column" gap="3">
<label>
<Text as="div" size="2" mb="1" weight="bold">
Task description
</Text>
<TextField.Root
defaultValue=""
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Enter task description"
/>
</label>
<label>
<Text as="div" size="2" mb="1" weight="bold">
Deadline
</Text>
<input
type="date"
onChange={(e) => setDeadline(e.target.value)}
></input>
</label>
</Flex>
<Flex gap="3" mt="4" justify="end">
<Dialog.Close>
<Button variant="soft" color="gray">
Cancel
</Button>
</Dialog.Close>
<Dialog.Close>
<Button
onClick={() => {
props.onClose(text, deadline);
setDeadline('');
setText('');
}}
>
Create
</Button>
</Dialog.Close>
</Flex>
</Dialog.Content>
</Dialog.Root>
);
}

View File

@ -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 (
<Dialog.Root>
<Dialog.Trigger>{props.children}</Dialog.Trigger>
<Dialog.Content maxWidth="450px">
<Dialog.Title>Delete project?</Dialog.Title>
<Flex gap="3" mt="4" justify="end">
<Dialog.Close>
<Button variant="soft" color="gray">
Cancel
</Button>
</Dialog.Close>
<Dialog.Close>
<Button
color="red"
onClick={() => {
props.onClose();
}}
>
Delete
</Button>
</Dialog.Close>
</Flex>
</Dialog.Content>
</Dialog.Root>
);
}

View File

@ -1,8 +1,9 @@
import { DragDropContext } from '@hello-pangea/dnd';
import { Button } from '@radix-ui/themes';
import { Button, Flex, ScrollArea } from '@radix-ui/themes';
import {
useCreateProjectMutation,
useCreateTaskMutation,
useGetTasksQuery,
useGetProjectsQuery,
} from '../../services/mainApi';
import CardGroup from '../CardGroup/CardGroup';
@ -11,26 +12,40 @@ export default function MainBoard() {
result;
};
const { data } = useGetTasksQuery();
const [create] = useCreateTaskMutation();
const [createProject] = useCreateProjectMutation();
const { data: cringe, isLoading } = useGetProjectsQuery({});
const onClick = () => {
create({
title: 'My Task 123',
projectId: 1,
assignedUserId: 2,
projectId: 4,
assignedUserId: 1,
deadline: '2025-03-01T12:00:00Z',
});
};
const onClick1 = () => {
createProject({
title: 'My project test 12',
description: 'Test desc 123 123 123',
});
};
return (
<>
<DragDropContext onDragEnd={dragEndHandle}>
<CardGroup id="qwhdf" />
<CardGroup id="123fsduiyuiyi" />
<ScrollArea scrollbars='horizontal'>
<Flex gap={'2'} className='min-w-fit'>
{!isLoading &&
(cringe as any[]).map((item: any) => (
<CardGroup id={item.id} title={item.title} />
))}
</Flex>
</ScrollArea>
</DragDropContext>
<Button onClick={onClick}>asdasdasd </Button>
<Button onClick={onClick}>Create task</Button>
<Button onClick={onClick1}>Create project</Button>
</>
);
}

View File

@ -1,12 +1,12 @@
import { Draggable } from '@hello-pangea/dnd';
import { DragHandleHorizontalIcon } from '@radix-ui/react-icons';
import { Box, Button, Card, Flex, Text } from '@radix-ui/themes';
import { Button, Card, Flex, Text } from '@radix-ui/themes';
type TTaskCard = {
title?: string;
description?: string;
id?: string;
index?: number;
status: "todo" | "in-progress" | "completed";
};
export default function TaskCard(props: TTaskCard) {
@ -21,14 +21,6 @@ export default function TaskCard(props: TTaskCard) {
<Flex direction="column" gap="2">
<Text wrap="pretty">{props.title}</Text>
<Button>Mark completed</Button>
<Flex className="w-full !justify-center transition-transform">
<Box
className="px-10 cursor-grab"
{...provided.dragHandleProps}
>
<DragHandleHorizontalIcon />
</Box>
</Flex>
</Flex>
</Card>
)}

View File

@ -1,4 +1,12 @@
import { Button, Card, Heading, Text, TextField } from '@radix-ui/themes';
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';
@ -66,9 +74,15 @@ export default function LoginPage() {
<Button
// onClick={(e) => e.preventDefault()}
className="mt-4"
>
>
Sign In
</Button>
<Box className='w-full !flex justify-center'>
<Link href="/register">
<Text>Register</Text>
</Link>
</Box>
</Form.Root>
</Card>
</>

View File

@ -1,11 +1,24 @@
import { Button, Card, Heading, Text, TextField } from '@radix-ui/themes';
import {
Box,
Button,
Card,
Heading,
Link,
Text,
TextField,
} from '@radix-ui/themes';
import { Form } from 'radix-ui';
export default function RegisterPage() {
return (
<Card className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-fit">
<Heading size="4" className='text-center !mb-2'>Register</Heading>
<Form.Root className="flex flex-col gap-4" onSubmit={e => e.preventDefault()}>
<Heading size="4" className="text-center !mb-2">
Register
</Heading>
<Form.Root
className="flex flex-col gap-4"
onSubmit={(e) => e.preventDefault()}
>
<Form.Field name="email">
<Form.Message match="valueMissing">
<Text>Email is required</Text>
@ -42,7 +55,15 @@ export default function RegisterPage() {
</Form.Control>
</Form.Field>
<Button onClick={e => e.preventDefault()} className="mt-4">Register</Button>
<Button onClick={(e) => e.preventDefault()} className="mt-4">
Register
</Button>
<Box className="w-full !flex justify-center">
<Link href="/login">
<Text>Login</Text>
</Link>
</Box>
</Form.Root>
</Card>
);

View File

@ -1,4 +1,5 @@
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';
@ -6,7 +7,14 @@ import MainPage from '../pages/main/MainPage';
const MyRoutes = createRoutesFromElements(
<>
<Route path="/" element={<MainPage />}>
<Route
path="/"
element={
<AuthWrapper>
<MainPage />
</AuthWrapper>
}
>
<Route index element={<MainBoard />} />
</Route>

View File

@ -30,6 +30,22 @@ export const mainApi = createApi({
]
: [{ type: 'Task', id: 'LIST' }],
}),
getTasksForGroup: builder.query<any[], string>({
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<string, string>({
query: (id) => ({
url: `tasks/${id}`,
@ -64,7 +80,7 @@ export const mainApi = createApi({
// PROJECTS
createProject: builder.mutation({
query: (newProject) => ({
url: 'tasks/create',
url: 'projects/create',
method: 'POST',
body: newProject,
}),
@ -141,5 +157,5 @@ export const authApi = createApi({
});
export const { useLoginMutation, useRegisterMutation } = authApi;
export const { useGetTasksQuery, useCreateTaskMutation, useUpdateTaskMutation, useDeleteTaskMutation } = mainApi;
export const { useGetTasksQuery, useCreateTaskMutation, useUpdateTaskMutation, useDeleteTaskMutation, useGetTasksForGroupQuery } = mainApi;
export const { useGetProjectsQuery, useCreateProjectMutation, useUpdateProjectMutation, useDeleteProjectMutation } = mainApi;