kanban-app/frontend/src/components/kanban/KanbanColumn.tsx

167 lines
5.2 KiB
TypeScript
Raw Normal View History

import { useDroppable } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { ListWithCards, Card as CardType } from '../../types/kanban';
import { KanbanCard } from './KanbanCard';
2026-02-27 07:53:36 +00:00
import { CreateCardModal } from './CreateCardModal';
2026-02-27 19:38:38 +00:00
import { EditListModal } from './EditListModal';
import { DeleteListModal } from './DeleteListModal';
import Edit2Icon from '../icons/Edit2Icon';
import Trash2Icon from '../icons/Trash2Icon';
2026-02-27 07:53:36 +00:00
import { useModal } from '../../context/modals/useModal';
2026-02-27 20:26:25 +00:00
export interface KanbanColumnProps {
list: ListWithCards;
cards: CardType[];
2026-02-27 19:25:34 +00:00
onOpenCardModal: (card: CardType) => void;
2026-02-27 07:53:36 +00:00
onCardCreate: (data: { name: string; description?: string }) => Promise<void>;
2026-02-27 19:38:38 +00:00
onListEdit?: (name: string) => Promise<void>;
onListDelete?: () => Promise<void>;
2026-02-27 20:26:25 +00:00
dragHandleProps?: {
attributes: any;
listeners: any;
};
}
2026-02-27 19:38:38 +00:00
export function KanbanColumn({
list,
cards,
onOpenCardModal,
onCardCreate,
onListEdit,
onListDelete,
2026-02-27 20:26:25 +00:00
dragHandleProps,
2026-02-27 19:38:38 +00:00
}: KanbanColumnProps) {
const { setNodeRef, isOver } = useDroppable({
2026-02-27 07:53:36 +00:00
id: `LIST_${list.id}`,
});
2026-02-27 07:53:36 +00:00
const { openModal } = useModal();
const handleAddCard = () => {
openModal((props) => <CreateCardModal {...props} onCreate={onCardCreate} />);
};
2026-02-27 19:38:38 +00:00
const handleEditList = () => {
if (!onListEdit) return;
openModal((props) => (
<EditListModal
{...props}
listName={list.name}
onSave={async (name) => {
await onListEdit(name);
}}
/>
));
};
const handleDeleteList = () => {
if (!onListDelete) return;
openModal((props) => (
<DeleteListModal
{...props}
listName={list.name}
onDelete={async () => {
await onListDelete();
}}
/>
));
};
return (
2026-02-27 07:53:36 +00:00
<div className="bg-gray-800 rounded-lg p-4 min-w-[300px] max-w-[300px] border border-gray-700 flex flex-col">
2026-02-27 19:38:38 +00:00
<div className="mb-4">
<div className="flex items-center justify-between mb-2">
2026-02-27 20:26:25 +00:00
<div className="flex items-center gap-2 flex-1">
{/* Drag Handle Icon */}
<div {...dragHandleProps?.attributes} {...dragHandleProps?.listeners}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-gray-500 cursor-grab hover:text-gray-300"
>
<circle cx="9" cy="12" r="1"></circle>
<circle cx="9" cy="5" r="1"></circle>
<circle cx="9" cy="19" r="1"></circle>
<circle cx="15" cy="12" r="1"></circle>
<circle cx="15" cy="5" r="1"></circle>
<circle cx="15" cy="19" r="1"></circle>
</svg>
</div>
<h2 className="text-white font-bold text-lg">{list.name}</h2>
</div>
2026-02-27 19:38:38 +00:00
<div className="flex items-center gap-2">
{onListEdit && (
<button
onClick={handleEditList}
className="text-gray-400 hover:text-blue-400 transition-colors"
title="Edit list"
>
<span className="w-4 h-4">
<Edit2Icon />
</span>
</button>
)}
{onListDelete && (
<button
onClick={handleDeleteList}
className="text-gray-400 hover:text-red-400 transition-colors"
title="Delete list"
>
<span className="w-4 h-4">
<Trash2Icon />
</span>
</button>
)}
</div>
</div>
<span className="bg-gray-600 text-gray-300 text-xs px-2 py-1 rounded-full inline-block">
{cards.length} cards
</span>
2026-02-27 19:38:38 +00:00
</div>
<SortableContext
id={list.id.toString()}
items={cards.map((card) => card.id.toString())}
strategy={verticalListSortingStrategy}
>
<div
ref={setNodeRef}
2026-02-27 07:53:36 +00:00
className={`min-h-[200px] flex-1 transition-colors ${isOver ? 'bg-gray-750' : ''}`}
>
{cards.map((card) => (
2026-02-27 19:25:34 +00:00
<KanbanCard key={card.id} card={card} onOpenModal={() => onOpenCardModal(card)} />
))}
</div>
</SortableContext>
2026-02-27 07:53:36 +00:00
<button
onClick={handleAddCard}
className="mt-3 w-full py-2 px-4 bg-gray-700 hover:bg-gray-600 text-gray-300 hover:text-white rounded-lg transition-colors flex items-center justify-center gap-2 text-sm font-medium"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Add Card
</button>
</div>
);
}