feat(admin): full Persian (fa) localization + RTL polish

- admin-resources: all config titles/descriptions/columns/fields → Persian
- AdminResource: generic UI strings (new/edit/delete/save/cancel/loading/empty/
  actions/confirm) → Persian; text-left/right → logical text-start/end
- TemplatesAdmin: all labels, table, modal, statuses, errors → Persian
- FileUploadField, WebsiteSettingsAdmin, FileManager → Persian
- layout: ms-auto logical prop (NodesTable/RenderQueue/AiContentStudio already
  i18n with Persian values)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-02 23:47:35 +03:30
parent 0cbfcbfdf7
commit 88a44b1349
7 changed files with 235 additions and 235 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ export default async function AdminLayout({
))}
<a
href="/dashboard"
className="ml-auto text-xs text-gray-500 transition-colors hover:text-gray-300"
className="ms-auto text-xs text-gray-500 transition-colors hover:text-gray-300"
>
{t("backToDashboard")}
</a>
+11 -11
View File
@@ -119,7 +119,7 @@ export function AdminResource({ config }: { config: ResourceConfig }) {
};
const remove = async (row: Record<string, unknown>) => {
if (!confirm(`Delete this ${config.title.replace(/s$/, "").toLowerCase()}?`)) return;
if (!confirm("حذف این مورد؟")) return;
const res = await fetch(url(`/${row[idKey]}`), { method: "DELETE" });
if (res.ok) reload();
else {
@@ -136,7 +136,7 @@ export function AdminResource({ config }: { config: ResourceConfig }) {
{config.description && <p className="mt-1 text-sm text-gray-400">{config.description}</p>}
</div>
{config.canCreate && config.fields && (
<button className={btn} onClick={openCreate}>+ New</button>
<button className={btn} onClick={openCreate}>+ مورد جدید</button>
)}
</div>
@@ -145,20 +145,20 @@ export function AdminResource({ config }: { config: ResourceConfig }) {
<div className={`${card} overflow-hidden`}>
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1e2235] text-left text-xs text-gray-500">
<tr className="border-b border-[#1e2235] text-start text-xs text-gray-500">
{config.columns.map((c) => (
<th key={c.key} className="px-4 py-3 font-medium">{c.label}</th>
))}
{(config.canEdit || config.canDelete || config.rowActions) && (
<th className="px-4 py-3 text-right font-medium">Actions</th>
<th className="px-4 py-3 text-end font-medium">عملیات</th>
)}
</tr>
</thead>
<tbody>
{loading ? (
<tr><td className="px-4 py-8 text-center text-gray-500" colSpan={99}>Loading</td></tr>
<tr><td className="px-4 py-8 text-center text-gray-500" colSpan={99}>در حال بارگذاری</td></tr>
) : rows.length === 0 ? (
<tr><td className="px-4 py-8 text-center text-gray-500" colSpan={99}>No records.</td></tr>
<tr><td className="px-4 py-8 text-center text-gray-500" colSpan={99}>رکوردی یافت نشد.</td></tr>
) : (
rows.map((row, i) => (
<tr key={String(row[idKey] ?? i)} className="border-b border-[#161a2e] hover:bg-[#12152a]">
@@ -172,14 +172,14 @@ export function AdminResource({ config }: { config: ResourceConfig }) {
<div className="flex items-center justify-end gap-2">
{config.rowActions?.(row, reload)}
{config.canEdit && config.fields && (
<button className={btnGhost} onClick={() => openEdit(row)}>Edit</button>
<button className={btnGhost} onClick={() => openEdit(row)}>ویرایش</button>
)}
{config.canDelete && (
<button
className="rounded-lg border border-red-500/30 px-3 py-1.5 text-xs text-red-300 hover:bg-red-500/10"
onClick={() => remove(row)}
>
Delete
حذف
</button>
)}
</div>
@@ -196,7 +196,7 @@ export function AdminResource({ config }: { config: ResourceConfig }) {
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" onClick={closeForm}>
<div className={`${card} w-full max-w-lg p-5`} onClick={(e) => e.stopPropagation()}>
<h2 className="text-sm font-semibold text-white">
{editing ? "Edit" : "New"} {config.title.replace(/s$/, "")}
{editing ? "ویرایش" : "افزودن"} {config.title}
</h2>
<div className="mt-4 grid max-h-[60vh] gap-3 overflow-y-auto pr-1">
{config.fields.map((f) => (
@@ -235,8 +235,8 @@ export function AdminResource({ config }: { config: ResourceConfig }) {
))}
</div>
<div className="mt-5 flex items-center justify-end gap-2">
<button className={btnGhost} onClick={closeForm}>Cancel</button>
<button className={btn} onClick={submit} disabled={saving}>{saving ? "Saving…" : "Save"}</button>
<button className={btnGhost} onClick={closeForm}>انصراف</button>
<button className={btn} onClick={submit} disabled={saving}>{saving ? "در حال ذخیره…" : "ذخیره"}</button>
</div>
</div>
</div>
+11 -11
View File
@@ -47,7 +47,7 @@ export function FileManager() {
const data = await res.json();
setFiles(data?.data ?? data?.items ?? (Array.isArray(data) ? data : []));
} catch {
setError("Failed to load files");
setError("بارگذاری فایل‌ها ناموفق بود");
} finally {
setLoading(false);
}
@@ -64,7 +64,7 @@ export function FileManager() {
const res = await fetch("/api/admin/files/upload", { method: "POST", body: fd });
if (!res.ok) {
const d = await res.json().catch(() => null);
setError(d?.error ?? `Failed to upload ${file.name}`);
setError(d?.error ?? `آپلود ${file.name} ناموفق بود`);
}
}
setUploading(false);
@@ -73,9 +73,9 @@ export function FileManager() {
};
const remove = async (f: FileItem) => {
if (!confirm(`Delete ${f.name}?`)) return;
if (!confirm(`«${f.name}» حذف شود؟`)) return;
const res = await fetch(`/api/admin/resource/files/${f.id}`, { method: "DELETE" });
if (res.ok) reload(); else setError("Delete failed");
if (res.ok) reload(); else setError("حذف ناموفق بود");
};
const copy = (url: string) => {
@@ -88,11 +88,11 @@ export function FileManager() {
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-semibold text-white">Media Library</h1>
<p className="mt-1 text-sm text-gray-400">Upload and manage images & files. Public URLs can be copied into any field.</p>
<h1 className="text-xl font-semibold text-white">کتابخانه رسانه</h1>
<p className="mt-1 text-sm text-gray-400">آپلود و مدیریت تصاویر و فایلها. نشانیهای عمومی را میتوان در هر فیلدی کپی کرد.</p>
</div>
<button className={btn} onClick={() => inputRef.current?.click()} disabled={uploading}>
{uploading ? "Uploading…" : "+ Upload files"}
{uploading ? "در حال آپلود…" : "+ آپلود فایل"}
</button>
<input ref={inputRef} type="file" multiple className="hidden" onChange={(e) => e.target.files && uploadFiles(e.target.files)} />
</div>
@@ -105,9 +105,9 @@ export function FileManager() {
onDrop={(e) => { e.preventDefault(); if (e.dataTransfer.files.length) uploadFiles(e.dataTransfer.files); }}
>
{loading ? (
<p className="py-10 text-center text-sm text-gray-500">Loading</p>
<p className="py-10 text-center text-sm text-gray-500">در حال بارگذاری</p>
) : files.length === 0 ? (
<p className="py-10 text-center text-sm text-gray-500">No files yet. Drag & drop here or click Upload.</p>
<p className="py-10 text-center text-sm text-gray-500">هنوز فایلی وجود ندارد. فایل را اینجا بکشید و رها کنید یا روی آپلود بزنید.</p>
) : (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6">
{files.map((f) => {
@@ -127,10 +127,10 @@ export function FileManager() {
<div className="mt-1 flex gap-1">
{url && (
<button className="flex-1 rounded border border-[#262b40] px-1 py-0.5 text-[10px] text-gray-400 hover:bg-[#161a2e]" onClick={() => copy(url)}>
{copied === url ? "Copied!" : "Copy URL"}
{copied === url ? "کپی شد!" : "کپی نشانی"}
</button>
)}
<button className="rounded border border-red-500/30 px-1.5 py-0.5 text-[10px] text-red-300 hover:bg-red-500/10" onClick={() => remove(f)}>Del</button>
<button className="rounded border border-red-500/30 px-1.5 py-0.5 text-[10px] text-red-300 hover:bg-red-500/10" onClick={() => remove(f)}>حذف</button>
</div>
</div>
);
+5 -5
View File
@@ -28,10 +28,10 @@ export function FileUploadField({
fd.append("file", file);
const res = await fetch("/api/admin/files/upload", { method: "POST", body: fd });
const data = await res.json();
if (!res.ok || !data?.url) throw new Error(data?.error ?? "Upload failed");
if (!res.ok || !data?.url) throw new Error(data?.error ?? "بارگذاری ناموفق بود");
onChange(data.url);
} catch (e) {
setError(e instanceof Error ? e.message : "Upload failed");
setError(e instanceof Error ? e.message : "بارگذاری ناموفق بود");
} finally {
setUploading(false);
if (inputRef.current) inputRef.current.value = "";
@@ -53,7 +53,7 @@ export function FileUploadField({
)
) : (
<span className="flex h-14 w-14 items-center justify-center rounded-lg border border-dashed border-[#262b40] text-[10px] text-gray-600">
none
ندارد
</span>
)}
<div className="flex flex-col gap-1.5">
@@ -73,7 +73,7 @@ export function FileUploadField({
onClick={() => inputRef.current?.click()}
className="rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-500 disabled:opacity-50"
>
{uploading ? "Uploading…" : value ? "Replace" : "Upload"}
{uploading ? "در حال بارگذاری…" : value ? "تعویض" : "بارگذاری"}
</button>
{value && (
<button
@@ -81,7 +81,7 @@ export function FileUploadField({
onClick={() => onChange("")}
className="rounded-lg border border-[#262b40] px-3 py-1.5 text-xs text-gray-400 hover:bg-[#161a2e]"
>
Remove
حذف
</button>
)}
</div>
+42 -42
View File
@@ -80,7 +80,7 @@ export function TemplatesAdmin() {
});
if (!res.ok) {
const d = await res.json().catch(() => null);
setError(d?.error ?? "Failed to save variant");
setError(d?.error ?? "ذخیرهٔ نسخه ناموفق بود");
}
setSavingProj(null);
};
@@ -97,7 +97,7 @@ export function TemplatesAdmin() {
setCats(Array.isArray(ct) ? ct : ct?.items ?? []);
setTags(tg?.items ?? (Array.isArray(tg) ? tg : []));
} catch {
setError("Failed to load templates");
setError("بارگذاری قالب‌ها ناموفق بود");
} finally {
setLoading(false);
}
@@ -136,24 +136,24 @@ export function TemplatesAdmin() {
});
const data = await res.json().catch(() => null);
if (res.ok) { setOpen(false); reload(); }
else setError(data?.error ?? "Save failed");
else setError(data?.error ?? "ذخیره ناموفق بود");
setSaving(false);
};
const remove = async (row: Container) => {
if (!confirm(`Delete template "${row.name}"?`)) return;
if (!confirm(`قالب «${row.name}» حذف شود؟`)) return;
const res = await fetch(api(`templates/${row.id}`), { method: "DELETE" });
if (res.ok) reload(); else setError("Delete failed");
if (res.ok) reload(); else setError("حذف ناموفق بود");
};
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-semibold text-white">Templates</h1>
<p className="mt-1 text-sm text-gray-400">Template packs name, description, keywords, categories, tags, publishing.</p>
<h1 className="text-xl font-semibold text-white">قالبها</h1>
<p className="mt-1 text-sm text-gray-400">بستههای قالب نام، توضیحات، کلمات کلیدی، دستهبندی، برچسب و انتشار. هر قالب میتواند چند نسخه (تناسب/کیفیت) داشته باشد.</p>
</div>
<button className={btn} onClick={openNew}>+ New template</button>
<button className={btn} onClick={openNew}>+ قالب جدید</button>
</div>
{error && <p className="rounded-lg bg-red-500/10 px-3 py-2 text-sm text-red-300">{error}</p>}
@@ -161,17 +161,17 @@ export function TemplatesAdmin() {
<div className={`${card} overflow-hidden`}>
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1e2235] text-left text-xs text-gray-500">
<th className="px-4 py-3">Image</th><th className="px-4 py-3">Name</th><th className="px-4 py-3">Slug</th>
<th className="px-4 py-3">Status</th><th className="px-4 py-3">Mode</th>
<th className="px-4 py-3">Sort</th><th className="px-4 py-3 text-right">Actions</th>
<tr className="border-b border-[#1e2235] text-start text-xs text-gray-500">
<th className="px-4 py-3">تصویر</th><th className="px-4 py-3">نام</th><th className="px-4 py-3">اسلاگ</th>
<th className="px-4 py-3">وضعیت</th><th className="px-4 py-3">حالت</th>
<th className="px-4 py-3">ترتیب</th><th className="px-4 py-3 text-end">عملیات</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-gray-500">Loading</td></tr>
<tr><td colSpan={7} className="px-4 py-8 text-center text-gray-500">در حال بارگذاری</td></tr>
) : rows.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-gray-500">No templates.</td></tr>
<tr><td colSpan={7} className="px-4 py-8 text-center text-gray-500">قالبی وجود ندارد.</td></tr>
) : rows.map((r) => (
<tr key={r.id} className="border-b border-[#161a2e] hover:bg-[#12152a]">
<td className="px-4 py-3"><AdminThumb src={r.image ?? r.mini_demo ?? r.demo} size={48} /></td>
@@ -179,16 +179,16 @@ export function TemplatesAdmin() {
<td className="px-4 py-3 text-gray-400">{r.slug}</td>
<td className="px-4 py-3">
<span className={r.is_published ? "rounded bg-emerald-500/15 px-1.5 py-0.5 text-[11px] text-emerald-300" : "rounded bg-gray-500/15 px-1.5 py-0.5 text-[11px] text-gray-400"}>
{r.is_published ? "published" : "draft"}
{r.is_published ? "منتشرشده" : "پیش‌نویس"}
</span>
{r.is_premium ? <span className="ms-1 rounded bg-amber-500/15 px-1.5 py-0.5 text-[11px] text-amber-300">premium</span> : null}
{r.is_premium ? <span className="ms-1 rounded bg-amber-500/15 px-1.5 py-0.5 text-[11px] text-amber-300">ویژه</span> : null}
</td>
<td className="px-4 py-3 text-gray-400">{r.primary_mode}</td>
<td className="px-4 py-3 text-gray-400">{r.sort}</td>
<td className="px-4 py-3">
<div className="flex items-center justify-end gap-2">
<button className={ghost} onClick={() => openEdit(r)}>Edit</button>
<button className="rounded-lg border border-red-500/30 px-3 py-1.5 text-xs text-red-300 hover:bg-red-500/10" onClick={() => remove(r)}>Delete</button>
<button className={ghost} onClick={() => openEdit(r)}>ویرایش</button>
<button className="rounded-lg border border-red-500/30 px-3 py-1.5 text-xs text-red-300 hover:bg-red-500/10" onClick={() => remove(r)}>حذف</button>
</div>
</td>
</tr>
@@ -200,37 +200,37 @@ export function TemplatesAdmin() {
{open && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" onClick={() => setOpen(false)}>
<div className={`${card} w-full max-w-2xl p-5`} onClick={(e) => e.stopPropagation()}>
<h2 className="text-sm font-semibold text-white">{editId ? "Edit" : "New"} template</h2>
<h2 className="text-sm font-semibold text-white">{editId ? "ویرایش قالب" : "قالب جدید"}</h2>
<div className="mt-4 grid max-h-[65vh] gap-3 overflow-y-auto pr-1">
<div className="grid gap-3 sm:grid-cols-2">
<div><label className={lbl}>Name *</label><input className={inp} value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} /></div>
<div><label className={lbl}>Slug *</label><input className={inp} value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} /></div>
<div><label className={lbl}>نام *</label><input className={inp} value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} /></div>
<div><label className={lbl}>اسلاگ (نشانی) *</label><input className={inp} value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} /></div>
</div>
<div><label className={lbl}>Description</label><textarea className={`${inp} min-h-[80px]`} value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} /></div>
<div><label className={lbl}>توضیحات</label><textarea className={`${inp} min-h-[80px]`} value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} /></div>
<div className="grid gap-3 sm:grid-cols-2">
<div><label className={lbl}>Cover image</label><FileUploadField value={form.image} onChange={(u) => setForm({ ...form, image: u })} accept="image/*" /></div>
<div><label className={lbl}>Mini demo (thumbnail/gif)</label><FileUploadField value={form.mini_demo} onChange={(u) => setForm({ ...form, mini_demo: u })} accept="image/*,video/*" /></div>
<div><label className={lbl}>Demo (preview video)</label><FileUploadField value={form.demo} onChange={(u) => setForm({ ...form, demo: u })} accept="video/*,image/*" /></div>
<div><label className={lbl}>Full demo</label><FileUploadField value={form.full_demo} onChange={(u) => setForm({ ...form, full_demo: u })} accept="video/*,image/*" /></div>
<div><label className={lbl}>تصویر کاور</label><FileUploadField value={form.image} onChange={(u) => setForm({ ...form, image: u })} accept="image/*" /></div>
<div><label className={lbl}>دموی کوچک (تصویر/گیف)</label><FileUploadField value={form.mini_demo} onChange={(u) => setForm({ ...form, mini_demo: u })} accept="image/*,video/*" /></div>
<div><label className={lbl}>دمو (ویدیوی پیشنمایش)</label><FileUploadField value={form.demo} onChange={(u) => setForm({ ...form, demo: u })} accept="video/*,image/*" /></div>
<div><label className={lbl}>دموی کامل</label><FileUploadField value={form.full_demo} onChange={(u) => setForm({ ...form, full_demo: u })} accept="video/*,image/*" /></div>
</div>
<div><label className={lbl}>Keywords (SEO)</label><input className={inp} value={form.keywords} onChange={(e) => setForm({ ...form, keywords: e.target.value })} /></div>
<div><label className={lbl}>News / announcement text</label><textarea className={`${inp} min-h-[50px]`} value={form.news_text} onChange={(e) => setForm({ ...form, news_text: e.target.value })} /></div>
<div><label className={lbl}>کلمات کلیدی (سئو)</label><input className={inp} value={form.keywords} onChange={(e) => setForm({ ...form, keywords: e.target.value })} /></div>
<div><label className={lbl}>متن خبر / اعلان</label><textarea className={`${inp} min-h-[50px]`} value={form.news_text} onChange={(e) => setForm({ ...form, news_text: e.target.value })} /></div>
<div className="grid gap-3 sm:grid-cols-2">
<div>
<label className={lbl}>Primary mode</label>
<label className={lbl}>حالت اصلی</label>
<select className={inp} value={form.primary_mode} onChange={(e) => setForm({ ...form, primary_mode: e.target.value })}>
{PRIMARY_MODES.map((m) => <option key={m} value={m}>{m}</option>)}
</select>
</div>
<div><label className={lbl}>Sort</label><input type="number" className={inp} value={form.sort} onChange={(e) => setForm({ ...form, sort: Number(e.target.value) })} /></div>
<div><label className={lbl}>ترتیب</label><input type="number" className={inp} value={form.sort} onChange={(e) => setForm({ ...form, sort: Number(e.target.value) })} /></div>
</div>
<div className="flex flex-wrap gap-4 text-sm text-gray-300">
<label className="flex items-center gap-2"><input type="checkbox" checked={form.is_published} onChange={(e) => setForm({ ...form, is_published: e.target.checked })} /> Published</label>
<label className="flex items-center gap-2"><input type="checkbox" checked={form.is_premium} onChange={(e) => setForm({ ...form, is_premium: e.target.checked })} /> Premium</label>
<label className="flex items-center gap-2"><input type="checkbox" checked={form.is_mockup} onChange={(e) => setForm({ ...form, is_mockup: e.target.checked })} /> Mockup</label>
<label className="flex items-center gap-2"><input type="checkbox" checked={form.is_published} onChange={(e) => setForm({ ...form, is_published: e.target.checked })} /> منتشرشده</label>
<label className="flex items-center gap-2"><input type="checkbox" checked={form.is_premium} onChange={(e) => setForm({ ...form, is_premium: e.target.checked })} /> ویژه</label>
<label className="flex items-center gap-2"><input type="checkbox" checked={form.is_mockup} onChange={(e) => setForm({ ...form, is_mockup: e.target.checked })} /> ماکاپ</label>
</div>
<div>
<label className={lbl}>Categories</label>
<label className={lbl}>دستهبندیها</label>
<div className="flex flex-wrap gap-2">
{cats.map((c) => (
<button key={c.id} type="button" onClick={() => toggle("category_ids", c.id)}
@@ -238,11 +238,11 @@ export function TemplatesAdmin() {
{c.name}
</button>
))}
{cats.length === 0 && <span className="text-xs text-gray-600">No categories yet.</span>}
{cats.length === 0 && <span className="text-xs text-gray-600">هنوز دستهبندیای نیست.</span>}
</div>
</div>
<div>
<label className={lbl}>Tags</label>
<label className={lbl}>برچسبها</label>
<div className="flex flex-wrap gap-2">
{tags.map((tg) => (
<button key={tg.id} type="button" onClick={() => toggle("tag_ids", tg.id)}
@@ -250,12 +250,12 @@ export function TemplatesAdmin() {
{tg.name}
</button>
))}
{tags.length === 0 && <span className="text-xs text-gray-600">No tags yet.</span>}
{tags.length === 0 && <span className="text-xs text-gray-600">هنوز برچسبی نیست.</span>}
</div>
</div>
{editId && projects.length > 0 && (
<div>
<label className={lbl}>Variants aspect &amp; resolution</label>
<label className={lbl}>نسخهها تناسب و کیفیت</label>
<div className="space-y-2 rounded-lg border border-[#262b40] p-2">
{projects.map((p) => (
<div key={p.id} className="flex items-center gap-2">
@@ -274,18 +274,18 @@ export function TemplatesAdmin() {
{RESOLUTIONS.map((r) => <option key={r} value={r}>{r}</option>)}
</select>
<button type="button" className={ghost} onClick={() => saveProj(p)} disabled={savingProj === p.id}>
{savingProj === p.id ? "…" : "Save"}
{savingProj === p.id ? "…" : "ذخیره"}
</button>
</div>
))}
</div>
<p className="mt-1 text-[11px] text-gray-600">Edits use a partial update other render/colour data is preserved.</p>
<p className="mt-1 text-[11px] text-gray-600">ویرایش بهصورت جزئی انجام میشود سایر دادههای رندر/رنگ حفظ میشوند.</p>
</div>
)}
</div>
<div className="mt-5 flex items-center justify-end gap-2">
<button className={ghost} onClick={() => setOpen(false)}>Cancel</button>
<button className={btn} onClick={save} disabled={saving || !form.name || !form.slug}>{saving ? "Saving…" : "Save"}</button>
<button className={ghost} onClick={() => setOpen(false)}>انصراف</button>
<button className={btn} onClick={save} disabled={saving || !form.name || !form.slug}>{saving ? "در حال ذخیره…" : "ذخیره"}</button>
</div>
</div>
</div>
+20 -20
View File
@@ -44,7 +44,7 @@ export function WebsiteSettingsAdmin() {
const data = await res.json();
setRows(Array.isArray(data) ? data : data?.items ?? data?.data ?? []);
} catch {
setError("Failed to load settings");
setError("بارگذاری تنظیمات ناموفق بود");
} finally {
setLoading(false);
}
@@ -63,32 +63,32 @@ export function WebsiteSettingsAdmin() {
description: form.description || null, is_secret: !!form.is_secret,
}),
});
if (res.ok) { setMsg("Saved"); setForm(empty); reload(); }
else { const d = await res.json().catch(() => null); setError(d?.error ?? "Save failed"); }
if (res.ok) { setMsg("ذخیره شد"); setForm(empty); reload(); }
else { const d = await res.json().catch(() => null); setError(d?.error ?? "ذخیره‌سازی ناموفق بود"); }
setSaving(false);
};
return (
<div className="space-y-4">
<div>
<h1 className="text-xl font-semibold text-white">Website Settings</h1>
<p className="mt-1 text-sm text-gray-400">Key/value site settings. Editing a key overwrites its value (upsert).</p>
<h1 className="text-xl font-semibold text-white">تنظیمات وبسایت</h1>
<p className="mt-1 text-sm text-gray-400">تنظیمات کلید/مقدار سایت. ویرایش یک کلید، مقدار آن را بازنویسی میکند.</p>
</div>
{error && <p className="rounded-lg bg-red-500/10 px-3 py-2 text-sm text-red-300">{error}</p>}
<section className={`${card} p-5`}>
<h2 className="text-sm font-semibold text-white">{form.id ? "Edit setting" : "Add / update setting"}</h2>
<h2 className="text-sm font-semibold text-white">{form.id ? "ویرایش تنظیم" : "افزودن / به‌روزرسانی تنظیم"}</h2>
<div className="mt-4 grid gap-3 sm:grid-cols-2">
<div><label className={lbl}>Key *</label><input className={inp} value={form.key} onChange={(e) => setForm({ ...form, key: e.target.value })} placeholder="e.g. site_title" /></div>
<div><label className={lbl}>Value</label><input className={inp} value={form.value} onChange={(e) => setForm({ ...form, value: e.target.value })} /></div>
<div className="sm:col-span-2"><label className={lbl}>Description</label><input className={inp} value={form.description ?? ""} onChange={(e) => setForm({ ...form, description: e.target.value })} /></div>
<div><label className={lbl}>کلید *</label><input className={inp} value={form.key} onChange={(e) => setForm({ ...form, key: e.target.value })} placeholder="مثلاً site_title" /></div>
<div><label className={lbl}>مقدار</label><input className={inp} value={form.value} onChange={(e) => setForm({ ...form, value: e.target.value })} /></div>
<div className="sm:col-span-2"><label className={lbl}>توضیحات</label><input className={inp} value={form.description ?? ""} onChange={(e) => setForm({ ...form, description: e.target.value })} /></div>
<label className="flex items-center gap-2 text-sm text-gray-300">
<input type="checkbox" checked={!!form.is_secret} onChange={(e) => setForm({ ...form, is_secret: e.target.checked })} /> Secret (hidden from public API)
<input type="checkbox" checked={!!form.is_secret} onChange={(e) => setForm({ ...form, is_secret: e.target.checked })} /> محرمانه (از API عمومی پنهان میشود)
</label>
</div>
<div className="mt-4 flex items-center gap-2">
<button className={btn} onClick={save} disabled={saving || !form.key}>{saving ? "Saving…" : "Save setting"}</button>
{form.key && <button className={ghost} onClick={() => setForm(empty)}>Clear</button>}
<button className={btn} onClick={save} disabled={saving || !form.key}>{saving ? "در حال ذخیره…" : "ذخیره تنظیم"}</button>
{form.key && <button className={ghost} onClick={() => setForm(empty)}>پاک کردن</button>}
{msg && <span className="text-xs text-gray-400">{msg}</span>}
</div>
</section>
@@ -96,25 +96,25 @@ export function WebsiteSettingsAdmin() {
<div className={`${card} overflow-hidden`}>
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1e2235] text-left text-xs text-gray-500">
<th className="px-4 py-3">Key</th><th className="px-4 py-3">Value</th>
<th className="px-4 py-3">Description</th><th className="px-4 py-3">Secret</th>
<th className="px-4 py-3 text-right">Actions</th>
<tr className="border-b border-[#1e2235] text-start text-xs text-gray-500">
<th className="px-4 py-3">کلید</th><th className="px-4 py-3">مقدار</th>
<th className="px-4 py-3">توضیحات</th><th className="px-4 py-3">محرمانه</th>
<th className="px-4 py-3 text-end">عملیات</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr><td colSpan={5} className="px-4 py-8 text-center text-gray-500">Loading</td></tr>
<tr><td colSpan={5} className="px-4 py-8 text-center text-gray-500">در حال بارگذاری</td></tr>
) : rows.length === 0 ? (
<tr><td colSpan={5} className="px-4 py-8 text-center text-gray-500">No settings yet. Add one above.</td></tr>
<tr><td colSpan={5} className="px-4 py-8 text-center text-gray-500">هنوز تنظیمی وجود ندارد. از بالا یکی اضافه کنید.</td></tr>
) : rows.map((s) => (
<tr key={s.key} className="border-b border-[#161a2e] hover:bg-[#12152a]">
<td className="px-4 py-3 font-mono text-xs text-gray-200">{s.key}</td>
<td className="px-4 py-3 text-gray-300">{s.is_secret ? "••••••" : (decode(s.value) || "—")}</td>
<td className="px-4 py-3 text-gray-500">{s.description || "—"}</td>
<td className="px-4 py-3">{s.is_secret ? "✓" : "—"}</td>
<td className="px-4 py-3 text-right">
<button className={ghost} onClick={() => setForm({ ...s, value: decode(s.value), description: s.description ?? "" })}>Edit</button>
<td className="px-4 py-3 text-end">
<button className={ghost} onClick={() => setForm({ ...s, value: decode(s.value), description: s.description ?? "" })}>ویرایش</button>
</td>
</tr>
))}
+145 -145
View File
@@ -20,7 +20,7 @@ const banAction = (row: Record<string, unknown>, reload: () => void) => {
: "rounded-lg border border-red-500/30 px-3 py-1.5 text-xs text-red-300 hover:bg-red-500/10"
}
onClick={async () => {
const reason = banned ? "" : prompt("Ban reason?") ?? "";
const reason = banned ? "" : prompt("دلیل مسدودسازی؟") ?? "";
if (!banned && !reason) return;
const res = await fetch(`/api/admin/resource/users/${row.id}/ban`, {
method: "POST",
@@ -30,197 +30,197 @@ const banAction = (row: Record<string, unknown>, reload: () => void) => {
if (res.ok) reload();
}}
>
{banned ? "Unban" : "Ban"}
{banned ? "رفع مسدودی" : "مسدود"}
</button>
);
};
export const categoriesConfig: ResourceConfig = {
title: "Categories",
description: "Taxonomy used across templates and the public site.",
title: "دسته‌بندی‌ها",
description: "دسته‌بندی‌های مورد استفاده در قالب‌ها و سایت عمومی.",
basePath: "categories",
canCreate: true,
canEdit: true,
canDelete: true,
columns: [
{ key: "image_url", label: "Image", type: "image" },
{ key: "icon", label: "Icon", type: "image" },
{ key: "name", label: "Name" },
{ key: "slug", label: "Slug" },
{ key: "is_active", label: "Active", render: (r) => badge(!!r.is_active, "active", "hidden") },
{ key: "sort", label: "Sort" },
{ key: "image_url", label: "تصویر", type: "image" },
{ key: "icon", label: "آیکون", type: "image" },
{ key: "name", label: "نام" },
{ key: "slug", label: "اسلاگ" },
{ key: "is_active", label: "وضعیت", render: (r) => badge(!!r.is_active, "فعال", "مخفی") },
{ key: "sort", label: "ترتیب" },
],
fields: [
{ key: "name", label: "Name", required: true },
{ key: "slug", label: "Slug", required: true },
{ key: "description", label: "Description / content", type: "textarea" },
{ key: "image_url", label: "Image", type: "image" },
{ key: "icon", label: "Icon (SVG markup or icon name)", type: "textarea" },
{ key: "name", label: "نام", required: true },
{ key: "slug", label: "اسلاگ (نشانی)", required: true },
{ key: "description", label: "توضیحات / محتوا", type: "textarea" },
{ key: "image_url", label: "تصویر", type: "image" },
{ key: "icon", label: "آیکون (کد SVG یا نام آیکون)", type: "textarea" },
// SEO
{ key: "meta_title", label: "SEO · Meta title" },
{ key: "meta_description", label: "SEO · Meta description", type: "textarea" },
{ key: "meta_keywords", label: "SEO · Meta keywords (comma separated)" },
{ key: "bot_follow", label: "Allow search engines to follow", type: "checkbox", defaultValue: true },
{ key: "sort", label: "Sort order", type: "number", defaultValue: 0 },
{ key: "is_active", label: "Active (visible on site)", type: "checkbox", defaultValue: true },
{ key: "meta_title", label: "سئو · عنوان متا" },
{ key: "meta_description", label: "سئو · توضیحات متا", type: "textarea" },
{ key: "meta_keywords", label: "سئو · کلمات کلیدی (با کاما)" },
{ key: "bot_follow", label: "اجازه دنبال‌کردن توسط موتورهای جستجو", type: "checkbox", defaultValue: true },
{ key: "sort", label: "ترتیب نمایش", type: "number", defaultValue: 0 },
{ key: "is_active", label: "فعال (نمایش در سایت)", type: "checkbox", defaultValue: true },
],
};
export const tagsConfig: ResourceConfig = {
title: "Tags",
description: "Keyword tags for templates and content.",
title: "برچسب‌ها",
description: "برچسب‌های کلیدواژه برای قالب‌ها و محتوا.",
basePath: "tags",
listKey: "items",
canCreate: true,
canEdit: true,
canDelete: true,
columns: [
{ key: "name", label: "Name" },
{ key: "slug", label: "Slug" },
{ key: "is_active", label: "Active", render: (r) => badge(!!r.is_active, "active", "hidden") },
{ key: "name", label: "نام" },
{ key: "slug", label: "اسلاگ" },
{ key: "is_active", label: "وضعیت", render: (r) => badge(!!r.is_active, "فعال", "مخفی") },
],
fields: [
{ key: "name", label: "Name", required: true },
{ key: "latin_name", label: "Latin name" },
{ key: "slug", label: "Slug", required: true },
{ key: "is_active", label: "Active", type: "checkbox", defaultValue: true },
{ key: "name", label: "نام", required: true },
{ key: "latin_name", label: "نام لاتین" },
{ key: "slug", label: "اسلاگ (نشانی)", required: true },
{ key: "is_active", label: "فعال", type: "checkbox", defaultValue: true },
],
};
export const fontsConfig: ResourceConfig = {
title: "Fonts",
description: "Fonts available in the studio editors.",
title: "فونت‌ها",
description: "فونت‌های در دسترس در ویرایشگرهای استودیو.",
basePath: "fonts",
listKey: "items",
canCreate: true,
canEdit: true,
canDelete: true,
columns: [
{ key: "name", label: "Name" },
{ key: "family", label: "Family" },
{ key: "weight", label: "Weight" },
{ key: "style", label: "Style" },
{ key: "name", label: "نام" },
{ key: "family", label: "خانواده" },
{ key: "weight", label: "وزن" },
{ key: "style", label: "سبک" },
],
fields: [
{ key: "name", label: "Name", required: true },
{ key: "original_name", label: "Original name" },
{ key: "system_name", label: "System name" },
{ key: "family", label: "Family" },
{ key: "weight", label: "Weight", type: "number" },
{ key: "style", label: "Style" },
{ key: "name", label: "نام", required: true },
{ key: "original_name", label: "نام اصلی" },
{ key: "system_name", label: "نام سیستمی" },
{ key: "family", label: "خانواده فونت" },
{ key: "weight", label: "وزن", type: "number" },
{ key: "style", label: "سبک" },
],
};
export const musicConfig: ResourceConfig = {
title: "Music",
description: "Audio tracks available in the studio music library.",
title: "موسیقی",
description: "ترک‌های صوتی موجود در کتابخانهٔ موسیقی استودیو.",
basePath: "music",
listKey: "items",
canCreate: true,
canEdit: false,
canDelete: true,
columns: [
{ key: "name", label: "Name" },
{ key: "genre", label: "Genre" },
{ key: "mood", label: "Mood" },
{ key: "duration_sec", label: "Duration (s)" },
{ key: "is_premium", label: "Premium", render: (r) => badge(!!r.is_premium, "premium", "free") },
{ key: "name", label: "نام" },
{ key: "genre", label: "ژانر" },
{ key: "mood", label: "حال‌وهوا" },
{ key: "duration_sec", label: "مدت (ثانیه)" },
{ key: "is_premium", label: "ویژه", render: (r) => badge(!!r.is_premium, "ویژه", "رایگان") },
],
fields: [
{ key: "name", label: "Name", required: true },
{ key: "caption", label: "Caption" },
{ key: "keywords", label: "Keywords" },
{ key: "url", label: "Audio file", type: "file", required: true },
{ key: "duration_sec", label: "Duration (seconds)", type: "number", required: true },
{ key: "bpm", label: "BPM", type: "number" },
{ key: "genre", label: "Genre" },
{ key: "mood", label: "Mood" },
{ key: "is_premium", label: "Premium", type: "checkbox" },
{ key: "name", label: "نام", required: true },
{ key: "caption", label: "عنوان" },
{ key: "keywords", label: "کلمات کلیدی" },
{ key: "url", label: "فایل صوتی", type: "file", required: true },
{ key: "duration_sec", label: "مدت (ثانیه)", type: "number", required: true },
{ key: "bpm", label: "ضرب (BPM)", type: "number" },
{ key: "genre", label: "ژانر" },
{ key: "mood", label: "حال‌وهوا" },
{ key: "is_premium", label: "ویژه", type: "checkbox" },
],
};
export const blogsConfig: ResourceConfig = {
title: "Blog Posts",
description: "CMS articles (also created by the AI SEO generator).",
title: "مقالات بلاگ",
description: "مقالات سیستم مدیریت محتوا (تولیدشده توسط هوش مصنوعی نیز).",
basePath: "blogs",
listKey: "items",
canCreate: true,
canEdit: true,
canDelete: true,
columns: [
{ key: "title", label: "Title" },
{ key: "slug", label: "Slug" },
{ key: "is_published", label: "Published", render: (r) => badge(!!r.is_published, "live", "draft") },
{ key: "view_count", label: "Views" },
{ key: "title", label: "عنوان" },
{ key: "slug", label: "اسلاگ" },
{ key: "is_published", label: "انتشار", render: (r) => badge(!!r.is_published, "منتشرشده", "پیش‌نویس") },
{ key: "view_count", label: "بازدید" },
],
fields: [
{ key: "title", label: "Title", required: true },
{ key: "slug", label: "Slug", required: true },
{ key: "short_description", label: "Short description", type: "textarea" },
{ key: "content", label: "Content (HTML)", type: "textarea", required: true },
{ key: "meta_title", label: "Meta title" },
{ key: "meta_description", label: "Meta description", type: "textarea" },
{ key: "meta_keywords", label: "Meta keywords" },
{ key: "is_published", label: "Published", type: "checkbox" },
{ key: "include_in_site_map", label: "Include in sitemap", type: "checkbox", defaultValue: true },
{ key: "title", label: "عنوان", required: true },
{ key: "slug", label: "اسلاگ (نشانی)", required: true },
{ key: "short_description", label: "توضیح کوتاه", type: "textarea" },
{ key: "content", label: "محتوا (HTML)", type: "textarea", required: true },
{ key: "meta_title", label: "عنوان متا" },
{ key: "meta_description", label: "توضیحات متا", type: "textarea" },
{ key: "meta_keywords", label: "کلمات کلیدی متا" },
{ key: "is_published", label: "منتشرشده", type: "checkbox" },
{ key: "include_in_site_map", label: "نمایش در نقشهٔ سایت", type: "checkbox", defaultValue: true },
],
};
export const slidesConfig: ResourceConfig = {
title: "Home Slides",
description: "Hero/promo slides on the homepage.",
title: "اسلایدهای صفحه اصلی",
description: "اسلایدهای هیرو/تبلیغاتی در صفحهٔ اصلی.",
basePath: "slides",
canDelete: true,
columns: [
{ key: "image", label: "Image", type: "image" },
{ key: "title", label: "Title" },
{ key: "slide_type", label: "Type" },
{ key: "is_active", label: "Active", render: (r) => badge(!!r.is_active, "active", "hidden") },
{ key: "image", label: "تصویر", type: "image" },
{ key: "title", label: "عنوان" },
{ key: "slide_type", label: "نوع" },
{ key: "is_active", label: "وضعیت", render: (r) => badge(!!r.is_active, "فعال", "مخفی") },
],
};
export const homeEventsConfig: ResourceConfig = {
title: "Home Events",
description: "Promotional event banners on the homepage hero.",
title: "رویدادهای صفحه اصلی",
description: "بنرهای رویداد تبلیغاتی در بخش هیرو صفحهٔ اصلی.",
basePath: "home-events",
listQuery: "includeInactive=true",
canCreate: true,
canEdit: true,
canDelete: true,
columns: [
{ key: "image", label: "Image", type: "image" },
{ key: "title", label: "Title" },
{ key: "badge", label: "Badge" },
{ key: "sort", label: "Sort" },
{ key: "is_active", label: "Active", render: (r) => badge(!!r.is_active, "active", "hidden") },
{ key: "image", label: "تصویر", type: "image" },
{ key: "title", label: "عنوان" },
{ key: "badge", label: "نشان" },
{ key: "sort", label: "ترتیب" },
{ key: "is_active", label: "وضعیت", render: (r) => badge(!!r.is_active, "فعال", "مخفی") },
],
fields: [
{ key: "title", label: "Title", required: true },
{ key: "subtitle", label: "Subtitle" },
{ key: "description", label: "Description", type: "textarea" },
{ key: "badge", label: "Badge text" },
{ key: "image", label: "Image", type: "image" },
{ key: "button_text", label: "Button text" },
{ key: "button_url", label: "Button URL" },
{ key: "color", label: "Text color (hex)" },
{ key: "background_color", label: "Background color (hex)" },
{ key: "sort", label: "Sort", type: "number" },
{ key: "is_active", label: "Active", type: "checkbox" },
{ key: "title", label: "عنوان", required: true },
{ key: "subtitle", label: "زیرعنوان" },
{ key: "description", label: "توضیحات", type: "textarea" },
{ key: "badge", label: "متن نشان" },
{ key: "image", label: "تصویر", type: "image" },
{ key: "button_text", label: "متن دکمه" },
{ key: "button_url", label: "نشانی دکمه" },
{ key: "color", label: "رنگ متن (hex)" },
{ key: "background_color", label: "رنگ پس‌زمینه (hex)" },
{ key: "sort", label: "ترتیب", type: "number" },
{ key: "is_active", label: "فعال", type: "checkbox" },
],
};
export const commentsConfig: ResourceConfig = {
title: "Comments",
description: "Moderate user comments on blogs and templates.",
title: "نظرات",
description: "مدیریت نظرات کاربران روی مقالات و قالب‌ها.",
basePath: "comments",
listKey: "data",
canCreate: false,
canEdit: false,
canDelete: true,
columns: [
{ key: "author_name", label: "Author" },
{ key: "content", label: "Comment" },
{ key: "is_approved", label: "Approved", render: (r) => badge(!!r.is_approved, "approved", "pending") },
{ key: "author_name", label: "نویسنده" },
{ key: "content", label: "نظر" },
{ key: "is_approved", label: "تأیید", render: (r) => badge(!!r.is_approved, "تأییدشده", "در انتظار") },
],
rowActions: (row, reload) => {
const id = String(row.id);
@@ -240,37 +240,37 @@ export const commentsConfig: ResourceConfig = {
};
export const routesConfig: ResourceConfig = {
title: "Internal Routes",
description: "Curated internal routes / featured links (slug, image, priority).",
title: "مسیرهای داخلی",
description: "مسیرها / لینک‌های ویژهٔ داخلی (اسلاگ، تصویر، اولویت).",
basePath: "routes",
canCreate: true,
canEdit: true,
canDelete: true,
columns: [
{ key: "image", label: "Image", type: "image" },
{ key: "name", label: "Name" },
{ key: "slug", label: "Slug" },
{ key: "priority", label: "Priority" },
{ key: "image", label: "تصویر", type: "image" },
{ key: "name", label: "نام" },
{ key: "slug", label: "اسلاگ" },
{ key: "priority", label: "اولویت" },
],
fields: [
{ key: "slug", label: "Slug (path)", required: true },
{ key: "name", label: "Name" },
{ key: "image", label: "Image", type: "image" },
{ key: "priority", label: "Priority", type: "number" },
{ key: "slug", label: "اسلاگ (مسیر)", required: true },
{ key: "name", label: "نام" },
{ key: "image", label: "تصویر", type: "image" },
{ key: "priority", label: "اولویت", type: "number" },
],
};
export const usersConfig: ResourceConfig = {
title: "Users",
description: "Accounts in this tenant. Ban or unban below.",
title: "کاربران",
description: "حساب‌های این مجموعه. مسدودسازی یا مدیریت در زیر.",
basePath: "users",
listKey: "data",
columns: [
{ key: "email", label: "Email" },
{ key: "full_name", label: "Name" },
{ key: "is_admin", label: "Admin", render: (r) => badge(!!r.is_admin, "admin", "—") },
{ key: "register_mode", label: "Source" },
{ key: "ban_account", label: "Status", render: (r) => badge(!r.ban_account, "active", "banned") },
{ key: "email", label: "ایمیل" },
{ key: "full_name", label: "نام" },
{ key: "is_admin", label: "مدیر", render: (r) => badge(!!r.is_admin, "مدیر", "—") },
{ key: "register_mode", label: "منبع" },
{ key: "ban_account", label: "وضعیت", render: (r) => badge(!r.ban_account, "فعال", "مسدود") },
],
rowActions: (row, reload) => (
<>
@@ -281,49 +281,49 @@ export const usersConfig: ResourceConfig = {
};
export const plansConfig: ResourceConfig = {
title: "Plans",
description: "Subscription plans (read-only view).",
title: "پلن‌ها",
description: "پلن‌های اشتراک (نمایش فقط‌خواندنی).",
basePath: "plans",
listKey: "data",
columns: [
{ key: "code", label: "Code" },
{ key: "name", label: "Name" },
{ key: "price_minor", label: "Price (minor)" },
{ key: "billing_period", label: "Period" },
{ key: "is_active", label: "Active", render: (r) => badge(!!r.is_active, "active", "off") },
{ key: "code", label: "کد" },
{ key: "name", label: "نام" },
{ key: "price_minor", label: "قیمت (ریال)" },
{ key: "billing_period", label: "دوره" },
{ key: "is_active", label: "وضعیت", render: (r) => badge(!!r.is_active, "فعال", "غیرفعال") },
],
};
export const discountsConfig: ResourceConfig = {
title: "Discounts",
description: "Discount / coupon codes. (Codes are created here; the backend has no edit/delete API yet.)",
title: "تخفیف‌ها",
description: "کدهای تخفیف / کوپن. (کدها اینجا ساخته می‌شوند؛ هنوز API ویرایش/حذف وجود ندارد.)",
basePath: "discounts",
listKey: "data",
canCreate: true,
columns: [
{ key: "code", label: "Code" },
{ key: "kind", label: "Kind" },
{ key: "value", label: "Value" },
{ key: "used_count", label: "Used" },
{ key: "max_use_count", label: "Max uses" },
{ key: "is_active", label: "Active", render: (r) => badge(!!r.is_active, "active", "off") },
{ key: "expires_at", label: "Expires" },
{ key: "code", label: "کد" },
{ key: "kind", label: "نوع" },
{ key: "value", label: "مقدار" },
{ key: "used_count", label: "استفاده‌شده" },
{ key: "max_use_count", label: "حداکثر استفاده" },
{ key: "is_active", label: "وضعیت", render: (r) => badge(!!r.is_active, "فعال", "غیرفعال") },
{ key: "expires_at", label: "انقضا" },
],
fields: [
{ key: "name", label: "Name", required: true },
{ key: "code", label: "Code", required: true },
{ key: "name", label: "نام", required: true },
{ key: "code", label: "کد", required: true },
{
key: "kind", label: "Kind", type: "select", required: true,
key: "kind", label: "نوع", type: "select", required: true,
options: [
{ value: "Percentage", label: "Percentage (%)" },
{ value: "FixedAmount", label: "Fixed amount" },
{ value: "FreeMonths", label: "Free months" },
{ value: "RenderCredits", label: "Render credits" },
{ value: "Percentage", label: "درصدی (٪)" },
{ value: "FixedAmount", label: "مبلغ ثابت" },
{ value: "FreeMonths", label: "ماه رایگان" },
{ value: "RenderCredits", label: "اعتبار رندر" },
],
defaultValue: "Percentage",
},
{ key: "value", label: "Value", type: "number", required: true },
{ key: "max_use_count", label: "Max use count (blank = unlimited)", type: "number" },
{ key: "expires_at", label: "Expires at (ISO date, optional)", placeholder: "2026-12-31T00:00:00Z" },
{ key: "value", label: "مقدار", type: "number", required: true },
{ key: "max_use_count", label: "حداکثر دفعات استفاده (خالی = نامحدود)", type: "number" },
{ key: "expires_at", label: "تاریخ انقضا (ISO، اختیاری)", placeholder: "2026-12-31T00:00:00Z" },
],
};