From e0c09fe316c1796946ef9171e2019fb7bd44f126 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 03:26:28 +0000 Subject: [PATCH 1/2] =?UTF-8?q?refactor(api):=20genres=20=E2=86=92=20src/a?= =?UTF-8?q?pi=20modules=20(Effort=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanical move of the genres feature from src/hooks/queries/genres to src/api/genres, adding queryOptions factories for the query hooks. No behavior change. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01K513rsQz6Lg1HbbfYiafrE --- src/api/genres/types.ts | 8 +++++++ .../genres/useCreateGenreMutation.ts | 4 ++-- .../queries => api}/genres/useGenres.ts | 24 +++++++++---------- src/components/GenreBadge.test.tsx | 4 ++-- src/components/GenreBadge.tsx | 2 +- .../ArtistsTab/filters/FilterSortControls.tsx | 2 +- src/pages/admin/AddGenreDialog.tsx | 2 +- .../ArtistsManagement/AddArtistDialog.tsx | 2 +- .../BulkEditor/GenresCell.tsx | 2 +- 9 files changed, 28 insertions(+), 22 deletions(-) create mode 100644 src/api/genres/types.ts rename src/{hooks/queries => api}/genres/useCreateGenreMutation.ts (91%) rename src/{hooks/queries => api}/genres/useGenres.ts (57%) 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/hooks/queries/genres/useGenres.ts b/src/api/genres/useGenres.ts similarity index 57% rename from src/hooks/queries/genres/useGenres.ts rename to src/api/genres/useGenres.ts index 51cb2d78..9b39480c 100644 --- a/src/hooks/queries/genres/useGenres.ts +++ b/src/api/genres/useGenres.ts @@ -1,13 +1,8 @@ -import { useQuery } from "@tanstack/react-query"; +import { queryOptions, useQuery } from "@tanstack/react-query"; import { supabase } from "@/integrations/supabase/client"; +import { Genre, genresKeys } from "./types"; -// Query key factory -export const genresKeys = { - all: ["genres"] as const, -}; - -// Business logic function -async function fetchGenres(): Promise> { +export async function fetchGenres(): Promise { const { data, error } = await supabase .from("music_genres") .select("id, name") @@ -20,15 +15,18 @@ async function fetchGenres(): Promise> { return data || []; } -// Hook -export function useGenresQuery() { - return useQuery({ - queryKey: genresKeys.all, +export function genresQuery() { + return queryOptions({ + queryKey: genresKeys.all(), queryFn: fetchGenres, - staleTime: 10 * 60 * 1000, // 10 minutes - genres don't change often + staleTime: 10 * 60 * 1000, }); } +export function useGenresQuery() { + return useQuery(genresQuery()); +} + export function useGenres() { const { data: genres = [], isLoading, error } = useGenresQuery(); diff --git a/src/components/GenreBadge.test.tsx b/src/components/GenreBadge.test.tsx index 80770109..843501ad 100644 --- a/src/components/GenreBadge.test.tsx +++ b/src/components/GenreBadge.test.tsx @@ -1,9 +1,9 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; import { render, screen } from "@testing-library/react"; import { GenreBadge } from "./GenreBadge"; -import * as useGenresModule from "@/hooks/queries/genres/useGenres"; +import * as useGenresModule from "@/api/genres/useGenres"; -vi.mock("@/hooks/queries/genres/useGenres"); +vi.mock("@/api/genres/useGenres"); describe("GenreBadge", () => { beforeEach(() => { diff --git a/src/components/GenreBadge.tsx b/src/components/GenreBadge.tsx index 09bbdccd..8a1a63e5 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 { useGenres } from "@/api/genres/useGenres"; interface GenreBadgeProps { genreId: string; diff --git a/src/pages/EditionView/tabs/ArtistsTab/filters/FilterSortControls.tsx b/src/pages/EditionView/tabs/ArtistsTab/filters/FilterSortControls.tsx index 6b0cb788..2cfb8373 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 { useGenres } from "@/api/genres/useGenres"; import { SortControls } from "./SortControls"; import { MobileFilters } from "./MobileFilters"; import { DesktopFilters } from "./DesktopFilters"; 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 { From 0a8a77fc52ca76a038f76881b6b34905f90a9077 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 09:45:48 +0000 Subject: [PATCH 2/2] refactor(genres): collapse useGenres adapter into useGenresQuery Remove the useGenres() adapter and point its two consumers (GenreBadge, FilterSortControls) at useGenresQuery() directly. Update GenreBadge.test to mock useGenresQuery. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01K513rsQz6Lg1HbbfYiafrE --- src/api/genres/useGenres.ts | 10 --- src/components/GenreBadge.test.tsx | 68 +++++++------------ src/components/GenreBadge.tsx | 6 +- .../ArtistsTab/filters/FilterSortControls.tsx | 4 +- 4 files changed, 31 insertions(+), 57 deletions(-) diff --git a/src/api/genres/useGenres.ts b/src/api/genres/useGenres.ts index 9b39480c..62e203ef 100644 --- a/src/api/genres/useGenres.ts +++ b/src/api/genres/useGenres.ts @@ -26,13 +26,3 @@ export function genresQuery() { export function useGenresQuery() { return useQuery(genresQuery()); } - -export function useGenres() { - const { data: genres = [], isLoading, error } = useGenresQuery(); - - return { - genres, - loading: isLoading, - error, - }; -} diff --git a/src/components/GenreBadge.test.tsx b/src/components/GenreBadge.test.tsx index 843501ad..6eab87b3 100644 --- a/src/components/GenreBadge.test.tsx +++ b/src/components/GenreBadge.test.tsx @@ -1,23 +1,35 @@ 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 "@/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(() => { vi.clearAllMocks(); }); 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 8a1a63e5..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 "@/api/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/pages/EditionView/tabs/ArtistsTab/filters/FilterSortControls.tsx b/src/pages/EditionView/tabs/ArtistsTab/filters/FilterSortControls.tsx index 2cfb8373..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 "@/api/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() {