import type { PlazaCategoriesResponse, PlazaCommentsResponse, PlazaCreatePostResponse, PlazaFeedResponse, PlazaPostResponse, PlazaSitemapData, PlazaUserProfileResponse, } from '@/types/plaza'; import { apiBase } from '@/lib/site'; type FetchOptions = { revalidate?: number | false; cache?: RequestCache; cookie?: string; }; export type PlazaAuthStatus = { authenticated: boolean; user?: { id: string; username: string; slug?: string; displayName: string } | null; }; class PlazaApiError extends Error { code: string; constructor(message: string, code: string) { super(message); this.code = code; } } async function plazaFetch(path: string, init?: RequestInit, options?: FetchOptions): Promise { const base = apiBase(); const url = `${base}${path.startsWith('/') ? path : `/${path}`}`; const headers = new Headers(init?.headers); if (options?.cookie) headers.set('cookie', options.cookie); if (!headers.has('Accept')) headers.set('Accept', 'application/json'); const next = options?.revalidate === false ? { revalidate: 0 } : options?.revalidate != null ? { revalidate: options.revalidate } : undefined; const response = await fetch(url, { ...init, headers, credentials: typeof window !== 'undefined' ? 'include' : init?.credentials, cache: options?.cache ?? (options?.revalidate === false ? 'no-store' : undefined), next, }); const payload = await response.json().catch(() => ({})); if (!response.ok) { const code = payload?.error?.code ?? 'request_failed'; const message = payload?.error?.message ?? `请求失败 (${response.status})`; throw new PlazaApiError(message, code); } return payload.data as T; } export async function fetchAuthStatus(cookie?: string): Promise { try { const base = apiBase(); const url = `${base}/auth/status`; const headers = new Headers({ Accept: 'application/json' }); if (cookie) headers.set('cookie', cookie); const response = await fetch(url, { headers, credentials: typeof window !== 'undefined' ? 'include' : undefined, cache: 'no-store', }); if (!response.ok) return { authenticated: false }; const status = (await response.json()) as PlazaAuthStatus; return status; } catch { return { authenticated: false }; } } export async function fetchCategories(options?: FetchOptions) { return plazaFetch('/api/plaza/v1/categories', undefined, { revalidate: 300, ...options, }); } export async function fetchFeed( params: { sort?: 'hot' | 'new'; category?: string; cursor?: string; limit?: number; } = {}, options?: FetchOptions, ) { const query = new URLSearchParams(); if (params.sort) query.set('sort', params.sort); if (params.category) query.set('category', params.category); if (params.cursor) query.set('cursor', params.cursor); if (params.limit) query.set('limit', String(params.limit)); const suffix = query.size ? `?${query.toString()}` : ''; return plazaFetch(`/api/plaza/v1/feed${suffix}`, undefined, { revalidate: 60, ...options, }); } export async function fetchPost(id: string, options?: FetchOptions) { return plazaFetch(`/api/plaza/v1/posts/${encodeURIComponent(id)}`, undefined, { revalidate: false, cache: 'no-store', ...options, }); } export async function createPlazaPost( body: { publication_id: string; category_id: string; tags?: string[]; cover_url?: string; allow_comment?: boolean; }, cookie: string, ) { return plazaFetch( '/api/plaza/v1/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }, { revalidate: false, cookie }, ); } export async function togglePostReaction( postId: string, type: 'like' | 'collect' | 'share', active: boolean, ) { if (type === 'share') { return plazaFetch<{ post_id: string; type: string; active: boolean }>( `/api/plaza/v1/posts/${encodeURIComponent(postId)}/reactions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'share' }), }, { revalidate: false }, ); } if (active) { return plazaFetch<{ post_id: string; type: string; active: boolean }>( `/api/plaza/v1/posts/${encodeURIComponent(postId)}/reactions/${type}`, { method: 'DELETE' }, { revalidate: false }, ); } return plazaFetch<{ post_id: string; type: string; active: boolean }>( `/api/plaza/v1/posts/${encodeURIComponent(postId)}/reactions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type }), }, { revalidate: false }, ); } export async function fetchComments( postId: string, params: { parent_id?: string; cursor?: string } = {}, ) { const query = new URLSearchParams(); if (params.parent_id) query.set('parent_id', params.parent_id); if (params.cursor) query.set('cursor', params.cursor); const suffix = query.size ? `?${query.toString()}` : ''; return plazaFetch( `/api/plaza/v1/posts/${encodeURIComponent(postId)}/comments${suffix}`, undefined, { revalidate: false, cache: 'no-store' }, ); } export async function createComment(postId: string, content: string, parentId?: string | null) { return plazaFetch<{ comment: { id: string } }>( `/api/plaza/v1/posts/${encodeURIComponent(postId)}/comments`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content, parent_id: parentId ?? null }), }, { revalidate: false }, ); } export async function toggleCommentLike(commentId: string, liked: boolean) { return plazaFetch<{ id: string; liked: boolean }>( `/api/plaza/v1/comments/${encodeURIComponent(commentId)}/reactions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ liked }), }, { revalidate: false }, ); } export async function fetchUserProfile(slug: string, options?: FetchOptions) { return plazaFetch( `/api/plaza/v1/users/${encodeURIComponent(slug)}`, undefined, { revalidate: 300, ...options }, ); } export async function fetchUserPosts( slug: string, params: { cursor?: string; limit?: number } = {}, options?: FetchOptions, ) { const query = new URLSearchParams(); if (params.cursor) query.set('cursor', params.cursor); if (params.limit) query.set('limit', String(params.limit)); const suffix = query.size ? `?${query.toString()}` : ''; return plazaFetch>( `/api/plaza/v1/users/${encodeURIComponent(slug)}/posts${suffix}`, undefined, { revalidate: false, cache: 'no-store', ...options }, ); } export async function toggleFollow(slug: string, following: boolean) { if (following) { return plazaFetch<{ following: boolean }>( `/api/plaza/v1/users/${encodeURIComponent(slug)}/follow`, { method: 'DELETE' }, { revalidate: false }, ); } return plazaFetch<{ following: boolean }>( `/api/plaza/v1/users/${encodeURIComponent(slug)}/follow`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }, { revalidate: false }, ); } export async function fetchSitemapData(options?: FetchOptions) { return plazaFetch('/api/plaza/v1/seo/sitemap', undefined, { revalidate: 3600, ...options, }); } export async function recordAttributionEvent(body: { event_type: 'landing' | 'signup'; utm_source: string; utm_medium?: string; utm_campaign?: string; ref_id?: string; }) { return plazaFetch<{ id: string; event_type: string }>( '/api/plaza/v1/attribution/events', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }, { revalidate: false }, ); } export async function submitPostReport(postId: string, reason: string, detail?: string) { return plazaFetch<{ report: { id: string; status: string } }>( `/api/plaza/v1/posts/${encodeURIComponent(postId)}/reports`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reason, detail: detail ?? '' }), }, { revalidate: false }, ); } export async function submitCommentReport(commentId: string, reason: string, detail?: string) { return plazaFetch<{ report: { id: string; status: string } }>( `/api/plaza/v1/comments/${encodeURIComponent(commentId)}/reports`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reason, detail: detail ?? '' }), }, { revalidate: false }, ); } export { PlazaApiError };