diff --git a/src/api/genres/types.ts b/src/api/genres/types.ts new file mode 100644 index 00000000..eab4226f --- /dev/null +++ b/src/api/genres/types.ts @@ -0,0 +1,8 @@ +export type Genre = { + id: string; + name: string; +}; + +export const genresKeys = { + all: () => ["genres"] as const, +}; diff --git a/src/hooks/queries/genres/useCreateGenreMutation.ts b/src/api/genres/useCreateGenreMutation.ts similarity index 91% rename from src/hooks/queries/genres/useCreateGenreMutation.ts rename to src/api/genres/useCreateGenreMutation.ts index 8d23870f..b255f382 100644 --- a/src/hooks/queries/genres/useCreateGenreMutation.ts +++ b/src/api/genres/useCreateGenreMutation.ts @@ -1,6 +1,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { supabase } from "@/integrations/supabase/client"; import { useToast } from "@/components/ui/use-toast"; +import { genresKeys } from "./types"; interface CreateGenreParams { name: string; @@ -29,8 +30,7 @@ export function useCreateGenreMutation() { return data; }, onSuccess: () => { - // Invalidate genres query if it exists - queryClient.invalidateQueries({ queryKey: ["genres"] }); + queryClient.invalidateQueries({ queryKey: genresKeys.all() }); toast({ title: "Success", diff --git a/src/api/genres/useGenres.ts b/src/api/genres/useGenres.ts new file mode 100644 index 00000000..62e203ef --- /dev/null +++ b/src/api/genres/useGenres.ts @@ -0,0 +1,28 @@ +import { queryOptions, useQuery } from "@tanstack/react-query"; +import { supabase } from "@/integrations/supabase/client"; +import { Genre, genresKeys } from "./types"; + +export async function fetchGenres(): Promise { + const { data, error } = await supabase + .from("music_genres") + .select("id, name") + .order("name"); + + if (error) { + throw new Error("Failed to load genres"); + } + + return data || []; +} + +export function genresQuery() { + return queryOptions({ + queryKey: genresKeys.all(), + queryFn: fetchGenres, + staleTime: 10 * 60 * 1000, + }); +} + +export function useGenresQuery() { + return useQuery(genresQuery()); +} diff --git a/src/components/GenreBadge.test.tsx b/src/components/GenreBadge.test.tsx index 80770109..6eab87b3 100644 --- a/src/components/GenreBadge.test.tsx +++ b/src/components/GenreBadge.test.tsx @@ -1,9 +1,23 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; import { render, screen } from "@testing-library/react"; +import type { UseQueryResult } from "@tanstack/react-query"; import { GenreBadge } from "./GenreBadge"; -import * as useGenresModule from "@/hooks/queries/genres/useGenres"; - -vi.mock("@/hooks/queries/genres/useGenres"); +import * as useGenresModule from "@/api/genres/useGenres"; +import type { Genre } from "@/api/genres/types"; + +vi.mock("@/api/genres/useGenres"); + +function mockGenresQuery(result: { + data?: Genre[]; + isLoading?: boolean; + error?: Error | null; +}) { + vi.spyOn(useGenresModule, "useGenresQuery").mockReturnValue({ + data: result.data ?? [], + isLoading: result.isLoading ?? false, + error: result.error ?? null, + } as unknown as UseQueryResult); +} describe("GenreBadge", () => { beforeEach(() => { @@ -11,13 +25,11 @@ describe("GenreBadge", () => { }); it("renders genre name when genre is found", () => { - vi.spyOn(useGenresModule, "useGenres").mockReturnValue({ - genres: [ + mockGenresQuery({ + data: [ { id: "1", name: "Rock" }, { id: "2", name: "Pop" }, ], - loading: false, - error: null, }); render(); @@ -25,35 +37,25 @@ describe("GenreBadge", () => { }); it("renders null when loading", () => { - vi.spyOn(useGenresModule, "useGenres").mockReturnValue({ - genres: [], - loading: true, - error: null, - }); + mockGenresQuery({ isLoading: true }); const { container } = render(); expect(container.firstChild).toBeNull(); }); it("renders null when error", () => { - vi.spyOn(useGenresModule, "useGenres").mockReturnValue({ - genres: [], - loading: false, - error: new Error("Failed to load"), - }); + mockGenresQuery({ error: new Error("Failed to load") }); const { container } = render(); expect(container.firstChild).toBeNull(); }); it("renders null when genre is not found", () => { - vi.spyOn(useGenresModule, "useGenres").mockReturnValue({ - genres: [ + mockGenresQuery({ + data: [ { id: "1", name: "Rock" }, { id: "2", name: "Pop" }, ], - loading: false, - error: null, }); const { container } = render(); @@ -61,11 +63,7 @@ describe("GenreBadge", () => { }); it("renders with default size", () => { - vi.spyOn(useGenresModule, "useGenres").mockReturnValue({ - genres: [{ id: "1", name: "Rock" }], - loading: false, - error: null, - }); + mockGenresQuery({ data: [{ id: "1", name: "Rock" }] }); const { container } = render(); const badge = container.querySelector("div"); @@ -73,11 +71,7 @@ describe("GenreBadge", () => { }); it("renders with small size", () => { - vi.spyOn(useGenresModule, "useGenres").mockReturnValue({ - genres: [{ id: "1", name: "Rock" }], - loading: false, - error: null, - }); + mockGenresQuery({ data: [{ id: "1", name: "Rock" }] }); const { container } = render(); const badge = container.querySelector("div"); @@ -85,11 +79,7 @@ describe("GenreBadge", () => { }); it("has correct styling classes", () => { - vi.spyOn(useGenresModule, "useGenres").mockReturnValue({ - genres: [{ id: "1", name: "Rock" }], - loading: false, - error: null, - }); + mockGenresQuery({ data: [{ id: "1", name: "Rock" }] }); const { container } = render(); const badge = container.querySelector("div"); @@ -97,14 +87,12 @@ describe("GenreBadge", () => { }); it("finds correct genre from multiple genres", () => { - vi.spyOn(useGenresModule, "useGenres").mockReturnValue({ - genres: [ + mockGenresQuery({ + data: [ { id: "1", name: "Rock" }, { id: "2", name: "Pop" }, { id: "3", name: "Jazz" }, ], - loading: false, - error: null, }); render(); @@ -114,11 +102,7 @@ describe("GenreBadge", () => { }); it("renders when genres list is empty", () => { - vi.spyOn(useGenresModule, "useGenres").mockReturnValue({ - genres: [], - loading: false, - error: null, - }); + mockGenresQuery({ data: [] }); const { container } = render(); expect(container.firstChild).toBeNull(); diff --git a/src/components/GenreBadge.tsx b/src/components/GenreBadge.tsx index 09bbdccd..6792a639 100644 --- a/src/components/GenreBadge.tsx +++ b/src/components/GenreBadge.tsx @@ -1,5 +1,5 @@ import { Badge } from "@/components/ui/badge"; -import { useGenres } from "@/hooks/queries/genres/useGenres"; +import { useGenresQuery } from "@/api/genres/useGenres"; interface GenreBadgeProps { genreId: string; @@ -7,9 +7,9 @@ interface GenreBadgeProps { } export function GenreBadge({ genreId, size = "default" }: GenreBadgeProps) { - const { genres, loading, error } = useGenres(); + const { data: genres = [], isLoading, error } = useGenresQuery(); - if (loading || error) return null; + if (isLoading || error) return null; const genre = genres.find((g) => g.id === genreId); if (!genre) return null; diff --git a/src/hooks/queries/genres/useGenres.ts b/src/hooks/queries/genres/useGenres.ts deleted file mode 100644 index 51cb2d78..00000000 --- a/src/hooks/queries/genres/useGenres.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { supabase } from "@/integrations/supabase/client"; - -// Query key factory -export const genresKeys = { - all: ["genres"] as const, -}; - -// Business logic function -async function fetchGenres(): Promise> { - const { data, error } = await supabase - .from("music_genres") - .select("id, name") - .order("name"); - - if (error) { - throw new Error("Failed to load genres"); - } - - return data || []; -} - -// Hook -export function useGenresQuery() { - return useQuery({ - queryKey: genresKeys.all, - queryFn: fetchGenres, - staleTime: 10 * 60 * 1000, // 10 minutes - genres don't change often - }); -} - -export function useGenres() { - const { data: genres = [], isLoading, error } = useGenresQuery(); - - return { - genres, - loading: isLoading, - error, - }; -} diff --git a/src/pages/EditionView/tabs/ArtistsTab/filters/FilterSortControls.tsx b/src/pages/EditionView/tabs/ArtistsTab/filters/FilterSortControls.tsx index 6b0cb788..a835cd5b 100644 --- a/src/pages/EditionView/tabs/ArtistsTab/filters/FilterSortControls.tsx +++ b/src/pages/EditionView/tabs/ArtistsTab/filters/FilterSortControls.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import type { SortOption, FilterSortState } from "@/hooks/useUrlState"; -import { useGenres } from "@/hooks/queries/genres/useGenres"; +import { useGenresQuery } from "@/api/genres/useGenres"; import { SortControls } from "./SortControls"; import { MobileFilters } from "./MobileFilters"; import { DesktopFilters } from "./DesktopFilters"; @@ -24,7 +24,7 @@ export function FilterSortControls({ }: FilterSortControlsProps) { const [isFiltersExpanded, setIsFiltersExpanded] = useState(false); const [isMobile, setIsMobile] = useState(false); - const { genres } = useGenres(); + const { data: genres = [] } = useGenresQuery(); useEffect(() => { function checkMobile() { diff --git a/src/pages/admin/AddGenreDialog.tsx b/src/pages/admin/AddGenreDialog.tsx index dff5651e..2699263e 100644 --- a/src/pages/admin/AddGenreDialog.tsx +++ b/src/pages/admin/AddGenreDialog.tsx @@ -1,5 +1,5 @@ import { useForm } from "react-hook-form"; -import { useCreateGenreMutation } from "@/hooks/queries/genres/useCreateGenreMutation"; +import { useCreateGenreMutation } from "@/api/genres/useCreateGenreMutation"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; diff --git a/src/pages/admin/ArtistsManagement/AddArtistDialog.tsx b/src/pages/admin/ArtistsManagement/AddArtistDialog.tsx index a18d9093..8b6c4fa5 100644 --- a/src/pages/admin/ArtistsManagement/AddArtistDialog.tsx +++ b/src/pages/admin/ArtistsManagement/AddArtistDialog.tsx @@ -23,7 +23,7 @@ import { import { Music } from "lucide-react"; import { useAuth } from "@/contexts/AuthContext"; import { useUserPermissionsQuery } from "@/hooks/queries/auth/useUserPermissions"; -import { useGenresQuery } from "@/hooks/queries/genres/useGenres"; +import { useGenresQuery } from "@/api/genres/useGenres"; import { useCreateArtistMutation } from "@/hooks/queries/artists/useCreateArtist"; import { useUpdateArtistMutation } from "@/hooks/queries/artists/useUpdateArtist"; import { GenreMultiSelect } from "./GenreMultiSelect"; diff --git a/src/pages/admin/ArtistsManagement/BulkEditor/GenresCell.tsx b/src/pages/admin/ArtistsManagement/BulkEditor/GenresCell.tsx index 88f817eb..07acf75f 100644 --- a/src/pages/admin/ArtistsManagement/BulkEditor/GenresCell.tsx +++ b/src/pages/admin/ArtistsManagement/BulkEditor/GenresCell.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { Button } from "@/components/ui/button"; import { GenreMultiSelect } from "../GenreMultiSelect"; -import { useGenresQuery } from "@/hooks/queries/genres/useGenres"; +import { useGenresQuery } from "@/api/genres/useGenres"; import { Check, X } from "lucide-react"; interface GenresCellProps {