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

167 lines
5 KiB
TypeScript

import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Card as CardType } from '../../types/kanban';
import MessageSquareIcon from '../icons/MessageSquareIcon';
import CheckSquareIcon from '../icons/CheckSquareIcon';
import ChevronLeftIcon from '../icons/ChevronLeftIcon';
import { Link } from 'react-router-dom';
interface KanbanCardProps {
card: CardType;
onOpenModal: () => void;
}
export function KanbanCard({ card, onOpenModal }: KanbanCardProps) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: `CARD_${card.id}`,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
// Calculate checklist progress
const checklists = (card as any).checklists || [];
const totalItems = checklists.reduce(
(sum: number, checklist: any) => sum + checklist.items.length,
0
);
const completedItems = checklists.reduce(
(sum: number, checklist: any) =>
sum + checklist.items.filter((item: any) => item.state === 'complete').length,
0
);
const hasChecklists = totalItems > 0;
// Calculate comment count
const comments = (card as any).comments || [];
const commentCount = comments.length;
const hasComments = commentCount > 0;
// Get labels
const labels = (card as any).labels || [];
const hasLabels = labels.length > 0;
// Get epic
const epic = (card as any).epic;
const hasEpic = epic !== null && epic !== undefined;
// Get parent card name
const parentCardName = card.parent_card_name;
const hasParent = !!parentCardName;
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
onClick={(e) => {
// Only open modal if not clicking title link
if (!(e.target as HTMLElement).closest('a')) {
onOpenModal();
}
}}
className="bg-gray-700 rounded-lg p-4 mb-3 cursor-pointer hover:bg-gray-600 transition-colors border border-gray-600 shadow-sm"
>
{/* Parent Card Indicator */}
{hasParent && (
<div className="mb-2">
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium bg-purple-900/40 text-purple-300 border border-purple-700/50">
<span className="w-2.5 h-2.5">
<ChevronLeftIcon />
</span>
{parentCardName}
</span>
</div>
)}
{/* Epic Badge */}
{hasEpic && (
<div className="mb-2">
<span
className="px-2 py-0.5 rounded text-xs font-medium"
style={{
backgroundColor: epic.color ? `${epic.color}33` : '#3b82f633',
color: epic.color || '#3b82f6',
border: `1px solid ${epic.color || '#3b82f6'}66`,
}}
>
{epic.name}
</span>
</div>
)}
{/* Labels */}
{hasLabels && (
<div className="flex flex-wrap gap-1 mb-2">
{labels.slice(0, 3).map((label: any) => (
<span
key={label.id}
className="px-2 py-0.5 rounded text-xs font-medium text-white"
style={{ backgroundColor: label.color }}
>
{label.name}
</span>
))}
{labels.length > 3 && (
<span className="px-2 py-0.5 rounded text-xs font-medium text-gray-400">
+{labels.length - 3}
</span>
)}
</div>
)}
<h3 className="text-white font-medium mb-2 hover:text-blue-400 transition-colors">
<Link
to={`/boards/${card.board_id}/cards/${card.id}`}
onClick={(e) => e.stopPropagation()}
className="hover:underline"
>
{card.name}
</Link>
</h3>
{card.description && (
<p className="text-gray-400 text-sm mb-3 line-clamp-2">{card.description}</p>
)}
{/* Badges */}
<div className="flex items-center gap-3 text-xs text-gray-400 mt-3 pt-2 border-t border-gray-600">
{/* Comments Badge */}
{hasComments && (
<div className="flex items-center gap-1">
<span className="w-3.5 h-3.5">
<MessageSquareIcon />
</span>
<span>{commentCount}</span>
</div>
)}
{/* Checklist Badge */}
{hasChecklists && (
<div className="flex items-center gap-1">
<span className="w-3.5 h-3.5">
<CheckSquareIcon />
</span>
<span>
{completedItems}/{totalItems}
</span>
</div>
)}
{/* Due Date Badge */}
{card.due && (
<span className={card.due_complete ? 'text-green-400' : 'text-yellow-400'}>
Due: {new Date(card.due).toLocaleDateString()}
</span>
)}
{/* Card ID */}
<span className="text-gray-500">#{card.id_short}</span>
</div>
</div>
);
}