Files
pinkudex/components/entities/EntityRenameInline.tsx
T
2026-05-26 22:46:00 +02:00

100 lines
3.2 KiB
TypeScript

"use client";
import { useEffect, useRef, useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { Pencil, Check, X, Loader2 } from "lucide-react";
interface Props {
initialName: string;
/** Server action that renames and returns the new slug/name (or null on no-op). */
onRename: (name: string) => Promise<{ slug?: string; name?: string } | null>;
/** Path prefix for the post-rename redirect (e.g. "/studios/"). The new slug or URL-encoded name is appended. */
redirectBase?: string;
/** Which field of the rename result to append to redirectBase. Defaults to "slug". */
redirectKey?: "slug" | "name";
}
export function EntityRenameInline({ initialName, onRename, redirectBase, redirectKey = "slug" }: Props) {
const router = useRouter();
const [editing, setEditing] = useState(false);
const [value, setValue] = useState(initialName);
const [pending, start] = useTransition();
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => { setValue(initialName); }, [initialName]);
useEffect(() => {
if (editing) {
inputRef.current?.focus();
inputRef.current?.select();
}
}, [editing]);
function cancel() {
setEditing(false);
setValue(initialName);
}
function save() {
const next = value.trim();
if (!next || next === initialName) { cancel(); return; }
start(async () => {
const r = await onRename(next);
setEditing(false);
if (r && redirectBase) {
const key = redirectKey === "name" ? r.name : r.slug;
if (key) {
router.push(`${redirectBase}${redirectKey === "name" ? encodeURIComponent(key) : key}`);
return;
}
}
router.refresh();
});
}
if (!editing) {
return (
<button
type="button"
onClick={() => setEditing(true)}
className="flex items-center gap-1.5 text-xs px-2 py-1 rounded-lg glass text-[var(--color-fg-muted)] hover:text-[var(--color-fg)]"
title="Rename"
>
<Pencil className="w-3.5 h-3.5" /> Rename
</button>
);
}
return (
<form
onSubmit={(e) => { e.preventDefault(); save(); }}
className="flex items-center gap-1"
>
<input
ref={inputRef}
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={(e) => { if (e.key === "Escape") cancel(); }}
maxLength={120}
disabled={pending}
className="glass rounded-lg px-2 py-1 text-sm outline-none focus:border-[var(--color-cyan)] w-64"
/>
<button
type="submit"
disabled={pending || !value.trim() || value.trim() === initialName}
className="flex items-center justify-center w-7 h-7 rounded-lg bg-[var(--color-cyan)] text-black disabled:opacity-40"
title="Save"
>
{pending ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Check className="w-3.5 h-3.5" />}
</button>
<button
type="button"
onClick={cancel}
disabled={pending}
className="flex items-center justify-center w-7 h-7 rounded-lg glass text-[var(--color-fg-muted)] hover:text-[var(--color-fg)]"
title="Cancel"
>
<X className="w-3.5 h-3.5" />
</button>
</form>
);
}