100 lines
3.2 KiB
TypeScript
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>
|
|
);
|
|
}
|