7f50783600
Extracted TicketFilters, BulkActions, and TicketListItem into client/src/pages/tickets/. The main Tickets.tsx remains as the page orchestrator with state management and pagination. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
75 lines
2.6 KiB
TypeScript
75 lines
2.6 KiB
TypeScript
import { Link } from 'react-router-dom';
|
||
import { formatDistanceToNow } from 'date-fns';
|
||
import { SEVERITY_BG } from '../../lib/severityColors';
|
||
import SeverityBadge from '../../components/SeverityBadge';
|
||
import StatusBadge from '../../components/StatusBadge';
|
||
import Avatar from '../../components/Avatar';
|
||
import type { Ticket } from '../../types';
|
||
|
||
interface TicketListItemProps {
|
||
ticket: Ticket;
|
||
selected: boolean;
|
||
focused: boolean;
|
||
onToggle: () => void;
|
||
}
|
||
|
||
export default function TicketListItem({ ticket, selected, focused, onToggle }: TicketListItemProps) {
|
||
return (
|
||
<li
|
||
className={`flex items-center gap-3 px-4 py-3 transition-colors ${
|
||
focused
|
||
? 'bg-accent/50 ring-1 ring-inset ring-primary'
|
||
: 'hover:bg-accent/30'
|
||
}`}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={selected}
|
||
onChange={onToggle}
|
||
aria-label={`Select ${ticket.displayId}`}
|
||
className="cursor-pointer flex-shrink-0"
|
||
/>
|
||
<div
|
||
className={`w-1 self-stretch rounded-full flex-shrink-0 ${SEVERITY_BG[ticket.severity] ?? 'bg-gray-600'}`}
|
||
/>
|
||
<Link
|
||
to={`/${ticket.displayId}`}
|
||
className="flex-1 min-w-0 flex items-center gap-3 group"
|
||
>
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 mb-0.5 flex-wrap">
|
||
<span className="text-sm font-medium text-foreground group-hover:text-primary truncate">
|
||
{ticket.title}
|
||
</span>
|
||
<span className="text-xs font-mono text-muted-foreground">
|
||
{ticket.displayId}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center gap-2 text-xs text-muted-foreground flex-wrap">
|
||
<SeverityBadge severity={ticket.severity} />
|
||
<StatusBadge status={ticket.status} />
|
||
<span>
|
||
opened {formatDistanceToNow(new Date(ticket.createdAt), {
|
||
addSuffix: true,
|
||
})}{' '}
|
||
by {ticket.createdBy.displayName}
|
||
</span>
|
||
<span className="hidden md:inline">
|
||
· {ticket.category.name} › {ticket.type.name} › {ticket.item.name}
|
||
</span>
|
||
{ticket.assignee && (
|
||
<span className="hidden md:inline">· assigned {ticket.assignee.displayName}</span>
|
||
)}
|
||
<span>· {ticket._count?.comments ?? 0} comments</span>
|
||
</div>
|
||
</div>
|
||
<div className="hidden sm:flex items-center flex-shrink-0">
|
||
{ticket.assignee ? (
|
||
<Avatar name={ticket.assignee.displayName} size="sm" />
|
||
) : null}
|
||
</div>
|
||
</Link>
|
||
</li>
|
||
);
|
||
}
|