diff --git a/frontend/src/api/photos.ts b/frontend/src/api/photos.ts index f3143c0..ebbe18c 100644 --- a/frontend/src/api/photos.ts +++ b/frontend/src/api/photos.ts @@ -73,7 +73,7 @@ export const photosApi = { }, searchPhotos: async (params: { - search_type: 'name' | 'date' | 'tags' | 'no_faces' | 'no_tags' | 'processed' | 'unprocessed' + search_type: 'name' | 'date' | 'tags' | 'no_faces' | 'no_tags' | 'processed' | 'unprocessed' | 'favorites' person_name?: string tag_names?: string match_all?: boolean @@ -89,6 +89,36 @@ export const photosApi = { return data }, + toggleFavorite: async (photoId: number): Promise<{ photo_id: number; is_favorite: boolean; message: string }> => { + const { data } = await apiClient.post( + `/api/v1/photos/${photoId}/toggle-favorite` + ) + return data + }, + + checkFavorite: async (photoId: number): Promise<{ photo_id: number; is_favorite: boolean }> => { + const { data } = await apiClient.get( + `/api/v1/photos/${photoId}/is-favorite` + ) + return data + }, + + bulkAddFavorites: async (photoIds: number[]): Promise<{ message: string; added_count: number; already_favorite_count: number; total_requested: number }> => { + const { data } = await apiClient.post( + '/api/v1/photos/bulk-add-favorites', + { photo_ids: photoIds } + ) + return data + }, + + bulkRemoveFavorites: async (photoIds: number[]): Promise<{ message: string; removed_count: number; not_favorite_count: number; total_requested: number }> => { + const { data } = await apiClient.post( + '/api/v1/photos/bulk-remove-favorites', + { photo_ids: photoIds } + ) + return data + }, + openFolder: async (photoId: number): Promise<{ message: string; folder: string }> => { const { data } = await apiClient.post<{ message: string; folder: string }>( `/api/v1/photos/${photoId}/open-folder` @@ -115,6 +145,7 @@ export interface PhotoSearchResult { tags: string[] has_faces: boolean face_count: number + is_favorite?: boolean } export interface SearchPhotosResponse { diff --git a/frontend/src/components/PhotoViewer.tsx b/frontend/src/components/PhotoViewer.tsx index df185f7..1eec0ac 100644 --- a/frontend/src/components/PhotoViewer.tsx +++ b/frontend/src/components/PhotoViewer.tsx @@ -1,5 +1,5 @@ import { useEffect, useState, useRef } from 'react' -import { PhotoSearchResult } from '../api/photos' +import { PhotoSearchResult, photosApi } from '../api/photos' import { apiClient } from '../api/client' interface PhotoViewerProps { @@ -37,6 +37,10 @@ export default function PhotoViewer({ photos, initialIndex, onClose }: PhotoView const [isPlaying, setIsPlaying] = useState(false) const [slideshowInterval, setSlideshowInterval] = useState(3) // seconds const slideshowTimerRef = useRef(null) + + // Favorite state + const [isFavorite, setIsFavorite] = useState(false) + const [loadingFavorite, setLoadingFavorite] = useState(false) const currentPhoto = photos[currentIndex] const canGoPrev = currentIndex > 0 @@ -180,10 +184,34 @@ export default function PhotoViewer({ photos, initialIndex, onClose }: PhotoView setPanX(0) setPanY(0) + // Load favorite status when photo changes + photosApi.checkFavorite(currentPhoto.id) + .then(result => setIsFavorite(result.is_favorite)) + .catch(err => { + console.error('Error checking favorite:', err) + setIsFavorite(false) + }) + // Preload adjacent images when current photo changes preloadAdjacent(currentIndex) }, [currentIndex, currentPhoto, photos.length]) + // Toggle favorite + const toggleFavorite = async () => { + if (loadingFavorite || !currentPhoto) return + + setLoadingFavorite(true) + try { + const result = await photosApi.toggleFavorite(currentPhoto.id) + setIsFavorite(result.is_favorite) + } catch (error) { + console.error('Error toggling favorite:', error) + alert('Error updating favorite status') + } finally { + setLoadingFavorite(false) + } + } + // Keyboard navigation useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -255,9 +283,24 @@ export default function PhotoViewer({ photos, initialIndex, onClose }: PhotoView - {/* Top Right Play Button */} + {/* Top Right Controls */}
+ {/* Favorite button */} + + + {/* Slideshow controls */} {isPlaying && (