From 24313fd020ec94659479078fe5020d62f5ede4eb Mon Sep 17 00:00:00 2001 From: DeDxYk594 Date: Mon, 2 Dec 2024 00:44:49 +0300 Subject: [PATCH] =?UTF-8?q?Drag-n-drop=20=D0=B4=D0=BB=D1=8F=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BB=D0=BE=D0=BD=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/boards.ts | 1 + src/api/mocks/activeBoard.ts | 1 + src/api/users.ts | 2 +- src/components/Button.tsx | 9 +- src/components/KanbanColumn.tsx | 44 ++++++- src/components/button.scss | 3 + src/components/kanbanColumn.scss | 16 +-- src/containers/KanbanBoard.tsx | 206 +++++++++++++++++++++---------- src/stores/dndStore.ts | 8 +- src/types/activeBoard.ts | 1 + src/utils/validation.ts | 14 +-- 11 files changed, 220 insertions(+), 85 deletions(-) diff --git a/src/api/boards.ts b/src/api/boards.ts index ff58e1c..803ed7a 100644 --- a/src/api/boards.ts +++ b/src/api/boards.ts @@ -71,6 +71,7 @@ export const getBoardContent = async ( columnIndex.set(column.id, idx); return { ...column, + isStub: false, cards: [], // инициализация пустым массивом для типа BoardColumn }; } diff --git a/src/api/mocks/activeBoard.ts b/src/api/mocks/activeBoard.ts index 74b2215..cd16991 100644 --- a/src/api/mocks/activeBoard.ts +++ b/src/api/mocks/activeBoard.ts @@ -6,6 +6,7 @@ export const activeBoardMock: ActiveBoard = { columns: [ { id: 1, + isStub: false, title: 'Задачи', cards: [ { diff --git a/src/api/users.ts b/src/api/users.ts index 1266041..aabd3fa 100644 --- a/src/api/users.ts +++ b/src/api/users.ts @@ -139,7 +139,7 @@ export const loginUser = async ( return true; case HTTP_STATUS_UNAUTHORIZED: showToast('Неверные учётные данные', 'error'); - throw new Error('Неверные учетные данные'); + return false; default: showToast('Неизвестная ошибка', 'error'); throw new Error('Беды на бэке'); diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 7fe9bb0..e7cb99f 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,10 +1,11 @@ import { ComponentProps } from '@/jsxCore/types'; -import './button.scss' +import './button.scss'; interface ButtonProps extends ComponentProps { text?: string; icon?: string; callback?: (event: PointerEvent) => void; variant?: 'default' | 'negative' | 'positive' | 'accent' | 'transparent'; + extraRounded?: boolean; fullWidth?: true; // Флаг; указывает, должна ли кнопка принимать ширину родителя } @@ -17,7 +18,11 @@ export const Button = (props: ButtonProps) => { return (
diff --git a/src/components/KanbanColumn.tsx b/src/components/KanbanColumn.tsx index c4a9dc0..97ebdd4 100644 --- a/src/components/KanbanColumn.tsx +++ b/src/components/KanbanColumn.tsx @@ -12,17 +12,24 @@ import { useEffectRefs, useState } from '@/jsxCore/hooks'; import { Input } from './Input'; import './kanbanColumn.scss'; import { KanbanCard } from './KanbanCard'; -import { colHeaderHeights } from '@/stores/dndStore'; +import { colHeaderHeights, setDndStore, useDndStore } from '@/stores/dndStore'; interface KanbanColumnProps extends ComponentProps { columnIndex: number; columnId: number; } + +const DND_THRESHOLD = 10; + export const KanbanColumn = (props: KanbanColumnProps) => { const [isInputOpened, setIsInputOpened] = useState(false); const [newCardText, setNewCardText] = useState(''); const activeBoard = useActiveBoardStore() as ActiveBoard; const columnData = activeBoard.columns[props.columnIndex]; + const [dragStart, setDragStart] = useState< + [x: number, y: number] | undefined + >(undefined); + const [dragOffset, setDragOffset] = useState<[x: number, y: number]>([0, 0]); const submitCreateCard = (newText: string) => { if (newText.length < 3) { @@ -44,7 +51,38 @@ export const KanbanColumn = (props: KanbanColumnProps) => { }, 200); }); return ( -
+
{ + setDragStart([ev.x, ev.y]); + setDragOffset([ev.offsetX, ev.offsetY]); + }} + ON_mousemove={(ev: PointerEvent) => { + if (dragStart !== undefined) { + if ( + Math.sqrt( + Math.pow(dragStart[0] - ev.x, 2) + + Math.pow(dragStart[1] - ev.y, 2) + ) > DND_THRESHOLD + ) { + const dndStore = useDndStore(); + if (dndStore === undefined) { + setDndStore({ + type: 'column', + activeColumnIdx: props.columnIndex, + offset: dragOffset, + activeColumn: activeBoard.columns[props.columnIndex], + }); + console.log('offset', dragOffset); + setDragStart(undefined); + } + } + } + }} + ON_mouseleave={() => { + setDragStart(undefined); + }} + >
{ setIsInputOpened(true); }} fullWidth + extraRounded /> )} -
{activeBoard?.myRole !== 'viewer' && isInputOpened && (
{ }; return ( -
+
{!isOpened && (
@@ -210,17 +250,18 @@ interface CardPosition { cardId: number; } -const calculateCardPositions = ( +const calculateCardBoundingBoxes = ( columnIdx: number, cardIds: number[] ): CardPosition[] => { - const ret: CardPosition[] = []; + //Рассчитать реальные позиции карточек + const cardPositions: CardPosition[] = []; let accHeight = 74; accHeight += colHeaderHeights.get(columnIdx) ?? 0; accHeight += 8; const xCoord = 14 + columnIdx * 286 + 8; cardIds.forEach((cardId) => { - ret.push({ + cardPositions.push({ x: xCoord, y: accHeight, w: 256, @@ -230,7 +271,46 @@ const calculateCardPositions = ( accHeight += 8; accHeight += cardHeights.get(cardId) ?? 0; }); - return ret; + + // Пересчитать Bounding Box, сделав коррекцию на разницу высот карточек + const boundingBoxes: CardPosition[] = []; + cardIds.forEach((cardId, idx) => { + const currPos = cardPositions[idx]; + let topY: number = 0; + let bottomY: number = 0; + // Пересчёт верхней границы карточки + if (idx > 0) { + const prevPos = cardPositions[idx - 1]; + topY = (currPos.y + currPos.h + prevPos.y) / 2; + } else { + topY = currPos.y; + } + // Пересчёт нижней границы карточки + if (idx !== cardIds.length - 1) { + const nextPos = cardPositions[idx + 1]; + bottomY = (currPos.y + nextPos.y + nextPos.h) / 2; + } else { + bottomY = currPos.y + currPos.h; + } + boundingBoxes.push({ + x: currPos.x, + w: currPos.w, + y: topY, + h: bottomY - topY, + cardId, + }); + }); + return boundingBoxes; +}; + +const calculateColumnDestinationIdx = (x: number) => { + if (x < 14 + 272 / 2) { + return 0; + } + return Math.min( + (useActiveBoardStore()?.columns.length ?? 0) - 1, + Math.floor((x - 14 - 272 / 2) / (272 + 14) + 1) + ); }; // Вычислить индекс колонки, в которой находится карточка diff --git a/src/stores/dndStore.ts b/src/stores/dndStore.ts index e1c97e6..9274e10 100644 --- a/src/stores/dndStore.ts +++ b/src/stores/dndStore.ts @@ -1,8 +1,14 @@ import { defineStore } from '@/jsxCore/hooks'; +import { BoardColumn } from '@/types/activeBoard'; import { RealCard } from '@/types/card'; type DndType = undefined | CardDndType | ColumnDndType; -type ColumnDndType = never; +type ColumnDndType = { + type: 'column'; + activeColumn: BoardColumn; + activeColumnIdx: number; + offset: [x: number, y: number]; +}; interface CardDndType { type: 'card'; offset: [x: number, y: number]; diff --git a/src/types/activeBoard.ts b/src/types/activeBoard.ts index 4652a0c..999e775 100644 --- a/src/types/activeBoard.ts +++ b/src/types/activeBoard.ts @@ -4,6 +4,7 @@ import { UserToBoard } from './user'; export interface BoardColumn { id: number; + isStub: boolean; // Надо ли для этой колонки рендерить содержимое (нужно для Drag-n-Drop) title: string; cards: Card[]; } diff --git a/src/utils/validation.ts b/src/utils/validation.ts index d07a722..ed90e3b 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -3,7 +3,7 @@ interface IValidationResult { validationMessage: string | undefined; } -const EMAIL_ALLOWED_SYMBOLS = /[a-zA-Z0-9_@.-]*/; +const EMAIL_ALLOWED_SYMBOLS = /[a-zA-Z0-9_.@-]*/; const NICKNAME_ALLOWED_SYMBOLS = /[a-zA-Z0-9_.]*/; const EMAIL_REGEX = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i; @@ -37,13 +37,13 @@ export const validateNickname = (nickname: string): IValidationResult => { if (nickname.length < 3) { return { allowed: false, - validationMessage: 'Угодным считается только длиннее 3 символов', + validationMessage: 'Не менее 3 символов', }; } if (nickname.length > 20) { return { allowed: false, - validationMessage: 'Длинноват никнейм, 20 символов - высшая длина', + validationMessage: 'Не более 20 символов', }; } return { allowed: true, validationMessage: undefined }; @@ -53,16 +53,14 @@ export const validatePassword = (password: string): IValidationResult => { if (password === '') { return { allowed: false, validationMessage: undefined }; } + const validationMessage: string[] = []; if (password.length < 8) { - return { - allowed: false, - validationMessage: 'Пароль должен быть не меньше 8 символов', - }; + validationMessage.push('должен быт не менее 8 символов'); } if (password.length > 50) { return { allowed: false, - validationMessage: 'Великоват пароль! Укоротите хотя бы до 50 символов', + validationMessage: 'должен быть не более 50 символов', }; } return { allowed: true, validationMessage: undefined };