Initial release v1.0.0
This commit is contained in:
@@ -0,0 +1,795 @@
|
||||
/**
|
||||
* React Query Hooks for API Caching
|
||||
*
|
||||
* This file provides custom React Query hooks for the most frequently called APIs
|
||||
* in the music streaming app. These hooks implement smart caching strategies to:
|
||||
* - Reduce unnecessary API calls
|
||||
* - Improve perceived performance
|
||||
* - Provide automatic background refetching
|
||||
* - Handle loading and error states consistently
|
||||
*
|
||||
* Stale time configuration rationale:
|
||||
* - Artist data: 10 minutes (rarely changes)
|
||||
* - Album data: 10 minutes (rarely changes)
|
||||
* - Library/Home data: 2 minutes (may change as user adds music)
|
||||
* - Search results: 5 minutes (relatively static for same query)
|
||||
* - Playlists: 1 minute (user may be actively modifying)
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
// ============================================================================
|
||||
// QUERY KEY FACTORIES
|
||||
// ============================================================================
|
||||
// These functions generate consistent query keys for caching
|
||||
// See: https://tanstack.com/query/latest/docs/react/guides/query-keys
|
||||
|
||||
export const queryKeys = {
|
||||
// Artist queries
|
||||
artist: (id: string) => ["artist", id] as const,
|
||||
artistLibrary: (id: string) => ["artist", "library", id] as const,
|
||||
artistDiscovery: (id: string) => ["artist", "discovery", id] as const,
|
||||
|
||||
// Album queries
|
||||
album: (id: string) => ["album", id] as const,
|
||||
albumLibrary: (id: string) => ["album", "library", id] as const,
|
||||
albumDiscovery: (id: string) => ["album", "discovery", id] as const,
|
||||
albums: (filters?: Record<string, any>) => ["albums", filters] as const,
|
||||
|
||||
// Library queries
|
||||
library: () => ["library"] as const,
|
||||
recentlyListened: (limit?: number) => ["library", "recently-listened", limit] as const,
|
||||
recentlyAdded: (limit?: number) => ["library", "recently-added", limit] as const,
|
||||
|
||||
// Recommendations
|
||||
recommendations: (limit?: number) => ["recommendations", limit] as const,
|
||||
similarArtists: (seedArtistId: string, limit?: number) => ["recommendations", "artists", seedArtistId, limit] as const,
|
||||
similarAlbums: (seedAlbumId: string, limit?: number) => ["recommendations", "albums", seedAlbumId, limit] as const,
|
||||
|
||||
// Search
|
||||
search: (query: string, type?: string, limit?: number) => ["search", query, type, limit] as const,
|
||||
discoverSearch: (query: string, type?: string, limit?: number) => ["search", "discover", query, type, limit] as const,
|
||||
|
||||
// Playlists
|
||||
playlists: () => ["playlists"] as const,
|
||||
playlist: (id: string) => ["playlist", id] as const,
|
||||
|
||||
// Mixes
|
||||
mixes: () => ["mixes"] as const,
|
||||
mix: (id: string) => ["mix", id] as const,
|
||||
|
||||
// Popular artists
|
||||
popularArtists: (limit?: number) => ["popular-artists", limit] as const,
|
||||
|
||||
// Audiobooks
|
||||
audiobooks: () => ["audiobooks"] as const,
|
||||
audiobook: (id: string) => ["audiobook", id] as const,
|
||||
|
||||
// Podcasts
|
||||
podcasts: () => ["podcasts"] as const,
|
||||
podcast: (id: string) => ["podcast", id] as const,
|
||||
topPodcasts: (limit?: number, genreId?: number) => ["podcasts", "top", limit, genreId] as const,
|
||||
|
||||
// Browse (Deezer playlists/radios)
|
||||
browseAll: () => ["browse", "all"] as const,
|
||||
browseFeatured: (limit?: number) => ["browse", "featured", limit] as const,
|
||||
browseRadios: (limit?: number) => ["browse", "radios", limit] as const,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// ARTIST QUERIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch artist data with automatic library/discovery fallback
|
||||
*
|
||||
* Tries library first, falls back to discovery if not found.
|
||||
* Cache time: 10 minutes (artist data rarely changes)
|
||||
*
|
||||
* @param id - Artist ID or MusicBrainz ID
|
||||
* @returns Query result with artist data
|
||||
*
|
||||
* @example
|
||||
* const { data: artist, isLoading, error } = useArtistQuery("artist-123");
|
||||
*/
|
||||
export function useArtistQuery(id: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.artist(id || ""),
|
||||
queryFn: async () => {
|
||||
if (!id) throw new Error("Artist ID is required");
|
||||
|
||||
// Try library first
|
||||
try {
|
||||
return await api.getArtist(id);
|
||||
} catch (error) {
|
||||
// Fallback to discovery
|
||||
return await api.getArtistDiscovery(id);
|
||||
}
|
||||
},
|
||||
enabled: !!id,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
retry: 1,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch artist data from library only
|
||||
*
|
||||
* @param id - Artist ID
|
||||
* @returns Query result with artist data from library
|
||||
*/
|
||||
export function useArtistLibraryQuery(id: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.artistLibrary(id || ""),
|
||||
queryFn: async () => {
|
||||
if (!id) throw new Error("Artist ID is required");
|
||||
return await api.getArtist(id);
|
||||
},
|
||||
enabled: !!id,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch artist data from discovery only
|
||||
*
|
||||
* @param id - Artist name or MusicBrainz ID
|
||||
* @returns Query result with artist data from Last.fm
|
||||
*/
|
||||
export function useArtistDiscoveryQuery(nameOrMbid: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.artistDiscovery(nameOrMbid || ""),
|
||||
queryFn: async () => {
|
||||
if (!nameOrMbid) throw new Error("Artist name or MBID is required");
|
||||
return await api.getArtistDiscovery(nameOrMbid);
|
||||
},
|
||||
enabled: !!nameOrMbid,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ALBUM QUERIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch album data with automatic library/discovery fallback
|
||||
*
|
||||
* Cache time: 10 minutes (album data rarely changes)
|
||||
*
|
||||
* @param id - Album ID or Release Group MBID
|
||||
* @returns Query result with album data
|
||||
*
|
||||
* @example
|
||||
* const { data: album, isLoading, error } = useAlbumQuery("album-123");
|
||||
*/
|
||||
export function useAlbumQuery(id: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.album(id || ""),
|
||||
queryFn: async () => {
|
||||
if (!id) throw new Error("Album ID is required");
|
||||
|
||||
// Try library first
|
||||
try {
|
||||
return await api.getAlbum(id);
|
||||
} catch (error) {
|
||||
// Fallback to discovery
|
||||
return await api.getAlbumDiscovery(id);
|
||||
}
|
||||
},
|
||||
enabled: !!id,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
retry: 1,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch album data from library only
|
||||
*
|
||||
* @param id - Album ID
|
||||
* @returns Query result with album data from library
|
||||
*/
|
||||
export function useAlbumLibraryQuery(id: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.albumLibrary(id || ""),
|
||||
queryFn: async () => {
|
||||
if (!id) throw new Error("Album ID is required");
|
||||
return await api.getAlbum(id);
|
||||
},
|
||||
enabled: !!id,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch album data from discovery only
|
||||
*
|
||||
* @param rgMbid - Release Group MusicBrainz ID
|
||||
* @returns Query result with album data from Last.fm
|
||||
*/
|
||||
export function useAlbumDiscoveryQuery(rgMbid: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.albumDiscovery(rgMbid || ""),
|
||||
queryFn: async () => {
|
||||
if (!rgMbid) throw new Error("Album MBID is required");
|
||||
return await api.getAlbumDiscovery(rgMbid);
|
||||
},
|
||||
enabled: !!rgMbid,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch albums list with optional filters
|
||||
*
|
||||
* @param params - Filter parameters (artistId, limit, offset)
|
||||
* @returns Query result with albums array
|
||||
*
|
||||
* @example
|
||||
* const { data } = useAlbumsQuery({ artistId: "123", limit: 20 });
|
||||
*/
|
||||
export function useAlbumsQuery(params?: {
|
||||
artistId?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.albums(params),
|
||||
queryFn: () => api.getAlbums(params),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LIBRARY QUERIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch recently listened items (Continue Listening)
|
||||
*
|
||||
* Cache time: 2 minutes (may change frequently)
|
||||
*
|
||||
* @param limit - Number of items to fetch (default: 10)
|
||||
* @returns Query result with recently listened items
|
||||
*
|
||||
* @example
|
||||
* const { data } = useRecentlyListenedQuery(10);
|
||||
*/
|
||||
export function useRecentlyListenedQuery(limit: number = 10) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.recentlyListened(limit),
|
||||
queryFn: () => api.getRecentlyListened(limit),
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch recently added artists
|
||||
*
|
||||
* Cache time: 2 minutes (may change as user adds music)
|
||||
*
|
||||
* @param limit - Number of items to fetch (default: 10)
|
||||
* @returns Query result with recently added artists
|
||||
*
|
||||
* @example
|
||||
* const { data } = useRecentlyAddedQuery(10);
|
||||
*/
|
||||
export function useRecentlyAddedQuery(limit: number = 10) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.recentlyAdded(limit),
|
||||
queryFn: () => api.getRecentlyAdded(limit),
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RECOMMENDATION QUERIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch personalized recommendations
|
||||
*
|
||||
* Cache time: 5 minutes
|
||||
*
|
||||
* @param limit - Number of recommendations (default: 10)
|
||||
* @returns Query result with recommended artists
|
||||
*
|
||||
* @example
|
||||
* const { data } = useRecommendationsQuery(10);
|
||||
*/
|
||||
export function useRecommendationsQuery(limit: number = 10) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.recommendations(limit),
|
||||
queryFn: () => api.getRecommendationsForYou(limit),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch similar artists based on a seed artist
|
||||
*
|
||||
* @param seedArtistId - Artist ID to find similar artists for
|
||||
* @param limit - Number of recommendations (default: 20)
|
||||
* @returns Query result with similar artists
|
||||
*
|
||||
* @example
|
||||
* const { data } = useSimilarArtistsQuery("artist-123", 20);
|
||||
*/
|
||||
export function useSimilarArtistsQuery(seedArtistId: string | undefined, limit: number = 20) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.similarArtists(seedArtistId || "", limit),
|
||||
queryFn: async () => {
|
||||
if (!seedArtistId) throw new Error("Seed artist ID is required");
|
||||
return await api.getSimilarArtists(seedArtistId, limit);
|
||||
},
|
||||
enabled: !!seedArtistId,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch similar albums based on a seed album
|
||||
*
|
||||
* @param seedAlbumId - Album ID to find similar albums for
|
||||
* @param limit - Number of recommendations (default: 20)
|
||||
* @returns Query result with similar albums
|
||||
*/
|
||||
export function useSimilarAlbumsQuery(seedAlbumId: string | undefined, limit: number = 20) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.similarAlbums(seedAlbumId || "", limit),
|
||||
queryFn: async () => {
|
||||
if (!seedAlbumId) throw new Error("Seed album ID is required");
|
||||
return await api.getSimilarAlbums(seedAlbumId, limit);
|
||||
},
|
||||
enabled: !!seedAlbumId,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SEARCH QUERIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to search library with debouncing
|
||||
*
|
||||
* Cache time: 5 minutes (search results are relatively static)
|
||||
*
|
||||
* @param query - Search query string
|
||||
* @param type - Type filter (all, artists, albums, tracks, audiobooks, podcasts)
|
||||
* @param limit - Number of results per type (default: 20)
|
||||
* @returns Query result with search results
|
||||
*
|
||||
* @example
|
||||
* const { data } = useSearchQuery("radiohead", "all", 20);
|
||||
*/
|
||||
export function useSearchQuery(
|
||||
query: string,
|
||||
type: "all" | "artists" | "albums" | "tracks" | "audiobooks" | "podcasts" = "all",
|
||||
limit: number = 20
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.search(query, type, limit),
|
||||
queryFn: () => api.search(query, type, limit),
|
||||
enabled: query.length >= 2, // Only search if query is at least 2 characters
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to search discovery/Last.fm with debouncing
|
||||
*
|
||||
* @param query - Search query string
|
||||
* @param type - Type filter (music, podcasts, all)
|
||||
* @param limit - Number of results (default: 20)
|
||||
* @returns Query result with discovery search results
|
||||
*
|
||||
* @example
|
||||
* const { data } = useDiscoverSearchQuery("radiohead", "music", 20);
|
||||
*/
|
||||
export function useDiscoverSearchQuery(
|
||||
query: string,
|
||||
type: "music" | "podcasts" | "all" = "music",
|
||||
limit: number = 20
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.discoverSearch(query, type, limit),
|
||||
queryFn: () => api.discoverSearch(query, type, limit),
|
||||
enabled: query.length >= 2,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PLAYLIST QUERIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch all playlists
|
||||
*
|
||||
* Cache time: 1 minute (playlists may be actively modified)
|
||||
*
|
||||
* @returns Query result with playlists array
|
||||
*
|
||||
* @example
|
||||
* const { data: playlists } = usePlaylistsQuery();
|
||||
*/
|
||||
export function usePlaylistsQuery() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.playlists(),
|
||||
queryFn: () => api.getPlaylists(),
|
||||
staleTime: 1 * 60 * 1000, // 1 minute
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch a single playlist
|
||||
*
|
||||
* @param id - Playlist ID
|
||||
* @returns Query result with playlist data
|
||||
*
|
||||
* @example
|
||||
* const { data: playlist } = usePlaylistQuery("playlist-123");
|
||||
*/
|
||||
export function usePlaylistQuery(id: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.playlist(id || ""),
|
||||
queryFn: async () => {
|
||||
if (!id) throw new Error("Playlist ID is required");
|
||||
return await api.getPlaylist(id);
|
||||
},
|
||||
enabled: !!id,
|
||||
staleTime: 1 * 60 * 1000, // 1 minute
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MIX QUERIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch all mixes (Made For You)
|
||||
*
|
||||
* Cache time: 5 minutes
|
||||
*
|
||||
* @returns Query result with mixes array
|
||||
*
|
||||
* @example
|
||||
* const { data: mixes } = useMixesQuery();
|
||||
*/
|
||||
export function useMixesQuery() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.mixes(),
|
||||
queryFn: () => api.getMixes(),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch a single mix
|
||||
*
|
||||
* @param id - Mix ID
|
||||
* @returns Query result with mix data
|
||||
*/
|
||||
export function useMixQuery(id: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.mix(id || ""),
|
||||
queryFn: async () => {
|
||||
if (!id) throw new Error("Mix ID is required");
|
||||
return await api.getMix(id);
|
||||
},
|
||||
enabled: !!id,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// POPULAR ARTISTS QUERY
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch popular artists from Last.fm
|
||||
*
|
||||
* Cache time: 10 minutes (popular charts don't change frequently)
|
||||
*
|
||||
* @param limit - Number of artists to fetch (default: 20)
|
||||
* @returns Query result with popular artists
|
||||
*
|
||||
* @example
|
||||
* const { data } = usePopularArtistsQuery(20);
|
||||
*/
|
||||
export function usePopularArtistsQuery(limit: number = 20) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.popularArtists(limit),
|
||||
queryFn: () => api.getPopularArtists(limit),
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AUDIOBOOK QUERIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch all audiobooks
|
||||
*
|
||||
* @returns Query result with audiobooks array
|
||||
*/
|
||||
export function useAudiobooksQuery() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.audiobooks(),
|
||||
queryFn: () => api.getAudiobooks(),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch a single audiobook
|
||||
*
|
||||
* @param id - Audiobook ID
|
||||
* @returns Query result with audiobook data
|
||||
*/
|
||||
export function useAudiobookQuery(id: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.audiobook(id || ""),
|
||||
queryFn: async () => {
|
||||
if (!id) throw new Error("Audiobook ID is required");
|
||||
return await api.getAudiobook(id);
|
||||
},
|
||||
enabled: !!id,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PODCAST QUERIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch all subscribed podcasts
|
||||
*
|
||||
* @returns Query result with podcasts array
|
||||
*/
|
||||
export function usePodcastsQuery() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.podcasts(),
|
||||
queryFn: () => api.getPodcasts(),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch a single podcast
|
||||
*
|
||||
* Returns null if podcast is not found (404), allowing the page to handle preview mode.
|
||||
*
|
||||
* @param id - Podcast ID
|
||||
* @returns Query result with podcast data
|
||||
*/
|
||||
export function usePodcastQuery(id: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.podcast(id || ""),
|
||||
queryFn: async () => {
|
||||
if (!id) throw new Error("Podcast ID is required");
|
||||
|
||||
try {
|
||||
return await api.getPodcast(id);
|
||||
} catch (error: any) {
|
||||
// If podcast not found (404), return null to allow preview mode
|
||||
if (error?.status === 404 || error?.message?.includes('not found') || error?.message?.includes('not subscribed')) {
|
||||
return null;
|
||||
}
|
||||
// For other errors, throw to trigger error state
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
enabled: !!id,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
retry: false, // Don't retry 404 errors
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch top podcasts
|
||||
*
|
||||
* @param limit - Number of podcasts (default: 20)
|
||||
* @param genreId - Optional genre ID filter
|
||||
* @returns Query result with top podcasts
|
||||
*/
|
||||
export function useTopPodcastsQuery(limit: number = 20, genreId?: number) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.topPodcasts(limit, genreId),
|
||||
queryFn: () => api.getTopPodcasts(limit, genreId),
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MUTATION HOOKS
|
||||
// ============================================================================
|
||||
// These hooks handle data modifications and automatically invalidate related queries
|
||||
|
||||
/**
|
||||
* Hook to refresh mixes with cache invalidation
|
||||
*
|
||||
* @returns Mutation object with mutate function
|
||||
*
|
||||
* @example
|
||||
* const { mutate: refreshMixes, isPending } = useRefreshMixesMutation();
|
||||
* refreshMixes();
|
||||
*/
|
||||
export function useRefreshMixesMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => api.refreshMixes(),
|
||||
onSuccess: () => {
|
||||
// Invalidate mixes query to refetch
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.mixes() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to add track to playlist with cache invalidation
|
||||
*
|
||||
* @returns Mutation object with mutate function
|
||||
*
|
||||
* @example
|
||||
* const { mutate: addToPlaylist } = useAddToPlaylistMutation();
|
||||
* addToPlaylist({ playlistId: "123", trackId: "456" });
|
||||
*/
|
||||
export function useAddToPlaylistMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ playlistId, trackId }: { playlistId: string; trackId: string }) =>
|
||||
api.addTrackToPlaylist(playlistId, trackId),
|
||||
onSuccess: (_, variables) => {
|
||||
// Invalidate the specific playlist query
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.playlist(variables.playlistId)
|
||||
});
|
||||
// Also invalidate the playlists list
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.playlists()
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to create a new playlist with cache invalidation
|
||||
*
|
||||
* @returns Mutation object with mutate function
|
||||
*
|
||||
* @example
|
||||
* const { mutate: createPlaylist } = useCreatePlaylistMutation();
|
||||
* createPlaylist({ name: "My Playlist", isPublic: false });
|
||||
*/
|
||||
export function useCreatePlaylistMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ name, isPublic }: { name: string; isPublic?: boolean }) =>
|
||||
api.createPlaylist(name, isPublic),
|
||||
onSuccess: () => {
|
||||
// Invalidate playlists list to show new playlist
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.playlists()
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to delete a playlist with cache invalidation
|
||||
*
|
||||
* @returns Mutation object with mutate function
|
||||
*
|
||||
* @example
|
||||
* const { mutate: deletePlaylist } = useDeletePlaylistMutation();
|
||||
* deletePlaylist("playlist-123");
|
||||
*/
|
||||
export function useDeletePlaylistMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (playlistId: string) => api.deletePlaylist(playlistId),
|
||||
onSuccess: () => {
|
||||
// Invalidate playlists list
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.playlists()
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BROWSE QUERIES (Deezer Playlists/Radios)
|
||||
// ============================================================================
|
||||
|
||||
interface PlaylistPreview {
|
||||
id: string;
|
||||
source: string;
|
||||
type: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
creator: string;
|
||||
imageUrl: string | null;
|
||||
trackCount: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface Genre {
|
||||
id: number;
|
||||
name: string;
|
||||
picture?: string;
|
||||
}
|
||||
|
||||
interface GenreWithRadios {
|
||||
id: number;
|
||||
name: string;
|
||||
radios: PlaylistPreview[];
|
||||
}
|
||||
|
||||
interface BrowseAllResponse {
|
||||
playlists: PlaylistPreview[];
|
||||
radios: PlaylistPreview[];
|
||||
genres: Genre[];
|
||||
radiosByGenre: GenreWithRadios[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch all browse content (playlists, radios, genres) from Deezer
|
||||
*
|
||||
* @returns Query result with all browse content
|
||||
*/
|
||||
export function useBrowseAllQuery() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.browseAll(),
|
||||
queryFn: async (): Promise<BrowseAllResponse> => {
|
||||
return api.get<BrowseAllResponse>("/browse/all");
|
||||
},
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes - playlists don't change often
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch featured playlists from Deezer
|
||||
*
|
||||
* @param limit - Maximum number of playlists to fetch
|
||||
* @returns Query result with featured playlists
|
||||
*/
|
||||
export function useFeaturedPlaylistsQuery(limit: number = 50) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.browseFeatured(limit),
|
||||
queryFn: async (): Promise<PlaylistPreview[]> => {
|
||||
const response = await api.get<{ playlists: PlaylistPreview[] }>(
|
||||
`/browse/playlists/featured?limit=${limit}`
|
||||
);
|
||||
return response.playlists;
|
||||
},
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch radio stations from Deezer
|
||||
*
|
||||
* @param limit - Maximum number of radios to fetch
|
||||
* @returns Query result with radio stations
|
||||
*/
|
||||
export function useRadiosQuery(limit: number = 50) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.browseRadios(limit),
|
||||
queryFn: async (): Promise<PlaylistPreview[]> => {
|
||||
const response = await api.get<{ radios: PlaylistPreview[] }>(
|
||||
`/browse/radios?limit=${limit}`
|
||||
);
|
||||
return response.radios;
|
||||
},
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user