116 lines
3.2 KiB
TypeScript
116 lines
3.2 KiB
TypeScript
|
|
/**
|
||
|
|
* Server-side fetch from the FlatRender Admin API.
|
||
|
|
*
|
||
|
|
* All functions return hardcoded fallback data when:
|
||
|
|
* - ADMIN_API_URL is not set, or
|
||
|
|
* - The admin service is unreachable.
|
||
|
|
*
|
||
|
|
* This means the Next.js app works standalone with no admin service running.
|
||
|
|
*/
|
||
|
|
|
||
|
|
const BASE = process.env.ADMIN_API_URL?.replace(/\/$/, "");
|
||
|
|
|
||
|
|
export interface AdminCategory {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
slug: string;
|
||
|
|
description?: string;
|
||
|
|
iconUrl?: string;
|
||
|
|
type: "video" | "image" | "both";
|
||
|
|
sortOrder: number;
|
||
|
|
projectCount: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface AdminProject {
|
||
|
|
id: string;
|
||
|
|
title: string;
|
||
|
|
slug: string;
|
||
|
|
description?: string;
|
||
|
|
type: "video" | "image";
|
||
|
|
status: string;
|
||
|
|
categoryId?: string;
|
||
|
|
categoryName?: string;
|
||
|
|
coverImageUrl?: string;
|
||
|
|
previewVideoUrl?: string;
|
||
|
|
tags: string[];
|
||
|
|
metaJson?: string;
|
||
|
|
sortOrder: number;
|
||
|
|
mediaCount: number;
|
||
|
|
createdAt: string;
|
||
|
|
updatedAt: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface AdminProjectsResponse {
|
||
|
|
total: number;
|
||
|
|
page: number;
|
||
|
|
pageSize: number;
|
||
|
|
items: AdminProject[];
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Fetch helpers ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
async function safeFetch<T>(url: string): Promise<T | null> {
|
||
|
|
if (!BASE) return null;
|
||
|
|
try {
|
||
|
|
const res = await fetch(url, {
|
||
|
|
next: { revalidate: 60 }, // cache for 60 s (ISR)
|
||
|
|
headers: { Accept: "application/json" },
|
||
|
|
});
|
||
|
|
if (!res.ok) return null;
|
||
|
|
return res.json() as Promise<T>;
|
||
|
|
} catch {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Public API ────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
export async function fetchCategories(
|
||
|
|
type?: "video" | "image"
|
||
|
|
): Promise<AdminCategory[]> {
|
||
|
|
const qs = type ? `?type=${type}` : "";
|
||
|
|
return (
|
||
|
|
(await safeFetch<AdminCategory[]>(`${BASE}/api/public/categories${qs}`)) ??
|
||
|
|
[]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function fetchProjects(opts?: {
|
||
|
|
type?: "video" | "image";
|
||
|
|
categorySlug?: string;
|
||
|
|
search?: string;
|
||
|
|
page?: number;
|
||
|
|
pageSize?: number;
|
||
|
|
}): Promise<AdminProjectsResponse> {
|
||
|
|
const params = new URLSearchParams();
|
||
|
|
if (opts?.type) params.set("type", opts.type);
|
||
|
|
if (opts?.categorySlug) params.set("categorySlug", opts.categorySlug);
|
||
|
|
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));
|
||
|
|
|
||
|
|
const qs = params.size ? `?${params}` : "";
|
||
|
|
return (
|
||
|
|
(await safeFetch<AdminProjectsResponse>(
|
||
|
|
`${BASE}/api/public/projects${qs}`
|
||
|
|
)) ?? { total: 0, page: 1, pageSize: 20, items: [] }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function fetchProject(slug: string): Promise<AdminProject | null> {
|
||
|
|
return safeFetch<AdminProject>(`${BASE}/api/public/projects/${slug}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** True when admin API is configured and reachable. */
|
||
|
|
export async function isAdminApiAvailable(): Promise<boolean> {
|
||
|
|
if (!BASE) return false;
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${BASE}/api/public/categories`, {
|
||
|
|
next: { revalidate: 30 },
|
||
|
|
});
|
||
|
|
return res.ok;
|
||
|
|
} catch {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|