Files
flatrender/src/components/admin/admin-resources.tsx
T
soroush.asadi 9a1d60e9d0 feat(admin): Discounts and Website Settings sections
- /admin/discounts: list + create discount codes (kind, value, max uses, expiry)
  via /v1/discounts (backend has no edit/delete API yet)
- /admin/settings: key/value site settings with upsert + secret flag. The value
  column is jsonb, so values are JSON-encoded on save / decoded for display
- nav links + fa/en labels

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 15:20:07 +03:30

212 lines
7.5 KiB
TypeScript

"use client";
import type { ResourceConfig } from "@/components/admin/AdminResource";
const badge = (ok: boolean, yes: string, no: string) =>
ok ? (
<span className="rounded bg-emerald-500/15 px-1.5 py-0.5 text-[11px] text-emerald-300">{yes}</span>
) : (
<span className="rounded bg-gray-500/15 px-1.5 py-0.5 text-[11px] text-gray-400">{no}</span>
);
const banAction = (row: Record<string, unknown>, reload: () => void) => {
const banned = !!row.ban_account;
return (
<button
className={
banned
? "rounded-lg border border-emerald-500/30 px-3 py-1.5 text-xs text-emerald-300 hover:bg-emerald-500/10"
: "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?") ?? "";
if (!banned && !reason) return;
const res = await fetch(`/api/admin/resource/users/${row.id}/ban`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ reason: banned ? "unban" : reason, unbanned: banned }),
});
if (res.ok) reload();
}}
>
{banned ? "Unban" : "Ban"}
</button>
);
};
export const categoriesConfig: ResourceConfig = {
title: "Categories",
description: "Taxonomy used across templates and the public site.",
basePath: "categories",
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: "sort", label: "Sort" },
],
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" },
// 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 },
],
};
export const tagsConfig: ResourceConfig = {
title: "Tags",
description: "Keyword tags for templates and content.",
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") },
],
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 },
],
};
export const fontsConfig: ResourceConfig = {
title: "Fonts",
description: "Fonts available in the studio editors.",
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" },
],
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" },
],
};
export const blogsConfig: ResourceConfig = {
title: "Blog Posts",
description: "CMS articles (also created by the AI SEO generator).",
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" },
],
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 },
],
};
export const slidesConfig: ResourceConfig = {
title: "Home Slides",
description: "Hero/promo slides on the homepage.",
basePath: "slides",
canDelete: true,
columns: [
{ key: "title", label: "Title" },
{ key: "slide_type", label: "Type" },
{ key: "is_active", label: "Active", render: (r) => badge(!!r.is_active, "active", "hidden") },
],
};
export const usersConfig: ResourceConfig = {
title: "Users",
description: "Accounts in this tenant. Ban or unban below.",
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") },
],
rowActions: banAction,
};
export const plansConfig: ResourceConfig = {
title: "Plans",
description: "Subscription plans (read-only view).",
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") },
],
};
export const discountsConfig: ResourceConfig = {
title: "Discounts",
description: "Discount / coupon codes. (Codes are created here; the backend has no edit/delete API yet.)",
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" },
],
fields: [
{ key: "name", label: "Name", required: true },
{ key: "code", label: "Code", required: true },
{
key: "kind", label: "Kind", type: "select", required: true,
options: [
{ value: "Percentage", label: "Percentage (%)" },
{ value: "FixedAmount", label: "Fixed amount" },
{ value: "FreeMonths", label: "Free months" },
{ value: "RenderCredits", label: "Render credits" },
],
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" },
],
};