From cd90a1a2757976f399d488c27466f0b8b3fb0de6 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 27 Aug 2025 15:10:12 +0200 Subject: [PATCH 1/6] feat(festival): add festival info --- CLAUDE.md | 2 + src/components/router/GlobalRoutes.tsx | 2 + .../queries/festival-info/useFestivalInfo.ts | 42 +++ src/integrations/supabase/types.ts | 44 +++ src/pages/EditionView/MainTabNavigation.tsx | 6 +- src/pages/EditionView/tabs/InfoTab.tsx | 63 +++- src/pages/EditionView/tabs/MapTab.tsx | 37 ++- src/pages/EditionView/tabs/SocialTab.tsx | 137 ++++++++- src/pages/admin/festivals/FestivalDetail.tsx | 11 +- src/pages/admin/festivals/FestivalInfo.tsx | 283 ++++++++++++++++++ ...0250709094304_create_admin_roles_table.sql | 38 --- ...27000000_add_festival_edition_metadata.sql | 58 ++++ .../20250827000003_bootstrap_admin_fn.sql | 33 ++ supabase/seed.sql | 200 +++---------- 14 files changed, 746 insertions(+), 210 deletions(-) create mode 100644 src/hooks/queries/festival-info/useFestivalInfo.ts create mode 100644 src/pages/admin/festivals/FestivalInfo.tsx create mode 100644 supabase/migrations/20250827000000_add_festival_edition_metadata.sql create mode 100644 supabase/migrations/20250827000003_bootstrap_admin_fn.sql diff --git a/CLAUDE.md b/CLAUDE.md index b5c0b35..6c9f160 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -135,3 +135,5 @@ In this codebase, it is acceptable and preferred to define helper functions (suc ## Git Workflow - **Auto-commit Rule**: For every user message that requests code changes, automatically commit the changes after implementation with an appropriate commit message + +- never run "supabase db push" diff --git a/src/components/router/GlobalRoutes.tsx b/src/components/router/GlobalRoutes.tsx index 57c8cfb..5a9c7da 100644 --- a/src/components/router/GlobalRoutes.tsx +++ b/src/components/router/GlobalRoutes.tsx @@ -6,6 +6,7 @@ import FestivalDetail from "@/pages/admin/festivals/FestivalDetail"; import FestivalEdition from "@/pages/admin/festivals/FestivalEdition"; import FestivalSets from "@/pages/admin/festivals/FestivalSets"; import FestivalStages from "@/pages/admin/festivals/FestivalStages"; +import FestivalInfo from "@/pages/admin/festivals/FestivalInfo"; import AdminLayout from "@/pages/admin/AdminLayout"; import CookiePolicy from "@/pages/legal/CookiePolicy"; import GroupDetail from "@/pages/groups/GroupDetail"; @@ -31,6 +32,7 @@ export function GlobalRoutes() { } /> }> }> + } /> }> } /> } /> diff --git a/src/hooks/queries/festival-info/useFestivalInfo.ts b/src/hooks/queries/festival-info/useFestivalInfo.ts new file mode 100644 index 0000000..3118b6a --- /dev/null +++ b/src/hooks/queries/festival-info/useFestivalInfo.ts @@ -0,0 +1,42 @@ +import { useQuery } from "@tanstack/react-query"; +import { supabase } from "@/integrations/supabase/client"; +import { Tables } from "@/integrations/supabase/types"; + +export type FestivalInfo = Tables<"festival_info">; +export type CustomLink = { + title: string; + url: string; +}; + +export const festivalInfoKeys = { + all: ["festivalInfo"] as const, + byFestival: (festivalId: string) => + [...festivalInfoKeys.all, festivalId] as const, +}; + +async function fetchFestivalInfo(festivalId: string): Promise { + const { data, error } = await supabase + .from("festival_info") + .select("*") + .eq("festival_id", festivalId) + .single(); + + if (error) { + if (error.code === "PGRST116") { + throw new Error("Festival info not found"); + } + + console.error("Error fetching festival info:", error); + throw new Error("Failed to fetch festival info"); + } + + return data; +} + +export function useFestivalInfoQuery(festivalId: string | undefined) { + return useQuery({ + queryKey: festivalInfoKeys.byFestival(festivalId || ""), + queryFn: () => fetchFestivalInfo(festivalId!), + enabled: !!festivalId, + }); +} diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index b281e43..b8c1441 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -269,6 +269,50 @@ export type Database = { }, ]; }; + festival_info: { + Row: { + created_at: string; + custom_links: Json | null; + facebook_url: string | null; + festival_id: string; + id: string; + info_text: string | null; + instagram_url: string | null; + map_image_url: string | null; + updated_at: string; + }; + Insert: { + created_at?: string; + custom_links?: Json | null; + facebook_url?: string | null; + festival_id: string; + id?: string; + info_text?: string | null; + instagram_url?: string | null; + map_image_url?: string | null; + updated_at?: string; + }; + Update: { + created_at?: string; + custom_links?: Json | null; + facebook_url?: string | null; + festival_id?: string; + id?: string; + info_text?: string | null; + instagram_url?: string | null; + map_image_url?: string | null; + updated_at?: string; + }; + Relationships: [ + { + foreignKeyName: "festival_info_festival_id_fkey"; + columns: ["festival_id"]; + isOneToOne: true; + referencedRelation: "festivals"; + referencedColumns: ["id"]; + }, + ]; + }; festivals: { Row: { archived: boolean; diff --git a/src/pages/EditionView/MainTabNavigation.tsx b/src/pages/EditionView/MainTabNavigation.tsx index 223900a..b70b148 100644 --- a/src/pages/EditionView/MainTabNavigation.tsx +++ b/src/pages/EditionView/MainTabNavigation.tsx @@ -22,19 +22,19 @@ const TAB_CONFIG = { icon: Map, label: "Map", shortLabel: "Map", - disabled: true, + disabled: false, }, info: { icon: Info, label: "Info", shortLabel: "Info", - disabled: true, + disabled: false, }, social: { icon: MessageSquare, label: "Social", shortLabel: "Social", - disabled: true, + disabled: false, }, } as const; diff --git a/src/pages/EditionView/tabs/InfoTab.tsx b/src/pages/EditionView/tabs/InfoTab.tsx index edb2c8d..523a09e 100644 --- a/src/pages/EditionView/tabs/InfoTab.tsx +++ b/src/pages/EditionView/tabs/InfoTab.tsx @@ -1,18 +1,69 @@ import { useFestivalEdition } from "@/contexts/FestivalEditionContext"; +import { + useFestivalInfoQuery, + CustomLink, +} from "@/hooks/queries/festival-info/useFestivalInfo"; +import { ExternalLink } from "lucide-react"; export function InfoTab() { - const { edition } = useFestivalEdition(); + const { edition, festival } = useFestivalEdition(); + const { data: festivalInfo, isLoading } = useFestivalInfoQuery(festival?.id); + + if (isLoading) { + return ( +
+

Loading festival information...

+
+ ); + } + + const customLinks = (festivalInfo?.custom_links as CustomLink[]) || []; return ( - <> +

{edition?.name}

-
-

Festival info coming soon!

-
- + + {festivalInfo?.info_text && ( +
+
+
+ )} + + {/* Custom Links */} + {customLinks.length > 0 && ( +
+

Links

+
+ {customLinks.map((link, index) => ( + +
+ {link.title} +
+ +
+ ))} +
+
+ )} + + {!festivalInfo?.info_text && customLinks.length === 0 && ( +
+

Festival information not available yet.

+
+ )} +
); } diff --git a/src/pages/EditionView/tabs/MapTab.tsx b/src/pages/EditionView/tabs/MapTab.tsx index 6f5f070..fd2b290 100644 --- a/src/pages/EditionView/tabs/MapTab.tsx +++ b/src/pages/EditionView/tabs/MapTab.tsx @@ -1,7 +1,40 @@ +import { useFestivalEdition } from "@/contexts/FestivalEditionContext"; +import { useFestivalInfoQuery } from "@/hooks/queries/festival-info/useFestivalInfo"; + export function MapTab() { + const { festival } = useFestivalEdition(); + const { data: festivalInfo, isLoading } = useFestivalInfoQuery(festival?.id); + + if (isLoading) { + return ( +
+

Loading map...

+
+ ); + } + + if (!festivalInfo?.map_image_url) { + return ( +
+

Festival map not available yet.

+
+ ); + } + return ( -
-

Map view coming soon!

+
+
+

Festival Map

+
+ +
+ Festival Map +
); } diff --git a/src/pages/EditionView/tabs/SocialTab.tsx b/src/pages/EditionView/tabs/SocialTab.tsx index 0cfcb48..4437104 100644 --- a/src/pages/EditionView/tabs/SocialTab.tsx +++ b/src/pages/EditionView/tabs/SocialTab.tsx @@ -1,7 +1,140 @@ +import { useFestivalEdition } from "@/contexts/FestivalEditionContext"; +import { useFestivalInfoQuery } from "@/hooks/queries/festival-info/useFestivalInfo"; +import { ExternalLink } from "lucide-react"; + +function extractFacebookPageId(url: string): string | null { + // Extract Facebook page ID/username from various Facebook URL formats + const patterns = [/facebook\.com\/([^/?]+)/, /fb\.com\/([^/?]+)/]; + + for (const pattern of patterns) { + const match = url.match(pattern); + if (match) { + return match[1]; + } + } + return null; +} + +function extractInstagramUsername(url: string): string | null { + // Extract Instagram username from URL + const pattern = /instagram\.com\/([^/?]+)/; + const match = url.match(pattern); + return match ? match[1] : null; +} + export function SocialTab() { + const { festival } = useFestivalEdition(); + const { data: festivalInfo, isLoading } = useFestivalInfoQuery(festival?.id); + + if (isLoading || !festivalInfo) { + return ( +
+

Loading social feeds...

+
+ ); + } + + const facebookPageId = festivalInfo.facebook_url + ? extractFacebookPageId(festivalInfo.facebook_url) + : null; + const instagramUsername = festivalInfo.instagram_url + ? extractInstagramUsername(festivalInfo.instagram_url) + : null; + + if (!facebookPageId && !instagramUsername) { + return ( +
+

Social feeds not available yet.

+
+ ); + } + return ( -
-

Social feed coming soon!

+
+
+

Follow Us

+
+ +
+ {/* Facebook Embed */} + {facebookPageId && festivalInfo.facebook_url && ( +
+ + +
+