Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
378 lines
11 KiB
TypeScript
378 lines
11 KiB
TypeScript
// API client for Next.js API routes
|
|
|
|
const API_BASE_URL = '/api';
|
|
|
|
export interface ApiError {
|
|
message: string;
|
|
status: number;
|
|
}
|
|
|
|
async function handleResponse<T>(response: Response): Promise<T> {
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ error: 'An error occurred' }));
|
|
// Extract error message from various possible response formats
|
|
const message = error.error || error.message || 'An error occurred';
|
|
console.error('API Error:', { status: response.status, message, fullError: error });
|
|
throw { message, status: response.status } as ApiError;
|
|
}
|
|
|
|
// Handle empty responses
|
|
const text = await response.text();
|
|
return text ? JSON.parse(text) : ({} as T);
|
|
}
|
|
|
|
// Auth API
|
|
export const authApi = {
|
|
async session(payload: { adm?: string; usr?: string }) {
|
|
const response = await fetch(`${API_BASE_URL}/auth/session`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
return handleResponse<{ success: true; role: 'admin' | 'guest'; guestId: string | null }>(response);
|
|
},
|
|
|
|
async whoami() {
|
|
const response = await fetch(`${API_BASE_URL}/auth/session`, { credentials: 'include' });
|
|
return handleResponse<
|
|
| { role: 'admin'; guest: { id: string; name: string } | null }
|
|
| { role: 'guest'; guest: { id: string; name: string } }
|
|
| { role: 'none' }
|
|
>(response);
|
|
},
|
|
|
|
async logout() {
|
|
const response = await fetch(`${API_BASE_URL}/auth/logout`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<{ success: true }>(response);
|
|
},
|
|
};
|
|
|
|
export interface Guest {
|
|
id: string;
|
|
name: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export const guestsApi = {
|
|
async list() {
|
|
const response = await fetch(`${API_BASE_URL}/admin/guests`, { credentials: 'include' });
|
|
const data = await handleResponse<{ success: true; guests: Guest[] }>(response);
|
|
return data.guests;
|
|
},
|
|
|
|
async create(name: string) {
|
|
const response = await fetch(`${API_BASE_URL}/admin/guests`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({ name }),
|
|
});
|
|
const data = await handleResponse<{ success: true; guest: Guest }>(response);
|
|
return data.guest;
|
|
},
|
|
|
|
async rename(id: string, name: string) {
|
|
const response = await fetch(`${API_BASE_URL}/admin/guests/${id}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({ name }),
|
|
});
|
|
const data = await handleResponse<{ success: true; guest: Guest }>(response);
|
|
return data.guest;
|
|
},
|
|
|
|
async delete(id: string) {
|
|
const response = await fetch(`${API_BASE_URL}/admin/guests/${id}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<{ success: true }>(response);
|
|
},
|
|
};
|
|
|
|
export interface AdminClaim {
|
|
claimId: string;
|
|
quantity: number;
|
|
note: string | null;
|
|
isPurchased: boolean;
|
|
claimedAt: string;
|
|
itemId: string;
|
|
itemName: string;
|
|
itemQuantity: number;
|
|
wishlistSlug: string;
|
|
wishlistName: string;
|
|
guestId: string;
|
|
guestName: string;
|
|
}
|
|
|
|
export const claimsApi = {
|
|
async list() {
|
|
const response = await fetch(`${API_BASE_URL}/admin/claims`, { credentials: 'include' });
|
|
const data = await handleResponse<{ success: true; claims: AdminClaim[] }>(response);
|
|
return data.claims;
|
|
},
|
|
};
|
|
|
|
// Wishlist types
|
|
export interface Wishlist {
|
|
id: string;
|
|
name: string;
|
|
slug: string;
|
|
description: string | null;
|
|
preferences: string | null;
|
|
imageUrl: string | null;
|
|
isPublic: boolean;
|
|
sortOrder: number;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface ItemClaim {
|
|
id: string;
|
|
quantity: number;
|
|
note: string | null;
|
|
isPurchased: boolean;
|
|
claimedAt: string;
|
|
guest: { id: string; name: string };
|
|
}
|
|
|
|
export interface Item {
|
|
id: string;
|
|
wishlistId: string;
|
|
name: string;
|
|
description: string | null;
|
|
quantity: number;
|
|
imageUrl: string | null;
|
|
purchaseUrls: Array<{ label: string; url: string }> | null;
|
|
isArchived: boolean;
|
|
claims: ItemClaim[];
|
|
claimedQuantity: number;
|
|
remainingQuantity: number;
|
|
sortOrder: number;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
// Wishlists API
|
|
export const wishlistsApi = {
|
|
async getAll() {
|
|
const response = await fetch(`${API_BASE_URL}/wishlists`, {
|
|
credentials: 'include',
|
|
});
|
|
const data = await handleResponse<{ success: boolean; wishlists: Wishlist[] }>(response);
|
|
return data.wishlists;
|
|
},
|
|
|
|
async getAllPublic() {
|
|
const response = await fetch(`${API_BASE_URL}/public/wishlists`, {
|
|
credentials: 'include',
|
|
});
|
|
const data = await handleResponse<{ success: boolean; wishlists: Wishlist[] }>(response);
|
|
return data.wishlists;
|
|
},
|
|
|
|
async getOne(id: string) {
|
|
const response = await fetch(`${API_BASE_URL}/wishlists/${id}`, {
|
|
credentials: 'include',
|
|
});
|
|
const result = await handleResponse<{ success: boolean; wishlist: Wishlist }>(response);
|
|
return result.wishlist;
|
|
},
|
|
|
|
async getBySlug(slug: string) {
|
|
const response = await fetch(`${API_BASE_URL}/${slug}`, {
|
|
credentials: 'include',
|
|
});
|
|
const result = await handleResponse<{ success: boolean; wishlist: Wishlist }>(response);
|
|
return result.wishlist;
|
|
},
|
|
|
|
async create(data: Partial<Wishlist>) {
|
|
const response = await fetch(`${API_BASE_URL}/wishlists`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify(data),
|
|
});
|
|
const result = await handleResponse<{ success: boolean; wishlist: Wishlist }>(response);
|
|
return result.wishlist;
|
|
},
|
|
|
|
async update(id: string, data: Partial<Wishlist>) {
|
|
const response = await fetch(`${API_BASE_URL}/wishlists/${id}`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify(data),
|
|
});
|
|
const result = await handleResponse<{ success: boolean; wishlist: Wishlist }>(response);
|
|
return result.wishlist;
|
|
},
|
|
|
|
async delete(id: string) {
|
|
const response = await fetch(`${API_BASE_URL}/wishlists/${id}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<void>(response);
|
|
},
|
|
|
|
async reorder(id: string, newSortOrder: number) {
|
|
const response = await fetch(`${API_BASE_URL}/wishlists/${id}/reorder`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify({ newSortOrder }),
|
|
});
|
|
const result = await handleResponse<{ success: boolean; wishlist: Wishlist }>(response);
|
|
return result.wishlist;
|
|
},
|
|
};
|
|
|
|
// Items API
|
|
export const itemsApi = {
|
|
async getAll(wishlistId: string) {
|
|
const response = await fetch(`${API_BASE_URL}/wishlists/${wishlistId}/items`, {
|
|
credentials: 'include',
|
|
});
|
|
const result = await handleResponse<{ success: boolean; items: Item[] }>(response);
|
|
return result.items;
|
|
},
|
|
|
|
async getOne(id: string) {
|
|
const response = await fetch(`${API_BASE_URL}/items/${id}`, {
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<Item>(response);
|
|
},
|
|
|
|
async create(wishlistId: string, data: Partial<Item>) {
|
|
const response = await fetch(`${API_BASE_URL}/wishlists/${wishlistId}/items`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify(data),
|
|
});
|
|
return handleResponse<Item>(response);
|
|
},
|
|
|
|
async update(id: string, data: Partial<Item>) {
|
|
const response = await fetch(`${API_BASE_URL}/items/${id}`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify(data),
|
|
});
|
|
return handleResponse<Item>(response);
|
|
},
|
|
|
|
async delete(id: string) {
|
|
const response = await fetch(`${API_BASE_URL}/items/${id}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<void>(response);
|
|
},
|
|
|
|
async reorder(id: string, newSortOrder: number) {
|
|
const response = await fetch(`${API_BASE_URL}/items/${id}/reorder`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify({ newSortOrder }),
|
|
});
|
|
const result = await handleResponse<{ success: boolean; item: Item }>(response);
|
|
return result.item;
|
|
},
|
|
};
|
|
|
|
// Claiming API (public)
|
|
export const claimingApi = {
|
|
async claim(itemId: string, opts: { quantity?: number; note?: string } = {}) {
|
|
const response = await fetch(`${API_BASE_URL}/public/items/${itemId}/claim`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({ quantity: opts.quantity ?? 1, note: opts.note }),
|
|
});
|
|
return handleResponse<{ success: true; item: Item }>(response);
|
|
},
|
|
|
|
async unclaim(itemId: string, opts: { guestId?: string } = {}) {
|
|
const response = await fetch(`${API_BASE_URL}/public/items/${itemId}/unclaim`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(opts.guestId ? { guestId: opts.guestId } : {}),
|
|
});
|
|
return handleResponse<{ success: true }>(response);
|
|
},
|
|
};
|
|
|
|
// Scraping API
|
|
export interface ScrapedData {
|
|
title?: string;
|
|
description?: string;
|
|
imageUrl?: string;
|
|
}
|
|
|
|
export const scrapingApi = {
|
|
async scrapeUrl(url: string) {
|
|
const response = await fetch(`${API_BASE_URL}/scrape`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify({ url }),
|
|
});
|
|
return handleResponse<ScrapedData>(response);
|
|
},
|
|
};
|
|
|
|
// Settings API
|
|
export interface Settings {
|
|
siteTitle: string;
|
|
homepageSubtext: string;
|
|
}
|
|
|
|
export const settingsApi = {
|
|
async getSettings() {
|
|
const response = await fetch(`${API_BASE_URL}/settings`, {
|
|
credentials: 'include',
|
|
});
|
|
const result = await handleResponse<{ success: boolean; settings: Settings }>(response);
|
|
return result.settings;
|
|
},
|
|
|
|
async updateSettings(settings: Partial<Settings>) {
|
|
const response = await fetch(`${API_BASE_URL}/settings`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify(settings),
|
|
});
|
|
return handleResponse<{ success: boolean; message: string }>(response);
|
|
},
|
|
};
|