Use same-origin /api in the browser so builds are not stuck with baked-in localhost. Server-side fetches use INTERNAL_API_URL, NEXT_PUBLIC_API_URL, or loopback. Centralize logic in lib/api-base.ts. Made-with: Cursor
182 lines
8.2 KiB
TypeScript
182 lines
8.2 KiB
TypeScript
import { apiUrl } from "./api-base";
|
|
|
|
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
|
const token = typeof window !== "undefined" ? localStorage.getItem("bbe_token") : null;
|
|
const headers: HeadersInit = {
|
|
"Content-Type": "application/json",
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
...options?.headers,
|
|
};
|
|
|
|
const res = await fetch(apiUrl(path), { ...options, headers });
|
|
if (!res.ok) {
|
|
const error = await res.json().catch(() => ({ message: "Request failed" }));
|
|
throw new Error(error.message || `HTTP ${res.status}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
export const api = {
|
|
// Auth
|
|
getChallenge: (pubkey: string) =>
|
|
request<{ challenge: string }>("/auth/challenge", {
|
|
method: "POST",
|
|
body: JSON.stringify({ pubkey }),
|
|
}),
|
|
verify: (pubkey: string, signedEvent: any) =>
|
|
request<{ token: string; user: { pubkey: string; role: string; username?: string } }>("/auth/verify", {
|
|
method: "POST",
|
|
body: JSON.stringify({ pubkey, signedEvent }),
|
|
}),
|
|
|
|
// Posts
|
|
getPosts: (params?: { category?: string; page?: number; limit?: number; all?: boolean }) => {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.category) searchParams.set("category", params.category);
|
|
if (params?.page) searchParams.set("page", String(params.page));
|
|
if (params?.limit) searchParams.set("limit", String(params.limit));
|
|
if (params?.all) searchParams.set("all", "true");
|
|
return request<{ posts: any[]; total: number }>(`/posts?${searchParams}`);
|
|
},
|
|
getPost: (slug: string) => request<any>(`/posts/${slug}`),
|
|
getPostReactions: (slug: string) =>
|
|
request<{ count: number; reactions: any[] }>(`/posts/${slug}/reactions`),
|
|
getPostReplies: (slug: string) =>
|
|
request<{ count: number; replies: any[] }>(`/posts/${slug}/replies`),
|
|
importPost: (data: { eventId?: string; naddr?: string }) =>
|
|
request<any>("/posts/import", { method: "POST", body: JSON.stringify(data) }),
|
|
updatePost: (id: string, data: any) =>
|
|
request<any>(`/posts/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
|
|
deletePost: (id: string) =>
|
|
request<void>(`/posts/${id}`, { method: "DELETE" }),
|
|
|
|
// Meetups
|
|
getMeetups: (params?: { status?: string; admin?: boolean }) => {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.status) searchParams.set("status", params.status);
|
|
if (params?.admin) searchParams.set("admin", "true");
|
|
const qs = searchParams.toString();
|
|
return request<any[]>(`/meetups${qs ? `?${qs}` : ""}`);
|
|
},
|
|
getMeetup: (id: string) => request<any>(`/meetups/${id}`),
|
|
createMeetup: (data: any) =>
|
|
request<any>("/meetups", { method: "POST", body: JSON.stringify(data) }),
|
|
updateMeetup: (id: string, data: any) =>
|
|
request<any>(`/meetups/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
|
|
deleteMeetup: (id: string) =>
|
|
request<void>(`/meetups/${id}`, { method: "DELETE" }),
|
|
duplicateMeetup: (id: string) =>
|
|
request<any>(`/meetups/${id}/duplicate`, { method: "POST" }),
|
|
bulkMeetupAction: (action: string, ids: string[]) =>
|
|
request<any>("/meetups/bulk", { method: "POST", body: JSON.stringify({ action, ids }) }),
|
|
|
|
// Moderation
|
|
getHiddenContent: () => request<any[]>("/moderation/hidden"),
|
|
hideContent: (nostrEventId: string, reason?: string) =>
|
|
request<any>("/moderation/hide", { method: "POST", body: JSON.stringify({ nostrEventId, reason }) }),
|
|
unhideContent: (id: string) =>
|
|
request<void>(`/moderation/unhide/${id}`, { method: "DELETE" }),
|
|
getBlockedPubkeys: () => request<any[]>("/moderation/blocked"),
|
|
blockPubkey: (pubkey: string, reason?: string) =>
|
|
request<any>("/moderation/block", { method: "POST", body: JSON.stringify({ pubkey, reason }) }),
|
|
unblockPubkey: (id: string) =>
|
|
request<void>(`/moderation/unblock/${id}`, { method: "DELETE" }),
|
|
|
|
// Users
|
|
getUsers: () => request<any[]>("/users"),
|
|
promoteUser: (pubkey: string) =>
|
|
request<any>("/users/promote", { method: "POST", body: JSON.stringify({ pubkey }) }),
|
|
demoteUser: (pubkey: string) =>
|
|
request<any>("/users/demote", { method: "POST", body: JSON.stringify({ pubkey }) }),
|
|
|
|
// Categories
|
|
getCategories: () => request<any[]>("/categories"),
|
|
createCategory: (data: { name: string; slug: string }) =>
|
|
request<any>("/categories", { method: "POST", body: JSON.stringify(data) }),
|
|
updateCategory: (id: string, data: any) =>
|
|
request<any>(`/categories/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
|
|
deleteCategory: (id: string) =>
|
|
request<void>(`/categories/${id}`, { method: "DELETE" }),
|
|
|
|
// Relays
|
|
getRelays: () => request<any[]>("/relays"),
|
|
addRelay: (data: { url: string; priority?: number }) =>
|
|
request<any>("/relays", { method: "POST", body: JSON.stringify(data) }),
|
|
updateRelay: (id: string, data: any) =>
|
|
request<any>(`/relays/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
|
|
deleteRelay: (id: string) =>
|
|
request<void>(`/relays/${id}`, { method: "DELETE" }),
|
|
testRelay: (id: string) =>
|
|
request<{ success: boolean }>(`/relays/${id}/test`, { method: "POST" }),
|
|
|
|
// Settings
|
|
getSettings: () => request<Record<string, string>>("/settings"),
|
|
getPublicSettings: () => request<Record<string, string>>("/settings/public"),
|
|
updateSetting: (key: string, value: string) =>
|
|
request<any>("/settings", { method: "PATCH", body: JSON.stringify({ key, value }) }),
|
|
|
|
// Nostr tools
|
|
fetchNostrEvent: (data: { eventId?: string; naddr?: string }) =>
|
|
request<any>("/nostr/fetch", { method: "POST", body: JSON.stringify(data) }),
|
|
refreshCache: () =>
|
|
request<any>("/nostr/cache/refresh", { method: "POST" }),
|
|
debugEvent: (eventId: string) =>
|
|
request<any>(`/nostr/debug/${eventId}`),
|
|
|
|
// Media
|
|
uploadMedia: async (file: File) => {
|
|
const token = typeof window !== "undefined" ? localStorage.getItem("bbe_token") : null;
|
|
const formData = new FormData();
|
|
formData.append("file", file);
|
|
const res = await fetch(apiUrl("/media/upload"), {
|
|
method: "POST",
|
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
body: formData,
|
|
});
|
|
if (!res.ok) {
|
|
const error = await res.json().catch(() => ({ message: "Upload failed" }));
|
|
throw new Error(error.error || error.message || `HTTP ${res.status}`);
|
|
}
|
|
return res.json() as Promise<{ id: string; slug: string; url: string }>;
|
|
},
|
|
getMediaList: () => request<any[]>("/media"),
|
|
getMedia: (id: string) => request<any>(`/media/${id}`),
|
|
deleteMedia: (id: string) =>
|
|
request<void>(`/media/${id}`, { method: "DELETE" }),
|
|
updateMedia: (id: string, data: { title?: string; description?: string; altText?: string }) =>
|
|
request<any>(`/media/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
|
|
|
|
// FAQs
|
|
getFaqs: () => request<any[]>('/faqs'),
|
|
getFaqsAll: () => request<any[]>('/faqs?all=true'),
|
|
getAllFaqs: () => request<any[]>('/faqs/all'),
|
|
createFaq: (data: { question: string; answer: string; showOnHomepage?: boolean }) =>
|
|
request<any>('/faqs', { method: 'POST', body: JSON.stringify(data) }),
|
|
updateFaq: (id: string, data: { question?: string; answer?: string; showOnHomepage?: boolean }) =>
|
|
request<any>(`/faqs/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
|
|
deleteFaq: (id: string) =>
|
|
request<void>(`/faqs/${id}`, { method: 'DELETE' }),
|
|
reorderFaqs: (items: { id: string; order: number }[]) =>
|
|
request<any>('/faqs/reorder', { method: 'POST', body: JSON.stringify({ items }) }),
|
|
|
|
// Profile (self)
|
|
updateProfile: (data: { username?: string }) =>
|
|
request<any>('/users/me', { method: 'PATCH', body: JSON.stringify(data) }),
|
|
checkUsername: (username: string) =>
|
|
request<{ available: boolean; reason?: string }>(
|
|
`/users/me/username-check?username=${encodeURIComponent(username)}`
|
|
),
|
|
|
|
// Submissions
|
|
createSubmission: (data: { eventId?: string; naddr?: string; title: string }) =>
|
|
request<any>("/submissions", { method: "POST", body: JSON.stringify(data) }),
|
|
getMySubmissions: () =>
|
|
request<any[]>("/submissions/mine"),
|
|
getSubmissions: (status?: string) => {
|
|
const params = status ? `?status=${status}` : "";
|
|
return request<any[]>(`/submissions${params}`);
|
|
},
|
|
reviewSubmission: (id: string, data: { status: string; reviewNote?: string }) =>
|
|
request<any>(`/submissions/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
|
|
};
|