kanban-app/frontend/src/pages/BoardDetail.tsx

249 lines
7.6 KiB
TypeScript
Raw Normal View History

import { useParams, Link, useNavigate } from 'react-router-dom';
import { useBoard } from '../hooks/useBoard';
import { useCardMutations } from '../hooks/useCardMutations';
import { useListMutations } from '../hooks/useListMutations';
import { KanbanColumn } from '../components/kanban/KanbanColumn';
2026-02-27 07:53:36 +00:00
import { CreateListModal } from '../components/kanban/CreateListModal';
import { useModal } from '../context/modals/useModal';
import {
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
DragOverEvent,
PointerSensor,
useSensor,
useSensors,
closestCenter,
} from '@dnd-kit/core';
2026-02-27 07:53:36 +00:00
import { Card as CardType } from '../types/kanban';
import { useState } from 'react';
export function BoardDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { board, fetchBoard } = useBoard(parseInt(id || '0'));
2026-02-27 07:53:36 +00:00
const { createCard, moveCard } = useCardMutations(parseInt(id || '0'), fetchBoard);
const { createList } = useListMutations(parseInt(id || '0'), fetchBoard);
2026-02-27 07:53:36 +00:00
const { openModal } = useModal();
const [activeCard, setActiveCard] = useState<CardType | null>(null);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
})
);
const handleDragStart = (event: DragStartEvent) => {
const { active } = event;
2026-02-27 07:53:36 +00:00
const activeIdStr = (active.id as string).split('_')[1];
const cardId = parseInt(activeIdStr as string);
if (board) {
const card = board.lists.flatMap((list) => list.cards).find((c) => c.id === cardId);
2026-02-27 07:53:36 +00:00
// console.log('---handleDragStart', event, card)
// console.log('---handleDragStart.board', board)
if (card) {
setActiveCard(card);
}
}
};
const handleDragOver = (event: DragOverEvent) => {
const { active, over } = event;
2026-02-27 12:41:44 +00:00
// console.log('---handleDragOver', event);
if (!over) return;
2026-02-27 07:53:36 +00:00
// const activeId = parseInt(active.id as string);
// const overId = parseInt(over.id as string);
2026-02-27 07:53:36 +00:00
const overIdStr = (over.id as string).split('_')[1];
const overId = parseInt(overIdStr, 10);
const activeIdStr = (active.id as string).split('_')[1];
const activeId = parseInt(activeIdStr, 10);
if (activeId === overId) return;
2026-02-27 07:53:36 +00:00
// Find active card and its current list
if (!board) return;
const activeList = board.lists.find((list) => list.cards.some((card) => card.id === activeId));
2026-02-27 07:53:36 +00:00
// If we're hovering over a card in same list, do nothing
if (activeList) {
const overCard = activeList.cards.find((card) => card.id === overId);
if (overCard) return;
}
};
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
setActiveCard(null);
if (!over || !board) return;
2026-02-27 07:53:36 +00:00
// console.log('--------------over', over)
// console.log('--------------board', board)
const [overType, overIdStr] = (over.id as string).split('_');
const overId = parseInt(overIdStr, 10);
const activeIdStr = (active.id as string).split('_')[1];
const activeId = parseInt(activeIdStr, 10);
// debugger
2026-02-27 07:53:36 +00:00
if (active.id === over.id) return;
2026-02-27 07:53:36 +00:00
// Find active card
let activeCard: CardType | undefined;
let activeList: (typeof board.lists)[0] | undefined;
for (const list of board.lists) {
const card = list.cards.find((c) => c.id === activeId);
if (card) {
activeCard = card;
activeList = list;
break;
}
}
if (!activeCard || !activeList) return;
// Check if we're dropping on a list or a card
2026-02-27 07:53:36 +00:00
// debugger
if (overType.toLocaleLowerCase() === 'list') {
const overList = board.lists.find((list) => list.id === overId);
// Dropping on a list - append to end
2026-02-27 07:53:36 +00:00
if (!overList || overList.id === activeList.id) return; // Same list, do nothing
2026-02-27 07:53:36 +00:00
await moveCard(activeCard, activeList.id, overList.id, overList.cards.length);
return;
}
// Dropping on a card - find which list it belongs to
let overCard: CardType | undefined;
let overListContainingCard: (typeof board.lists)[0] | undefined;
for (const list of board.lists) {
const card = list.cards.find((c) => c.id === overId);
if (card) {
overCard = card;
overListContainingCard = list;
break;
}
}
if (!overCard || !overListContainingCard) return;
// Calculate new position
const overCardIndex = overListContainingCard.cards.findIndex((c) => c.id === overId);
2026-02-27 12:41:44 +00:00
// // If dropping on to same list and after of same card, do nothing
// if (
// overListContainingCard.id === activeList.id &&
// overCardIndex === activeList.cards.findIndex((c) => c.id === activeId) + 1
// ) {
// console.log('--------------over', over)
// console.log('--------------board', board)
// console.log('--------------activeCard', activeCard)
// console.log('--------------overListContainingCard', overListContainingCard)
// return;
// }
2026-02-27 07:53:36 +00:00
await moveCard(activeCard, activeList.id, overListContainingCard.id, overCardIndex);
};
const handleCardClick = (card: CardType) => {
navigate(`/boards/${id}/cards/${card.id}`);
};
2026-02-27 07:53:36 +00:00
const handleAddList = () => {
openModal((props) => (
<CreateListModal
{...props}
onCreate={async (name) => {
await createList(name, board ? board.lists.length : 0);
fetchBoard(); // Refresh board data
}}
/>
));
};
2026-02-27 07:53:36 +00:00
const handleAddCard =
(listId: number) => async (data: { name: string; description?: string }) => {
await createCard(listId, {
name: data.name,
description: data.description,
pos: board ? board.lists.find((list) => list.id === listId)?.cards.length || 0 : 0,
});
fetchBoard(); // Refresh board data
};
if (!board) {
return <div className="text-white">Loading...</div>;
}
return (
<div className="space-y-6">
<div className="flex justify-between items-start">
<div>
<Link to="/boards" className="text-gray-400 hover:text-white transition-colors text-sm">
Back to Boards
</Link>
<h1 className="text-3xl font-bold text-white mt-2">{board.name}</h1>
{board.description && <p className="text-gray-400 mt-1">{board.description}</p>}
</div>
<div className="flex gap-2">
<Link
to={`/boards/${id}/edit`}
className="bg-gray-700 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors"
>
Edit Board
</Link>
<button
onClick={handleAddList}
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors"
>
+ Add List
</button>
</div>
</div>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
<div className="flex gap-4 overflow-x-auto pb-4">
{board.lists.map((list) => (
<KanbanColumn
key={list.id}
list={list}
cards={list.cards}
onCardClick={handleCardClick}
2026-02-27 07:53:36 +00:00
onCardCreate={handleAddCard(list.id)}
/>
))}
</div>
<DragOverlay>
{activeCard ? (
<div className="bg-gray-700 rounded-lg p-4 cursor-grabbing border border-gray-600 shadow-lg opacity-80">
<h3 className="text-white font-medium">{activeCard.name}</h3>
</div>
) : null}
</DragOverlay>
</DndContext>
</div>
);
}