refactor(dashboard): modularize KanbanCard into clean sub-components and organize folder structure
This commit is contained in:
parent
1033f99d3f
commit
68e5070d78
5 changed files with 260 additions and 159 deletions
|
|
@ -1,59 +1,24 @@
|
||||||
/**
|
/**
|
||||||
* @file dashboard/KanbanCard.tsx
|
* @file dashboard/KanbanCard.tsx
|
||||||
* @description Client component rendering a single kanban card with native drag-and-drop source handlers, dynamic priority styling, and mobile status dropdown.
|
* @description Client component rendering a single kanban card container with native drag-and-drop source handlers and modular sub-components.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import { KanbanCardProps, TaskStatus } from "@/types/tasks";
|
||||||
AlertCircle,
|
import KanbanCardActions from "./components/KanbanCardActions";
|
||||||
Crown,
|
import KanbanCardAvatars from "./components/KanbanCardAvatars";
|
||||||
MoreHorizontal,
|
import KanbanCardDueDate from "./components/KanbanCardDueDate";
|
||||||
CornerDownRight,
|
import KanbanCardPriority from "./components/KanbanCardPriority";
|
||||||
CalendarDays,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { KANBAN_COLUMNS, KanbanCardProps, TaskStatus } from "@/types/tasks";
|
|
||||||
import { useState, useEffect, useRef } from "react";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders an interactive kanban card featuring title, description, priority levels,
|
* Renders an interactive kanban card container holding title, description,
|
||||||
* deadline alerts, team avatars, and a mobile-friendly status-shifting dropdown menu.
|
* modular priority badges, deadline elements, assignees, and action triggers.
|
||||||
*
|
*
|
||||||
* @param {KanbanCardProps} props - The component props containing the task object and status change handler.
|
* @param {KanbanCardProps} props - The component props containing the task object and status change handler.
|
||||||
* @returns {JSX.Element} The rendered kanban card component.
|
* @returns {JSX.Element} The rendered kanban card container component.
|
||||||
*/
|
*/
|
||||||
export default function KanbanCard({ task, onStatusChange }: KanbanCardProps) {
|
export default function KanbanCard({ task, onStatusChange }: KanbanCardProps) {
|
||||||
const [showMobileActions, setShowMobileActions] = useState(false);
|
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
dropdownRef.current &&
|
|
||||||
!dropdownRef.current.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
setShowMobileActions(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (showMobileActions) {
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
};
|
|
||||||
}, [showMobileActions]);
|
|
||||||
|
|
||||||
const isOverdue =
|
|
||||||
task.dueDate &&
|
|
||||||
new Date(task.dueDate) < new Date() &&
|
|
||||||
task.status !== "done";
|
|
||||||
|
|
||||||
const filteredAssignees = (task.assignees || []).filter(
|
|
||||||
(a) => a !== task.creator,
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiates the drag action on a task card, storing its ID and current status in the data transfer payload.
|
* Initiates the drag action on a task card, storing its ID and current status in the data transfer payload.
|
||||||
*
|
*
|
||||||
|
|
@ -66,17 +31,14 @@ export default function KanbanCard({ task, onStatusChange }: KanbanCardProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles shifting the task to a new status category.
|
* Triggers the status change callback with the target task ID and new status.
|
||||||
*
|
*
|
||||||
* @param {React.MouseEvent} e - The mouse event triggered by clicking a column destination.
|
|
||||||
* @param {TaskStatus} newStatus - The target task status to transition to.
|
* @param {TaskStatus} newStatus - The target task status to transition to.
|
||||||
*/
|
*/
|
||||||
const handleMove = (e: React.MouseEvent, newStatus: TaskStatus) => {
|
const handleMove = (newStatus: TaskStatus) => {
|
||||||
e.stopPropagation();
|
|
||||||
if (onStatusChange) {
|
if (onStatusChange) {
|
||||||
onStatusChange(task.id, newStatus);
|
onStatusChange(task.id, newStatus);
|
||||||
}
|
}
|
||||||
setShowMobileActions(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -85,25 +47,13 @@ export default function KanbanCard({ task, onStatusChange }: KanbanCardProps) {
|
||||||
onDragStart={handleDragStart}
|
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"
|
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 --- */}
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<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>
|
||||||
|
{/* Card Priority */}
|
||||||
{task.priority && (
|
<KanbanCardPriority priority={task.priority} />
|
||||||
<span
|
|
||||||
className={`gap-1.5 px-2.5 py-1 rounded-lg text-xs font-bold border-2 shrink-0 ${
|
|
||||||
task.priority === "high"
|
|
||||||
? "bg-rose-500/10 text-rose-500 border-rose-500/20"
|
|
||||||
: task.priority === "medium"
|
|
||||||
? "bg-amber-500/10 text-amber-500 border-amber-500/20"
|
|
||||||
: "bg-zinc-500/10 text-zinc-400 border-zinc-500/20"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{task.priority.charAt(0).toUpperCase() + task.priority.slice(1)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Card Description */}
|
{/* Card Description */}
|
||||||
|
|
@ -113,104 +63,16 @@ export default function KanbanCard({ task, onStatusChange }: KanbanCardProps) {
|
||||||
|
|
||||||
{/* --- Card Footer --- */}
|
{/* --- 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">
|
||||||
{/* Date Badge */}
|
{/* Date Badge Component */}
|
||||||
{task.dueDate && (
|
<KanbanCardDueDate task={task} />
|
||||||
<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 ${
|
|
||||||
isOverdue
|
|
||||||
? "bg-destructive/10 text-destructive border-destructive/40 animate-pulse"
|
|
||||||
: "bg-background/50 text-foreground-muted border-border/40"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isOverdue ? <AlertCircle size={13} /> : <CalendarDays size={13} />}
|
|
||||||
<span>
|
|
||||||
{new Date(task.dueDate).toLocaleDateString("de-DE", {
|
|
||||||
day: "2-digit",
|
|
||||||
month: "short",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<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">
|
|
||||||
{new Date(task.dueDate).toLocaleTimeString("de-DE", {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Avatars & Mobile Switcher */}
|
{/* Avatars & Mobile Switcher */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{(task.creator || filteredAssignees.length > 0) && (
|
<KanbanCardAvatars
|
||||||
<div className="flex items-center shrink-0 -space-x-2.5 group/avatars hover:space-x-0.5 transition-all duration-300">
|
creator={task.creator}
|
||||||
{filteredAssignees.map((assignee, index) => (
|
assignees={task.assignees}
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="w-7 h-7 rounded-full bg-primary/25 border-2 border-card flex items-center justify-center text-xs font-bold text-primary shadow-md group-hover/avatars:scale-110 transition-transform duration-200 ring-2 ring-border/50 group-hover/avatars:ring-primary/20 cursor-default"
|
|
||||||
title={`Assignee: ${assignee}`}
|
|
||||||
>
|
|
||||||
{assignee.substring(0, 2).toUpperCase()}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{task.creator && (
|
|
||||||
<div
|
|
||||||
className="relative w-7 h-7 rounded-full bg-amber-500 text-white border-2 border-card flex items-center justify-center text-xs font-bold shadow-lg group-hover/avatars:scale-110 transition-transform duration-200 ring-2 ring-border/50 group-hover/avatars:ring-amber-500/20 cursor-default"
|
|
||||||
title={`Creator: ${task.creator}`}
|
|
||||||
>
|
|
||||||
{task.creator.substring(0, 2).toUpperCase()}
|
|
||||||
<span className="absolute -top-1.5 -right-1.5 w-4 h-4 bg-amber-600 rounded-full border-2 border-card flex items-center justify-center">
|
|
||||||
<Crown size={8} className="text-white" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Mobile Switcher */}
|
|
||||||
<div className="relative sm:hidden" ref={dropdownRef}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowMobileActions(!showMobileActions);
|
|
||||||
}}
|
|
||||||
className="p-1.5 rounded-lg border border-border bg-background/50 text-foreground-muted transition-all cursor-pointer hover:text-foreground hover:border-primary-hover flex items-center justify-center"
|
|
||||||
title="Move Task"
|
|
||||||
>
|
|
||||||
<MoreHorizontal size={14} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<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 ${
|
|
||||||
showMobileActions
|
|
||||||
? "opacity-100 scale-100 pointer-events-auto"
|
|
||||||
: "opacity-0 scale-10 pointer-events-none"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{KANBAN_COLUMNS.map((col) => {
|
|
||||||
if (col.id === task.status) return null;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={col.id}
|
|
||||||
onClick={(e) => handleMove(e, col.id)}
|
|
||||||
className="w-full text-left px-3 py-2 rounded-lg hover:bg-border flex items-center justify-between hover:text-primary group/btn transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
<span className="flex items-center gap-2 font-medium">
|
|
||||||
<span
|
|
||||||
className={`w-2.5 h-2.5 rounded-full ${col.color}`}
|
|
||||||
/>
|
/>
|
||||||
{col.title}
|
<KanbanCardActions currentStatus={task.status} onMove={handleMove} />
|
||||||
</span>
|
|
||||||
<CornerDownRight
|
|
||||||
size={12}
|
|
||||||
className="text-foreground-muted group-hover/btn:text-primary transition-all"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
105
app/(app)/dashboard/components/KanbanCardActions.tsx
Normal file
105
app/(app)/dashboard/components/KanbanCardActions.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
/**
|
||||||
|
* @file dashboard/components/KanbanCardActions.tsx
|
||||||
|
* @description Client component rendering the mobile status transition dropdown for a kanban card.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { MoreHorizontal, CornerDownRight } from "lucide-react";
|
||||||
|
import { KANBAN_COLUMNS, TaskStatus } from "@/types/tasks";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a mobile-only action menu component allowing users to move a task
|
||||||
|
* between different kanban columns via a dropdown interface.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The component props.
|
||||||
|
* @param {TaskStatus} props.currentStatus - The current status category of the task.
|
||||||
|
* @param {(newStatus: TaskStatus) => void} props.onMove - Callback function triggered when a new status column is selected.
|
||||||
|
* @returns {JSX.Element} The rendered mobile card actions component.
|
||||||
|
*/
|
||||||
|
export default function KanbanCardActions({
|
||||||
|
currentStatus,
|
||||||
|
onMove,
|
||||||
|
}: {
|
||||||
|
currentStatus: TaskStatus;
|
||||||
|
onMove: (newStatus: TaskStatus) => void;
|
||||||
|
}) {
|
||||||
|
const [showMobileActions, setShowMobileActions] = useState(false);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
dropdownRef.current &&
|
||||||
|
!dropdownRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setShowMobileActions(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (showMobileActions) {
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [showMobileActions]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles clicking an action item to move the task to a target status category.
|
||||||
|
*
|
||||||
|
* @param {React.MouseEvent} e - The mouse event object.
|
||||||
|
* @param {TaskStatus} targetStatus - The target status category to move the task to.
|
||||||
|
*/
|
||||||
|
const handleActionClick = (e: React.MouseEvent, targetStatus: TaskStatus) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onMove(targetStatus);
|
||||||
|
setShowMobileActions(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative sm:hidden" ref={dropdownRef}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowMobileActions(!showMobileActions);
|
||||||
|
}}
|
||||||
|
className="p-1.5 rounded-lg border border-border bg-background/50 text-foreground-muted transition-all cursor-pointer hover:text-foreground hover:border-primary-hover flex items-center justify-center"
|
||||||
|
title="Move Task"
|
||||||
|
>
|
||||||
|
<MoreHorizontal size={14} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<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 ${
|
||||||
|
showMobileActions
|
||||||
|
? "opacity-100 scale-100 pointer-events-auto"
|
||||||
|
: "opacity-0 scale-10 pointer-events-none"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{KANBAN_COLUMNS.map((col) => {
|
||||||
|
if (col.id === currentStatus) return null;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={col.id}
|
||||||
|
onClick={(e) => handleActionClick(e, col.id)}
|
||||||
|
className="w-full text-left px-3 py-2 rounded-lg hover:bg-border flex items-center justify-between hover:text-primary group/btn transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2 font-medium">
|
||||||
|
<span className={`w-2.5 h-2.5 rounded-full ${col.color}`} />
|
||||||
|
{col.title}
|
||||||
|
</span>
|
||||||
|
<CornerDownRight
|
||||||
|
size={12}
|
||||||
|
className="text-foreground-muted group-hover/btn:text-primary transition-all"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
app/(app)/dashboard/components/KanbanCardAvatars.tsx
Normal file
55
app/(app)/dashboard/components/KanbanCardAvatars.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
/**
|
||||||
|
* @file dashboard/components/KanbanCardAvatars.tsx
|
||||||
|
* @description Client component rendering team avatars and creator badge for a kanban card.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Crown } from "lucide-react";
|
||||||
|
import { Task } from "@/types/tasks";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders overlapping avatar indicators for task assignees and a dedicated, crowned badge for the task creator.
|
||||||
|
* Filters out duplicate entries where the assignee matches the creator.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The component props.
|
||||||
|
* @param {Task["creator"]} props.creator - The email or identifier of the task creator.
|
||||||
|
* @param {Task["assignees"]} [props.assignees=[]] - An array of emails or identifiers for users assigned to the task.
|
||||||
|
* @returns {JSX.Element | null} The rendered avatars container, or null if neither creator nor assignees exist.
|
||||||
|
*/
|
||||||
|
export default function KanbanCardAvatars({
|
||||||
|
creator,
|
||||||
|
assignees = [],
|
||||||
|
}: {
|
||||||
|
creator: Task["creator"];
|
||||||
|
assignees: Task["assignees"];
|
||||||
|
}) {
|
||||||
|
const filteredAssignees = (assignees || []).filter((a) => a !== creator);
|
||||||
|
|
||||||
|
if (!creator && filteredAssignees.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="w-7 h-7 rounded-full bg-primary/25 border-2 border-card flex items-center justify-center text-xs font-bold text-primary shadow-md group-hover/avatars:scale-110 transition-transform duration-200 ring-2 ring-border/50 group-hover/avatars:ring-primary/20 cursor-default"
|
||||||
|
title={`Assignee: ${assignee}`}
|
||||||
|
>
|
||||||
|
{assignee.substring(0, 2).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{creator && (
|
||||||
|
<div
|
||||||
|
className="relative w-7 h-7 rounded-full bg-amber-500 text-white border-2 border-card flex items-center justify-center text-xs font-bold shadow-lg group-hover/avatars:scale-110 transition-transform duration-200 ring-2 ring-border/50 group-hover/avatars:ring-amber-500/20 cursor-default"
|
||||||
|
title={`Creator: ${creator}`}
|
||||||
|
>
|
||||||
|
{creator.substring(0, 2).toUpperCase()}
|
||||||
|
<span className="absolute -top-1.5 -right-1.5 w-4 h-4 bg-amber-600 rounded-full border-2 border-card flex items-center justify-center">
|
||||||
|
<Crown size={8} className="text-white" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
app/(app)/dashboard/components/KanbanCardDueDate.tsx
Normal file
48
app/(app)/dashboard/components/KanbanCardDueDate.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
/**
|
||||||
|
* @file dashboard/components/KanbanCardDueDate.tsx
|
||||||
|
* @description Client component rendering the due date badge with overdue status indicators and hover time display.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AlertCircle, CalendarDays } from "lucide-react";
|
||||||
|
import { Task } from "@/types/tasks";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a due date badge for a kanban card, showing an overdue alert animation
|
||||||
|
* if the deadline has passed and the task is not completed, alongside a hoverable time display.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The component props.
|
||||||
|
* @param {Task} props.task - The task object containing the due date and status information.
|
||||||
|
* @returns {JSX.Element | null} The rendered due date badge component or null if no due date is set.
|
||||||
|
*/
|
||||||
|
export default function KanbanCardDueDate({ task }: { task: Task }) {
|
||||||
|
if (!task.dueDate) return null;
|
||||||
|
|
||||||
|
const isOverdue =
|
||||||
|
new Date(task.dueDate) < new Date() && task.status !== "done";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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 ${
|
||||||
|
isOverdue
|
||||||
|
? "bg-destructive/10 text-destructive border-destructive/40 animate-pulse"
|
||||||
|
: "bg-background/50 text-foreground-muted border-border/40"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isOverdue ? <AlertCircle size={13} /> : <CalendarDays size={13} />}
|
||||||
|
<span>
|
||||||
|
{new Date(task.dueDate).toLocaleDateString("de-DE", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "short",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<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">
|
||||||
|
{new Date(task.dueDate).toLocaleTimeString("de-DE", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
app/(app)/dashboard/components/KanbanCardPriority.tsx
Normal file
31
app/(app)/dashboard/components/KanbanCardPriority.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* @file dashboard/components/KanbanCardPriority.tsx
|
||||||
|
* @description Client component rendering the dynamic priority badge for a kanban card based on configuration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { TaskPriority, PRIORITY_CONFIG } from "@/types/tasks";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a styled priority badge for a kanban card.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The component props.
|
||||||
|
* @param {TaskPriority} props.priority - The priority level of the task.
|
||||||
|
* @returns {JSX.Element | null} The rendered priority badge component, or null if priority is invalid.
|
||||||
|
*/
|
||||||
|
export default function KanbanCardPriority({
|
||||||
|
priority,
|
||||||
|
}: {
|
||||||
|
priority: TaskPriority;
|
||||||
|
}) {
|
||||||
|
if (!priority || !PRIORITY_CONFIG[priority]) return null;
|
||||||
|
|
||||||
|
const { label, className } = PRIORITY_CONFIG[priority];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`gap-1.5 px-2.5 py-1 rounded-lg text-xs font-bold border-2 shrink-0 ${className}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue