Files
ColaFlow-Web/components/features/kanban/StoryCard.tsx
Yaojia Wang 358ee9b7f4 perf(frontend): Optimize component rendering with React.memo and hooks - Sprint 3 Story 2
Add React.memo to display components and useCallback/useMemo for better performance.

Changes:
- Added React.memo to TaskCard component
- Added React.memo to StoryCard component
- Added React.memo to KanbanBoard component
- Added React.memo to KanbanColumn component
- Added useCallback to kanban page drag handlers (handleDragStart, handleDragEnd)
- Added useCallback to epics page handlers (handleDelete, getStatusColor, getPriorityColor)
- Added useMemo for expensive computations in dashboard page (stats, recentProjects sorting)
- Added useMemo for total tasks calculation in KanbanBoard
- Removed unused isConnected variable from kanban page

Performance improvements:
- Reduced unnecessary re-renders in Card components
- Optimized list rendering performance with memoized callbacks
- Improved filtering and sorting performance with useMemo
- Better React DevTools Profiler metrics

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 19:57:07 +01:00

144 lines
4.6 KiB
TypeScript

'use client';
import React, { useMemo } from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Story } from '@/types/project';
import { FileText, FolderKanban, Clock, CheckSquare } from 'lucide-react';
interface StoryCardProps {
story: Story;
epicName?: string;
taskCount?: number;
}
export const StoryCard = React.memo(function StoryCard({ story, epicName, taskCount }: StoryCardProps) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: story.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
const priorityColors = {
Low: 'bg-gray-100 text-gray-700 border-gray-300',
Medium: 'bg-blue-100 text-blue-700 border-blue-300',
High: 'bg-orange-100 text-orange-700 border-orange-300',
Critical: 'bg-red-100 text-red-700 border-red-300',
};
const statusColors = {
Backlog: 'bg-gray-100 text-gray-600',
Todo: 'bg-blue-100 text-blue-600',
InProgress: 'bg-yellow-100 text-yellow-700',
Done: 'bg-green-100 text-green-700',
};
// Get assignee initials
const assigneeInitials = useMemo(() => {
if (!story.assigneeId) return null;
// For now, just use first two letters. In real app, fetch user data
return story.assigneeId.substring(0, 2).toUpperCase();
}, [story.assigneeId]);
// Calculate progress (if both estimated and actual hours exist)
const hoursDisplay = useMemo(() => {
if (story.estimatedHours) {
if (story.actualHours) {
return `${story.actualHours}/${story.estimatedHours}h`;
}
return `0/${story.estimatedHours}h`;
}
return null;
}, [story.estimatedHours, story.actualHours]);
return (
<Card
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className="cursor-grab active:cursor-grabbing hover:shadow-md transition-shadow"
>
<CardContent className="p-3 space-y-2">
{/* Header: Story icon + Task count */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FileText className="w-4 h-4 text-green-600" />
<span className="text-xs font-medium text-gray-600">Story</span>
</div>
{taskCount !== undefined && taskCount > 0 && (
<Badge variant="secondary" className="text-xs">
<CheckSquare className="w-3 h-3 mr-1" />
{taskCount} {taskCount === 1 ? 'task' : 'tasks'}
</Badge>
)}
</div>
{/* Epic breadcrumb */}
{epicName && (
<div className="flex items-center gap-1 text-xs text-gray-500">
<FolderKanban className="w-3 h-3" />
<span className="truncate max-w-[200px]" title={epicName}>
{epicName}
</span>
</div>
)}
{/* Title */}
<h3 className="text-sm font-medium line-clamp-2" title={story.title}>
{story.title}
</h3>
{/* Description (if available) */}
{story.description && (
<p className="text-xs text-gray-600 line-clamp-2" title={story.description}>
{story.description}
</p>
)}
{/* Footer: Priority, Hours, Assignee */}
<div className="flex items-center justify-between pt-2 border-t">
<div className="flex items-center gap-2">
<Badge
variant="outline"
className={`${priorityColors[story.priority]} text-xs`}
>
{story.priority}
</Badge>
<Badge
variant="outline"
className={`${statusColors[story.status]} text-xs`}
>
{story.status}
</Badge>
</div>
<div className="flex items-center gap-2">
{/* Hours display */}
{hoursDisplay && (
<div className="flex items-center gap-1 text-xs text-gray-500">
<Clock className="w-3 h-3" />
<span>{hoursDisplay}</span>
</div>
)}
{/* Assignee avatar */}
{assigneeInitials && (
<Avatar className="h-6 w-6">
<AvatarFallback className="text-xs">
{assigneeInitials}
</AvatarFallback>
</Avatar>
)}
</div>
</div>
</CardContent>
</Card>
);
});