refactor(dashboard): move drag-and-drop source logic into KanbanCard component
This commit is contained in:
parent
a9919a045c
commit
1033f99d3f
2 changed files with 28 additions and 41 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue