From 2e41d5b695ece3fbd9eea5bde4bfdf249ffde697 Mon Sep 17 00:00:00 2001 From: JSC Date: Sat, 16 Aug 2025 21:16:13 +0200 Subject: [PATCH] feat: implement favorites functionality with SoundCard integration and FavoritesService --- src/components/sounds/SoundCard.tsx | 60 ++++++++++++--- src/lib/api/services/favorites.ts | 112 ++++++++++++++++++++++++++++ src/lib/api/services/index.ts | 1 + src/lib/api/services/sounds.ts | 2 + src/pages/SoundsPage.tsx | 69 +++++++++++++++-- 5 files changed, 228 insertions(+), 16 deletions(-) create mode 100644 src/lib/api/services/favorites.ts diff --git a/src/components/sounds/SoundCard.tsx b/src/components/sounds/SoundCard.tsx index dbf285d..0289cfa 100644 --- a/src/components/sounds/SoundCard.tsx +++ b/src/components/sounds/SoundCard.tsx @@ -4,43 +4,83 @@ import { cn } from '@/lib/utils' import { formatDuration } from '@/utils/format-duration' import { formatSize } from '@/utils/format-size' import NumberFlow from '@number-flow/react' -import { Clock, Play, Weight } from 'lucide-react' +import { Clock, Heart, Play, Weight } from 'lucide-react' interface SoundCardProps { sound: Sound playSound: (sound: Sound) => void + onFavoriteToggle: (soundId: number, isFavorited: boolean) => void colorClasses: string } -export function SoundCard({ sound, playSound, colorClasses }: SoundCardProps) { - const handlePlaySound = () => { +export function SoundCard({ sound, playSound, onFavoriteToggle, colorClasses }: SoundCardProps) { + const handlePlaySound = (e: React.MouseEvent) => { + // Don't play sound if clicking on favorite button + if ((e.target as HTMLElement).closest('[data-favorite-button]')) { + return + } playSound(sound) } + const handleFavoriteToggle = (e: React.MouseEvent) => { + e.stopPropagation() + onFavoriteToggle(sound.id, !sound.is_favorited) + } + return ( -

{sound.name}

-
+ {/* Favorite button */} + + +

{sound.name}

+
{formatDuration(sound.duration)}
-
- - {formatSize(sound.size)} -
+
+ + {/* Show favorite count if > 0 */} +
+
+ + {formatSize(sound.size)} +
+
+ + +
+
) diff --git a/src/lib/api/services/favorites.ts b/src/lib/api/services/favorites.ts new file mode 100644 index 0000000..f0f053f --- /dev/null +++ b/src/lib/api/services/favorites.ts @@ -0,0 +1,112 @@ +import { apiClient } from '../client' + +export interface Favorite { + id: number + user_id: number + sound_id?: number + playlist_id?: number + created_at: string + updated_at: string +} + +export interface FavoriteCountsResponse { + total: number + sounds: number + playlists: number +} + +export interface FavoritesListResponse { + favorites: Favorite[] +} + +export class FavoritesService { + /** + * Add a sound to favorites + */ + async addSoundFavorite(soundId: number): Promise { + const response = await apiClient.post(`/api/v1/favorites/sounds/${soundId}`) + return response + } + + /** + * Remove a sound from favorites + */ + async removeSoundFavorite(soundId: number): Promise { + await apiClient.delete(`/api/v1/favorites/sounds/${soundId}`) + } + + /** + * Add a playlist to favorites + */ + async addPlaylistFavorite(playlistId: number): Promise { + const response = await apiClient.post(`/api/v1/favorites/playlists/${playlistId}`) + return response + } + + /** + * Remove a playlist from favorites + */ + async removePlaylistFavorite(playlistId: number): Promise { + await apiClient.delete(`/api/v1/favorites/playlists/${playlistId}`) + } + + /** + * Get all favorites for the current user + */ + async getFavorites(limit = 50, offset = 0): Promise { + const response = await apiClient.get( + `/api/v1/favorites/?limit=${limit}&offset=${offset}` + ) + return response.favorites + } + + /** + * Get sound favorites for the current user + */ + async getSoundFavorites(limit = 50, offset = 0): Promise { + const response = await apiClient.get( + `/api/v1/favorites/sounds?limit=${limit}&offset=${offset}` + ) + return response.favorites + } + + /** + * Get playlist favorites for the current user + */ + async getPlaylistFavorites(limit = 50, offset = 0): Promise { + const response = await apiClient.get( + `/api/v1/favorites/playlists?limit=${limit}&offset=${offset}` + ) + return response.favorites + } + + /** + * Get favorite counts for the current user + */ + async getFavoriteCounts(): Promise { + const response = await apiClient.get('/api/v1/favorites/counts') + return response + } + + /** + * Check if a sound is favorited + */ + async isSoundFavorited(soundId: number): Promise { + const response = await apiClient.get<{ is_favorited: boolean }>( + `/api/v1/favorites/sounds/${soundId}/check` + ) + return response.is_favorited + } + + /** + * Check if a playlist is favorited + */ + async isPlaylistFavorited(playlistId: number): Promise { + const response = await apiClient.get<{ is_favorited: boolean }>( + `/api/v1/favorites/playlists/${playlistId}/check` + ) + return response.is_favorited + } +} + +export const favoritesService = new FavoritesService() \ No newline at end of file diff --git a/src/lib/api/services/index.ts b/src/lib/api/services/index.ts index c2b1d7f..7e3d8cb 100644 --- a/src/lib/api/services/index.ts +++ b/src/lib/api/services/index.ts @@ -3,3 +3,4 @@ export * from './sounds' export * from './player' export * from './files' export * from './extractions' +export * from './favorites' diff --git a/src/lib/api/services/sounds.ts b/src/lib/api/services/sounds.ts index 4d590f2..520ba66 100644 --- a/src/lib/api/services/sounds.ts +++ b/src/lib/api/services/sounds.ts @@ -17,6 +17,8 @@ export interface Sound { thumbnail?: string is_music: boolean is_deletable: boolean + is_favorited: boolean + favorite_count: number created_at: string updated_at: string } diff --git a/src/pages/SoundsPage.tsx b/src/pages/SoundsPage.tsx index 938b981..4d90053 100644 --- a/src/pages/SoundsPage.tsx +++ b/src/pages/SoundsPage.tsx @@ -17,9 +17,11 @@ import { type SoundSortField, soundsService, } from '@/lib/api/services/sounds' +import { favoritesService } from '@/lib/api/services/favorites' import { SOUND_EVENTS, soundEvents } from '@/lib/events' import { AlertCircle, + Heart, RefreshCw, Search, SortAsc, @@ -77,6 +79,7 @@ export function SoundsPage() { const [searchQuery, setSearchQuery] = useState('') const [sortBy, setSortBy] = useState('name') const [sortOrder, setSortOrder] = useState('asc') + const [showFavoritesOnly, setShowFavoritesOnly] = useState(false) const handlePlaySound = async (sound: Sound) => { try { @@ -89,6 +92,39 @@ export function SoundsPage() { } } + const handleFavoriteToggle = async (soundId: number, shouldFavorite: boolean) => { + try { + if (shouldFavorite) { + await favoritesService.addSoundFavorite(soundId) + toast.success('Added to favorites') + } else { + await favoritesService.removeSoundFavorite(soundId) + toast.success('Removed from favorites') + } + + // Update the sound in the local state + setSounds(prevSounds => + prevSounds.map(sound => + sound.id === soundId + ? { + ...sound, + is_favorited: shouldFavorite, + favorite_count: shouldFavorite + ? sound.favorite_count + 1 + : Math.max(0, sound.favorite_count - 1), + } + : sound, + ), + ) + } catch (error) { + toast.error( + `Failed to ${shouldFavorite ? 'add to' : 'remove from'} favorites: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, + ) + } + } + const { theme } = useTheme() useEffect(() => { @@ -188,15 +224,23 @@ export function SoundsPage() { ) } - if (sounds.length === 0) { + // Filter sounds based on favorites filter + const filteredSounds = showFavoritesOnly ? sounds.filter(sound => sound.is_favorited) : sounds + + if (filteredSounds.length === 0) { return (
- 🎵 + {showFavoritesOnly ? '💝' : '🎵'}
-

No sounds found

+

+ {showFavoritesOnly ? 'No favorite sounds found' : 'No sounds found'} +

- No SDB type sounds are available in your library. + {showFavoritesOnly + ? 'You haven\'t favorited any sounds yet. Click the heart icon on sounds to add them to your favorites.' + : 'No SDB type sounds are available in your library.' + }

) @@ -204,11 +248,12 @@ export function SoundsPage() { return (
- {sounds.map((sound, idx) => ( + {filteredSounds.map((sound, idx) => ( ))} @@ -232,7 +277,10 @@ export function SoundsPage() {
{!loading && !error && (
- {sounds.length} sound{sounds.length !== 1 ? 's' : ''} + {showFavoritesOnly + ? `${sounds.filter(s => s.is_favorited).length} favorite sound${sounds.filter(s => s.is_favorited).length !== 1 ? 's' : ''}` + : `${sounds.length} sound${sounds.length !== 1 ? 's' : ''}` + }
)}
@@ -293,6 +341,15 @@ export function SoundsPage() { )} + +