Skip to content

Commit f4b7221

Browse files
chiptusclaude
andauthored
feat(festivals): add stage order and color (#18)
* wip * feat: add stage ordering and color coding system - Add stage_order column and color column to stages table - Update stage queries to order by stage_order then by name - Implement stage color coding in timeline components - Update timeline calculations to use stage ordering - Apply stage colors to labels and timeline tracks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * feat(admin): add stage order and color management UI - Add stage_order and color fields to create/update mutations - Extend StageManagement component with order and color controls - Add number input for stage ordering with minimum value of 0 - Add color picker with hex input for stage colors - Display order and color columns in admin stage table - Show color preview swatches in table rows 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * fix(stages): prioritize explicitly ordered stages over default order - Stages with order > 0 now appear first, sorted by their order value - Stages with order 0 (default/unordered) appear last, sorted alphabetically - This ensures main stages and other explicitly ordered stages are prioritized - Updated sorting logic across all timeline and schedule components 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * feat: create reusable StageBadge component with color backgrounds - Add StageBadge component with stage color as background - Use stage color with 80% opacity for subtle but visible background - Apply StageBadge to list view cards (MobileSetCard) - Apply StageBadge to voting view (SetMetadata in ArtistsTab) - Remove redundant colored dot since background now uses stage color - Make badge text white with font-medium for better contrast 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * refactor: extract shared stage sorting logic into utility functions - Create stageUtils.ts with reusable sorting functions - Add sortStagesByOrder for generic item sorting based on stage data - Add sortStages for direct stage array sorting - Replace duplicated sorting logic in useStagesByEdition - Replace duplicated sorting logic in useScheduleData - Replace duplicated sorting logic in timelineCalculator (both horizontal and vertical) - Maintain consistent stage ordering: priority stages (order > 0) first, then default stages (order 0) alphabetically 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * refactor(stages): sort * refactor(timeline): remove vertical timeline calc * refactor(admin): break stage management components * fix(stages): update stage * fix(infra): remove caching for supabase * refactor(stages): move components * refactor(stages): move default color to constants --------- Co-authored-by: Claude <[email protected]>
1 parent 195005f commit f4b7221

File tree

24 files changed

+661
-361
lines changed

24 files changed

+661
-361
lines changed

src/components/StageBadge.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { MapPin } from "lucide-react";
2+
3+
interface StageBadgeProps {
4+
stageName: string;
5+
stageColor?: string;
6+
size?: "sm" | "md";
7+
showIcon?: boolean;
8+
}
9+
10+
export function StageBadge({
11+
stageName,
12+
stageColor,
13+
size = "sm",
14+
showIcon = true,
15+
}: StageBadgeProps) {
16+
const sizeClasses = {
17+
sm: "text-xs px-2 py-1 gap-1",
18+
md: "text-sm px-3 py-1.5 gap-2",
19+
};
20+
21+
const iconSize = {
22+
sm: "h-3 w-3",
23+
md: "h-4 w-4",
24+
};
25+
26+
return (
27+
<div
28+
className={`inline-flex items-center rounded-full backdrop-blur-sm border text-white font-medium ${sizeClasses[size]}`}
29+
style={{
30+
backgroundColor: stageColor ? `${stageColor}80` : "#7c3aed80",
31+
borderColor: stageColor ? stageColor : "#7c3aed",
32+
}}
33+
>
34+
{showIcon && <MapPin className={iconSize[size]} />}
35+
<span>{stageName}</span>
36+
</div>
37+
);
38+
}

src/hooks/queries/stages/useCreateStage.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { generateSlug } from "@/lib/slug";
77
async function createStage(stageData: {
88
name: string;
99
festival_edition_id: string;
10+
stage_order?: number;
11+
color?: string;
1012
}) {
1113
const { data, error } = await supabase
1214
.from("stages")
@@ -27,8 +29,10 @@ export function useCreateStageMutation() {
2729

2830
return useMutation({
2931
mutationFn: createStage,
30-
onSuccess: () => {
31-
queryClient.invalidateQueries({ queryKey: stagesKeys.all });
32+
onSuccess: async () => {
33+
await queryClient.invalidateQueries({
34+
queryKey: stagesKeys.all,
35+
});
3236
toast({
3337
title: "Success",
3438
description: "Stage created successfully",

src/hooks/queries/stages/useDeleteStage.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ export function useDeleteStageMutation() {
1818

1919
return useMutation({
2020
mutationFn: deleteStage,
21-
onSuccess: () => {
22-
queryClient.invalidateQueries({ queryKey: stagesKeys.all });
21+
onSuccess: async () => {
22+
await queryClient.invalidateQueries({
23+
queryKey: stagesKeys.all,
24+
});
2325
toast({
2426
title: "Success",
2527
description: "Stage deleted successfully",

src/hooks/queries/stages/useStagesByEdition.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useQuery } from "@tanstack/react-query";
22
import { supabase } from "@/integrations/supabase/client";
33
import { Stage, stagesKeys } from "./types";
4+
import { sortStagesByOrder } from "@/lib/stageUtils";
45

56
async function fetchStagesByEdition(editionId: string): Promise<Stage[]> {
67
const { data, error } = await supabase
@@ -14,7 +15,8 @@ async function fetchStagesByEdition(editionId: string): Promise<Stage[]> {
1415
throw new Error("Failed to load stages for edition");
1516
}
1617

17-
return data || [];
18+
// Apply custom sorting using shared utility
19+
return sortStagesByOrder(data || []);
1820
}
1921

2022
export function useStagesByEditionQuery(editionId: string | undefined) {

src/hooks/queries/stages/useUpdateStage.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { useToast } from "@/hooks/use-toast";
33
import { supabase } from "@/integrations/supabase/client";
44
import { stagesKeys } from "./types";
55

6-
async function updateStage(stageId: string, stageData: { name: string }) {
6+
async function updateStage(
7+
stageId: string,
8+
stageData: { name: string; stage_order?: number; color?: string },
9+
) {
710
const { data, error } = await supabase
811
.from("stages")
912
.update(stageData)
@@ -25,10 +28,12 @@ export function useUpdateStageMutation() {
2528
stageData,
2629
}: {
2730
stageId: string;
28-
stageData: { name: string };
31+
stageData: { name: string; stage_order?: number; color?: string };
2932
}) => updateStage(stageId, stageData),
30-
onSuccess: () => {
31-
queryClient.invalidateQueries({ queryKey: stagesKeys.all });
33+
onSuccess: async () => {
34+
await queryClient.invalidateQueries({
35+
queryKey: stagesKeys.all,
36+
});
3237
toast({
3338
title: "Success",
3439
description: "Stage updated successfully",

src/hooks/useScheduleData.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { formatDateTime } from "@/lib/timeUtils";
33
import { format, startOfDay } from "date-fns";
44
import type { FestivalSet } from "@/hooks/queries/sets/useSets";
55
import { Stage } from "./queries/stages/types";
6+
import { sortStagesByOrder } from "@/lib/stageUtils";
67

78
export interface ScheduleDay {
89
date: string;
@@ -13,6 +14,7 @@ export interface ScheduleDay {
1314
export interface ScheduleStage {
1415
id: string;
1516
name: string;
17+
stage_order: number;
1618
sets: ScheduleSet[];
1719
}
1820

@@ -127,18 +129,19 @@ export function useScheduleData(
127129
return {
128130
id: stageId,
129131
name: stage?.name,
132+
stage_order: stage?.stage_order,
130133
sets: stageSets.sort((a, b) => {
131134
if (!a.startTime || !b.startTime) return 0;
132135
return a.startTime.getTime() - b.startTime.getTime();
133136
}),
134-
};
137+
} satisfies ScheduleStage;
135138
})
136139
.filter((v: ScheduleStage | null): v is ScheduleStage => !!v);
137140

138141
return {
139142
date: dateKey,
140143
displayDate: format(date, "EEEE, MMM d"),
141-
stages: scheduleStages.sort((a, b) => a.name.localeCompare(b.name)),
144+
stages: sortStagesByOrder(scheduleStages),
142145
};
143146
});
144147

src/integrations/supabase/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,29 +640,35 @@ export type Database = {
640640
stages: {
641641
Row: {
642642
archived: boolean;
643+
color: string | null;
643644
created_at: string;
644645
festival_edition_id: string;
645646
id: string;
646647
name: string;
647648
slug: string;
649+
stage_order: number;
648650
updated_at: string;
649651
};
650652
Insert: {
651653
archived?: boolean;
654+
color?: string | null;
652655
created_at?: string;
653656
festival_edition_id: string;
654657
id?: string;
655658
name: string;
656659
slug: string;
660+
stage_order?: number;
657661
updated_at?: string;
658662
};
659663
Update: {
660664
archived?: boolean;
665+
color?: string | null;
661666
created_at?: string;
662667
festival_edition_id?: string;
663668
id?: string;
664669
name?: string;
665670
slug?: string;
671+
stage_order?: number;
666672
updated_at?: string;
667673
};
668674
Relationships: [];

src/lib/constants/stages.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const DEFAULT_STAGE_COLOR = "#6b7280";

src/lib/stageUtils.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { Stage } from "@/hooks/queries/stages/types";
2+
3+
/**
4+
* Sorts stages by priority: stages with order > 0 come first (sorted by order),
5+
* then stages with order 0 come last (sorted alphabetically by name)
6+
*/
7+
export function sortStagesByOrder<
8+
T extends Pick<Stage, "stage_order" | "name">,
9+
>(items: T[]): T[] {
10+
return items.sort((stageA, stageB) => {
11+
const orderA = stageA.stage_order ?? 0;
12+
const orderB = stageB.stage_order ?? 0;
13+
14+
// Stages with order > 0 come first, sorted by order
15+
// Stages with order 0 come last, sorted by name
16+
if (orderA > 0 && orderB > 0) {
17+
return orderA - orderB;
18+
}
19+
if (orderA > 0 && orderB === 0) {
20+
return -1; // A comes before B
21+
}
22+
if (orderA === 0 && orderB > 0) {
23+
return 1; // B comes before A
24+
}
25+
// Both are 0, sort by name
26+
return stageA.name.localeCompare(stageB.name);
27+
});
28+
}

0 commit comments

Comments
 (0)