Compare commits

...

2 Commits

Author SHA1 Message Date
b40b2e4430 del old version 2025-04-02 00:06:19 +03:00
c77dd23dd1 fixed mobx 2025-04-02 00:05:33 +03:00
13 changed files with 42 additions and 209 deletions

View File

@ -44,14 +44,15 @@ export class ProjectsController {
return this.projectsService.findAll(); return this.projectsService.findAll();
} }
@ApiOperation({ summary: 'Get my projects' }) @ApiOperation({ summary: 'Get all projects where I participate' })
@ApiResponse({ status: 200, description: 'List of projects', type: Project, isArray: true }) @ApiResponse({ status: 200, description: 'List of projects', type: Project, isArray: true })
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Get('my') @Get('my')
async findMyProjects(@Request() req): Promise<Project[]> { async findUserProjects(@Request() req): Promise<Project[]> {
return this.projectsService.findByOwner(req.user.sub); return this.projectsService.findUserProjects(req.user.sub);
} }
@ApiOperation({ summary: 'Get single project by ID' }) @ApiOperation({ summary: 'Get single project by ID' })
@ApiParam({ name: 'id', type: 'number' }) @ApiParam({ name: 'id', type: 'number' })
@ApiResponse({ status: 200, description: 'Found project', type: Project }) @ApiResponse({ status: 200, description: 'Found project', type: Project })

View File

@ -29,6 +29,14 @@ export class ProjectsService {
return this.projectsRepository.find({ where: { ownerId } }); return this.projectsRepository.find({ where: { ownerId } });
} }
async findUserProjects(userId: number): Promise<Project[]> {
return this.projectsRepository
.createQueryBuilder('project')
.leftJoin('project_members', 'member', 'member.projectId = project.id')
.where('project.ownerId = :userId OR member.userId = :userId', { userId })
.getMany();
}
async findOneById(id: number): Promise<Project | null> { async findOneById(id: number): Promise<Project | null> {
return this.projectsRepository.findOne({ where: { id } }); return this.projectsRepository.findOne({ where: { id } });
} }

View File

@ -46,7 +46,7 @@ export class AuthController {
description: 'User created successfully', description: 'User created successfully',
}) })
@ApiResponse({ @ApiResponse({
status: 409, status: 401,
description: 'Username already exists', description: 'Username already exists',
}) })
@Post('register') @Post('register')

View File

@ -1,11 +1,8 @@
import { Theme } from '@radix-ui/themes'; import { Theme } from '@radix-ui/themes';
import '@radix-ui/themes/styles.css'; import '@radix-ui/themes/styles.css';
import { QueryClientProvider } from '@tanstack/react-query';
import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import queryClient from './api/queryClient';
import './App.css'; import './App.css';
import MyRoutes from './routes/routes'; import MyRoutes from './routes/routes';
import React from 'react';
import { appStore } from './stores/AppStore'; import { appStore } from './stores/AppStore';
import { Ctx } from './context'; import { Ctx } from './context';

View File

@ -9,6 +9,6 @@ export const axiosBase = axios.create({
export const axiosAuth = axios.create({ export const axiosAuth = axios.create({
baseURL: BASE_URL, baseURL: BASE_URL,
headers: { headers: {
Authorization: `Bearer ${localStorage.getItem('token')}` // Maybe we will use cookies Authorization: `Bearer ${localStorage.getItem('token')}`
} }
}); });

View File

@ -1,11 +0,0 @@
import { QueryClient } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 2,
},
},
})
export default queryClient;

View File

@ -9,7 +9,6 @@ import AddUserToProjectDialog from '../dialogs/AddUserToProjectDialog/AddUserToP
import CreateTaskDialog from '../dialogs/CreateTaskDialog/CreateTaskDialog'; import CreateTaskDialog from '../dialogs/CreateTaskDialog/CreateTaskDialog';
import DeleteProjectDialog from '../dialogs/DeleteProjectDialog/CreateTaskDialog'; import DeleteProjectDialog from '../dialogs/DeleteProjectDialog/CreateTaskDialog';
// Импортируем наш mobx-store
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
type TCardGroup = { type TCardGroup = {
@ -19,18 +18,15 @@ type TCardGroup = {
function CardGroup(props: TCardGroup) { function CardGroup(props: TCardGroup) {
// При монтировании (и при изменении `props.id`) грузим задачи для проекта
const store = useStore(); const store = useStore();
useEffect(() => { useEffect(() => {
store.mainStore.fetchTasksForGroup(props.id).catch(console.error); store.mainStore.fetchTasksForGroup(props.id).catch(console.error);
}, [props.id, store.mainStore]); }, [props.id, store.mainStore]);
// Получаем задачи для данного проекта из стора (или пустой массив, если их нет)
const tasks = store.mainStore.tasksByProject.get(props.id) ?? []; const tasks = store.mainStore.tasksByProject.get(props.id) ?? [];
const isLoading = store.mainStore.isLoading; // общее isLoading из стора const isLoading = store.mainStore.isLoading;
// Создать задачу
const createTask = (taskText: string, date: string) => { const createTask = (taskText: string, date: string) => {
store.mainStore.createTask({ store.mainStore.createTask({
title: taskText, title: taskText,
@ -40,7 +36,6 @@ function CardGroup(props: TCardGroup) {
}); });
}; };
// Удалить проект (аналог deleteProjectMutation)
const deleteGroup = () => { const deleteGroup = () => {
store.mainStore.deleteProject(props.id); store.mainStore.deleteProject(props.id);
}; };

View File

@ -7,22 +7,17 @@ import CardGroup from '../CardGroup/CardGroup';
import CreateProjectDialog from '../dialogs/CreateProjectDialog/CreateProjectDialog'; import CreateProjectDialog from '../dialogs/CreateProjectDialog/CreateProjectDialog';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
type TDragResult = any; // или определите, если надо
function MainBoard() { function MainBoard() {
const store = useStore(); const store = useStore();
// Загрузка проектов при монтировании
useEffect(() => { useEffect(() => {
store.mainStore.fetchProjects().catch(console.error); store.mainStore.fetchProjects().catch(console.error);
}, []); }, []);
const dragEndHandle = (result: TDragResult) => { const dragEndHandle = (result: TDragResult) => {
// Логика перетаскивания, если нужно
console.log(result); console.log(result);
}; };
// Для кнопки "Create project"
const createProject = (newProjectData: any) => { const createProject = (newProjectData: any) => {
store.mainStore.createProject(newProjectData); store.mainStore.createProject(newProjectData);
}; };

View File

@ -2,7 +2,6 @@ import { Draggable } from '@hello-pangea/dnd';
import { Badge, Button, Card, Flex, Text } from '@radix-ui/themes'; import { Badge, Button, Card, Flex, Text } from '@radix-ui/themes';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { useEffect } from 'react';
type TTaskCard = { type TTaskCard = {
title?: string; title?: string;
@ -36,10 +35,6 @@ const TaskCard = observer((props: TTaskCard) => {
// @ts-ignore // @ts-ignore
console.log(`rerendered with`, store.mainStore.tasksByProject.get(2) ); console.log(`rerendered with`, store.mainStore.tasksByProject.get(2) );
useEffect(() => {
console.log(store.mainStore.fakeData)
}, [store.mainStore.fakeData])
return ( return (
<Draggable draggableId={props.id || '123'} index={props.index || 0}> <Draggable draggableId={props.id || '123'} index={props.index || 0}>
@ -50,7 +45,6 @@ const TaskCard = observer((props: TTaskCard) => {
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
> >
<h1>{store.mainStore.fakeData.toString()}</h1>
<Flex direction="column" gap="2"> <Flex direction="column" gap="2">
<Flex justify={'between'}> <Flex justify={'between'}>
<Text wrap="pretty">{props.title}</Text> <Text wrap="pretty">{props.title}</Text>
@ -83,11 +77,6 @@ const TaskCard = observer((props: TTaskCard) => {
Completed Completed
</Button> </Button>
)} )}
<Button onClick={() => {
store.mainStore.fetchPosts()
}}>
posts
</Button>
</Flex> </Flex>
</Card> </Card>
)} )}

View File

@ -2,7 +2,6 @@ import { PropsWithChildren, useEffect, useState } from 'react';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { Button, Dialog, Flex, Select } from '@radix-ui/themes'; import { Button, Dialog, Flex, Select } from '@radix-ui/themes';
// Импортируем сторы
import { useStore } from '../../../hooks/useStore'; import { useStore } from '../../../hooks/useStore';
type TAddUserToProjectDialog = { type TAddUserToProjectDialog = {
@ -15,8 +14,7 @@ function AddUserToProjectDialog(props: TAddUserToProjectDialog) {
const store = useStore(); const store = useStore();
// При открытии диалога (или при ререндере) можно обновлять список пользователей
// (В RTK Query был refetch. Тут можно просто вызвать fetchAllUsers(force=true) если надо всегда свежий список)
useEffect(() => { useEffect(() => {
store.authStore.fetchAllUsers(); store.authStore.fetchAllUsers();
}, []); }, []);
@ -24,7 +22,6 @@ function AddUserToProjectDialog(props: TAddUserToProjectDialog) {
const handleAddUser = async () => { const handleAddUser = async () => {
if (selectedUserId) { if (selectedUserId) {
try { try {
// В store.mainStore метод addProjectMember(projectId: string, memberId: string/number)
await store.mainStore.addProjectMember(projectId.toString(), selectedUserId.toString()); await store.mainStore.addProjectMember(projectId.toString(), selectedUserId.toString());
setSelectedUserId(null); setSelectedUserId(null);
} catch (err) { } catch (err) {

View File

@ -1,4 +1,4 @@
import { makeAutoObservable, runInAction } from 'mobx'; import { makeAutoObservable } from 'mobx';
import { MainStore } from './MainStore'; import { MainStore } from './MainStore';
import { AuthStore } from './AuthStore'; import { AuthStore } from './AuthStore';

View File

@ -2,18 +2,13 @@ import { makeAutoObservable, runInAction } from 'mobx';
import axios from 'axios'; import axios from 'axios';
import { AppStore } from './AppStore'; import { AppStore } from './AppStore';
/**
* AuthStore
* Хранит логику для авторизации, регистрации и получения всех пользователей.
*/
export class AuthStore { export class AuthStore {
appStore: AppStore appStore: AppStore
// Можно также хранить здесь данные о текущем пользователе, ошибках и т.д.
isLoading = false; isLoading = false;
error: string | null = null; error: string | null = null;
// Список всех пользователей (если вам нужно его кэшировать)
allUsers: any[] = []; allUsers: any[] = [];
constructor(appStore:any) { constructor(appStore:any) {
@ -25,9 +20,6 @@ export class AuthStore {
return localStorage.getItem('token') || ''; return localStorage.getItem('token') || '';
} }
/**
* Логин: отправляет запрос на сервер и сохраняет токен в localStorage
*/
async login(credentials: { username: string; password: string }) { async login(credentials: { username: string; password: string }) {
this.isLoading = true; this.isLoading = true;
this.error = null; this.error = null;
@ -40,7 +32,6 @@ export class AuthStore {
runInAction(() => { runInAction(() => {
if (response.data.access_token) { if (response.data.access_token) {
localStorage.setItem('token', response.data.access_token); localStorage.setItem('token', response.data.access_token);
// Если нужно перенаправить после логина:
window.location.href = '/'; window.location.href = '/';
} }
this.isLoading = false; this.isLoading = false;
@ -54,9 +45,6 @@ export class AuthStore {
} }
} }
/**
* Регистрация: отправляет запрос на сервер
*/
async register(credentials: { username: string; password: string }) { async register(credentials: { username: string; password: string }) {
this.isLoading = true; this.isLoading = true;
this.error = null; this.error = null;
@ -67,7 +55,6 @@ export class AuthStore {
); );
runInAction(() => { runInAction(() => {
// Если регистрация успешна, например, статус 201 перенаправляем на /login
if (response.status === 201) { if (response.status === 201) {
window.location.href = '/login'; window.location.href = '/login';
} }
@ -82,11 +69,7 @@ export class AuthStore {
} }
} }
/**
* Получить всех пользователей
*/
async fetchAllUsers(force = false) { async fetchAllUsers(force = false) {
// Если уже есть пользователи и не запрошен force, просто возвращаем
if (this.allUsers.length && !force) { if (this.allUsers.length && !force) {
return this.allUsers; return this.allUsers;
} }

View File

@ -2,40 +2,18 @@ import { makeAutoObservable, runInAction } from 'mobx';
import axios from 'axios'; import axios from 'axios';
import { AppStore } from './AppStore'; import { AppStore } from './AppStore';
export type Posts = Post[]
export interface Post {
userId: number
id: number
title: string
body: string
}
/**
* MainStore
* Хранит логику для задач, проектов и участников проектов
*/
export class MainStore { export class MainStore {
appStore: AppStore appStore: AppStore
// ----- Общие поля -----
isLoading = false; isLoading = false;
error: string | null = null; error: string | null = null;
// ----- Задачи -----
tasks: any[] = []; tasks: any[] = [];
// Кэшированный список задач по ID проекта
tasksByProject = new Map<string, any[]>(); tasksByProject = new Map<string, any[]>();
// ----- Проекты -----
projects: any[] = []; projects: any[] = [];
// Детальные данные по конкретному проекту (если нужно кэшировать)
projectById = new Map<string, any>(); projectById = new Map<string, any>();
fakeData: Posts = []
// ----- Участники проекта -----
// Если нужно кэшировать участников по проекту
projectMembersById = new Map<string, any[]>(); projectMembersById = new Map<string, any[]>();
constructor(appStore:any) { constructor(appStore:any) {
@ -43,75 +21,18 @@ export class MainStore {
makeAutoObservable(this); makeAutoObservable(this);
} }
/**
* Получить токен из localStorage
*/
get token() { get token() {
return localStorage.getItem('token') || ''; return localStorage.getItem('token') || '';
} }
/**
* Заготовка для заголовков (проставляем Bearer токен)
*/
get headers() { get headers() {
return { return {
Authorization: `Bearer ${this.token}`, Authorization: `Bearer ${this.token}`,
}; };
} }
/**
* --------------------------------------------------------------------------
* ЗАДАЧИ
* --------------------------------------------------------------------------
*/
/**
* Получить все задачи (кэширование: если уже загружены, второй раз не грузим)
*/
async fetchPosts(){
fetch('https://jsonplaceholder.typicode.com/posts?limit=10')
.then(response => response.json())
.then(json => {
this.fakeData = json
console.log(this.fakeData);
})
}
async fetchTasks(force = false) {
console.error(`Aboba`);
if (this.tasks.length && !force) {
return this.tasks;
}
this.isLoading = true;
this.error = null;
try {
const response = await axios.get('http://109.107.166.17:5000/api/tasks', {
headers: this.headers,
});
runInAction(() => {
console.log(`This is all tasks`,response.data);
this.tasks = response.data;
this.isLoading = false;
});
return this.tasks;
} catch (error: any) {
runInAction(() => {
this.error = error?.response?.data?.message || 'Ошибка при загрузке задач';
this.isLoading = false;
});
throw error;
}
}
/**
* Получить задачи для конкретного проекта (группы)
*/
async fetchTasksForGroup(projectId: string, force = false) { async fetchTasksForGroup(projectId: string, force = false) {
if (this.tasksByProject.has(projectId) && !force) { if (this.tasksByProject.has(projectId) && !force) {
return this.tasksByProject.get(projectId); return this.tasksByProject.get(projectId);
@ -129,8 +50,6 @@ export class MainStore {
}, },
); );
runInAction(() => { runInAction(() => {
console.log(`This is tasks for ${projectId}`,response.data);
this.tasksByProject.set(projectId, response.data); this.tasksByProject.set(projectId, response.data);
this.isLoading = false; this.isLoading = false;
}); });
@ -144,9 +63,6 @@ export class MainStore {
} }
} }
/**
* Получить одну задачу по ID
*/
async fetchTask(taskId: string) { async fetchTask(taskId: string) {
this.isLoading = true; this.isLoading = true;
this.error = null; this.error = null;
@ -169,18 +85,17 @@ export class MainStore {
} }
} }
/**
* Создать задачу
*/
async createTask(newTask: any) { async createTask(newTask: any) {
try { try {
await axios.post( const response = await axios.post(
'http://109.107.166.17:5000/api/tasks/create', 'http://109.107.166.17:5000/api/tasks/create',
newTask, newTask,
{ headers: this.headers }, { headers: this.headers },
); );
// Инвалидируем кэш (чтобы при следующем getTasks() данные были актуальны) const createdTask = response.data;
this.tasks = []; this.tasks.push(createdTask);
const oldTasks = this.tasksByProject.get(newTask.projectId) ?? [];
this.tasksByProject.set(newTask.projectId, [...oldTasks, createdTask]);
} catch (error: any) { } catch (error: any) {
runInAction(() => { runInAction(() => {
this.error = error?.response?.data?.message || 'Ошибка при создании задачи'; this.error = error?.response?.data?.message || 'Ошибка при создании задачи';
@ -189,22 +104,25 @@ export class MainStore {
} }
} }
/**
* Обновить задачу
*/
async updateTask(task: { id: string; status: string }) { async updateTask(task: { id: string; status: string }) {
try { try {
await axios.patch( const response = await axios.patch(
`http://109.107.166.17:5000/api/tasks/${task.id}`, `http://109.107.166.17:5000/api/tasks/${task.id}`,
{ status: task.status }, { status: task.status },
{ headers: this.headers }, { headers: this.headers },
); );
// Локально обновим статус в массиве tasks
runInAction(() => { runInAction(() => {
const index = this.tasks.findIndex((t) => t.id === task.id); const index = this.tasks.findIndex((t) => t.id === task.id);
if (index !== -1) { if (index !== -1) {
this.tasks[index].status = task.status; this.tasks[index].status = task.status;
} }
const projectId = response.data.project.id;
const oldArray = this.tasksByProject.get(projectId) || [];
const indexInMap = oldArray.findIndex((t) => t.id === +task.id);
if (indexInMap !== -1) {
oldArray[indexInMap].status = task.status;
this.tasksByProject.set(projectId, [...oldArray]);
}
}); });
} catch (error: any) { } catch (error: any) {
runInAction(() => { runInAction(() => {
@ -214,9 +132,7 @@ export class MainStore {
} }
} }
/**
* Удалить задачу
*/
async deleteTask(taskId: string) { async deleteTask(taskId: string) {
try { try {
await axios.delete(`http://109.107.166.17:5000/api/tasks/${taskId}`, { await axios.delete(`http://109.107.166.17:5000/api/tasks/${taskId}`, {
@ -234,15 +150,7 @@ export class MainStore {
} }
} }
/**
* --------------------------------------------------------------------------
* ПРОЕКТЫ
* --------------------------------------------------------------------------
*/
/**
* Получить все проекты текущего пользователя
*/
async fetchProjects(force = false) { async fetchProjects(force = false) {
if (this.projects.length && !force) { if (this.projects.length && !force) {
return this.projects; return this.projects;
@ -269,11 +177,7 @@ export class MainStore {
} }
} }
/**
* Получить данные одного проекта по ID
*/
async fetchProject(projectId: string) { async fetchProject(projectId: string) {
// Если хотим кэшировать отдельно проекты по id
if (this.projectById.has(projectId)) { if (this.projectById.has(projectId)) {
return this.projectById.get(projectId); return this.projectById.get(projectId);
} }
@ -299,18 +203,17 @@ export class MainStore {
} }
} }
/**
* Создать проект
*/
async createProject(newProject: any) { async createProject(newProject: any) {
try { try {
await axios.post( const response = await axios.post(
'http://109.107.166.17:5000/api/projects/create', 'http://109.107.166.17:5000/api/projects/create',
newProject, newProject,
{ headers: this.headers }, { headers: this.headers },
); );
// Инвалидируем кэш const created = response.data;
this.projects = []; runInAction(() => {
this.projects.push(created);
});
} catch (error: any) { } catch (error: any) {
runInAction(() => { runInAction(() => {
this.error = error?.response?.data?.message || 'Ошибка при создании проекта'; this.error = error?.response?.data?.message || 'Ошибка при создании проекта';
@ -319,9 +222,6 @@ export class MainStore {
} }
} }
/**
* Обновить проект
*/
async updateProject(id: string, projectData: any) { async updateProject(id: string, projectData: any) {
try { try {
await axios.patch( await axios.patch(
@ -329,7 +229,6 @@ export class MainStore {
projectData, projectData,
{ headers: this.headers }, { headers: this.headers },
); );
// Можно либо перезагрузить список проектов, либо локально обновить
this.fetchProjects(true); this.fetchProjects(true);
} catch (error: any) { } catch (error: any) {
runInAction(() => { runInAction(() => {
@ -339,9 +238,6 @@ export class MainStore {
} }
} }
/**
* Удалить проект
*/
async deleteProject(id: string) { async deleteProject(id: string) {
try { try {
await axios.delete(`http://109.107.166.17:5000/api/projects/${id}`, { await axios.delete(`http://109.107.166.17:5000/api/projects/${id}`, {
@ -359,15 +255,6 @@ export class MainStore {
} }
} }
/**
* --------------------------------------------------------------------------
* УЧАСТНИКИ ПРОЕКТА
* --------------------------------------------------------------------------
*/
/**
* Добавить участника в проект
*/
async addProjectMember(projectId: string, memberId: string) { async addProjectMember(projectId: string, memberId: string) {
try { try {
await axios.post( await axios.post(
@ -378,7 +265,6 @@ export class MainStore {
}, },
{ headers: this.headers }, { headers: this.headers },
); );
// Инвалидируем список участников
this.projectMembersById.delete(projectId); this.projectMembersById.delete(projectId);
} catch (error: any) { } catch (error: any) {
runInAction(() => { runInAction(() => {
@ -387,10 +273,6 @@ export class MainStore {
throw error; throw error;
} }
} }
/**
* Получить участников проекта
*/
async fetchProjectMembers(projectId: string, force = false) { async fetchProjectMembers(projectId: string, force = false) {
if (this.projectMembersById.has(projectId) && !force) { if (this.projectMembersById.has(projectId) && !force) {
return this.projectMembersById.get(projectId); return this.projectMembersById.get(projectId);
@ -418,9 +300,6 @@ export class MainStore {
} }
} }
/**
* Удалить участника из проекта
*/
async removeProjectMember(projectId: string, memberId: string) { async removeProjectMember(projectId: string, memberId: string) {
try { try {
await axios.delete( await axios.delete(
@ -430,7 +309,7 @@ export class MainStore {
headers: this.headers, headers: this.headers,
}, },
); );
// Инвалидируем список участников
this.projectMembersById.delete(projectId); this.projectMembersById.delete(projectId);
} catch (error: any) { } catch (error: any) {
runInAction(() => { runInAction(() => {