first commit

Made-with: Cursor
This commit is contained in:
Michilis
2026-04-01 02:46:53 +00:00
commit 76210db03d
126 changed files with 20208 additions and 0 deletions

181
frontend/lib/api.ts Normal file
View File

@@ -0,0 +1,181 @@
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000/api";
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(`${API_URL}${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(`${API_URL}/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) }),
};