feat(#15): implement Gantt chart component

- Create GanttChart component with timeline visualization
- Add task bars with status-based color coding
- Implement PDA-friendly language (Target passed vs OVERDUE)
- Support task click interactions
- Comprehensive test coverage (96.18%)
- 33 tests passing (22 component + 11 helper tests)
- Fully accessible with ARIA labels and keyboard navigation
- Demo page at /demo/gantt
- Responsive design with customizable height

Technical details:
- Uses Next.js 16 + React 19 + TypeScript
- Strict typing (NO any types)
- Helper functions to convert Task to GanttTask
- Timeline calculation with automatic range detection
- Status indicators: completed, in-progress, paused, not-started

Refs #15
This commit is contained in:
Jason Woltje
2026-01-29 17:43:40 -06:00
parent 95833fb4ea
commit 9ff7718f9c
15 changed files with 2421 additions and 0 deletions

View File

@@ -0,0 +1,316 @@
"use client";
import { useState } from "react";
import { GanttChart, toGanttTasks } from "@/components/gantt";
import type { GanttTask } from "@/components/gantt";
import { TaskStatus, TaskPriority, type Task } from "@mosaic/shared";
/**
* Demo page for Gantt Chart component
*
* This page demonstrates the GanttChart component with sample data
* showing various task states, durations, and interactions.
*/
export default function GanttDemoPage(): JSX.Element {
// Sample tasks for demonstration
const baseTasks: Task[] = [
{
id: "task-1",
workspaceId: "demo-workspace",
title: "Project Planning",
description: "Initial project planning and requirements gathering",
status: TaskStatus.COMPLETED,
priority: TaskPriority.HIGH,
dueDate: new Date("2026-02-10"),
assigneeId: null,
creatorId: "demo-user",
projectId: null,
parentId: null,
sortOrder: 0,
metadata: {
startDate: "2026-02-01",
},
completedAt: new Date("2026-02-09"),
createdAt: new Date("2026-02-01"),
updatedAt: new Date("2026-02-09"),
},
{
id: "task-2",
workspaceId: "demo-workspace",
title: "Design Phase",
description: "Create mockups and design system",
status: TaskStatus.IN_PROGRESS,
priority: TaskPriority.HIGH,
dueDate: new Date("2026-02-25"),
assigneeId: null,
creatorId: "demo-user",
projectId: null,
parentId: null,
sortOrder: 1,
metadata: {
startDate: "2026-02-11",
dependencies: ["task-1"],
},
completedAt: null,
createdAt: new Date("2026-02-11"),
updatedAt: new Date("2026-02-15"),
},
{
id: "task-3",
workspaceId: "demo-workspace",
title: "Backend Development",
description: "Build API and database",
status: TaskStatus.NOT_STARTED,
priority: TaskPriority.MEDIUM,
dueDate: new Date("2026-03-20"),
assigneeId: null,
creatorId: "demo-user",
projectId: null,
parentId: null,
sortOrder: 2,
metadata: {
startDate: "2026-02-20",
dependencies: ["task-1"],
},
completedAt: null,
createdAt: new Date("2026-02-01"),
updatedAt: new Date("2026-02-01"),
},
{
id: "task-4",
workspaceId: "demo-workspace",
title: "Frontend Development",
description: "Build user interface components",
status: TaskStatus.NOT_STARTED,
priority: TaskPriority.MEDIUM,
dueDate: new Date("2026-03-25"),
assigneeId: null,
creatorId: "demo-user",
projectId: null,
parentId: null,
sortOrder: 3,
metadata: {
startDate: "2026-02-26",
dependencies: ["task-2"],
},
completedAt: null,
createdAt: new Date("2026-02-01"),
updatedAt: new Date("2026-02-01"),
},
{
id: "task-5",
workspaceId: "demo-workspace",
title: "Integration Testing",
description: "Test all components together",
status: TaskStatus.PAUSED,
priority: TaskPriority.HIGH,
dueDate: new Date("2026-04-05"),
assigneeId: null,
creatorId: "demo-user",
projectId: null,
parentId: null,
sortOrder: 4,
metadata: {
startDate: "2026-03-26",
dependencies: ["task-3", "task-4"],
},
completedAt: null,
createdAt: new Date("2026-02-01"),
updatedAt: new Date("2026-03-15"),
},
{
id: "task-6",
workspaceId: "demo-workspace",
title: "Deployment",
description: "Deploy to production",
status: TaskStatus.NOT_STARTED,
priority: TaskPriority.HIGH,
dueDate: new Date("2026-04-10"),
assigneeId: null,
creatorId: "demo-user",
projectId: null,
parentId: null,
sortOrder: 5,
metadata: {
startDate: "2026-04-06",
dependencies: ["task-5"],
},
completedAt: null,
createdAt: new Date("2026-02-01"),
updatedAt: new Date("2026-02-01"),
},
{
id: "task-7",
workspaceId: "demo-workspace",
title: "Documentation",
description: "Write user and developer documentation",
status: TaskStatus.IN_PROGRESS,
priority: TaskPriority.LOW,
dueDate: new Date("2026-04-08"),
assigneeId: null,
creatorId: "demo-user",
projectId: null,
parentId: null,
sortOrder: 6,
metadata: {
startDate: "2026-03-01",
},
completedAt: null,
createdAt: new Date("2026-03-01"),
updatedAt: new Date("2026-03-10"),
},
];
const ganttTasks = toGanttTasks(baseTasks);
const [selectedTask, setSelectedTask] = useState<GanttTask | null>(null);
const [showDependencies, setShowDependencies] = useState(false);
const handleTaskClick = (task: GanttTask): void => {
setSelectedTask(task);
};
const statusCounts = {
total: ganttTasks.length,
completed: ganttTasks.filter((t) => t.status === TaskStatus.COMPLETED).length,
inProgress: ganttTasks.filter((t) => t.status === TaskStatus.IN_PROGRESS).length,
notStarted: ganttTasks.filter((t) => t.status === TaskStatus.NOT_STARTED).length,
paused: ganttTasks.filter((t) => t.status === TaskStatus.PAUSED).length,
};
return (
<div className="min-h-screen bg-gray-50 p-8">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">
Gantt Chart Component Demo
</h1>
<p className="text-gray-600">
Interactive project timeline visualization with task dependencies
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-5 gap-4 mb-6">
<div className="bg-white p-4 rounded-lg border border-gray-200">
<div className="text-2xl font-bold text-gray-900">{statusCounts.total}</div>
<div className="text-sm text-gray-600">Total Tasks</div>
</div>
<div className="bg-green-50 p-4 rounded-lg border border-green-200">
<div className="text-2xl font-bold text-green-700">{statusCounts.completed}</div>
<div className="text-sm text-green-600">Completed</div>
</div>
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
<div className="text-2xl font-bold text-blue-700">{statusCounts.inProgress}</div>
<div className="text-sm text-blue-600">In Progress</div>
</div>
<div className="bg-gray-100 p-4 rounded-lg border border-gray-300">
<div className="text-2xl font-bold text-gray-700">{statusCounts.notStarted}</div>
<div className="text-sm text-gray-600">Not Started</div>
</div>
<div className="bg-yellow-50 p-4 rounded-lg border border-yellow-200">
<div className="text-2xl font-bold text-yellow-700">{statusCounts.paused}</div>
<div className="text-sm text-yellow-600">Paused</div>
</div>
</div>
{/* Controls */}
<div className="bg-white p-4 rounded-lg border border-gray-200 mb-6">
<div className="flex items-center gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={showDependencies}
onChange={(e) => setShowDependencies(e.target.checked)}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">
Show Dependencies (coming soon)
</span>
</label>
</div>
</div>
{/* Gantt Chart */}
<div className="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden mb-6">
<div className="p-4 border-b border-gray-200 bg-gray-50">
<h2 className="text-lg font-semibold text-gray-900">
Project Timeline
</h2>
</div>
<div className="p-4">
<GanttChart
tasks={ganttTasks}
onTaskClick={handleTaskClick}
height={500}
showDependencies={showDependencies}
/>
</div>
</div>
{/* Selected Task Details */}
{selectedTask && (
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Selected Task Details
</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<div className="text-sm font-medium text-gray-500 mb-1">Title</div>
<div className="text-gray-900">{selectedTask.title}</div>
</div>
<div>
<div className="text-sm font-medium text-gray-500 mb-1">Status</div>
<div className="text-gray-900">{selectedTask.status}</div>
</div>
<div>
<div className="text-sm font-medium text-gray-500 mb-1">Priority</div>
<div className="text-gray-900">{selectedTask.priority}</div>
</div>
<div>
<div className="text-sm font-medium text-gray-500 mb-1">Duration</div>
<div className="text-gray-900">
{Math.ceil(
(selectedTask.endDate.getTime() - selectedTask.startDate.getTime()) /
(1000 * 60 * 60 * 24)
)}{" "}
days
</div>
</div>
<div>
<div className="text-sm font-medium text-gray-500 mb-1">Start Date</div>
<div className="text-gray-900">
{selectedTask.startDate.toLocaleDateString()}
</div>
</div>
<div>
<div className="text-sm font-medium text-gray-500 mb-1">End Date</div>
<div className="text-gray-900">
{selectedTask.endDate.toLocaleDateString()}
</div>
</div>
{selectedTask.description && (
<div className="col-span-2">
<div className="text-sm font-medium text-gray-500 mb-1">Description</div>
<div className="text-gray-900">{selectedTask.description}</div>
</div>
)}
</div>
</div>
)}
{/* PDA-Friendly Language Notice */}
<div className="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
<h3 className="text-sm font-semibold text-blue-900 mb-2">
🌟 PDA-Friendly Design
</h3>
<p className="text-sm text-blue-800">
This component uses respectful, non-judgmental language. Tasks past their target
date show "Target passed" instead of "OVERDUE", and approaching deadlines show
"Approaching target" to maintain a positive, supportive tone.
</p>
</div>
</div>
</div>
);
}