From 5c29eef6922f5ae28e46f531b55fdbba2c8536d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 03:35:32 +0000 Subject: [PATCH 1/2] =?UTF-8?q?refactor(api):=20stages=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 stages feature from src/hooks/queries/stages to src/api/stages, 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/{hooks/queries => api}/stages/types.ts | 5 +---- .../queries => api}/stages/useCreateStage.ts | 0 .../queries => api}/stages/useDeleteStage.ts | 0 .../queries => api}/stages/useStageQuery.ts | 18 +++++++++++------ .../stages/useStagesByEdition.ts | 20 +++++++++++++------ .../queries => api}/stages/useUpdateStage.ts | 0 .../Admin/ScheduleImport/ReviewStage.tsx | 4 ++-- src/components/StagePin.test.tsx | 4 ++-- src/components/StagePin.tsx | 2 +- src/hooks/useScheduleData.ts | 2 +- src/lib/stageUtils.ts | 2 +- src/lib/timelineCalculator.ts | 2 +- .../tabs/ArtistsTab/SetCard/SetMetadata.tsx | 2 +- .../ArtistsTab/filters/DesktopFilters.tsx | 2 +- .../tabs/ArtistsTab/filters/MobileFilters.tsx | 2 +- .../tabs/ScheduleTab/StageFilterButtons.tsx | 2 +- .../tabs/ScheduleTab/horizontal/Timeline.tsx | 2 +- .../tabs/ScheduleTab/list/ListSchedule.tsx | 2 +- .../SetExploreCard/SetCardHeader.tsx | 2 +- src/pages/admin/festivals/SetsTable.tsx | 2 +- src/pages/admin/festivals/StageManagement.tsx | 6 +++--- .../StageManagement/CreateStageDialog.tsx | 2 +- .../StageManagement/EditStageDialog.tsx | 4 ++-- .../festivals/StageManagement/StagesTable.tsx | 2 +- src/pages/admin/festivals/StageSelector.tsx | 2 +- 25 files changed, 51 insertions(+), 40 deletions(-) rename src/{hooks/queries => api}/stages/types.ts (56%) rename src/{hooks/queries => api}/stages/useCreateStage.ts (100%) rename src/{hooks/queries => api}/stages/useDeleteStage.ts (100%) rename src/{hooks/queries => api}/stages/useStageQuery.ts (53%) rename src/{hooks/queries => api}/stages/useStagesByEdition.ts (53%) rename src/{hooks/queries => api}/stages/useUpdateStage.ts (100%) diff --git a/src/hooks/queries/stages/types.ts b/src/api/stages/types.ts similarity index 56% rename from src/hooks/queries/stages/types.ts rename to src/api/stages/types.ts index df89dff2..69ef317e 100644 --- a/src/hooks/queries/stages/types.ts +++ b/src/api/stages/types.ts @@ -1,8 +1,5 @@ -import type { Database } from "@/integrations/supabase/types"; +export type { Stage } from "@/api/sets/types"; -export type Stage = Database["public"]["Tables"]["stages"]["Row"]; - -// Query key factory export const stagesKeys = { all: ["stages"] as const, byEdition: (editionId: string) => ["stages", { editionId }] as const, diff --git a/src/hooks/queries/stages/useCreateStage.ts b/src/api/stages/useCreateStage.ts similarity index 100% rename from src/hooks/queries/stages/useCreateStage.ts rename to src/api/stages/useCreateStage.ts diff --git a/src/hooks/queries/stages/useDeleteStage.ts b/src/api/stages/useDeleteStage.ts similarity index 100% rename from src/hooks/queries/stages/useDeleteStage.ts rename to src/api/stages/useDeleteStage.ts diff --git a/src/hooks/queries/stages/useStageQuery.ts b/src/api/stages/useStageQuery.ts similarity index 53% rename from src/hooks/queries/stages/useStageQuery.ts rename to src/api/stages/useStageQuery.ts index d09de5ec..8bce6bde 100644 --- a/src/hooks/queries/stages/useStageQuery.ts +++ b/src/api/stages/useStageQuery.ts @@ -1,8 +1,9 @@ -import { useQuery } from "@tanstack/react-query"; +import { queryOptions, useQuery } from "@tanstack/react-query"; import { supabase } from "@/integrations/supabase/client"; -import { Stage, stagesKeys } from "./types"; +import type { Stage } from "./types"; +import { stagesKeys } from "./types"; -async function fetchStage(stageId: string): Promise { +export async function fetchStage(stageId: string): Promise { const { data, error } = await supabase .from("stages") .select("*") @@ -12,7 +13,6 @@ async function fetchStage(stageId: string): Promise { if (error) { if (error.code === "PGRST116") { - // No rows returned return null; } throw new Error("Failed to load stage"); @@ -21,10 +21,16 @@ async function fetchStage(stageId: string): Promise { return data; } +export function stageQuery(stageId: string) { + return queryOptions({ + queryKey: stagesKeys.byId(stageId), + queryFn: () => fetchStage(stageId), + }); +} + export function useStageQuery(stageId: string | undefined | null) { return useQuery({ - queryKey: stagesKeys.byId(stageId || ""), - queryFn: () => fetchStage(stageId!), + ...stageQuery(stageId!), enabled: !!stageId, }); } diff --git a/src/hooks/queries/stages/useStagesByEdition.ts b/src/api/stages/useStagesByEdition.ts similarity index 53% rename from src/hooks/queries/stages/useStagesByEdition.ts rename to src/api/stages/useStagesByEdition.ts index f2a16d0f..fc125caf 100644 --- a/src/hooks/queries/stages/useStagesByEdition.ts +++ b/src/api/stages/useStagesByEdition.ts @@ -1,9 +1,12 @@ -import { useQuery } from "@tanstack/react-query"; +import { queryOptions, useQuery } from "@tanstack/react-query"; import { supabase } from "@/integrations/supabase/client"; -import { Stage, stagesKeys } from "./types"; +import type { Stage } from "./types"; +import { stagesKeys } from "./types"; import { sortStagesByOrder } from "@/lib/stageUtils"; -async function fetchStagesByEdition(editionId: string): Promise { +export async function fetchStagesByEdition( + editionId: string, +): Promise { const { data, error } = await supabase .from("stages") .select("*") @@ -15,14 +18,19 @@ async function fetchStagesByEdition(editionId: string): Promise { throw new Error("Failed to load stages for edition"); } - // Apply custom sorting using shared utility return sortStagesByOrder(data || []); } +export function stagesByEditionQuery(editionId: string) { + return queryOptions({ + queryKey: stagesKeys.byEdition(editionId), + queryFn: () => fetchStagesByEdition(editionId), + }); +} + export function useStagesByEditionQuery(editionId: string | undefined) { return useQuery({ - queryKey: stagesKeys.byEdition(editionId || ""), - queryFn: () => fetchStagesByEdition(editionId!), + ...stagesByEditionQuery(editionId!), enabled: !!editionId, }); } diff --git a/src/hooks/queries/stages/useUpdateStage.ts b/src/api/stages/useUpdateStage.ts similarity index 100% rename from src/hooks/queries/stages/useUpdateStage.ts rename to src/api/stages/useUpdateStage.ts diff --git a/src/components/Admin/ScheduleImport/ReviewStage.tsx b/src/components/Admin/ScheduleImport/ReviewStage.tsx index 153583cb..87b82455 100644 --- a/src/components/Admin/ScheduleImport/ReviewStage.tsx +++ b/src/components/Admin/ScheduleImport/ReviewStage.tsx @@ -10,8 +10,8 @@ import { } from "@/services/scheduleImport/types"; import { artistsKeys } from "@/hooks/queries/artists/useArtists"; import { setsKeys } from "@/api/sets/types"; -import { stagesKeys } from "@/hooks/queries/stages/types"; -import { useStagesByEditionQuery } from "@/hooks/queries/stages/useStagesByEdition"; +import { stagesKeys } from "@/api/stages/types"; +import { useStagesByEditionQuery } from "@/api/stages/useStagesByEdition"; import type { RevealLevel } from "@/lib/scheduleReveal"; import { DiffReviewStep } from "./DiffReviewStep"; diff --git a/src/components/StagePin.test.tsx b/src/components/StagePin.test.tsx index 5ef4f6cc..58c86886 100644 --- a/src/components/StagePin.test.tsx +++ b/src/components/StagePin.test.tsx @@ -1,11 +1,11 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; import { render, screen } from "@testing-library/react"; import { StagePin } from "./StagePin"; -import * as useStageQueryModule from "@/hooks/queries/stages/useStageQuery"; +import * as useStageQueryModule from "@/api/stages/useStageQuery"; type StageQueryResult = ReturnType; -vi.mock("@/hooks/queries/stages/useStageQuery"); +vi.mock("@/api/stages/useStageQuery"); describe("StagePin", () => { beforeEach(() => { diff --git a/src/components/StagePin.tsx b/src/components/StagePin.tsx index 4412d689..b2e16b3d 100644 --- a/src/components/StagePin.tsx +++ b/src/components/StagePin.tsx @@ -1,4 +1,4 @@ -import { useStageQuery } from "@/hooks/queries/stages/useStageQuery"; +import { useStageQuery } from "@/api/stages/useStageQuery"; import { MapPin } from "lucide-react"; export function StagePin({ stageId }: { stageId: string | null }) { diff --git a/src/hooks/useScheduleData.ts b/src/hooks/useScheduleData.ts index d7b40785..8c770167 100644 --- a/src/hooks/useScheduleData.ts +++ b/src/hooks/useScheduleData.ts @@ -2,7 +2,7 @@ import { useMemo } from "react"; import { formatDateTime } from "@/lib/timeUtils"; import { format, startOfDay } from "date-fns"; import type { FestivalSet } from "@/api/sets/types"; -import { Stage } from "./queries/stages/types"; +import type { Stage } from "@/api/stages/types"; import { sortStagesByOrder } from "@/lib/stageUtils"; export interface ScheduleDay { diff --git a/src/lib/stageUtils.ts b/src/lib/stageUtils.ts index 4cdb387d..0ffe057e 100644 --- a/src/lib/stageUtils.ts +++ b/src/lib/stageUtils.ts @@ -1,4 +1,4 @@ -import type { Stage } from "@/hooks/queries/stages/types"; +import type { Stage } from "@/api/stages/types"; /** * Sorts stages by priority: stages with order > 0 come first (sorted by order), diff --git a/src/lib/timelineCalculator.ts b/src/lib/timelineCalculator.ts index b7cc2db1..9eefac13 100644 --- a/src/lib/timelineCalculator.ts +++ b/src/lib/timelineCalculator.ts @@ -1,6 +1,6 @@ import { differenceInMinutes } from "date-fns"; import type { ScheduleSet, ScheduleDay } from "@/hooks/useScheduleData"; -import type { Stage } from "@/hooks/queries/stages/types"; +import type { Stage } from "@/api/stages/types"; import { sortStagesByOrder } from "@/lib/stageUtils"; export interface HorizontalTimelineSet extends ScheduleSet { diff --git a/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetMetadata.tsx b/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetMetadata.tsx index 68b8bcb9..8d78d349 100644 --- a/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetMetadata.tsx +++ b/src/pages/EditionView/tabs/ArtistsTab/SetCard/SetMetadata.tsx @@ -3,7 +3,7 @@ import { formatDayOnly, formatTimeRange } from "@/lib/timeUtils"; import { GenreBadge } from "@/components/GenreBadge"; import { StageBadge } from "@/components/StageBadge"; import { useFestivalSet } from "../FestivalSetContext"; -import { useStageQuery } from "@/hooks/queries/stages/useStageQuery"; +import { useStageQuery } from "@/api/stages/useStageQuery"; import { useScheduleReveal } from "@/hooks/useScheduleReveal"; export function SetMetadata() { diff --git a/src/pages/EditionView/tabs/ArtistsTab/filters/DesktopFilters.tsx b/src/pages/EditionView/tabs/ArtistsTab/filters/DesktopFilters.tsx index 3c5587b0..1acf6a2e 100644 --- a/src/pages/EditionView/tabs/ArtistsTab/filters/DesktopFilters.tsx +++ b/src/pages/EditionView/tabs/ArtistsTab/filters/DesktopFilters.tsx @@ -1,6 +1,6 @@ import { Button } from "@/components/ui/button"; import type { FilterSortState } from "@/hooks/useUrlState"; -import { useStagesByEditionQuery } from "@/hooks/queries/stages/useStagesByEdition"; +import { useStagesByEditionQuery } from "@/api/stages/useStagesByEdition"; import { useScheduleReveal } from "@/hooks/useScheduleReveal"; interface DesktopFiltersProps { diff --git a/src/pages/EditionView/tabs/ArtistsTab/filters/MobileFilters.tsx b/src/pages/EditionView/tabs/ArtistsTab/filters/MobileFilters.tsx index fd9b5e77..46a8dc96 100644 --- a/src/pages/EditionView/tabs/ArtistsTab/filters/MobileFilters.tsx +++ b/src/pages/EditionView/tabs/ArtistsTab/filters/MobileFilters.tsx @@ -6,7 +6,7 @@ import { SelectValue, } from "@/components/ui/select"; import type { FilterSortState } from "@/hooks/useUrlState"; -import { useStagesByEditionQuery } from "@/hooks/queries/stages/useStagesByEdition"; +import { useStagesByEditionQuery } from "@/api/stages/useStagesByEdition"; import { useScheduleReveal } from "@/hooks/useScheduleReveal"; interface MobileFiltersProps { diff --git a/src/pages/EditionView/tabs/ScheduleTab/StageFilterButtons.tsx b/src/pages/EditionView/tabs/ScheduleTab/StageFilterButtons.tsx index 33af7eb3..005af3c8 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/StageFilterButtons.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/StageFilterButtons.tsx @@ -1,6 +1,6 @@ import { Button } from "@/components/ui/button"; import { MapPin } from "lucide-react"; -import { useStagesByEditionQuery } from "@/hooks/queries/stages/useStagesByEdition"; +import { useStagesByEditionQuery } from "@/api/stages/useStagesByEdition"; import { useFestivalEdition } from "@/contexts/FestivalEditionContext"; interface StageFilterButtonsProps { diff --git a/src/pages/EditionView/tabs/ScheduleTab/horizontal/Timeline.tsx b/src/pages/EditionView/tabs/ScheduleTab/horizontal/Timeline.tsx index 35593a40..863cf355 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/horizontal/Timeline.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/horizontal/Timeline.tsx @@ -7,7 +7,7 @@ import { useFestivalEdition } from "@/contexts/FestivalEditionContext"; import { useSetsByEditionQuery as useEditionSetsQuery } from "@/api/sets/useSetsByEdition"; import { useTimelineUrlState } from "@/hooks/useTimelineUrlState"; import { format } from "date-fns"; -import { useStagesByEditionQuery } from "@/hooks/queries/stages/useStagesByEdition"; +import { useStagesByEditionQuery } from "@/api/stages/useStagesByEdition"; import { useScheduleReveal } from "@/hooks/useScheduleReveal"; import { ScheduleNotRevealedPlaceholder } from "../ScheduleNotRevealedPlaceholder"; diff --git a/src/pages/EditionView/tabs/ScheduleTab/list/ListSchedule.tsx b/src/pages/EditionView/tabs/ScheduleTab/list/ListSchedule.tsx index deb53098..04de26d6 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/list/ListSchedule.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/list/ListSchedule.tsx @@ -6,7 +6,7 @@ import { isSameDay, format } from "date-fns"; import { TimeSlotGroup } from "./TimeSlotGroup"; import type { ScheduleSet } from "@/hooks/useScheduleData"; import { useTimelineUrlState } from "@/hooks/useTimelineUrlState"; -import { useStagesByEditionQuery } from "@/hooks/queries/stages/useStagesByEdition"; +import { useStagesByEditionQuery } from "@/api/stages/useStagesByEdition"; import { useScheduleReveal } from "@/hooks/useScheduleReveal"; import { ScheduleNotRevealedPlaceholder } from "../ScheduleNotRevealedPlaceholder"; diff --git a/src/pages/ExploreSetPage/SetExploreCard/SetCardHeader.tsx b/src/pages/ExploreSetPage/SetExploreCard/SetCardHeader.tsx index d64e5cb3..1f9b56d3 100644 --- a/src/pages/ExploreSetPage/SetExploreCard/SetCardHeader.tsx +++ b/src/pages/ExploreSetPage/SetExploreCard/SetCardHeader.tsx @@ -1,7 +1,7 @@ import { Badge } from "@/components/ui/badge"; import { Clock } from "lucide-react"; import { StageBadge } from "@/components/StageBadge"; -import { useStageQuery } from "@/hooks/queries/stages/useStageQuery"; +import { useStageQuery } from "@/api/stages/useStageQuery"; import { useScheduleReveal } from "@/hooks/useScheduleReveal"; interface SetCardHeaderProps { diff --git a/src/pages/admin/festivals/SetsTable.tsx b/src/pages/admin/festivals/SetsTable.tsx index 9ebc308a..2a9b49d2 100644 --- a/src/pages/admin/festivals/SetsTable.tsx +++ b/src/pages/admin/festivals/SetsTable.tsx @@ -1,4 +1,4 @@ -import { useStagesByEditionQuery } from "@/hooks/queries/stages/useStagesByEdition"; +import { useStagesByEditionQuery } from "@/api/stages/useStagesByEdition"; import { Table, TableBody, diff --git a/src/pages/admin/festivals/StageManagement.tsx b/src/pages/admin/festivals/StageManagement.tsx index 0f6a15dc..0ca09dfe 100644 --- a/src/pages/admin/festivals/StageManagement.tsx +++ b/src/pages/admin/festivals/StageManagement.tsx @@ -1,8 +1,8 @@ import { useState } from "react"; import { useParams } from "@tanstack/react-router"; -import { useStagesByEditionQuery } from "@/hooks/queries/stages/useStagesByEdition"; -import { useDeleteStageMutation } from "@/hooks/queries/stages/useDeleteStage"; -import { Stage } from "@/hooks/queries/stages/types"; +import { useStagesByEditionQuery } from "@/api/stages/useStagesByEdition"; +import { useDeleteStageMutation } from "@/api/stages/useDeleteStage"; +import type { Stage } from "@/api/stages/types"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Loader2, MapPin } from "lucide-react"; import { StagesTable } from "./StageManagement/StagesTable"; diff --git a/src/pages/admin/festivals/StageManagement/CreateStageDialog.tsx b/src/pages/admin/festivals/StageManagement/CreateStageDialog.tsx index 34556a46..979bb9a3 100644 --- a/src/pages/admin/festivals/StageManagement/CreateStageDialog.tsx +++ b/src/pages/admin/festivals/StageManagement/CreateStageDialog.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { useCreateStageMutation } from "@/hooks/queries/stages/useCreateStage"; +import { useCreateStageMutation } from "@/api/stages/useCreateStage"; import { Dialog, DialogContent, diff --git a/src/pages/admin/festivals/StageManagement/EditStageDialog.tsx b/src/pages/admin/festivals/StageManagement/EditStageDialog.tsx index f3823cd4..607dc682 100644 --- a/src/pages/admin/festivals/StageManagement/EditStageDialog.tsx +++ b/src/pages/admin/festivals/StageManagement/EditStageDialog.tsx @@ -1,5 +1,5 @@ -import { useUpdateStageMutation } from "@/hooks/queries/stages/useUpdateStage"; -import { Stage } from "@/hooks/queries/stages/types"; +import { useUpdateStageMutation } from "@/api/stages/useUpdateStage"; +import type { Stage } from "@/api/stages/types"; import { Dialog, DialogContent, diff --git a/src/pages/admin/festivals/StageManagement/StagesTable.tsx b/src/pages/admin/festivals/StageManagement/StagesTable.tsx index 1a45ff26..f099b5c0 100644 --- a/src/pages/admin/festivals/StageManagement/StagesTable.tsx +++ b/src/pages/admin/festivals/StageManagement/StagesTable.tsx @@ -1,4 +1,4 @@ -import { Stage } from "@/hooks/queries/stages/types"; +import type { Stage } from "@/api/stages/types"; import { Table, TableBody, diff --git a/src/pages/admin/festivals/StageSelector.tsx b/src/pages/admin/festivals/StageSelector.tsx index d7cb220e..6ff19381 100644 --- a/src/pages/admin/festivals/StageSelector.tsx +++ b/src/pages/admin/festivals/StageSelector.tsx @@ -6,7 +6,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { useStagesByEditionQuery } from "@/hooks/queries/stages/useStagesByEdition"; +import { useStagesByEditionQuery } from "@/api/stages/useStagesByEdition"; export function StageSelector({ value, From d8ee4a1aa125ec33998d7b9a54c496d5f71714e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 03:41:24 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(api):=20keep=20stages=20move=20faithful?= =?UTF-8?q?=20=E2=80=94=20local=20Stage=20type=20+=20empty-string=20key=20?= =?UTF-8?q?normalization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two deviations the mechanical move introduced: - types.ts re-exported Stage from sets (violates no-export-from rule); restore the original local definition (Database stages Row), as on main. - useStageQuery/useStagesByEdition switched stagesKeys.byId(stageId || "") to stageQuery(stageId!); restore ?? "" so the disabled-state key matches main (stageId can be null). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01K513rsQz6Lg1HbbfYiafrE --- src/api/stages/types.ts | 4 +++- src/api/stages/useStageQuery.ts | 2 +- src/api/stages/useStagesByEdition.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/api/stages/types.ts b/src/api/stages/types.ts index 69ef317e..e7a1fcbe 100644 --- a/src/api/stages/types.ts +++ b/src/api/stages/types.ts @@ -1,4 +1,6 @@ -export type { Stage } from "@/api/sets/types"; +import type { Database } from "@/integrations/supabase/types"; + +export type Stage = Database["public"]["Tables"]["stages"]["Row"]; export const stagesKeys = { all: ["stages"] as const, diff --git a/src/api/stages/useStageQuery.ts b/src/api/stages/useStageQuery.ts index 8bce6bde..c2b5724b 100644 --- a/src/api/stages/useStageQuery.ts +++ b/src/api/stages/useStageQuery.ts @@ -30,7 +30,7 @@ export function stageQuery(stageId: string) { export function useStageQuery(stageId: string | undefined | null) { return useQuery({ - ...stageQuery(stageId!), + ...stageQuery(stageId ?? ""), enabled: !!stageId, }); } diff --git a/src/api/stages/useStagesByEdition.ts b/src/api/stages/useStagesByEdition.ts index fc125caf..4335963f 100644 --- a/src/api/stages/useStagesByEdition.ts +++ b/src/api/stages/useStagesByEdition.ts @@ -30,7 +30,7 @@ export function stagesByEditionQuery(editionId: string) { export function useStagesByEditionQuery(editionId: string | undefined) { return useQuery({ - ...stagesByEditionQuery(editionId!), + ...stagesByEditionQuery(editionId ?? ""), enabled: !!editionId, }); }