feat(admin): auto-slug from name + "add project" on Projects page

- slug fields auto-fill from the name (slugify keeps Persian + latin letters,
  spaces → "-") until the slug is edited by hand; applies to all data-driven
  forms (categories/tags/blogs/…) and the Templates form
- Projects page (/admin/projects) gains "+ پروژه جدید": pick a template
  (container) + name/aspect/resolution/size/duration/fps/mode → POST /v1/projects.
  Previously a project could only be added while editing a template.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 00:00:56 +03:30
parent d955d951b5
commit 08d2de8e92
3 changed files with 120 additions and 4 deletions
+27 -1
View File
@@ -45,6 +45,17 @@ const inputCls = "w-full rounded-lg border border-[#262b40] bg-[#0c0e1a] px-3 py
const PAGE_SIZE = 25;
/** URL-safe slug; keeps unicode letters (incl. Persian) + digits, spaces → "-". */
export function slugify(s: string): string {
return s
.trim()
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^\p{L}\p{N}-]+/gu, "")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
}
export function AdminResource({ config }: { config: ResourceConfig }) {
const idKey = config.idKey ?? "id";
const [rows, setRows] = useState<Record<string, unknown>[]>([]);
@@ -55,6 +66,7 @@ export function AdminResource({ config }: { config: ResourceConfig }) {
const [form, setForm] = useState<Record<string, unknown>>({});
const [query, setQuery] = useState("");
const [page, setPage] = useState(1);
const [slugTouched, setSlugTouched] = useState(false);
const [saving, setSaving] = useState(false);
const url = (suffix = "") => `/api/admin/resource/${config.basePath}${suffix}`;
@@ -80,10 +92,23 @@ export function AdminResource({ config }: { config: ResourceConfig }) {
reload();
}, [reload]);
const hasSlug = !!config.fields?.some((f) => f.key === "slug");
// Update a field; auto-fill slug from name until the slug is edited by hand.
const setField = (key: string, value: unknown) => {
setForm((prev) => {
const next = { ...prev, [key]: value };
if (key === "name" && hasSlug && !slugTouched) next.slug = slugify(String(value ?? ""));
return next;
});
if (key === "slug") setSlugTouched(true);
};
const openCreate = () => {
const init: Record<string, unknown> = {};
config.fields?.forEach((f) => (init[f.key] = f.defaultValue ?? (f.type === "checkbox" ? false : "")));
setForm(init);
setSlugTouched(false); // new record → keep syncing slug from name
setCreating(true);
setEditing(null);
};
@@ -92,6 +117,7 @@ export function AdminResource({ config }: { config: ResourceConfig }) {
const init: Record<string, unknown> = {};
config.fields?.forEach((f) => (init[f.key] = row[f.key] ?? (f.type === "checkbox" ? false : "")));
setForm(init);
setSlugTouched(true); // existing record → never auto-rewrite its slug
setEditing(row);
setCreating(false);
};
@@ -266,7 +292,7 @@ export function AdminResource({ config }: { config: ResourceConfig }) {
) : (
<input type={f.type === "number" ? "number" : "text"} className={inputCls} placeholder={f.placeholder}
value={String(form[f.key] ?? "")}
onChange={(e) => setForm({ ...form, [f.key]: f.type === "number" ? Number(e.target.value) : e.target.value })} />
onChange={(e) => setField(f.key, f.type === "number" ? Number(e.target.value) : e.target.value)} />
)}
</div>
))}