feat: full studio build -- light theme, canvas thumbnails, i18n (fa/en)
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user