feat: AI SEO generator, full admin panel, i18n sweep, new logo + auth/RTL fixes
Build backend images / build content-svc (push) Failing after 3m39s
Build backend images / build file-svc (push) Failing after 52s
Build backend images / build gateway (push) Failing after 58s
Build backend images / build identity-svc (push) Failing after 1m21s
Build backend images / build notification-svc (push) Failing after 1m0s
Build backend images / build render-svc (push) Failing after 58s
Build backend images / build studio-svc (push) Failing after 55s
Build backend images / build content-svc (push) Failing after 3m39s
Build backend images / build file-svc (push) Failing after 52s
Build backend images / build gateway (push) Failing after 58s
Build backend images / build identity-svc (push) Failing after 1m21s
Build backend images / build notification-svc (push) Failing after 1m0s
Build backend images / build render-svc (push) Failing after 58s
Build backend images / build studio-svc (push) Failing after 55s
AI SEO content generator - content-svc: per-tenant OpenAI config (ai_settings) + /v1/ai endpoints (settings GET/PUT, seo-post) with SEO-expert prompt → structured article - admin UI to configure token/base-url/model and generate + save as blog - configurable base URL for restricted networks Full data-driven admin panel - generic /api/admin/resource proxy + reusable AdminResource component - categories/tags/fonts/blogs (CRUD), users (list + ban), plans/slides - AI content section; nav + i18n i18n localization sweep - localized 116 user-facing + studio/editor components to next-intl (fa+en) under the auto.* namespace; merge tooling in scripts/merge-i18n.js Branding + assets - Monoline F logo (LogoMark + favicon) - offline SVG placeholder generator (/api/placeholder), dropped picsum.photos Fixes - JWT issuer mismatch on content/studio (flatrender → flatrender-identity) - missing role claim → [Authorize(Roles="Admin")] now works (RBAC) - Secure cookies broke HTTP sessions → gated behind AUTH_COOKIE_SECURE - Radix RTL via DirectionProvider (right-aligned menus in fa) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
"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", type: "textarea" },
|
||||
{ key: "image_url", label: "Image URL" },
|
||||
{ key: "icon", label: "Icon" },
|
||||
],
|
||||
};
|
||||
|
||||
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") },
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user