182 lines
6.6 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|