style(dashboard): update design kanban column and task cards with enhanced UI
This commit is contained in:
parent
3057310b13
commit
73bc52e41a
3 changed files with 148 additions and 54 deletions
|
|
@ -1,62 +1,117 @@
|
|||
/**
|
||||
* @file dashboard/KanbanCard.tsx
|
||||
* @description Client component rendering a simplified kanban task card displaying title, priority, description, and metadata.
|
||||
* @description Client component rendering an individual task card within a kanban column, featuring priority configuration, due dates, overdue highlights, and user avatars.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { AlertCircle, Crown, CalendarDays } from "lucide-react";
|
||||
import { KanbanCardProps } from "@/types/tasks";
|
||||
|
||||
/**
|
||||
* Renders a task card component with a basic layout for title, priority, description, due date, and creator info.
|
||||
* Renders a task card component displaying its title, priority badge, description,
|
||||
* deadline with hover time details, and creator/assignee avatars.
|
||||
*
|
||||
* @param {KanbanCardProps} props - The component props containing the task object.
|
||||
* @returns {JSX.Element} The rendered kanban card component.
|
||||
*/
|
||||
export default function KanbanCard({ task }: KanbanCardProps) {
|
||||
const assignees = task.assignees || [];
|
||||
const isOverdue =
|
||||
task.dueDate &&
|
||||
new Date(task.dueDate) < new Date() &&
|
||||
task.status !== "done";
|
||||
|
||||
const filteredAssignees = (task.assignees || []).filter(
|
||||
(a) => a !== task.creator,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 text-xs">
|
||||
{/* Title & Priority */}
|
||||
<div className="flex flex-col h-full gap-3">
|
||||
{/* --- Card Header (Title & Priority) --- */}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<span className="font-medium text-base">{task.title}</span>
|
||||
{/* Title */}
|
||||
<h3 className="font-semibold leading-snug group-hover/card:text-primary transition-colors line-clamp-2">
|
||||
{task.title}
|
||||
</h3>
|
||||
|
||||
{/* Priority Badge */}
|
||||
{task.priority && (
|
||||
<span className="text-xs uppercase">{task.priority}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
{task.description && (
|
||||
<p className="text-gray-500 line-clamp-2 text-sm">{task.description}</p>
|
||||
)}
|
||||
|
||||
{/* Footer / Meta & People */}
|
||||
<div className="flex items-center justify-between pt-2 mt-1 border-t text-xs">
|
||||
<div>
|
||||
{task.dueDate && (
|
||||
<span>{new Date(task.dueDate).toLocaleDateString()}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
{task.creator && (
|
||||
<span
|
||||
className="px-1.5 py-0.5 bg-gray-900 text-white rounded text-xs font-medium"
|
||||
title={`Creator: ${task.creator}`}
|
||||
className={`gap-1.5 px-2.5 py-1 rounded-lg text-xs font-bold border-2 ${
|
||||
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.creator.substring(0, 2).toUpperCase()}
|
||||
{task.priority.charAt(0).toUpperCase() + task.priority.slice(1)}
|
||||
</span>
|
||||
)}
|
||||
{assignees.map((assignee, index) => (
|
||||
<span
|
||||
</div>
|
||||
|
||||
{/* Card Description */}
|
||||
<p className="text-sm leading-relaxed text-foreground-muted line-clamp-3">
|
||||
{task.description}
|
||||
</p>
|
||||
|
||||
{/* --- Card Footer (Date, Mobile Action Menu, Avatars) --- */}
|
||||
<div className="flex items-center justify-between pt-3 mt-auto text-xs border-t border-border">
|
||||
{/* Left Side: Date Badge */}
|
||||
{task.dueDate && (
|
||||
<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>
|
||||
|
||||
{/* 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="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>
|
||||
)}
|
||||
|
||||
{/* Right Side: Avatars & Mobile Switcher */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Avatars */}
|
||||
{(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">
|
||||
{filteredAssignees.map((assignee, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="px-1.5 py-0.5 bg-gray-100 rounded text-xs text-gray-600"
|
||||
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()}
|
||||
</span>
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* @file dashboard/KanbanColumn.tsx
|
||||
* @description Client component rendering a single column in the kanban board, containing a status header, count, task list, and fallback empty states.
|
||||
* @description Client component rendering an individual kanban column container with its header, task count, action button, and list of task cards.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
|
@ -10,39 +10,49 @@ import KanbanCard from "./KanbanCard";
|
|||
import { KanbanColumnProps } from "@/types/tasks";
|
||||
|
||||
/**
|
||||
* Renders a kanban column with an indicator color, title, task count, quick-add trigger,
|
||||
* and a list of rendered task cards or an empty placeholder.
|
||||
* Renders a kanban column containing a header with category indicator and counter,
|
||||
* an add button, and a sorted list of associated task cards or an empty placeholder.
|
||||
*
|
||||
* @param {KanbanColumnProps} props - The component props defining column metadata and task lists.
|
||||
* @param {KanbanColumnProps} props - The component props including column details, task list, and styling options.
|
||||
* @returns {JSX.Element} The rendered kanban column component.
|
||||
*/
|
||||
export default function KanbanColumn(props: KanbanColumnProps) {
|
||||
const indicatorColor = props.color ?? "bg-primary";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-2 gap-2 w-full bg-background-muted rounded">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-1 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-full flex flex-col rounded-2xl p-4 transition-all duration-300 bg-background-muted/40 border border-border">
|
||||
{/* Column Header */}
|
||||
<div className="flex items-center justify-between mb-4 pb-2 border-b border-border/40">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span
|
||||
className={`w-2 h-2 rounded-full ${props.color ?? "bg-gray-400"}`}
|
||||
className={`w-2.5 h-2.5 rounded-full shadow-sm ${indicatorColor}`}
|
||||
/>
|
||||
<h2 className="text-sm font-medium">{props.title}</h2>
|
||||
<span className="text-sm">{props.count}</span>
|
||||
<h2 className="font-bold text-xs uppercase tracking-wider text-foreground">
|
||||
{props.title}
|
||||
</h2>
|
||||
<span className="text-xs bg-card text-foreground-muted px-2 py-0.5 rounded-full font-semibold border border-border/60">
|
||||
{props.count}
|
||||
</span>
|
||||
</div>
|
||||
<button className="p-1 hover:bg-gray-100 rounded cursor-pointer">
|
||||
<button className="text-background hover:text-foreground p-1.5 rounded-lg bg-primary hover:bg-primary-hover transition-all duration-200 cursor-pointer shadow-sm active:scale-95">
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Task List */}
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Card List */}
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{props.tasks.length === 0 ? (
|
||||
<div className="p-4 text-sm text-center border border-dashed rounded bg-card">
|
||||
No tasks
|
||||
<div className="h-28 rounded-xl border border-dashed border-border/80 flex flex-col items-center justify-center text-xs text-foreground-muted/60 bg-card/20 gap-1">
|
||||
<span>No tasks</span>
|
||||
</div>
|
||||
) : (
|
||||
props.tasks.map((task) => (
|
||||
<div key={task.id} className="p-3 border rounded shadow-sm bg-card">
|
||||
<KanbanCard task={task} />
|
||||
<div
|
||||
key={task.id}
|
||||
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 hover:-translate-y-0.5"
|
||||
>
|
||||
<KanbanCard key={task.id} task={task} />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,18 @@
|
|||
* @description Type definitions, interfaces, and UI configuration mappings for task management and kanban views.
|
||||
*/
|
||||
|
||||
import { taskStatusEnum, type Task as DbTask } from "@/db/schema";
|
||||
import {
|
||||
taskPriorityEnum,
|
||||
taskStatusEnum,
|
||||
type Task as DbTask,
|
||||
} from "@/db/schema";
|
||||
|
||||
// ==========================================
|
||||
// Types
|
||||
// ==========================================
|
||||
|
||||
export type TaskStatus = (typeof taskStatusEnum.enumValues)[number];
|
||||
export type TaskPriority = (typeof taskPriorityEnum.enumValues)[number];
|
||||
|
||||
// ==========================================
|
||||
// Interfaces
|
||||
|
|
@ -21,6 +26,15 @@ export interface Task extends Omit<DbTask, "dueDate"> {
|
|||
creator?: string;
|
||||
}
|
||||
|
||||
export interface RouteContext {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export interface TaskPriorityConfig {
|
||||
label: string;
|
||||
className: string;
|
||||
}
|
||||
|
||||
export interface KanbanColumnConfig {
|
||||
id: TaskStatus;
|
||||
title: string;
|
||||
|
|
@ -41,6 +55,21 @@ export interface KanbanCardProps {
|
|||
// UI Configurations
|
||||
// ==========================================
|
||||
|
||||
export const PRIORITY_CONFIG = {
|
||||
high: {
|
||||
label: "High",
|
||||
className: "bg-rose-500/10 text-rose-500 border-rose-500/20",
|
||||
},
|
||||
medium: {
|
||||
label: "Medium",
|
||||
className: "bg-amber-500/10 text-amber-500 border-amber-500/20",
|
||||
},
|
||||
low: {
|
||||
label: "Low",
|
||||
className: "bg-zinc-500/10 text-zinc-400 border-zinc-500/20",
|
||||
},
|
||||
} as const satisfies Record<TaskPriority, TaskPriorityConfig>;
|
||||
|
||||
export const KANBAN_COLUMNS = [
|
||||
{ id: "todo", title: "To-do", color: "bg-zinc-400" },
|
||||
{ id: "in_progress", title: "In Progress", color: "bg-indigo-500" },
|
||||
|
|
|
|||
Loading…
Reference in a new issue