Initial commit
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user