refactor(dashboard): move drag-and-drop source logic into KanbanCard component

This commit is contained in:
Chneemann 2026-08-10 12:49:05 +02:00
parent a9919a045c
commit 1033f99d3f
No known key found for this signature in database
2 changed files with 28 additions and 41 deletions

View file

@ -1,6 +1,6 @@
/** /**
* @file dashboard/KanbanCard.tsx * @file dashboard/KanbanCard.tsx
* @description Client component rendering a single kanban card with support for priority indicators, due date alerts, team avatars, and a mobile status dropdown. * @description Client component rendering a single kanban card with native drag-and-drop source handlers, dynamic priority styling, and mobile status dropdown.
*/ */
"use client"; "use client";
@ -54,6 +54,17 @@ export default function KanbanCard({ task, onStatusChange }: KanbanCardProps) {
(a) => a !== task.creator, (a) => a !== task.creator,
); );
/**
* Initiates the drag action on a task card, storing its ID and current status in the data transfer payload.
*
* @param {React.DragEvent<HTMLDivElement>} e - The drag event object.
*/
const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => {
e.dataTransfer.setData("text/plain", task.id);
e.dataTransfer.setData("sourceStatus", task.status);
e.dataTransfer.effectAllowed = "move";
};
/** /**
* Handles shifting the task to a new status category. * Handles shifting the task to a new status category.
* *
@ -69,18 +80,20 @@ export default function KanbanCard({ task, onStatusChange }: KanbanCardProps) {
}; };
return ( return (
<div className="flex flex-col h-full gap-3"> <div
draggable
onDragStart={handleDragStart}
className="group relative bg-card/40 border border-border/80 hover:border-primary/60 p-4 rounded-2xl shadow-sm hover:shadow-xl transition-all duration-300 space-y-3 cursor-grab active:cursor-grabbing hover:-translate-y-0.5 flex flex-col h-full"
>
{/* --- Card Header (Title & Priority) --- */} {/* --- Card Header (Title & Priority) --- */}
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
{/* Title */}
<h3 className="font-semibold leading-snug group-hover/card:text-primary transition-colors line-clamp-2"> <h3 className="font-semibold leading-snug group-hover/card:text-primary transition-colors line-clamp-2">
{task.title} {task.title}
</h3> </h3>
{/* Priority Badge */}
{task.priority && ( {task.priority && (
<span <span
className={`gap-1.5 px-2.5 py-1 rounded-lg text-xs font-bold border-2 ${ className={`gap-1.5 px-2.5 py-1 rounded-lg text-xs font-bold border-2 shrink-0 ${
task.priority === "high" task.priority === "high"
? "bg-rose-500/10 text-rose-500 border-rose-500/20" ? "bg-rose-500/10 text-rose-500 border-rose-500/20"
: task.priority === "medium" : task.priority === "medium"
@ -98,9 +111,9 @@ export default function KanbanCard({ task, onStatusChange }: KanbanCardProps) {
{task.description} {task.description}
</p> </p>
{/* --- Card Footer (Date, Mobile Action Menu, Avatars) --- */} {/* --- Card Footer --- */}
<div className="flex items-center justify-between pt-3 mt-auto text-xs border-t border-border"> <div className="flex items-center justify-between pt-3 mt-auto text-xs border-t border-border">
{/* Left Side: Date Badge */} {/* Date Badge */}
{task.dueDate && ( {task.dueDate && (
<div <div
className={`group/date relative inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-semibold border border-border transition-all duration-200 ease-out cursor-default ${ className={`group/date relative inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-semibold border border-border transition-all duration-200 ease-out cursor-default ${
@ -116,8 +129,6 @@ export default function KanbanCard({ task, onStatusChange }: KanbanCardProps) {
month: "short", month: "short",
})} })}
</span> </span>
{/* Time displayed when hovering over the badge */}
<span className="grid grid-cols-[0fr] group-hover/date:grid-cols-[1fr] transition-all duration-200 ease-out"> <span className="grid grid-cols-[0fr] group-hover/date:grid-cols-[1fr] transition-all duration-200 ease-out">
<span className="overflow-hidden whitespace-nowrap opacity-0 group-hover/date:opacity-100 transition-opacity duration-200"> <span className="overflow-hidden whitespace-nowrap opacity-0 group-hover/date:opacity-100 transition-opacity duration-200">
{new Date(task.dueDate).toLocaleTimeString("de-DE", { {new Date(task.dueDate).toLocaleTimeString("de-DE", {
@ -129,9 +140,8 @@ export default function KanbanCard({ task, onStatusChange }: KanbanCardProps) {
</div> </div>
)} )}
{/* Right Side: Avatars & Mobile Switcher */} {/* Avatars & Mobile Switcher */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* Avatars */}
{(task.creator || filteredAssignees.length > 0) && ( {(task.creator || filteredAssignees.length > 0) && (
<div className="flex items-center shrink-0 -space-x-2.5 group/avatars hover:space-x-0.5 transition-all duration-300"> <div className="flex items-center shrink-0 -space-x-2.5 group/avatars hover:space-x-0.5 transition-all duration-300">
{filteredAssignees.map((assignee, index) => ( {filteredAssignees.map((assignee, index) => (
@ -171,7 +181,6 @@ export default function KanbanCard({ task, onStatusChange }: KanbanCardProps) {
<MoreHorizontal size={14} /> <MoreHorizontal size={14} />
</button> </button>
{/* Dropdown Menu */}
<div <div
className={`absolute right-0 bottom-8 z-50 w-50 bg-card border border-border rounded-xl shadow-2xl p-1.5 space-y-1 text-xs origin-bottom-right transition-all duration-200 ease-out ${ className={`absolute right-0 bottom-8 z-50 w-50 bg-card border border-border rounded-xl shadow-2xl p-1.5 space-y-1 text-xs origin-bottom-right transition-all duration-200 ease-out ${
showMobileActions showMobileActions

View file

@ -1,6 +1,6 @@
/** /**
* @file dashboard/KanbanColumn.tsx * @file dashboard/KanbanColumn.tsx
* @description Client component rendering an single kanban column container supporting drag-and-drop task reordering, status updates, and interactive card layouts. * @description Client component rendering a single kanban column container supporting drag-and-drop drop targets and status updates.
*/ */
"use client"; "use client";
@ -58,6 +58,7 @@ export default function KanbanColumn(props: KanbanColumnProps) {
*/ */
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => { const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault(); e.preventDefault();
e.dataTransfer.dropEffect = "move";
setIsDraggingOver(true); setIsDraggingOver(true);
}; };
@ -68,22 +69,6 @@ export default function KanbanColumn(props: KanbanColumnProps) {
setIsDraggingOver(false); setIsDraggingOver(false);
}; };
/**
* Initiates the drag action on a task card, storing its ID and current status in the data transfer payload.
*
* @param {React.DragEvent<HTMLDivElement>} e - The drag event object.
* @param {string} taskId - The identifier of the task being dragged.
* @param {TaskStatus} currentStatus - The original status category of the task.
*/
const handleDragStart = (
e: React.DragEvent<HTMLDivElement>,
taskId: string,
currentStatus: TaskStatus,
) => {
e.dataTransfer.setData("text/plain", taskId);
e.dataTransfer.setData("sourceStatus", currentStatus);
};
/** /**
* Handles dropping a task card onto the column, triggering status updates if valid. * Handles dropping a task card onto the column, triggering status updates if valid.
* *
@ -118,7 +103,7 @@ export default function KanbanColumn(props: KanbanColumnProps) {
<span <span
className={`w-2.5 h-2.5 rounded-full shadow-sm ${indicatorColor}`} className={`w-2.5 h-2.5 rounded-full shadow-sm ${indicatorColor}`}
/> />
<h2 className="font-bold text-xs uppercase tracking-wider "> <h2 className="font-bold text-xs uppercase tracking-wider">
{props.title} {props.title}
</h2> </h2>
<span className="text-xs bg-card text-foreground-muted px-2 py-0.5 rounded-full font-semibold border border-border/60"> <span className="text-xs bg-card text-foreground-muted px-2 py-0.5 rounded-full font-semibold border border-border/60">
@ -138,18 +123,11 @@ export default function KanbanColumn(props: KanbanColumnProps) {
</div> </div>
) : ( ) : (
props.tasks.map((task) => ( props.tasks.map((task) => (
<div
key={task.id}
draggable
onDragStart={(e) => handleDragStart(e, task.id, task.status)}
className="group relative bg-card/40 border border-border/80 hover:border-primary/60 p-4 rounded-2xl shadow-sm hover:shadow-xl transition-all duration-300 space-y-3 cursor-grab active:cursor-grabbing hover:-translate-y-0.5"
>
<KanbanCard <KanbanCard
key={task.id} key={task.id}
task={task} task={task}
onStatusChange={updateTaskStatus} onStatusChange={updateTaskStatus}
/> />
</div>
)) ))
)} )}
</div> </div>