Initial commit

This commit is contained in:
twotalesanimation
2026-05-19 22:20:29 +02:00
commit 0fbe856dce
173 changed files with 38316 additions and 0 deletions
+214
View File
@@ -0,0 +1,214 @@
"use client";
import { useDroppable, useDraggable } from "@dnd-kit/core";
import { format, parseISO, formatDistanceToNow } from "date-fns";
import { cn } from "@/lib/utils";
import {
CalendarDays,
Clock,
GripVertical,
Layers,
ChevronDown,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { TaskStatus, TaskType } from "@prisma/client";
import {
ScheduleTask,
ActiveDragData,
} from "@/app/(dashboard)/schedule/SchedulePageClient";
import { TASK_STATUS_CONFIG, TASK_TYPE_LABELS } from "@/components/tasks/TaskCard";
const PRIORITY_DOT: Record<string, string> = {
LOW: "bg-zinc-500",
NORMAL: "bg-blue-500",
HIGH: "bg-amber-500",
URGENT: "bg-red-500",
};
function toDate(val: string | null | undefined): Date | null {
if (!val) return null;
try {
return parseISO(val);
} catch {
return new Date(val);
}
}
function BacklogItem({
task,
canEdit,
}: {
task: ScheduleTask;
canEdit: boolean;
}) {
const { attributes, listeners, setNodeRef, transform, isDragging } =
useDraggable({
id: task.id,
disabled: !canEdit,
data: {
type: "backlog",
taskId: task.id,
estimatedHours: task.estimatedHours,
} as ActiveDragData,
});
const cfg = TASK_STATUS_CONFIG[task.status];
const StatusIcon = cfg.icon;
const contextCode = task.shot?.shotCode ?? task.asset?.assetCode;
const dueDate = toDate(task.dueDate);
const isOverdue =
dueDate && dueDate < new Date() && task.status !== "DONE";
const style = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
zIndex: 50,
}
: undefined;
return (
<div
ref={setNodeRef}
style={style}
className={cn(
"group flex items-start gap-2 px-3 py-2.5 rounded-lg border transition-colors",
"border-zinc-800 bg-zinc-900 hover:border-zinc-700 hover:bg-zinc-800/80",
isDragging && "opacity-30 border-amber-500/40",
canEdit && "cursor-grab active:cursor-grabbing"
)}
{...listeners}
{...attributes}
>
{/* Grip */}
{canEdit && (
<GripVertical className="h-3.5 w-3.5 text-zinc-600 shrink-0 mt-0.5 group-hover:text-zinc-400" />
)}
{/* Priority dot */}
<div
className={cn(
"w-1.5 h-1.5 rounded-full shrink-0 mt-1.5",
PRIORITY_DOT[task.priority] ?? "bg-zinc-500"
)}
/>
{/* Content */}
<div className="flex-1 min-w-0 space-y-0.5">
<div className="flex items-center gap-1.5">
{contextCode && (
<span className="text-[10px] font-mono bg-zinc-800 text-zinc-400 px-1.5 py-0.5 rounded shrink-0">
{contextCode}
</span>
)}
<span className="text-xs font-medium text-zinc-200 truncate">
{task.title}
</span>
</div>
<div className="flex items-center gap-2 flex-wrap">
<span className="text-[10px] text-zinc-500">
{TASK_TYPE_LABELS[task.type as TaskType]} · {task.project.code}
</span>
{task.estimatedHours && (
<span className="flex items-center gap-0.5 text-[10px] text-zinc-600">
<Clock className="h-2.5 w-2.5" />
{task.estimatedHours}h
</span>
)}
</div>
<div className="flex items-center gap-2">
<Badge
className={cn(
"text-[9px] border px-1 py-0 h-3.5 gap-0.5 shrink-0",
cfg.color
)}
>
<StatusIcon className="h-2 w-2" />
{cfg.label}
</Badge>
{dueDate && (
<span
className={cn(
"flex items-center gap-0.5 text-[10px]",
isOverdue ? "text-red-400" : "text-zinc-600"
)}
>
<CalendarDays className="h-2.5 w-2.5" />
{formatDistanceToNow(dueDate, { addSuffix: true })}
</span>
)}
</div>
</div>
</div>
);
}
interface BacklogPanelProps {
tasks: ScheduleTask[];
canEdit: boolean;
}
export function BacklogPanel({ tasks, canEdit }: BacklogPanelProps) {
const { setNodeRef, isOver } = useDroppable({
id: "backlog-drop-zone",
});
return (
<div
className={cn(
"flex flex-col border-l border-zinc-800 bg-zinc-900 transition-colors",
"w-72 shrink-0"
)}
>
{/* Header */}
<div className="flex items-center justify-between px-3 py-2.5 border-b border-zinc-800 shrink-0">
<div className="flex items-center gap-2">
<Layers className="h-4 w-4 text-zinc-400" />
<span className="text-sm font-medium text-zinc-200">Backlog</span>
<span className="text-xs text-zinc-500 bg-zinc-800 px-1.5 py-0.5 rounded-full">
{tasks.length}
</span>
</div>
{canEdit && (
<span className="text-[10px] text-zinc-600">
Drag onto timeline
</span>
)}
</div>
{/* Drop zone + list */}
<div
ref={setNodeRef}
className={cn(
"flex-1 transition-colors",
isOver && "bg-amber-500/5 ring-1 ring-inset ring-amber-500/20"
)}
>
{tasks.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-zinc-600">
<Layers className="h-8 w-8 mb-2 opacity-30" />
<p className="text-xs">All tasks scheduled</p>
{canEdit && (
<p className="text-[10px] mt-1 text-zinc-700">
Drop here to unschedule
</p>
)}
</div>
) : (
<ScrollArea className="h-full">
<div className="p-2 space-y-1.5">
{canEdit && isOver && (
<div className="flex items-center justify-center py-3 rounded-lg border border-dashed border-amber-500/40 text-amber-400 text-xs">
Drop to unschedule
</div>
)}
{tasks.map((task) => (
<BacklogItem key={task.id} task={task} canEdit={canEdit} />
))}
</div>
</ScrollArea>
)}
</div>
</div>
);
}