Added status tracking & episode due dates
Deploy / deploy (push) Failing after 2m30s

This commit is contained in:
twotalesanimation
2026-06-03 14:49:12 +02:00
parent 15046892d1
commit e75a15132e
12 changed files with 915 additions and 25 deletions
@@ -1,7 +1,8 @@
"use client";
import { useState } from "react";
import { useState, useTransition } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ShotCard } from "@/components/shots/ShotCard";
import { NewShotDialog } from "@/components/shots/NewShotDialog";
import { ImportShotsDialog } from "@/components/shots/ImportShotsDialog";
@@ -11,9 +12,12 @@ import { TaskCard } from "@/components/tasks/TaskCard";
import { NewTaskDialog } from "@/components/tasks/NewTaskDialog";
import { KanbanBoard } from "@/components/tasks/KanbanBoard";
import { cn } from "@/lib/utils";
import { Film, Package, ListTodo, LayoutDashboard, Plus, Settings, FileUp, ChevronDown, ChevronRight } from "lucide-react";
import { Film, Package, ListTodo, LayoutDashboard, Plus, Settings, FileUp, ChevronDown, ChevronRight, Calendar, Pencil, Check, X } from "lucide-react";
import type { ShotWithDetails } from "@/types";
import { ProjectSettingsTab } from "@/components/projects/ProjectSettingsTab";
import { setEpisodeDueDate } from "@/actions/episode-due-dates";
import { useToast } from "@/components/ui/use-toast";
import { format } from "date-fns";
type Tab = "shots" | "assets" | "tasks" | "kanban" | "settings";
@@ -61,6 +65,7 @@ interface ProjectTabsClientProps {
tasks: any[];
artists: Artist[];
shotGroups: { id: string; name: string }[];
episodeDueDates: { episode: string; dueDate: Date | string }[];
canManage: boolean;
}
@@ -75,14 +80,42 @@ export function ProjectTabsClient({
tasks,
artists,
shotGroups,
episodeDueDates,
canManage,
}: ProjectTabsClientProps) {
const { toast } = useToast();
const [activeTab, setActiveTab] = useState<Tab>("shots");
const [showNewShot, setShowNewShot] = useState(false);
const [showImportShots, setShowImportShots] = useState(false);
const [showNewAsset, setShowNewAsset] = useState(false);
const [showNewTask, setShowNewTask] = useState(false);
const [collapsedEpisodes, setCollapsedEpisodes] = useState<Set<string>>(new Set());
const [editingEpisode, setEditingEpisode] = useState<string | null>(null);
const [episodeDateInput, setEpisodeDateInput] = useState("");
const [isPendingDate, startDateTransition] = useTransition();
// Build a lookup map for episode due dates
const episodeDueDateMap = new Map(
episodeDueDates.map((e) => [e.episode, new Date(e.dueDate)])
);
const handleSaveEpisodeDate = (episode: string) => {
startDateTransition(async () => {
try {
await setEpisodeDueDate({
projectId,
episode,
dueDate: episodeDateInput || null,
});
toast({ title: `Due date ${episodeDateInput ? "set" : "cleared"} for Episode ${episode}` });
} catch {
toast({ title: "Failed to save due date", variant: "destructive" });
} finally {
setEditingEpisode(null);
setEpisodeDateInput("");
}
});
};
const toggleEpisode = (ep: string) =>
setCollapsedEpisodes((prev) => {
@@ -211,23 +244,98 @@ export function ProjectTabsClient({
<div className="space-y-4">
{episodeGroups.map(([episode, episodeShots]) => {
const collapsed = collapsedEpisodes.has(episode);
const dueDate = episodeDueDateMap.get(episode);
const isOverdue = dueDate && dueDate < new Date();
const isEditing = editingEpisode === episode;
return (
<div key={episode}>
<button
onClick={() => toggleEpisode(episode)}
className="flex items-center gap-2 w-full mb-3 group text-left"
>
{collapsed
? <ChevronRight className="h-4 w-4 text-muted-foreground shrink-0" />
: <ChevronDown className="h-4 w-4 text-muted-foreground shrink-0" />}
<span className="font-semibold text-sm">
Episode {episode}
</span>
<span className="text-xs text-muted-foreground font-normal">
{episodeShots.length} shot{episodeShots.length !== 1 ? "s" : ""}
</span>
<div className="flex items-center gap-2 w-full mb-3">
<button
onClick={() => toggleEpisode(episode)}
className="flex items-center gap-2 shrink-0"
>
{collapsed
? <ChevronRight className="h-4 w-4 text-muted-foreground" />
: <ChevronDown className="h-4 w-4 text-muted-foreground" />}
</button>
<button
onClick={() => toggleEpisode(episode)}
className="flex items-center gap-2 text-left"
>
<span className="font-semibold text-sm">
Episode {episode}
</span>
<span className="text-xs text-muted-foreground font-normal">
{episodeShots.length} shot{episodeShots.length !== 1 ? "s" : ""}
</span>
</button>
{/* Due date display / edit */}
{isEditing ? (
<div className="flex items-center gap-1 ml-2">
<Input
type="date"
value={episodeDateInput}
onChange={(e) => setEpisodeDateInput(e.target.value)}
className="h-6 w-36 text-xs px-2 py-0"
/>
<button
onClick={() => handleSaveEpisodeDate(episode)}
disabled={isPendingDate}
className="text-emerald-400 hover:text-emerald-300 p-0.5"
>
<Check className="h-3.5 w-3.5" />
</button>
<button
onClick={() => { setEditingEpisode(null); setEpisodeDateInput(""); }}
className="text-zinc-500 hover:text-zinc-300 p-0.5"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
) : (
<div className="flex items-center gap-1.5 ml-2">
{dueDate && (
<span className={cn(
"flex items-center gap-1 text-xs",
isOverdue ? "text-red-400" : "text-zinc-400"
)}>
<Calendar className="h-3 w-3" />
{format(dueDate, "d MMM yyyy")}
</span>
)}
{canManage && (
<button
onClick={() => {
setEditingEpisode(episode);
setEpisodeDateInput(
dueDate ? format(dueDate, "yyyy-MM-dd") : ""
);
}}
className="text-zinc-600 hover:text-zinc-300 p-0.5 opacity-0 group-hover:opacity-100 transition-opacity"
title="Set due date"
>
<Pencil className="h-3 w-3" />
</button>
)}
</div>
)}
<div className="flex-1 h-px bg-border ml-1" />
</button>
{/* Always-visible edit button for managers */}
{canManage && !isEditing && (
<button
onClick={() => {
setEditingEpisode(episode);
setEpisodeDateInput(
dueDate ? format(dueDate, "yyyy-MM-dd") : ""
);
}}
className="text-zinc-600 hover:text-zinc-400 transition-colors shrink-0"
title="Set episode due date"
>
<Pencil className="h-3 w-3" />
</button>
)}
</div>
{!collapsed && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
{episodeShots.map((shot) => (