7884ebbfd4
Add demo mode with mock data provider, Docker deployment, Playwright tests, PostHog analytics, error boundaries, and SEO metadata. Fix residences API response unwrapping, kanban drag-and-drop with optimistic updates, trailing slash proxy redirects, and column name mismatches with Go API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
132 lines
4.2 KiB
TypeScript
132 lines
4.2 KiB
TypeScript
"use client";
|
|
|
|
import { useRef, useEffect } from "react";
|
|
import { useDraggable, useDroppable } from "@dnd-kit/core";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { cn } from "@/lib/utils";
|
|
import { TaskCard } from "./task-card";
|
|
import type { KanbanColumn as KanbanColumnType } from "@/lib/api/tasks";
|
|
|
|
const COLUMN_COLORS: Record<string, string> = {
|
|
overdue_tasks: "border-red-500/50 bg-red-50/50 dark:bg-red-950/20",
|
|
due_soon_tasks: "border-yellow-500/50 bg-yellow-50/50 dark:bg-yellow-950/20",
|
|
upcoming_tasks: "border-blue-500/50 bg-blue-50/50 dark:bg-blue-950/20",
|
|
in_progress_tasks: "border-green-500/50 bg-green-50/50 dark:bg-green-950/20",
|
|
completed_tasks: "border-gray-500/50 bg-gray-50/50 dark:bg-gray-950/20",
|
|
cancelled_tasks: "border-slate-500/50 bg-slate-50/50 dark:bg-slate-950/20",
|
|
};
|
|
|
|
const COLUMN_HEADER_COLORS: Record<string, string> = {
|
|
overdue_tasks: "text-red-700 dark:text-red-400",
|
|
due_soon_tasks: "text-yellow-700 dark:text-yellow-400",
|
|
upcoming_tasks: "text-blue-700 dark:text-blue-400",
|
|
in_progress_tasks: "text-green-700 dark:text-green-400",
|
|
completed_tasks: "text-gray-700 dark:text-gray-400",
|
|
cancelled_tasks: "text-slate-700 dark:text-slate-400",
|
|
};
|
|
|
|
const COUNT_BADGE_COLORS: Record<string, string> = {
|
|
overdue_tasks: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
|
|
due_soon_tasks: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
|
|
upcoming_tasks: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
|
|
in_progress_tasks: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
|
|
completed_tasks: "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200",
|
|
cancelled_tasks: "bg-slate-100 text-slate-800 dark:bg-slate-900 dark:text-slate-200",
|
|
};
|
|
|
|
interface KanbanColumnProps {
|
|
column: KanbanColumnType;
|
|
}
|
|
|
|
function DraggableTask({ task }: { task: import("@/lib/api/tasks").TaskResponse }) {
|
|
const {
|
|
attributes,
|
|
listeners,
|
|
setNodeRef,
|
|
transform,
|
|
isDragging,
|
|
} = useDraggable({ id: task.id });
|
|
|
|
const wasDragging = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (isDragging) {
|
|
wasDragging.current = true;
|
|
}
|
|
}, [isDragging]);
|
|
|
|
const style: React.CSSProperties = transform
|
|
? {
|
|
transform: `translate(${transform.x}px, ${transform.y}px)`,
|
|
opacity: isDragging ? 0.5 : 1,
|
|
zIndex: isDragging ? 50 : undefined,
|
|
position: isDragging ? "relative" : undefined,
|
|
}
|
|
: undefined as unknown as React.CSSProperties;
|
|
|
|
// Block the click that fires after a drag ends so the Link doesn't navigate
|
|
const handleClick = (e: React.MouseEvent) => {
|
|
if (wasDragging.current) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
wasDragging.current = false;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={setNodeRef}
|
|
style={style}
|
|
{...attributes}
|
|
{...listeners}
|
|
onClickCapture={handleClick}
|
|
>
|
|
<TaskCard task={task} isDragging={isDragging} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function KanbanColumn({ column }: KanbanColumnProps) {
|
|
const { setNodeRef, isOver } = useDroppable({
|
|
id: column.name,
|
|
});
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"flex flex-col min-w-[280px] sm:min-w-0 sm:flex-1 max-w-[320px] sm:max-w-none snap-center sm:snap-align-none rounded-lg border-2 p-3",
|
|
COLUMN_COLORS[column.name] ?? "border-border bg-muted/30",
|
|
isOver && "ring-2 ring-primary"
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<h3
|
|
className={cn(
|
|
"font-semibold text-sm",
|
|
COLUMN_HEADER_COLORS[column.name]
|
|
)}
|
|
>
|
|
{column.display_name}
|
|
</h3>
|
|
<Badge
|
|
variant="secondary"
|
|
className={cn("text-xs", COUNT_BADGE_COLORS[column.name])}
|
|
>
|
|
{column.count}
|
|
</Badge>
|
|
</div>
|
|
|
|
<div ref={setNodeRef} className="flex-1 space-y-2 min-h-[60px]">
|
|
{column.tasks.map((task) => (
|
|
<DraggableTask key={task.id} task={task} />
|
|
))}
|
|
{column.tasks.length === 0 && (
|
|
<div className="flex items-center justify-center h-[60px] text-xs text-muted-foreground rounded-md border border-dashed">
|
|
No tasks
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|