still rainbow vommit

This commit is contained in:
Max 2025-03-03 20:15:12 +03:00
parent f857ae3e2d
commit 8032f415dc
9 changed files with 324 additions and 54 deletions

View File

@ -1,9 +1,9 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import { getRepository } from 'typeorm'; import { getRepository } from 'typeorm';
import { Project } from './entities/Project'; import { Project } from './entities/Project';
import { ProjectMember } from './entities/ProjectMember' import { ProjectMember } from './entities/ProjectMember';
import { Task } from './entities/Task'; import { Task } from './entities/Task';
import jwt from 'jsonwebtoken';
import { User } from './entities/User'; import { User } from './entities/User';
const SECRET_KEY = process.env.JWT_SECRET || 'your_secret_key'; const SECRET_KEY = process.env.JWT_SECRET || 'your_secret_key';

View File

@ -6,8 +6,11 @@ import {
useGetTasksForGroupQuery, useGetTasksForGroupQuery,
} from '../../services/mainApi'; } from '../../services/mainApi';
import TaskCard from '../TaskCard/TaskCard'; import TaskCard from '../TaskCard/TaskCard';
import CreateTaskDialog from './CreateTaskDialog/CreateTaskDialog'; import AddUserToProjectDialog from '../dialogs/AddUserToProjectDialog/AddUserToProjectDialog';
import DeleteProjectDialog from './DeleteProjectDialog/CreateTaskDialog'; import CreateTaskDialog from '../dialogs/CreateTaskDialog/CreateTaskDialog';
import DeleteProjectDialog from '../dialogs/DeleteProjectDialog/CreateTaskDialog';
import { PlusIcon } from '@radix-ui/react-icons';
type TCardGroup = { type TCardGroup = {
id: string; id: string;
@ -56,14 +59,19 @@ export default function CardGroup(props: TCardGroup) {
</Box> </Box>
<Flex gap={'2'} className="mx-auto w-full mt-2"> <Flex gap={'2'} className="mx-auto w-full mt-2">
<CreateTaskDialog onClose={createTask}> <CreateTaskDialog onClose={createTask}>
<Button>Add Task</Button> <Button className='!grow-1'>Add Task</Button>
</CreateTaskDialog> </CreateTaskDialog>
<DeleteProjectDialog onClose={deleteGroup}> <DeleteProjectDialog onClose={deleteGroup}>
<Button color="red" onClick={deleteGroup}> <Button className='!grow-1' color="red">Delete Project</Button>
Delete Project
</Button>
</DeleteProjectDialog> </DeleteProjectDialog>
</Flex> </Flex>
<AddUserToProjectDialog projectId={+props.id}>
<Button className='!mt-2'>
<PlusIcon />
Add user
</Button>
</AddUserToProjectDialog>
</Box> </Box>
)} )}
</Droppable> </Droppable>

View File

@ -2,41 +2,24 @@ import { DragDropContext } from '@hello-pangea/dnd';
import { Button, Flex, ScrollArea } from '@radix-ui/themes'; import { Button, Flex, ScrollArea } from '@radix-ui/themes';
import { import {
useCreateProjectMutation, useCreateProjectMutation,
useCreateTaskMutation, useGetProjectsQuery
useGetProjectsQuery,
} from '../../services/mainApi'; } from '../../services/mainApi';
import CardGroup from '../CardGroup/CardGroup'; import CardGroup from '../CardGroup/CardGroup';
import CreateProjectDialog from '../dialogs/CreateProjectDialog/CreateProjectDialog';
export default function MainBoard() { export default function MainBoard() {
const dragEndHandle = (result: TDragResult) => { const dragEndHandle = (result: TDragResult) => {
result; result;
}; };
const [create] = useCreateTaskMutation();
const [createProject] = useCreateProjectMutation(); const [createProject] = useCreateProjectMutation();
const { data: cringe, isLoading } = useGetProjectsQuery({}); const { data: cringe, isLoading } = useGetProjectsQuery({});
const onClick = () => {
create({
title: 'My Task 123',
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 ( return (
<> <>
<DragDropContext onDragEnd={dragEndHandle}> <DragDropContext onDragEnd={dragEndHandle}>
<ScrollArea scrollbars='horizontal' className='pb-3'> <ScrollArea scrollbars="horizontal" className="pb-3">
<Flex gap={'2'} className='min-w-fit'> <Flex gap={'2'} className="min-w-fit">
{!isLoading && {!isLoading &&
(cringe as any[]).map((item: any) => ( (cringe as any[]).map((item: any) => (
<CardGroup id={item.id} title={item.title} /> <CardGroup id={item.id} title={item.title} />
@ -44,7 +27,10 @@ export default function MainBoard() {
</Flex> </Flex>
</ScrollArea> </ScrollArea>
</DragDropContext> </DragDropContext>
<Button onClick={onClick1}>Create project</Button>
<CreateProjectDialog onCreate={createProject}>
<Button>Create project</Button>
</CreateProjectDialog>
</> </>
); );
} }

View File

@ -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<number | null>(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 (
<Dialog.Root onOpenChange={() => refetch()}>
<Dialog.Trigger>{children}</Dialog.Trigger>
<Dialog.Content maxWidth="450px">
<Dialog.Title>Add User to Project</Dialog.Title>
<Flex direction="column" gap="3" mt="4">
{isLoading ? (
<span>Loading users...</span>
) : error ? (
<span>Error loading users.</span>
) : (
<Select.Root
value={selectedUserId?.toString() || ''}
onValueChange={(value) =>
setSelectedUserId(Number(value))
}
required
>
<Select.Trigger aria-label="User">
{selectedUserId
? users?.find(
(user: any) =>
user.id === selectedUserId,
)?.username
: 'Select a user'}
</Select.Trigger>
<Select.Content>
{users?.map((user: any) => (
<Select.Item key={user.id} value={user.id}>
{user.username}
</Select.Item>
))}
</Select.Content>
</Select.Root>
)}
</Flex>
<Flex gap="3" mt="4" justify="end">
<Dialog.Close>
<Button variant="soft" color="gray">
Cancel
</Button>
</Dialog.Close>
<Dialog.Close>
<Button
color="green"
onClick={handleAddUser}
disabled={!selectedUserId || isAdding}
>
{isAdding ? 'Adding...' : 'Add'}
</Button>
</Dialog.Close>
</Flex>
</Dialog.Content>
</Dialog.Root>
);
}

View File

@ -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 (
<Dialog.Root>
<Dialog.Trigger>
{props.children}
</Dialog.Trigger>
<Dialog.Content maxWidth="450px">
<Dialog.Title>Create a New Project</Dialog.Title>
<Flex direction="column" gap="3" mt="4">
<TextField.Root
value={title}
onChange={(e) => setTitle(e.target.value)}
required
></TextField.Root>
<TextArea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Optional description"
/>
</Flex>
<Flex gap="3" mt="4" justify="end">
<Dialog.Close>
<Button variant="soft" color="gray">
Cancel
</Button>
</Dialog.Close>
<Dialog.Close>
<Button
color="green"
onClick={handleCreate}
disabled={!title}
>
Create
</Button>
</Dialog.Close>
</Flex>
</Dialog.Content>
</Dialog.Root>
);
}

View File

@ -8,8 +8,33 @@ import {
TextField, TextField,
} from '@radix-ui/themes'; } from '@radix-ui/themes';
import { Form } from 'radix-ui'; import { Form } from 'radix-ui';
import { useState } from 'react';
import { useRegisterMutation } from '../../../services/mainApi';
export default function RegisterPage() { export default function RegisterPage() {
const [register, { isLoading, error }] = useRegisterMutation();
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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 ( return (
<Card className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-fit"> <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"> <Heading size="4" className="text-center !mb-2">
@ -17,8 +42,27 @@ export default function RegisterPage() {
</Heading> </Heading>
<Form.Root <Form.Root
className="flex flex-col gap-4" className="flex flex-col gap-4"
onSubmit={(e) => e.preventDefault()} onSubmit={handleSubmit}
> >
<Form.Field name="username">
<Form.Message match="valueMissing">
<Text>Username is required</Text>
</Form.Message>
<Form.Control asChild>
<TextField.Root
type="text"
name="username"
placeholder="Username"
required
value={formData.username}
onChange={handleChange}
>
<TextField.Slot />
</TextField.Root>
</Form.Control>
</Form.Field>
<Form.Field name="email"> <Form.Field name="email">
<Form.Message match="valueMissing"> <Form.Message match="valueMissing">
<Text>Email is required</Text> <Text>Email is required</Text>
@ -31,8 +75,11 @@ export default function RegisterPage() {
<Form.Control asChild> <Form.Control asChild>
<TextField.Root <TextField.Root
type="email" type="email"
name="email"
placeholder="Email" placeholder="Email"
required required
value={formData.email}
onChange={handleChange}
> >
<TextField.Slot /> <TextField.Slot />
</TextField.Root> </TextField.Root>
@ -49,16 +96,24 @@ export default function RegisterPage() {
type="password" type="password"
placeholder="Password" placeholder="Password"
required required
value={formData.password}
onChange={handleChange}
> >
<TextField.Slot /> <TextField.Slot />
</TextField.Root> </TextField.Root>
</Form.Control> </Form.Control>
</Form.Field> </Form.Field>
<Button onClick={(e) => e.preventDefault()} className="mt-4"> <Button type="submit" disabled={isLoading} className="mt-4">
Register Register
</Button> </Button>
{error && (
<Box className="text-red-500">
<Text>Error registering user.</Text>
</Box>
)}
<Box className="w-full !flex justify-center"> <Box className="w-full !flex justify-center">
<Link href="/login"> <Link href="/login">
<Text>Login</Text> <Text>Login</Text>

View File

@ -3,7 +3,7 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const mainApi = createApi({ export const mainApi = createApi({
reducerPath: 'api', reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseQuery: fetchBaseQuery({
baseUrl: 'http://109.107.166.17:3000/api/', baseUrl: 'http://109.107.166.17:5000/api/',
prepareHeaders: (headers) => { prepareHeaders: (headers) => {
headers.set( headers.set(
'Authorization', 'Authorization',
@ -12,7 +12,7 @@ export const mainApi = createApi({
return headers; return headers;
}, },
}), }),
tagTypes: ['Task', 'Project'], tagTypes: ['Task', 'Project', 'ProjectMembers'],
endpoints: (builder) => ({ endpoints: (builder) => ({
getTasks: builder.query<string[], void>({ getTasks: builder.query<string[], void>({
query: () => ({ query: () => ({
@ -57,7 +57,7 @@ export const mainApi = createApi({
query: (task) => ({ query: (task) => ({
url: `tasks/${task.id}`, url: `tasks/${task.id}`,
method: 'PATCH', method: 'PATCH',
body: {status: task.status}, body: { status: task.status },
}), }),
invalidatesTags: (result, error, id) => [{ type: 'Task', id }], invalidatesTags: (result, error, id) => [{ type: 'Task', id }],
}), }),
@ -88,7 +88,7 @@ export const mainApi = createApi({
}), }),
getProjects: builder.query({ getProjects: builder.query({
query: () => ({ query: () => ({
url: 'projects', url: 'projects/my',
method: 'GET', method: 'GET',
}), }),
providesTags: (result, error, id) => [ providesTags: (result, error, id) => [
@ -115,7 +115,40 @@ export const mainApi = createApi({
url: `projects/${id}`, url: `projects/${id}`,
method: 'DELETE', method: 'DELETE',
}), }),
invalidatesTags: (result, error, id) => [{ type: 'Project', id }], 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' },
],
}), }),
}), }),
}); });
@ -123,24 +156,24 @@ export const mainApi = createApi({
export const authApi = createApi({ export const authApi = createApi({
reducerPath: 'authApi', reducerPath: 'authApi',
baseQuery: fetchBaseQuery({ baseQuery: fetchBaseQuery({
baseUrl: 'http://109.107.166.17:3000/api/auth/', baseUrl: 'http://109.107.166.17:4000/api/',
}), }),
endpoints: (builder) => ({ endpoints: (builder) => ({
login: builder.mutation({ login: builder.mutation({
query: (credentials) => ({ query: (credentials) => ({
url: 'login', url: '/auth/login',
method: 'POST', method: 'POST',
body: credentials, body: credentials,
}), }),
async onQueryStarted(arg, { queryFulfilled }) { async onQueryStarted(arg, { queryFulfilled }) {
try { try {
const response = await queryFulfilled; const response = await queryFulfilled;
if (response.data) { if (response.data.access_token) {
localStorage.setItem( localStorage.setItem(
'token', 'token',
response.data.access_token, response.data.access_token,
); );
if (response.meta?.response?.status === 201)
window.location.href = '/'; window.location.href = '/';
} }
} catch (error) {} } catch (error) {}
@ -148,14 +181,51 @@ export const authApi = createApi({
}), }),
register: builder.mutation({ register: builder.mutation({
query: (credentials) => ({ query: (credentials) => ({
url: 'register', url: '/auth/register',
method: 'POST', method: 'POST',
body: credentials, 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 } = authApi; export const { useLoginMutation, useRegisterMutation, useGetAllUsersQuery } =
export const { useGetTasksQuery, useCreateTaskMutation, useUpdateTaskMutation, useDeleteTaskMutation, useGetTasksForGroupQuery } = mainApi; authApi;
export const { useGetProjectsQuery, useCreateProjectMutation, useUpdateProjectMutation, useDeleteProjectMutation } = mainApi;
export const {
useGetTasksQuery,
useCreateTaskMutation,
useUpdateTaskMutation,
useDeleteTaskMutation,
useGetTasksForGroupQuery,
} = mainApi;
export const {
useGetProjectsQuery,
useCreateProjectMutation,
useUpdateProjectMutation,
useDeleteProjectMutation,
} = mainApi;
export const {
useRemoveProjectMemberMutation,
useAddProjectMemberMutation,
useGetProjectMembersQuery,
} = mainApi;