Files
flatrender/src/components/studio/DraggableSceneItem.tsx
T

151 lines
4.3 KiB
TypeScript
Raw Normal View History

"use client";
import { useEffect, useRef, useState } from "react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { GripVertical } from "lucide-react";
import { SceneItemActions } from "@/components/studio/SceneItemActions";
import type { Scene } from "@/lib/studio-types";
import { cn } from "@/lib/utils";
export interface DraggableSceneItemProps {
scene: Scene;
isActive: boolean;
canDelete: boolean;
onSelect: () => void;
onDelete: () => void;
onDuplicate: () => void;
onRename: (name: string) => void;
}
export function DraggableSceneItem({
scene,
isActive,
canDelete,
onSelect,
onDelete,
onDuplicate,
onRename,
}: DraggableSceneItemProps) {
const [isEditing, setIsEditing] = useState(false);
const [editName, setEditName] = useState(scene.name);
const inputRef = useRef<HTMLInputElement>(null);
const {
attributes,
listeners,
setNodeRef,
setActivatorNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: scene.id });
useEffect(() => {
setEditName(scene.name);
}, [scene.name]);
useEffect(() => {
if (isEditing) {
inputRef.current?.focus();
inputRef.current?.select();
}
}, [isEditing]);
const commitRename = () => {
const trimmed = editName.trim();
if (trimmed && trimmed !== scene.name) {
onRename(trimmed);
} else {
setEditName(scene.name);
}
setIsEditing(false);
};
return (
<div
ref={setNodeRef}
style={{
transform: CSS.Transform.toString(transform),
transition,
}}
className={cn(
"group flex gap-1 rounded-r-lg",
isActive && "border-l-4 border-l-[#4c6ef5] bg-[#252938]",
isDragging && "z-10 opacity-60"
)}
>
<button
type="button"
ref={setActivatorNodeRef}
className="flex w-6 shrink-0 cursor-grab items-center justify-center text-gray-500 hover:text-gray-300 active:cursor-grabbing"
aria-label={`Drag scene ${scene.name}`}
{...attributes}
{...listeners}
>
<GripVertical className="h-4 w-4" aria-hidden />
</button>
<div className="min-w-0 flex-1 py-1 pr-1">
<button
type="button"
onClick={onSelect}
className="w-full rounded-md text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#4c6ef5]"
>
<div className="relative h-14 w-full overflow-hidden rounded-md bg-[#1a1d2e]">
{scene.thumbnailUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={scene.thumbnailUrl}
alt=""
className="h-full w-full object-cover"
/>
) : null}
<SceneItemActions
sceneName={scene.name}
canDelete={canDelete}
onDuplicate={onDuplicate}
onDelete={onDelete}
/>
<span className="absolute bottom-1 right-1 rounded bg-[#0f111a]/80 px-1.5 py-0.5 text-[10px] font-medium text-gray-300">
{scene.duration}s
</span>
</div>
{isEditing ? (
<input
ref={inputRef}
type="text"
value={editName}
onClick={(event) => event.stopPropagation()}
onChange={(event) => setEditName(event.target.value)}
onBlur={commitRename}
onKeyDown={(event) => {
if (event.key === "Enter") commitRename();
if (event.key === "Escape") {
setEditName(scene.name);
setIsEditing(false);
}
}}
className="mt-1.5 w-full rounded border border-[#2a2d3e] bg-[#1a1d2e] px-1.5 py-0.5 text-xs text-white focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[#4c6ef5]"
aria-label="Scene name"
/>
) : (
<p
className="mt-1.5 truncate text-xs font-medium text-gray-200"
onDoubleClick={(event) => {
event.preventDefault();
event.stopPropagation();
setIsEditing(true);
}}
>
{scene.name}
</p>
)}
</button>
</div>
</div>
);
}