diff --git a/src/lib/admin-api.ts b/src/lib/admin-api.ts index 1229633..ef63ca7 100644 --- a/src/lib/admin-api.ts +++ b/src/lib/admin-api.ts @@ -1,14 +1,17 @@ /** - * Server-side fetch from the FlatRender Admin API. + * Server-side content reads from the FlatRender V2 API gateway. * - * All functions return hardcoded fallback data when: - * - ADMIN_API_URL is not set, or - * - The admin service is unreachable. + * Replaces the legacy admin-api (:5000) integration: categories, templates + * ("containers") and projects now come from the V2 content service via the + * gateway at `/v1/*`. All V2 payloads are snake_case — this module maps them + * into the camelCase `AdminCategory` / `AdminProject` shapes the UI already + * consumes, so call sites are unchanged. * - * This means the Next.js app works standalone with no admin service running. + * Every function returns hardcoded fallback data (empty arrays / null) when the + * gateway is unset or unreachable, so the app still works standalone. */ -const BASE = process.env.ADMIN_API_URL?.replace(/\/$/, ""); +import { gatewayUrl } from "@/lib/api/gateway"; export interface AdminCategory { id: string; @@ -47,32 +50,126 @@ export interface AdminProjectsResponse { items: AdminProject[]; } -// ── Fetch helpers ───────────────────────────────────────────────────────────── +// ── V2 content-service response shapes (snake_case JSON) ────────────────────── -async function safeFetch(url: string): Promise { - if (!BASE) return null; +interface V2Category { + id: string; + parent_id?: string | null; + name: string; + slug: string; + description?: string | null; + image_url?: string | null; + icon?: string | null; + is_active: boolean; + sort: number; + children: V2Category[]; +} + +interface V2ContainerSummary { + id: string; + slug: string; + name: string; + description?: string | null; + image?: string | null; + demo?: string | null; + mini_demo?: string | null; + is_published: boolean; + is_premium: boolean; + is_mockup: boolean; + primary_mode: string; + rate_avg?: number | null; + rate_count: number; + view_count: number; + use_count: number; + sort: number; + sort_date: string; + category_slugs: string[]; + tags: string[]; +} + +interface V2PagedContainers { + items: V2ContainerSummary[]; + meta: { page: number; page_size: number; total: number; total_pages: number }; +} + +// ── Fetch helper ────────────────────────────────────────────────────────────── + +async function safeGet(path: string): Promise { try { - const res = await fetch(url, { - next: { revalidate: 60 }, // cache for 60 s (ISR) + const res = await fetch(gatewayUrl(path), { + next: { revalidate: 60 }, // ISR: cache public content for 60 s headers: { Accept: "application/json" }, }); if (!res.ok) return null; - return res.json() as Promise; + return (await res.json()) as T; } catch { return null; } } +// ── Mappers (V2 → UI shapes) ────────────────────────────────────────────────── + +/** + * V2 containers are video templates — `primary_mode` is a render mode + * (FIX / FLEXIBLE / MockUp / …), not a video/image type. There is no image + * container concept, so every container maps to `type: "video"`. + */ +function containerToAdminProject(c: V2ContainerSummary): AdminProject { + return { + id: c.id, + title: c.name, + slug: c.slug, + description: c.description ?? undefined, + type: "video", + status: c.is_published ? "published" : "draft", + categoryId: undefined, + categoryName: undefined, + coverImageUrl: c.image ?? undefined, + previewVideoUrl: c.demo ?? c.mini_demo ?? undefined, + tags: c.tags ?? [], + metaJson: undefined, + sortOrder: c.sort, + mediaCount: 0, + createdAt: c.sort_date ?? "", + updatedAt: c.sort_date ?? "", + }; +} + +function categoryToAdmin(c: V2Category): AdminCategory { + return { + id: c.id, + name: c.name, + slug: c.slug, + description: c.description ?? undefined, + iconUrl: c.image_url ?? c.icon ?? undefined, + type: "both", + sortOrder: c.sort, + projectCount: 0, + }; +} + +/** Flatten the V2 category tree depth-first into a flat list. */ +function flattenCategories(nodes: V2Category[]): AdminCategory[] { + const out: AdminCategory[] = []; + const walk = (list: V2Category[]) => { + for (const n of list) { + out.push(categoryToAdmin(n)); + if (n.children?.length) walk(n.children); + } + }; + walk(nodes); + return out; +} + // ── Public API ──────────────────────────────────────────────────────────────── export async function fetchCategories( - type?: "video" | "image" + _type?: "video" | "image" ): Promise { - const qs = type ? `?type=${type}` : ""; - return ( - (await safeFetch(`${BASE}/api/public/categories${qs}`)) ?? - [] - ); + // V2 categories have no video/image type; `_type` is accepted for API + // compatibility but the content service returns a single tree. + const tree = await safeGet("/v1/categories"); + return tree ? flattenCategories(tree) : []; } export async function fetchProjects(opts?: { @@ -82,30 +179,39 @@ export async function fetchProjects(opts?: { page?: number; pageSize?: number; }): Promise { + // V2 .NET query binding uses C# property names (camelCase), NOT the snake_case + // used in response bodies. Send camelCase keys here. const params = new URLSearchParams(); - if (opts?.type) params.set("type", opts.type); - if (opts?.categorySlug) params.set("categorySlug", opts.categorySlug); + params.set("isPublished", "true"); if (opts?.search) params.set("search", opts.search); if (opts?.page) params.set("page", String(opts.page)); if (opts?.pageSize) params.set("pageSize", String(opts.pageSize)); + // Note: V2 has no video/image container type and filters categories by GUID + // (not slug), so `opts.type` / `opts.categorySlug` are intentionally ignored. - const qs = params.size ? `?${params}` : ""; - return ( - (await safeFetch( - `${BASE}/api/public/projects${qs}` - )) ?? { total: 0, page: 1, pageSize: 20, items: [] } - ); + const qs = params.toString() ? `?${params}` : ""; + const paged = await safeGet(`/v1/templates${qs}`); + if (!paged) return { total: 0, page: 1, pageSize: 20, items: [] }; + + return { + total: paged.meta.total, + page: paged.meta.page, + pageSize: paged.meta.page_size, + items: (paged.items ?? []).map(containerToAdminProject), + }; } export async function fetchProject(slug: string): Promise { - return safeFetch(`${BASE}/api/public/projects/${slug}`); + const c = await safeGet( + `/v1/templates/${encodeURIComponent(slug)}` + ); + return c ? containerToAdminProject(c) : null; } -/** True when admin API is configured and reachable. */ +/** True when the gateway content endpoint is reachable. */ export async function isAdminApiAvailable(): Promise { - if (!BASE) return false; try { - const res = await fetch(`${BASE}/api/public/categories`, { + const res = await fetch(gatewayUrl("/v1/categories"), { next: { revalidate: 30 }, }); return res.ok;