import axios from 'axios' const API_BASE_URL = 'http://localhost:8001/api/v1' const api = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', }, }) // Source Folders API. Source roots are config-driven now (PHOTO_DIRS in // .env → bootstrap on backend startup), so the UI only reads them and // optionally renames the display label. export interface FolderTreeNode { id: string name: string path: string photo_count: number children: FolderTreeNode[] } export const sourceFolders = { list: async () => { const response = await api.get('/folders') return response.data }, /** Recursive folder tree, one root per active source root. */ tree: async (): Promise => { const response = await api.get('/folders/tree') return response.data }, scan: async (folderId: string) => { const response = await api.post(`/folders/${folderId}/scan`) return response.data }, /** Rename the display label only — the on-disk path is controlled by * the docker mount and cannot be changed from the UI. */ rename: async (folderId: string, name: string) => { const response = await api.patch(`/folders/${folderId}`, { name }) return response.data }, } // Photos API export const photos = { list: async (params?: { limit?: number offset?: number folder_id?: string heap_id?: string rating?: number flag?: string }) => { const response = await api.get('/photos', { params }) return response.data }, get: async (photoId: string) => { const response = await api.get(`/photos/${photoId}`) return response.data }, update: async (photoId: string, data: { filename?: string rating?: number user_title?: string | null user_notes?: string | null color_label?: string | null is_picked?: boolean is_discarded?: boolean taken_at?: string }) => { const response = await api.patch(`/photos/${photoId}`, data) return response.data }, /** Bulk discard — matches the backend BulkAction schema. */ bulkDiscard: async (photoIds: string[]) => { const response = await api.post('/photos/bulk', { ids: photoIds, action: 'discard', }) return response.data }, /** Bulk restore from discarded. */ bulkRestore: async (photoIds: string[]) => { const response = await api.post('/photos/bulk', { ids: photoIds, action: 'restore', }) return response.data }, /** Bulk set rating (0-5). */ bulkSetRating: async (photoIds: string[], rating: number) => { const response = await api.post('/photos/bulk', { ids: photoIds, action: 'set_rating', value: rating, }) return response.data }, /** Bulk set color label (or null to clear). */ bulkSetColor: async (photoIds: string[], color: string | null) => { const response = await api.post('/photos/bulk', { ids: photoIds, action: 'set_color', value: color, }) return response.data }, /** Move photos into a target folder (or source root). Returns * { moved, errors[] }. */ move: async (photoIds: string[], targetId: string) => { const response = await api.post('/photos/move', { photo_ids: photoIds, target_id: targetId, }) return response.data as { status: string; moved: number; errors: Array<{ id: string; error: string }> } }, /** Copy photos into a target folder. Originals are unaffected; new * rows are created with is_duplicate=true. */ copy: async (photoIds: string[], targetId: string) => { const response = await api.post('/photos/copy', { photo_ids: photoIds, target_id: targetId, }) return response.data as { status: string; copied: number; errors: Array<{ id: string; error: string }> } }, getThumbnailUrl: (photoId: string, size: 'small' | 'medium' | 'large' = 'medium') => { return `${API_BASE_URL}/photos/${photoId}/thumb/${size}` }, getOriginalUrl: (photoId: string) => { return `${API_BASE_URL}/photos/${photoId}/original` }, /** Full-resolution display URL. Backend serves the original for web-safe * formats and a transcoded WebP for RAW/HEIC/TIFF. */ getProxyUrl: (photoId: string) => { return `${API_BASE_URL}/photos/${photoId}/proxy` }, } // Library API export const library = { scan: async () => { const response = await api.post('/library/scan') return response.data }, scanStatus: async () => { const response = await api.get('/library/scan/status') return response.data }, stats: async () => { const response = await api.get('/library/stats') return response.data }, } // Heaps API export interface Heap { id: string name: string is_active: boolean created_at: string updated_at: string | null photo_count: number } export const heaps = { list: async (): Promise => { const response = await api.get('/heaps') return response.data }, create: async (name: string): Promise => { const response = await api.post('/heaps', { name }) return response.data }, update: async ( heapId: string, data: { name?: string; is_active?: boolean } ): Promise => { const response = await api.patch(`/heaps/${heapId}`, data) return response.data }, delete: async (heapId: string): Promise => { await api.delete(`/heaps/${heapId}`) }, /** Lightweight: just the photo ids in a heap, for client-side membership * lookups (the basket affordance on thumbnails). */ photoIds: async (heapId: string): Promise => { const response = await api.get(`/heaps/${heapId}/photo_ids`) return response.data }, addPhotos: async (heapId: string, photoIds: string[]) => { const response = await api.post(`/heaps/${heapId}/photos`, { photo_ids: photoIds, }) return response.data }, removePhotos: async (heapId: string, photoIds: string[]) => { const response = await api.delete(`/heaps/${heapId}/photos`, { data: { photo_ids: photoIds }, }) return response.data }, /** Convert a heap into a folder by moving (or copying) every member * photo into the target directory. Optionally creates a subfolder * inside the target by name. */ convert: async ( heapId: string, body: { target_id: string mode: 'move' | 'copy' delete_heap: boolean subfolder_name?: string | null } ) => { const response = await api.post(`/heaps/${heapId}/convert`, body) return response.data as { status: string mode: 'move' | 'copy' moved: number copied: number errors: Array<{ id: string; error: string }> heap_deleted: boolean } }, } // Tags API export interface Tag { id: string name: string color: string | null photo_count: number } export const tags = { list: async (): Promise => { const response = await api.get('/tags') return response.data }, create: async (name: string, color?: string): Promise => { const response = await api.post('/tags', { name, color }) return response.data }, update: async (tagId: string, data: { name?: string; color?: string }): Promise => { const response = await api.patch(`/tags/${tagId}`, data) return response.data }, delete: async (tagId: string): Promise => { await api.delete(`/tags/${tagId}`) }, /** Add one or more tags to a photo. */ addToPhoto: async (photoId: string, tagIds: string[]) => { const response = await api.post(`/photos/${photoId}/tags`, { tag_ids: tagIds }) return response.data }, /** Remove a tag from a photo. */ removeFromPhoto: async (photoId: string, tagId: string): Promise => { await api.delete(`/photos/${photoId}/tags/${tagId}`) }, } // Discard API export const discard = { list: async () => { const response = await api.get('/discard') return response.data }, restore: async (photoIds: string[]) => { const response = await api.post('/discard/restore', { photo_ids: photoIds, }) return response.data }, empty: async () => { const response = await api.delete('/discard/empty') return response.data }, /** Permanently delete a specific subset of discarded photos. The backend * silently skips ids that aren't in the pile, so this can never bypass * the soft-delete safety net. */ deletePermanent: async (photoIds: string[]) => { const response = await api.delete('/discard', { data: { photo_ids: photoIds }, }) return response.data }, } export default api