import { fetchApi, API_BASE, getToken } from './client'; // Client for the standalone photo-api Go service (photo-api/), reachable // under /api/photos via the Next rewrite (dev) or nginx (prod). export type GalleryVisibility = 'public' | 'private' | 'link' | 'ticket'; export interface PhotoGalleryEvent { id: string; slug: string; title: string; titleEs?: string; startDatetime: string; status: string; } export interface PhotoGallery { id: string; slug: string; title: string; titleEs?: string; description?: string; descriptionEs?: string; eventId?: string; visibility: GalleryVisibility; coverPhotoId?: string; photoCount: number; coverUrl?: string; createdAt: string; updatedAt: string; event?: PhotoGalleryEvent; // Present on admin responses only: shareToken?: string; shareUrl?: string; } export interface Photo { id: string; galleryId: string; position: number; originalFilename?: string; contentType: string; sizeBytes: number; width?: number; height?: number; takenAt?: string; status: 'queued' | 'processing' | 'ready' | 'failed'; lastError?: string; createdAt: string; urls: { thumb?: string; preview?: string; original: string; }; } export interface CreateGalleryInput { title: string; titleEs?: string; description?: string; descriptionEs?: string; eventId?: string; visibility?: GalleryVisibility; } export const photosApi = { // Admin (requires admin/organizer token) createGallery: (data: CreateGalleryInput) => fetchApi<{ gallery: PhotoGallery }>('/api/photos/galleries', { method: 'POST', body: JSON.stringify(data), }), getGalleries: (eventId?: string) => { const query = eventId ? `?eventId=${encodeURIComponent(eventId)}` : ''; return fetchApi<{ galleries: PhotoGallery[] }>(`/api/photos/galleries${query}`); }, getGallery: (id: string) => fetchApi<{ gallery: PhotoGallery; photos: Photo[] }>(`/api/photos/galleries/${id}`), updateGallery: (id: string, data: Partial & { coverPhotoId?: string }) => fetchApi<{ gallery: PhotoGallery }>(`/api/photos/galleries/${id}`, { method: 'PATCH', body: JSON.stringify(data), }), deleteGallery: (id: string) => fetchApi<{ message: string }>(`/api/photos/galleries/${id}`, { method: 'DELETE' }), rotateShareToken: (id: string) => fetchApi<{ gallery: PhotoGallery }>(`/api/photos/galleries/${id}/share-token`, { method: 'POST', }), uploadPhoto: async (galleryId: string, file: File) => { const token = getToken(); const formData = new FormData(); formData.append('files', file); const res = await fetch(`${API_BASE}/api/photos/galleries/${galleryId}/photos`, { method: 'POST', headers: token ? { Authorization: `Bearer ${token}` } : {}, body: formData, }); if (!res.ok) { const errorData = await res.json().catch(() => ({ error: 'Upload failed' })); throw new Error(errorData.error || 'Upload failed'); } return res.json() as Promise<{ photos: Photo[] }>; }, /** * Upload with byte-level progress (0..1). fetch() cannot report upload * progress, so this uses XMLHttpRequest; used by the admin uploader panel. */ uploadPhotoWithProgress: ( galleryId: string, file: File, onProgress: (fraction: number) => void ) => new Promise<{ photos: Photo[] }>((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', `${API_BASE}/api/photos/galleries/${galleryId}/photos`); const token = getToken(); if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`); xhr.upload.onprogress = (e) => { if (e.lengthComputable && e.total > 0) onProgress(e.loaded / e.total); }; xhr.onload = () => { try { const body = JSON.parse(xhr.responseText || '{}'); if (xhr.status >= 200 && xhr.status < 300) resolve(body); else reject(new Error(body.error || `Upload failed (${xhr.status})`)); } catch { reject(new Error(`Upload failed (${xhr.status})`)); } }; xhr.onerror = () => reject(new Error('Network error during upload')); xhr.onabort = () => reject(new Error('Upload cancelled')); const formData = new FormData(); formData.append('files', file); xhr.send(formData); }), reorderPhotos: (galleryId: string, photoIds: string[]) => fetchApi<{ message: string }>(`/api/photos/galleries/${galleryId}/order`, { method: 'PATCH', body: JSON.stringify({ photoIds }), }), deletePhoto: (photoId: string) => fetchApi<{ message: string }>(`/api/photos/photos/${photoId}`, { method: 'DELETE' }), retryPhoto: (photoId: string) => fetchApi<{ photo: Photo }>(`/api/photos/photos/${photoId}/retry`, { method: 'POST' }), // Viewer (anonymous or member token; share token via query param) getPublicGalleries: () => fetchApi<{ galleries: PhotoGallery[] }>('/api/photos/public/galleries'), getPublicGallery: (slug: string, token?: string) => { const query = token ? `?token=${encodeURIComponent(token)}` : ''; return fetchApi<{ gallery: PhotoGallery; photos: Photo[] }>( `/api/photos/public/galleries/${encodeURIComponent(slug)}${query}` ); }, /** Newest gallery linked to an event; backs /events/[slug]/gallery. */ getEventGallery: (eventSlug: string, token?: string) => { const query = token ? `?token=${encodeURIComponent(token)}` : ''; return fetchApi<{ gallery: PhotoGallery; photos: Photo[] }>( `/api/photos/public/events/${encodeURIComponent(eventSlug)}/gallery${query}` ); }, };