gitdataai/src/app/project/board/KanbanBoard.tsx

182 lines
6.6 KiB
TypeScript

import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useBoardDetailQuery } from "@/hooks/useBoardsQuery";
import { useBoardOperations } from "@/hooks/useBoardOperations";
import { LoadingState } from "@/components/ui/LoadingState";
import { ErrorState } from "@/components/ui/ErrorState";
import { AlertCircle, Plus } from "lucide-react";
import type { CardResponse } from "@/client/model";
import { BOARD_PAGE } from "@/css/app/board-styles";
import { BoardHeader } from "./BoardHeader";
import { BoardColumn } from "./BoardColumn";
import { BoardModals } from "./BoardModals";
interface KanbanBoardProps {
projectName: string;
boardId: string;
}
export function KanbanBoard({ projectName, boardId }: KanbanBoardProps) {
const navigate = useNavigate();
const { data: boardDetail, isLoading, refetch } = useBoardDetailQuery(projectName, boardId);
const ops = useBoardOperations(projectName);
const [isCreateColumnOpen, setIsCreateColumnOpen] = useState(false);
const [isCreateCardOpen, setIsCreateCardOpen] = useState<string | false>(false);
const [selectedCard, setSelectedCard] = useState<CardResponse | null>(null);
const [newColumnName, setNewColumnName] = useState("");
const [newCardTitle, setNewCardTitle] = useState("");
const [newCardDescription, setNewCardDescription] = useState("");
const [editCardTitle, setEditCardTitle] = useState("");
const [editCardDescription, setEditCardDescription] = useState("");
const handleCreateColumn = async () => {
if (!newColumnName.trim()) return;
await ops.createColumn.mutateAsync({
boardId,
params: { name: newColumnName.trim(), position: boardDetail?.columns.length || 0 },
});
setIsCreateColumnOpen(false);
setNewColumnName("");
refetch();
};
const handleCreateCard = async () => {
if (!newCardTitle.trim() || !isCreateCardOpen) return;
await ops.createCard.mutateAsync({
column_id: isCreateCardOpen,
title: newCardTitle.trim(),
description: newCardDescription.trim() || undefined,
});
setIsCreateCardOpen(false);
setNewCardTitle("");
setNewCardDescription("");
refetch();
};
const handleUpdateCard = async () => {
if (!selectedCard || !editCardTitle.trim()) return;
await ops.updateCard.mutateAsync({
id: selectedCard.id,
params: { title: editCardTitle.trim(), description: editCardDescription.trim() || null },
});
setSelectedCard(null);
refetch();
};
const handleMoveCard = async (cardId: string, targetColumnId: string) => {
await ops.moveCard.mutateAsync({
id: cardId,
params: { target_column_id: targetColumnId, position: 0 },
});
refetch();
};
const handleDeleteBoard = async () => {
if (confirm("Are you sure you want to delete this board?")) {
await ops.deleteBoard.mutateAsync(boardId);
navigate(`/${projectName}/board`);
}
};
const handleDeleteCard = async () => {
if (!selectedCard) return;
if (confirm("Delete this card?")) {
await ops.deleteCard.mutateAsync(selectedCard.id);
setSelectedCard(null);
refetch();
}
};
if (isLoading) return <LoadingState message="Opening board..." />;
if (!boardDetail)
return (
<ErrorState
title="Board not found"
message="This board might have been deleted."
onRetry={() => navigate(`/${projectName}/board`)}
/>
);
return (
<>
<div className={BOARD_PAGE.container}>
<BoardHeader
projectName={projectName}
boardName={boardDetail.board.name}
boardDescription={boardDetail.board.description ?? undefined}
onDeleteBoard={handleDeleteBoard}
onAddColumn={() => setIsCreateColumnOpen(true)}
/>
<div className={BOARD_PAGE.kanbanWrapper}>
{boardDetail.columns.map((col) => (
<BoardColumn
key={col.column.id}
column={col}
allColumns={boardDetail.columns}
onAddCard={(columnId) => setIsCreateCardOpen(columnId)}
onDeleteColumn={(columnId) => {
if (confirm("Delete column?")) {
ops.deleteColumn.mutate(columnId);
refetch();
}
}}
onCardClick={(card) => {
setSelectedCard(card);
setEditCardTitle(card.title);
setEditCardDescription(card.description || "");
}}
onMoveCard={handleMoveCard}
/>
))}
{boardDetail.columns.length === 0 && (
<div className="flex-1 flex flex-col items-center justify-center border-2 border-dashed border-border/40 rounded-2xl bg-muted/5 min-h-[300px]">
<AlertCircle className="w-10 h-10 opacity-10 mb-4" />
<h4 className="text-[14px] font-bold text-muted-foreground">Empty Board</h4>
<p className="text-[12px] text-muted-foreground/60 mt-1 mb-6 text-center max-w-[240px]">
Add your first column (e.g. To Do, Doing, Done) to start tracking work.
</p>
<button
onClick={() => setIsCreateColumnOpen(true)}
className="flex items-center gap-1.5 px-5 py-2 rounded-lg bg-accent text-accent-fg font-bold text-[13px]"
>
<Plus className="w-4 h-4" />
Add First Column
</button>
</div>
)}
</div>
<BoardModals
isCreateColumnOpen={isCreateColumnOpen}
newColumnName={newColumnName}
isCreatingColumn={ops.createColumn.isPending}
onNewColumnNameChange={setNewColumnName}
onCreateColumn={handleCreateColumn}
onCloseColumn={() => { setIsCreateColumnOpen(false); setNewColumnName(""); }}
isCreateCardOpen={isCreateCardOpen}
newCardTitle={newCardTitle}
newCardDescription={newCardDescription}
isCreatingCard={ops.createCard.isPending}
onNewCardTitleChange={setNewCardTitle}
onNewCardDescriptionChange={setNewCardDescription}
onCreateCard={handleCreateCard}
onCloseCard={() => { setIsCreateCardOpen(false); setNewCardTitle(""); setNewCardDescription(""); }}
selectedCard={selectedCard}
editCardTitle={editCardTitle}
editCardDescription={editCardDescription}
isUpdatingCard={ops.updateCard.isPending}
onEditTitleChange={setEditCardTitle}
onEditDescriptionChange={setEditCardDescription}
onUpdateCard={handleUpdateCard}
onDeleteCard={handleDeleteCard}
onCloseDetail={() => setSelectedCard(null)}
/>
</div>
</>
);
}