style(dashboard): update design kanban column and task cards with enhanced UI

This commit is contained in:
Chneemann 2026-08-08 13:22:41 +02:00
parent 3057310b13
commit 73bc52e41a
No known key found for this signature in database
3 changed files with 148 additions and 54 deletions

View file

@ -1,62 +1,117 @@
/** /**
* @file dashboard/KanbanCard.tsx * @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"; "use client";
import { AlertCircle, Crown, CalendarDays } from "lucide-react";
import { KanbanCardProps } from "@/types/tasks"; 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. * @param {KanbanCardProps} props - The component props containing the task object.
* @returns {JSX.Element} The rendered kanban card component. * @returns {JSX.Element} The rendered kanban card component.
*/ */
export default function KanbanCard({ task }: KanbanCardProps) { 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 ( return (
<div className="flex flex-col gap-2 text-xs"> <div className="flex flex-col h-full gap-3">
{/* Title & Priority */} {/* --- Card Header (Title & Priority) --- */}
<div className="flex items-start justify-between gap-2"> <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 && ( {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 <span
className="px-1.5 py-0.5 bg-gray-900 text-white rounded text-xs font-medium" className={`gap-1.5 px-2.5 py-1 rounded-lg text-xs font-bold border-2 ${
title={`Creator: ${task.creator}`} 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> </span>
)} )}
{assignees.map((assignee, index) => ( </div>
<span
{/* 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} 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}`} title={`Assignee: ${assignee}`}
> >
{assignee.substring(0, 2).toUpperCase()} {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> </div>
</div> </div>

View file

@ -1,6 +1,6 @@
/** /**
* @file dashboard/KanbanColumn.tsx * @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"; "use client";
@ -10,39 +10,49 @@ import KanbanCard from "./KanbanCard";
import { KanbanColumnProps } from "@/types/tasks"; import { KanbanColumnProps } from "@/types/tasks";
/** /**
* Renders a kanban column with an indicator color, title, task count, quick-add trigger, * Renders a kanban column containing a header with category indicator and counter,
* and a list of rendered task cards or an empty placeholder. * 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. * @returns {JSX.Element} The rendered kanban column component.
*/ */
export default function KanbanColumn(props: KanbanColumnProps) { export default function KanbanColumn(props: KanbanColumnProps) {
const indicatorColor = props.color ?? "bg-primary";
return ( return (
<div className="flex flex-col p-2 gap-2 w-full bg-background-muted rounded"> <div className="w-full flex flex-col rounded-2xl p-4 transition-all duration-300 bg-background-muted/40 border border-border">
{/* Header */} {/* Column Header */}
<div className="flex items-center justify-between px-1 py-2"> <div className="flex items-center justify-between mb-4 pb-2 border-b border-border/40">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2.5">
<span <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> <h2 className="font-bold text-xs uppercase tracking-wider text-foreground">
<span className="text-sm">{props.count}</span> {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> </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} /> <Plus size={14} />
</button> </button>
</div> </div>
{/* Task List */} {/* Card List */}
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-3">
{props.tasks.length === 0 ? ( {props.tasks.length === 0 ? (
<div className="p-4 text-sm text-center border border-dashed rounded bg-card"> <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">
No tasks <span>No tasks</span>
</div> </div>
) : ( ) : (
props.tasks.map((task) => ( props.tasks.map((task) => (
<div key={task.id} className="p-3 border rounded shadow-sm bg-card"> <div
<KanbanCard task={task} /> 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> </div>
)) ))
)} )}

View file

@ -3,13 +3,18 @@
* @description Type definitions, interfaces, and UI configuration mappings for task management and kanban views. * @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 // Types
// ========================================== // ==========================================
export type TaskStatus = (typeof taskStatusEnum.enumValues)[number]; export type TaskStatus = (typeof taskStatusEnum.enumValues)[number];
export type TaskPriority = (typeof taskPriorityEnum.enumValues)[number];
// ========================================== // ==========================================
// Interfaces // Interfaces
@ -21,6 +26,15 @@ export interface Task extends Omit<DbTask, "dueDate"> {
creator?: string; creator?: string;
} }
export interface RouteContext {
params: Promise<{ id: string }>;
}
export interface TaskPriorityConfig {
label: string;
className: string;
}
export interface KanbanColumnConfig { export interface KanbanColumnConfig {
id: TaskStatus; id: TaskStatus;
title: string; title: string;
@ -41,6 +55,21 @@ export interface KanbanCardProps {
// UI Configurations // 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 = [ export const KANBAN_COLUMNS = [
{ id: "todo", title: "To-do", color: "bg-zinc-400" }, { id: "todo", title: "To-do", color: "bg-zinc-400" },
{ id: "in_progress", title: "In Progress", color: "bg-indigo-500" }, { id: "in_progress", title: "In Progress", color: "bg-indigo-500" },