167 lines
5 KiB
TypeScript
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>
|
|
);
|
|
}
|