36e264f3e3
- Wire admin API into homepage + templates page (ISR 60s, null fallback) - Add src/lib/admin-api.ts with safeFetch helper - Add adminProjectToTemplateItem + adminProjectToCatalogTemplate mappers - Add LogoMark SVG component, replace Sparkles icon in Navbar/Footer/Sidebar - Add public/favicon.svg (SVG brand mark) - Rewrite opengraph-image.tsx with FlatRender branding - Add RTL/Persian font cascade: unlayered [dir=rtl] block forces Vazirmatn - Dashboard Settings page: Profile, Security, Billing, Notifications sections - Add src/lib/supabase/client.ts browser client - Admin API: GET /me, PATCH /profile, POST /change-password endpoints - Admin API DTOs: AdminUserDto, UpdateProfileRequest, ChangePasswordRequest - Admin UI Settings page with TanStack Query + mutations - Add CLAUDE.md + README.md to both repos for new-machine onboarding - Update PROJECT_MEMORY.md with session log - Add appsettings.Development.json.example template
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;
|
|
}
|
|
}
|