diff --git a/frontend/src/components/Gallery.tsx b/frontend/src/components/Gallery.tsx index e605c7b..c5f6a07 100644 --- a/frontend/src/components/Gallery.tsx +++ b/frontend/src/components/Gallery.tsx @@ -19,6 +19,7 @@ interface Clip { key?: string | null artist?: string | null album?: string | null + genre?: string | null comment?: string | null prompt_structure?: Record | null created_at: string @@ -46,13 +47,6 @@ interface SearchFilters { dateTo: string } -interface SavedSearch { - id: string - name: string - query: string - filters: SearchFilters -} - const DEFAULT_FILTERS: SearchFilters = { model: 'all', status: 'all', @@ -101,18 +95,28 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o const [search, setSearch] = useState('') const [filters, setFilters] = useState(DEFAULT_FILTERS) const [showFilters, setShowFilters] = useState(false) - const [showSaved, setShowSaved] = useState(false) + const [showFavorites, setShowFavorites] = useState(false) const debouncedSearch = useDebounce(search, 300) - // Saved searches - const [savedSearches, setSavedSearches] = useState(() => { + // Favorites + const [favorites, setFavorites] = useState>(() => { try { - const raw = localStorage.getItem('kithara-saved-searches') - return raw ? JSON.parse(raw) : [] + const raw = localStorage.getItem('kithara-favorites') + return raw ? new Set(JSON.parse(raw)) : new Set() } catch { - return [] + return new Set() } }) + + const toggleFavorite = useCallback((id: string) => { + setFavorites(prev => { + const next = new Set(prev) + if (next.has(id)) next.delete(id) + else next.add(id) + localStorage.setItem('kithara-favorites', JSON.stringify([...next])) + return next + }) + }, []) // Pagination + view const [page, setPage] = useState(1) @@ -132,6 +136,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o clip.prompt, clip.artist, clip.album, + clip.genre, clip.comment, clip.model, clip.status, @@ -154,7 +159,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o }) }, [clipsWithSearchText]) - // Apply search + filters + // Apply search + filters + favorites const filtered = useMemo(() => { let results = clipsWithSearchText @@ -174,9 +179,11 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o if (filters.bpmMax && clip.bpm && clip.bpm > Number(filters.bpmMax)) return false if (filters.dateFrom && new Date(clip.created_at) < new Date(filters.dateFrom)) return false if (filters.dateTo && new Date(clip.created_at) > new Date(filters.dateTo + 'T23:59:59')) return false + // Favorites filter + if (showFavorites && !favorites.has(clip.id)) return false return true }) - }, [clipsWithSearchText, fuse, debouncedSearch, filters]) + }, [clipsWithSearchText, fuse, debouncedSearch, filters, showFavorites, favorites]) const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)) const paged = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE) @@ -184,34 +191,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o // Reset page when search/filters change useEffect(() => { setPage(1) - }, [debouncedSearch, filters]) - - // Save searches to localStorage - const saveSearch = useCallback(() => { - const name = prompt('Name this search:') - if (!name?.trim()) return - const newSearch: SavedSearch = { - id: Date.now().toString(), - name: name.trim(), - query: search, - filters: { ...filters }, - } - const updated = [...savedSearches, newSearch] - setSavedSearches(updated) - localStorage.setItem('kithara-saved-searches', JSON.stringify(updated)) - }, [search, filters, savedSearches]) - - const loadSearch = useCallback((saved: SavedSearch) => { - setSearch(saved.query) - setFilters(saved.filters) - setShowSaved(false) - }, []) - - const deleteSearch = useCallback((id: string) => { - const updated = savedSearches.filter(s => s.id !== id) - setSavedSearches(updated) - localStorage.setItem('kithara-saved-searches', JSON.stringify(updated)) - }, [savedSearches]) + }, [debouncedSearch, filters, showFavorites]) const clearFilters = () => { setFilters(DEFAULT_FILTERS) @@ -326,14 +306,18 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o - {/* Saved searches button */} + {/* Favorites button */} @@ -398,9 +382,9 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o )} {/* Results count */} - {(search || hasActiveFilters) && ( + {(search || hasActiveFilters || showFavorites) && (

- Showing {filtered.length} of {clips.length} clips + {showFavorites ? `♥ ${filtered.length} of ${clips.length} clips` : `Showing ${filtered.length} of ${clips.length} clips`}

)} @@ -494,46 +478,16 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o > Reset all filters - )} - {/* Saved searches dropdown */} - {showSaved && savedSearches.length > 0 && ( -
-

Saved Searches

- {savedSearches.map(saved => ( -
- - -
- ))} -
- )} - {/* Clip list */} {filtered.length === 0 ? (
-

No clips match your search

+

+ {showFavorites ? 'No favorites yet. Heart a track to add it here!' : 'No clips match your search'} +

) : view === 'ribbons' ? ( /* ── RIBBON VIEW ── */ @@ -546,6 +500,19 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o currentClipId === clip.id ? 'bg-white/5' : '' }`} > + {/* Heart */} + +
{clip.artwork_filename ? (
+ {/* Heart on tile */} + {editingId === clip.id ? ( @@ -900,4 +879,4 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o /> ) -} +} \ No newline at end of file