Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/api/genres/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type Genre = {
id: string;
name: string;
};

export const genresKeys = {
all: () => ["genres"] as const,
};
Comment thread
chiptus marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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",
Expand Down
28 changes: 28 additions & 0 deletions src/api/genres/useGenres.ts
Original file line number Diff line number Diff line change
@@ -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<Genre[]> {
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() {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this hook?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collapsed it in 0a8a77f — removed the useGenres() adapter and pointed its two consumers (GenreBadge, FilterSortControls) at useGenresQuery() directly.

useGenresQuery stays: AddArtistDialog and GenresCell consume it directly, and genresQuery() remains as the loader-ready options factory. Typecheck + the 9 GenreBadge tests pass.


Generated by Claude Code

return useQuery(genresQuery());
}
74 changes: 29 additions & 45 deletions src/components/GenreBadge.test.tsx
Original file line number Diff line number Diff line change
@@ -1,110 +1,98 @@
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");
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

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<Genre[], Error>);
}

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(<GenreBadge genreId="1" />);
expect(screen.getByText("Rock")).toBeInTheDocument();
});

it("renders null when loading", () => {
vi.spyOn(useGenresModule, "useGenres").mockReturnValue({
genres: [],
loading: true,
error: null,
});
mockGenresQuery({ isLoading: true });

const { container } = render(<GenreBadge genreId="1" />);
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(<GenreBadge genreId="1" />);
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(<GenreBadge genreId="999" />);
expect(container.firstChild).toBeNull();
});

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(<GenreBadge genreId="1" />);
const badge = container.querySelector("div");
expect(badge).not.toHaveClass("text-xs", "px-2", "py-1");
});

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(<GenreBadge genreId="1" size="sm" />);
const badge = container.querySelector("div");
expect(badge).toHaveClass("text-xs", "px-2", "py-1");
});

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(<GenreBadge genreId="1" />);
const badge = container.querySelector("div");
expect(badge).toHaveClass("bg-purple-600/50", "text-purple-100");
});

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(<GenreBadge genreId="2" />);
Expand All @@ -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(<GenreBadge genreId="1" />);
expect(container.firstChild).toBeNull();
Expand Down
6 changes: 3 additions & 3 deletions src/components/GenreBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Badge } from "@/components/ui/badge";
import { useGenres } from "@/hooks/queries/genres/useGenres";
import { useGenresQuery } from "@/api/genres/useGenres";

interface GenreBadgeProps {
genreId: string;
size?: "default" | "sm";
}

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;
Expand Down
40 changes: 0 additions & 40 deletions src/hooks/queries/genres/useGenres.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/admin/AddGenreDialog.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion src/pages/admin/ArtistsManagement/AddArtistDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Loading