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
|
* @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>
|
<span
|
||||||
|
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.priority.charAt(0).toUpperCase() + task.priority.slice(1)}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Card Description */}
|
||||||
{task.description && (
|
<p className="text-sm leading-relaxed text-foreground-muted line-clamp-3">
|
||||||
<p className="text-gray-500 line-clamp-2 text-sm">{task.description}</p>
|
{task.description}
|
||||||
)}
|
</p>
|
||||||
|
|
||||||
{/* Footer / Meta & People */}
|
{/* --- Card Footer (Date, Mobile Action Menu, Avatars) --- */}
|
||||||
<div className="flex items-center justify-between pt-2 mt-1 border-t text-xs">
|
<div className="flex items-center justify-between pt-3 mt-auto text-xs border-t border-border">
|
||||||
<div>
|
{/* Left Side: Date Badge */}
|
||||||
{task.dueDate && (
|
{task.dueDate && (
|
||||||
<span>{new Date(task.dueDate).toLocaleDateString()}</span>
|
<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 ${
|
||||||
</div>
|
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>
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5">
|
{/* Time displayed when hovering over the badge */}
|
||||||
{task.creator && (
|
<span className="grid grid-cols-[0fr] group-hover/date:grid-cols-[1fr] transition-all duration-200 ease-out">
|
||||||
<span
|
<span className="overflow-hidden whitespace-nowrap opacity-0 group-hover/date:opacity-100 transition-opacity duration-200">
|
||||||
className="px-1.5 py-0.5 bg-gray-900 text-white rounded text-xs font-medium"
|
{new Date(task.dueDate).toLocaleTimeString("de-DE", {
|
||||||
title={`Creator: ${task.creator}`}
|
hour: "2-digit",
|
||||||
>
|
minute: "2-digit",
|
||||||
{task.creator.substring(0, 2).toUpperCase()}
|
})}
|
||||||
|
</span>
|
||||||
</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="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>
|
||||||
)}
|
)}
|
||||||
{assignees.map((assignee, index) => (
|
|
||||||
<span
|
|
||||||
key={index}
|
|
||||||
className="px-1.5 py-0.5 bg-gray-100 rounded text-xs text-gray-600"
|
|
||||||
title={`Assignee: ${assignee}`}
|
|
||||||
>
|
|
||||||
{assignee.substring(0, 2).toUpperCase()}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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" },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue