Skip to content

Commit 2c3d8f3

Browse files
authored
feat(festival): add festival info (#14)
* feat(festival): add festival info * feat(admin): add festival map bucket * festival info * feat(admin): update festival info * feat(festival): convert links into links table * fix(admin): bulk update custom links
1 parent 902e932 commit 2c3d8f3

36 files changed

+1651
-338
lines changed

CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,6 @@ In this codebase, it is acceptable and preferred to define helper functions (suc
135135
## Git Workflow
136136

137137
- **Auto-commit Rule**: For every user message that requests code changes, automatically commit the changes after implementation with an appropriate commit message
138+
139+
- never run "supabase db push"
140+
- don't run supabase db reset

src/components/layout/AppHeader/TitleSection.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,5 @@ export const TitleSection = forwardRef<HTMLDivElement, TitleSectionProps>(
4545
);
4646
},
4747
);
48+
49+
TitleSection.displayName = "TitleSection";
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import { supabase } from "@/integrations/supabase/client";
3+
import { Tables } from "@/integrations/supabase/types";
4+
5+
export type CustomLink = Tables<"custom_links">;
6+
7+
export const customLinksKeys = {
8+
all: ["customLinks"] as const,
9+
byFestival: (festivalId: string) =>
10+
[...customLinksKeys.all, festivalId] as const,
11+
};
12+
13+
async function fetchCustomLinks(festivalId: string): Promise<CustomLink[]> {
14+
const { data, error } = await supabase
15+
.from("custom_links")
16+
.select("*")
17+
.eq("festival_id", festivalId)
18+
.order("display_order", { ascending: true })
19+
.order("created_at", { ascending: true });
20+
21+
if (error) {
22+
console.error("Error fetching custom links:", error);
23+
throw new Error("Failed to fetch custom links");
24+
}
25+
26+
return data || [];
27+
}
28+
29+
export function useCustomLinksQuery(festivalId: string | undefined) {
30+
return useQuery({
31+
queryKey: customLinksKeys.byFestival(festivalId || ""),
32+
queryFn: () => fetchCustomLinks(festivalId!),
33+
enabled: !!festivalId,
34+
});
35+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { useMutation, useQueryClient } from "@tanstack/react-query";
2+
import { useToast } from "@/hooks/use-toast";
3+
import { supabase } from "@/integrations/supabase/client";
4+
import { customLinksKeys } from "./useCustomLinks";
5+
6+
interface BulkUpdateCustomLinksData {
7+
festivalId: string;
8+
links: Array<{
9+
id?: string;
10+
title: string;
11+
url: string;
12+
display_order: number;
13+
}>;
14+
}
15+
16+
async function bulkUpdateCustomLinks({
17+
festivalId,
18+
links,
19+
}: BulkUpdateCustomLinksData) {
20+
// First, get existing links
21+
const { data: existingLinks, error: fetchError } = await supabase
22+
.from("custom_links")
23+
.select("id")
24+
.eq("festival_id", festivalId);
25+
26+
if (fetchError) throw fetchError;
27+
28+
// Delete existing links that aren't in the new list
29+
const newLinkIds = links.filter((link) => link.id).map((link) => link.id);
30+
const linksToDelete =
31+
existingLinks?.filter((link) => !newLinkIds.includes(link.id)) || [];
32+
33+
if (linksToDelete.length > 0) {
34+
const { error: deleteError } = await supabase
35+
.from("custom_links")
36+
.delete()
37+
.in(
38+
"id",
39+
linksToDelete.map((link) => link.id),
40+
);
41+
42+
if (deleteError) throw deleteError;
43+
}
44+
45+
// Update or create links
46+
const promises = links.map(async (link, index) => {
47+
const linkData = {
48+
title: link.title,
49+
url: link.url,
50+
display_order: index,
51+
};
52+
53+
if (link.id) {
54+
// Update existing link
55+
const { error } = await supabase
56+
.from("custom_links")
57+
.update(linkData)
58+
.eq("id", link.id);
59+
60+
if (error) throw error;
61+
} else {
62+
// Create new link
63+
const { error } = await supabase.from("custom_links").insert({
64+
...linkData,
65+
festival_id: festivalId,
66+
});
67+
68+
if (error) throw error;
69+
}
70+
});
71+
72+
await Promise.all(promises);
73+
}
74+
75+
export function useBulkUpdateCustomLinksMutation() {
76+
const queryClient = useQueryClient();
77+
const { toast } = useToast();
78+
79+
return useMutation({
80+
mutationFn: bulkUpdateCustomLinks,
81+
onSuccess: (_, variables) => {
82+
queryClient.invalidateQueries({
83+
queryKey: customLinksKeys.byFestival(variables.festivalId),
84+
});
85+
toast({
86+
title: "Success",
87+
description: "Custom links updated successfully",
88+
});
89+
},
90+
onError: (error) => {
91+
console.error("Error updating custom links:", error);
92+
toast({
93+
title: "Error",
94+
description: "Failed to update custom links",
95+
variant: "destructive",
96+
});
97+
},
98+
});
99+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import { supabase } from "@/integrations/supabase/client";
3+
import { Tables } from "@/integrations/supabase/types";
4+
5+
export type FestivalInfo = Tables<"festival_info">;
6+
7+
export const festivalInfoKeys = {
8+
all: ["festivalInfo"] as const,
9+
byFestival: (festivalId: string) =>
10+
[...festivalInfoKeys.all, festivalId] as const,
11+
};
12+
13+
async function fetchFestivalInfo(festivalId: string): Promise<FestivalInfo> {
14+
const { data, error } = await supabase
15+
.from("festival_info")
16+
.select("*")
17+
.eq("festival_id", festivalId)
18+
.single();
19+
20+
if (error) {
21+
if (error.code === "PGRST116") {
22+
throw new Error("Festival info not found");
23+
}
24+
25+
console.error("Error fetching festival info:", error);
26+
throw new Error("Failed to fetch festival info");
27+
}
28+
29+
return data;
30+
}
31+
32+
export function useFestivalInfoQuery(festivalId: string | undefined) {
33+
return useQuery({
34+
queryKey: festivalInfoKeys.byFestival(festivalId || ""),
35+
queryFn: () => fetchFestivalInfo(festivalId!),
36+
enabled: !!festivalId,
37+
});
38+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useMutation, useQueryClient } from "@tanstack/react-query";
2+
import { supabase } from "@/integrations/supabase/client";
3+
import { Tables } from "@/integrations/supabase/types";
4+
import { festivalInfoKeys } from "./useFestivalInfo";
5+
6+
export function useFestivalInfoMutation(festivalId: string) {
7+
const queryClient = useQueryClient();
8+
9+
return useMutation({
10+
mutationFn: async (data: Partial<Tables<"festival_info">>) => {
11+
// Check if festival info already exists
12+
const { data: existingInfo } = await supabase
13+
.from("festival_info")
14+
.select("id")
15+
.eq("festival_id", festivalId)
16+
.single();
17+
18+
if (existingInfo) {
19+
// Update existing record
20+
const { error } = await supabase
21+
.from("festival_info")
22+
.update(data)
23+
.eq("festival_id", festivalId);
24+
if (error) throw error;
25+
} else {
26+
// Create new record
27+
const { error } = await supabase.from("festival_info").insert({
28+
festival_id: festivalId,
29+
...data,
30+
});
31+
if (error) throw error;
32+
}
33+
},
34+
onSuccess: () => {
35+
queryClient.invalidateQueries({
36+
queryKey: festivalInfoKeys.byFestival(festivalId),
37+
});
38+
},
39+
});
40+
}

src/hooks/queries/festivals/useCreateFestival.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ async function createFestival(festivalData: {
77
name: string;
88
slug: string;
99
description?: string;
10+
published?: boolean;
1011
logo_url?: string | null;
1112
}) {
1213
const { data, error } = await supabase

src/hooks/queries/festivals/useUpdateFestival.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import { festivalsKeys } from "./types";
55

66
async function updateFestival(
77
festivalId: string,
8-
festivalData: {
8+
festivalData: Partial<{
99
name: string;
1010
slug: string;
1111
description?: string;
12+
published?: boolean;
1213
logo_url?: string | null;
13-
},
14+
}>,
1415
) {
1516
const { data, error } = await supabase
1617
.from("festivals")
@@ -33,12 +34,13 @@ export function useUpdateFestivalMutation() {
3334
festivalData,
3435
}: {
3536
festivalId: string;
36-
festivalData: {
37+
festivalData: Partial<{
3738
name: string;
3839
slug: string;
3940
description?: string;
41+
published?: boolean;
4042
logo_url?: string | null;
41-
};
43+
}>;
4244
}) => updateFestival(festivalId, festivalData),
4345
onSuccess: () => {
4446
queryClient.invalidateQueries({ queryKey: festivalsKeys.all() });

src/integrations/supabase/types.ts

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,44 @@ export type Database = {
210210
};
211211
Relationships: [];
212212
};
213+
custom_links: {
214+
Row: {
215+
created_at: string | null;
216+
display_order: number | null;
217+
festival_id: string;
218+
id: string;
219+
title: string;
220+
updated_at: string | null;
221+
url: string;
222+
};
223+
Insert: {
224+
created_at?: string | null;
225+
display_order?: number | null;
226+
festival_id: string;
227+
id?: string;
228+
title: string;
229+
updated_at?: string | null;
230+
url: string;
231+
};
232+
Update: {
233+
created_at?: string | null;
234+
display_order?: number | null;
235+
festival_id?: string;
236+
id?: string;
237+
title?: string;
238+
updated_at?: string | null;
239+
url?: string;
240+
};
241+
Relationships: [
242+
{
243+
foreignKeyName: "custom_links_festival_id_fkey";
244+
columns: ["festival_id"];
245+
isOneToOne: false;
246+
referencedRelation: "festivals";
247+
referencedColumns: ["id"];
248+
},
249+
];
250+
};
213251
festival_editions: {
214252
Row: {
215253
archived: boolean;
@@ -269,6 +307,47 @@ export type Database = {
269307
},
270308
];
271309
};
310+
festival_info: {
311+
Row: {
312+
created_at: string;
313+
facebook_url: string | null;
314+
festival_id: string;
315+
id: string;
316+
info_text: string | null;
317+
instagram_url: string | null;
318+
map_image_url: string | null;
319+
updated_at: string;
320+
};
321+
Insert: {
322+
created_at?: string;
323+
facebook_url?: string | null;
324+
festival_id: string;
325+
id?: string;
326+
info_text?: string | null;
327+
instagram_url?: string | null;
328+
map_image_url?: string | null;
329+
updated_at?: string;
330+
};
331+
Update: {
332+
created_at?: string;
333+
facebook_url?: string | null;
334+
festival_id?: string;
335+
id?: string;
336+
info_text?: string | null;
337+
instagram_url?: string | null;
338+
map_image_url?: string | null;
339+
updated_at?: string;
340+
};
341+
Relationships: [
342+
{
343+
foreignKeyName: "festival_info_festival_id_fkey";
344+
columns: ["festival_id"];
345+
isOneToOne: true;
346+
referencedRelation: "festivals";
347+
referencedColumns: ["id"];
348+
},
349+
];
350+
};
272351
festivals: {
273352
Row: {
274353
archived: boolean;
@@ -280,7 +359,6 @@ export type Database = {
280359
published: boolean | null;
281360
slug: string;
282361
updated_at: string;
283-
website_url: string | null;
284362
};
285363
Insert: {
286364
archived?: boolean;
@@ -292,7 +370,6 @@ export type Database = {
292370
published?: boolean | null;
293371
slug: string;
294372
updated_at?: string;
295-
website_url?: string | null;
296373
};
297374
Update: {
298375
archived?: boolean;
@@ -304,7 +381,6 @@ export type Database = {
304381
published?: boolean | null;
305382
slug?: string;
306383
updated_at?: string;
307-
website_url?: string | null;
308384
};
309385
Relationships: [];
310386
};
@@ -631,6 +707,10 @@ export type Database = {
631707
[_ in never]: never;
632708
};
633709
Functions: {
710+
bootstrap_super_admin: {
711+
Args: { user_email: string };
712+
Returns: boolean;
713+
};
634714
can_edit_artists: {
635715
Args: { check_user_id: string };
636716
Returns: boolean;

0 commit comments

Comments
 (0)