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
+300
View File
@@ -0,0 +1,300 @@
"use client";
import { useDraggable } from "@dnd-kit/core";
import {
format,
isAfter,
parseISO,
} from "date-fns";
import { useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
import { CalendarDays, Clock, ExternalLink, CalendarOff } from "lucide-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import { TaskStatus, TaskType } from "@prisma/client";
import {
ScheduleTask,
ActiveDragData,
} from "@/app/(dashboard)/schedule/SchedulePageClient";
import { TASK_TYPE_LABELS } from "@/components/tasks/TaskCard";
const STATUS_STYLES: Record<
TaskStatus,
{ bg: string; border: string; text: string; dot: string }
> = {
TODO: {
bg: "bg-zinc-800/80",
border: "border-zinc-600/60",
text: "text-zinc-300",
dot: "bg-zinc-500",
},
IN_PROGRESS: {
bg: "bg-blue-900/50",
border: "border-blue-600/60",
text: "text-blue-200",
dot: "bg-blue-400",
},
INTERNAL_REVIEW: {
bg: "bg-purple-900/50",
border: "border-purple-600/60",
text: "text-purple-200",
dot: "bg-purple-400",
},
CLIENT_REVIEW: {
bg: "bg-amber-900/50",
border: "border-amber-600/60",
text: "text-amber-200",
dot: "bg-amber-400",
},
CHANGES: {
bg: "bg-orange-900/50",
border: "border-orange-600/60",
text: "text-orange-200",
dot: "bg-orange-400",
},
DONE: {
bg: "bg-emerald-900/50",
border: "border-emerald-600/60",
text: "text-emerald-200",
dot: "bg-emerald-400",
},
};
function toDate(val: string | null | undefined): Date | null {
if (!val) return null;
try {
return parseISO(val);
} catch {
return new Date(val);
}
}
interface ScheduleTaskBlockProps {
task: ScheduleTask;
dayIndex: number;
duration: number;
artistIndex: number;
viewStart: Date;
days: Date[];
canEdit: boolean;
isDragging: boolean;
laneTop: number;
taskHeight: number;
onResizeMouseDown: (
taskId: string,
currentEndDate: string
) => (e: React.MouseEvent) => void;
onUnschedule: (taskId: string) => void;
dayWidth: number;
}
export function ScheduleTaskBlock({
task,
dayIndex,
duration,
canEdit,
isDragging,
laneTop,
taskHeight,
onResizeMouseDown,
onUnschedule,
dayWidth,
}: ScheduleTaskBlockProps) {
const router = useRouter();
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: task.id,
disabled: !canEdit,
data: {
type: "scheduled",
taskId: task.id,
duration,
} as ActiveDragData,
});
const taskWidth = Math.max(20, (task.estimatedHours ?? 8) / 8 * dayWidth - 4);
const style = {
position: "absolute" as const,
left: Math.max(0, dayIndex) * dayWidth + 2,
width: taskWidth,
top: laneTop,
height: taskHeight,
transform: transform
? `translate3d(${transform.x}px, ${transform.y}px, 0)`
: undefined,
zIndex: isDragging ? 10 : 1,
};
const statusStyle = STATUS_STYLES[task.status];
const contextCode = task.shot?.shotCode ?? task.asset?.assetCode;
const contextName = task.shot?.shotCode ?? task.asset?.name ?? task.title;
const thumbnail = task.shot?.thumbnailUrl ?? null;
const isOverdue =
task.dueDate &&
task.scheduledEndDate &&
isAfter(
toDate(task.scheduledEndDate)!,
toDate(task.dueDate)!
);
const dueDate = toDate(task.dueDate);
const tooNarrow = (task.estimatedHours ?? 8) < 3; // < 3h = too narrow for labels
const tooShort = taskHeight < 16;
return (
<ContextMenu>
<ContextMenuTrigger asChild>
<Tooltip>
<TooltipTrigger asChild>
<div
ref={setNodeRef}
style={style}
className={cn(
"absolute rounded-md border flex items-center overflow-hidden select-none group",
"transition-opacity duration-100",
statusStyle.bg,
statusStyle.border,
isDragging && "opacity-30",
canEdit && "cursor-grab active:cursor-grabbing",
isOverdue && "ring-1 ring-red-500/50"
)}
{...listeners}
{...attributes}
>
{/* Status dot */}
<div
className={cn(
"shrink-0 w-1 self-stretch rounded-l-md",
statusStyle.dot
)}
/>
{/* Thumbnail */}
{thumbnail && !tooNarrow && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={thumbnail}
alt=""
className="shrink-0 h-full object-cover"
style={{ width: taskHeight * 2.39, minWidth: 0 }}
/>
)}
{/* Content */}
<div className="flex-1 min-w-0 flex items-center gap-1 overflow-hidden px-1">
{!tooShort && (
<span
className={cn(
"text-[10px] font-mono font-semibold truncate shrink-0 max-w-[50%]",
statusStyle.text
)}
>
{contextName}
</span>
)}
{!tooNarrow && !tooShort && (
<span className="text-[9px] text-zinc-400 truncate">
{TASK_TYPE_LABELS[task.type as TaskType]}
</span>
)}
{!tooNarrow && !tooShort && task.estimatedHours && (
<span className="text-[9px] text-zinc-500 shrink-0 ml-auto">
{task.estimatedHours}h
</span>
)}
</div>
{/* Overdue indicator */}
{isOverdue && (
<div className="shrink-0 w-1.5 h-1.5 rounded-full bg-red-400 mr-1" />
)}
{/* Resize handle */}
{canEdit && task.scheduledEndDate && (
<div
className="absolute right-0 top-0 bottom-0 w-2 cursor-ew-resize flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
onPointerDown={(e) => e.stopPropagation()}
onMouseDown={onResizeMouseDown(task.id, task.scheduledEndDate)}
onClick={(e) => e.stopPropagation()}
>
<div className="w-0.5 h-3/4 rounded-full bg-current opacity-50" />
</div>
)}
</div>
</TooltipTrigger>
<TooltipContent
side="top"
className="bg-zinc-900 border-zinc-700 text-zinc-100 max-w-[240px]"
>
<div className="space-y-1">
<div className="font-medium text-xs">
{contextCode && (
<span className="text-amber-400 mr-1">{contextCode}</span>
)}
{task.title}
</div>
<div className="text-[11px] text-zinc-400">
{TASK_TYPE_LABELS[task.type as TaskType]} ·{" "}
{task.project.code}
</div>
{task.scheduledStartDate && task.scheduledEndDate && (
<div className="flex items-center gap-1 text-[11px] text-zinc-400">
<CalendarDays className="h-3 w-3" />
{format(toDate(task.scheduledStartDate)!, "MMM d")} {" "}
{format(toDate(task.scheduledEndDate)!, "MMM d")}
</div>
)}
{task.estimatedHours && (
<div className="flex items-center gap-1 text-[11px] text-zinc-400">
<Clock className="h-3 w-3" />
{task.estimatedHours}h estimated
</div>
)}
{dueDate && (
<div
className={cn(
"flex items-center gap-1 text-[11px]",
isOverdue ? "text-red-400" : "text-zinc-400"
)}
>
<CalendarDays className="h-3 w-3" />
Due {format(dueDate, "MMM d")}
{isOverdue && " — Late!"}
</div>
)}
</div>
</TooltipContent>
</Tooltip>
</ContextMenuTrigger>
<ContextMenuContent className="bg-zinc-900 border-zinc-700 text-zinc-100 w-48">
<ContextMenuItem
className="gap-2 cursor-pointer focus:bg-zinc-800 focus:text-white"
onSelect={() => router.push(`/tasks/${task.id}`)}
>
<ExternalLink className="h-3.5 w-3.5 text-zinc-400" />
View Task
</ContextMenuItem>
{canEdit && (
<>
<ContextMenuSeparator className="bg-zinc-800" />
<ContextMenuItem
className="gap-2 cursor-pointer focus:bg-zinc-800 focus:text-red-400 text-red-400"
onSelect={() => onUnschedule(task.id)}
>
<CalendarOff className="h-3.5 w-3.5" />
Unschedule
</ContextMenuItem>
</>
)}
</ContextMenuContent>
</ContextMenu>
);
}