feat: Phase 4-5 — demo mode, polish, deploy, and bug fixes
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>
This commit is contained in:
@@ -1,62 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import { useDroppable } from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy, useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
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: "border-red-500/50 bg-red-50/50 dark:bg-red-950/20",
|
||||
due_today: "border-orange-500/50 bg-orange-50/50 dark:bg-orange-950/20",
|
||||
due_soon: "border-yellow-500/50 bg-yellow-50/50 dark:bg-yellow-950/20",
|
||||
upcoming: "border-blue-500/50 bg-blue-50/50 dark:bg-blue-950/20",
|
||||
in_progress: "border-green-500/50 bg-green-50/50 dark:bg-green-950/20",
|
||||
completed: "border-gray-500/50 bg-gray-50/50 dark:bg-gray-950/20",
|
||||
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: "text-red-700 dark:text-red-400",
|
||||
due_today: "text-orange-700 dark:text-orange-400",
|
||||
due_soon: "text-yellow-700 dark:text-yellow-400",
|
||||
upcoming: "text-blue-700 dark:text-blue-400",
|
||||
in_progress: "text-green-700 dark:text-green-400",
|
||||
completed: "text-gray-700 dark:text-gray-400",
|
||||
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: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
|
||||
due_today: "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200",
|
||||
due_soon: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
|
||||
upcoming: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
|
||||
in_progress: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
|
||||
completed: "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200",
|
||||
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 SortableTask({ task }: { task: import("@/lib/api/tasks").TaskResponse }) {
|
||||
function DraggableTask({ task }: { task: import("@/lib/api/tasks").TaskResponse }) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: task.id });
|
||||
} = useDraggable({ id: task.id });
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
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}>
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
onClickCapture={handleClick}
|
||||
>
|
||||
<TaskCard task={task} isDragging={isDragging} />
|
||||
</div>
|
||||
);
|
||||
@@ -67,12 +91,10 @@ export function KanbanColumn({ column }: KanbanColumnProps) {
|
||||
id: column.name,
|
||||
});
|
||||
|
||||
const taskIds = column.tasks.map((t) => t.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col min-w-[280px] max-w-[320px] rounded-lg border-2 p-3",
|
||||
"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"
|
||||
)}
|
||||
@@ -95,11 +117,9 @@ export function KanbanColumn({ column }: KanbanColumnProps) {
|
||||
</div>
|
||||
|
||||
<div ref={setNodeRef} className="flex-1 space-y-2 min-h-[60px]">
|
||||
<SortableContext items={taskIds} strategy={verticalListSortingStrategy}>
|
||||
{column.tasks.map((task) => (
|
||||
<SortableTask key={task.id} task={task} />
|
||||
))}
|
||||
</SortableContext>
|
||||
{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
|
||||
|
||||
Reference in New Issue
Block a user