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:
316
apps/web/src/app/demo/gantt/page.tsx
Normal file
316
apps/web/src/app/demo/gantt/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user