151 lines
4.3 KiB
TypeScript
151 lines
4.3 KiB
TypeScript
|
|
"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>
|
||
|
|
);
|
||
|
|
}
|