Files
stack/apps/web/src/components/kanban/TaskCard.tsx
Jason Woltje ac1f2c176f
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
fix: Resolve all ESLint errors and warnings in web package
Fixes all 542 ESLint problems in the web package to achieve 0 errors and 0 warnings.

Changes:
- Fixed 144 issues: nullish coalescing, return types, unused variables
- Fixed 118 issues: unnecessary conditions, type safety, template literals
- Fixed 79 issues: non-null assertions, unsafe assignments, empty functions
- Fixed 67 issues: explicit return types, promise handling, enum comparisons
- Fixed 45 final warnings: missing return types, optional chains
- Fixed 25 typecheck-related issues: async/await, type assertions, formatting
- Fixed JSX.Element namespace errors across 90+ files

All Quality Rails violations resolved. Lint and typecheck both pass with 0 problems.

Files modified: 118 components, tests, hooks, and utilities

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-31 00:10:03 -06:00

165 lines
5.0 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
"use client";
import React from "react";
import type { Task } from "@mosaic/shared";
import { TaskPriority } from "@mosaic/shared";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Calendar, Flag, User } from "lucide-react";
import { format } from "date-fns";
interface TaskCardProps {
task: Task & { assignee?: { name: string; image?: string | null } };
}
const priorityConfig = {
[TaskPriority.HIGH]: {
label: "High",
className: "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400",
},
[TaskPriority.MEDIUM]: {
label: "Medium",
className: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
},
[TaskPriority.LOW]: {
label: "Low",
className: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400",
},
};
/**
* Generate initials from a name (e.g., "John Doe" -> "JD")
*/
function getInitials(name: string): string {
return name
.split(" ")
.map((part) => part[0])
.join("")
.toUpperCase()
.slice(0, 2);
}
/**
* Task Card component for Kanban board
*
* Displays:
* - Task title
* - Priority badge
* - Assignee avatar (if assigned)
* - Due date (if set)
*/
export function TaskCard({ task }: TaskCardProps): React.ReactElement {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: task.id,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
const isOverdue =
task.dueDate && new Date(task.dueDate) < new Date() && task.status !== "COMPLETED";
const isDueSoon =
task.dueDate &&
!isOverdue &&
new Date(task.dueDate).getTime() - new Date().getTime() < 3 * 24 * 60 * 60 * 1000; // 3 days
const priorityInfo = priorityConfig[task.priority];
return (
<article
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className={`
bg-white dark:bg-gray-800
rounded-lg shadow-sm border border-gray-200 dark:border-gray-700
p-4 space-y-3
cursor-grab active:cursor-grabbing
transition-all duration-200
hover:shadow-md hover:border-gray-300 dark:hover:border-gray-600
${isDragging ? "opacity-50" : "opacity-100"}
`}
>
{/* Task Title */}
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 line-clamp-2">
{task.title}
</h4>
{/* Task Metadata */}
<div className="flex items-center gap-2 flex-wrap">
{/* Priority Badge */}
<span
data-priority={task.priority}
className={`
inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs font-medium
${priorityInfo.className}
`}
>
<Flag className="w-3 h-3" aria-hidden="true" />
{priorityInfo.label}
</span>
{/* Due Date */}
{task.dueDate && (
<span
className={`
inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs
${
isOverdue
? "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"
: isDueSoon
? "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
: "bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400"
}
`}
>
<Calendar className="w-3 h-3" aria-label="Due date" />
{format(new Date(task.dueDate), "MMM d")}
</span>
)}
</div>
{/* Assignee Avatar */}
{task.assignee && (
<div className="flex items-center gap-2 pt-2 border-t border-gray-100 dark:border-gray-700">
{task.assignee.image ? (
<img
src={task.assignee.image}
alt={task.assignee.name}
className="w-6 h-6 rounded-full object-cover"
/>
) : (
<div
className="w-6 h-6 rounded-full bg-indigo-500 text-white flex items-center justify-center text-xs font-medium"
aria-label={`Assigned to ${task.assignee.name}`}
>
{getInitials(task.assignee.name)}
</div>
)}
<span className="text-xs text-gray-600 dark:text-gray-400 truncate">
{task.assignee.name}
</span>
</div>
)}
{/* Fallback for unassigned tasks */}
{!task.assignee && task.assigneeId && (
<div className="flex items-center gap-2 pt-2 border-t border-gray-100 dark:border-gray-700">
<div
className="w-6 h-6 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center"
aria-label="Assigned user"
>
<User className="w-4 h-4 text-gray-600 dark:text-gray-400" />
</div>
<span className="text-xs text-gray-500 dark:text-gray-500">Assigned</span>
</div>
)}
</article>
);
}