diff --git a/CLAUDE.md b/CLAUDE.md index b5c0b35..d16cef1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -135,3 +135,6 @@ 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" +- don't run supabase db reset diff --git a/src/components/layout/AppHeader/TitleSection.tsx b/src/components/layout/AppHeader/TitleSection.tsx index 1fcf970..54187d7 100644 --- a/src/components/layout/AppHeader/TitleSection.tsx +++ b/src/components/layout/AppHeader/TitleSection.tsx @@ -45,3 +45,5 @@ export const TitleSection = forwardRef( ); }, ); + +TitleSection.displayName = "TitleSection"; diff --git a/src/hooks/queries/custom-links/useCustomLinks.ts b/src/hooks/queries/custom-links/useCustomLinks.ts new file mode 100644 index 0000000..f79bbaf --- /dev/null +++ b/src/hooks/queries/custom-links/useCustomLinks.ts @@ -0,0 +1,35 @@ +import { useQuery } from "@tanstack/react-query"; +import { supabase } from "@/integrations/supabase/client"; +import { Tables } from "@/integrations/supabase/types"; + +export type CustomLink = Tables<"custom_links">; + +export const customLinksKeys = { + all: ["customLinks"] as const, + byFestival: (festivalId: string) => + [...customLinksKeys.all, festivalId] as const, +}; + +async function fetchCustomLinks(festivalId: string): Promise { + const { data, error } = await supabase + .from("custom_links") + .select("*") + .eq("festival_id", festivalId) + .order("display_order", { ascending: true }) + .order("created_at", { ascending: true }); + + if (error) { + console.error("Error fetching custom links:", error); + throw new Error("Failed to fetch custom links"); + } + + return data || []; +} + +export function useCustomLinksQuery(festivalId: string | undefined) { + return useQuery({ + queryKey: customLinksKeys.byFestival(festivalId || ""), + queryFn: () => fetchCustomLinks(festivalId!), + enabled: !!festivalId, + }); +} diff --git a/src/hooks/queries/custom-links/useCustomLinksMutation.ts b/src/hooks/queries/custom-links/useCustomLinksMutation.ts new file mode 100644 index 0000000..c5ae6b8 --- /dev/null +++ b/src/hooks/queries/custom-links/useCustomLinksMutation.ts @@ -0,0 +1,99 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useToast } from "@/hooks/use-toast"; +import { supabase } from "@/integrations/supabase/client"; +import { customLinksKeys } from "./useCustomLinks"; + +interface BulkUpdateCustomLinksData { + festivalId: string; + links: Array<{ + id?: string; + title: string; + url: string; + display_order: number; + }>; +} + +async function bulkUpdateCustomLinks({ + festivalId, + links, +}: BulkUpdateCustomLinksData) { + // First, get existing links + const { data: existingLinks, error: fetchError } = await supabase + .from("custom_links") + .select("id") + .eq("festival_id", festivalId); + + if (fetchError) throw fetchError; + + // Delete existing links that aren't in the new list + const newLinkIds = links.filter((link) => link.id).map((link) => link.id); + const linksToDelete = + existingLinks?.filter((link) => !newLinkIds.includes(link.id)) || []; + + if (linksToDelete.length > 0) { + const { error: deleteError } = await supabase + .from("custom_links") + .delete() + .in( + "id", + linksToDelete.map((link) => link.id), + ); + + if (deleteError) throw deleteError; + } + + // Update or create links + const promises = links.map(async (link, index) => { + const linkData = { + title: link.title, + url: link.url, + display_order: index, + }; + + if (link.id) { + // Update existing link + const { error } = await supabase + .from("custom_links") + .update(linkData) + .eq("id", link.id); + + if (error) throw error; + } else { + // Create new link + const { error } = await supabase.from("custom_links").insert({ + ...linkData, + festival_id: festivalId, + }); + + if (error) throw error; + } + }); + + await Promise.all(promises); +} + +export function useBulkUpdateCustomLinksMutation() { + const queryClient = useQueryClient(); + const { toast } = useToast(); + + return useMutation({ + mutationFn: bulkUpdateCustomLinks, + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ + queryKey: customLinksKeys.byFestival(variables.festivalId), + }); + toast({ + title: "Success", + description: "Custom links updated successfully", + }); + }, + onError: (error) => { + console.error("Error updating custom links:", error); + toast({ + title: "Error", + description: "Failed to update custom links", + variant: "destructive", + }); + }, + }); +} diff --git a/src/hooks/queries/festival-info/useFestivalInfo.ts b/src/hooks/queries/festival-info/useFestivalInfo.ts new file mode 100644 index 0000000..4460838 --- /dev/null +++ b/src/hooks/queries/festival-info/useFestivalInfo.ts @@ -0,0 +1,38 @@ +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 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/hooks/queries/festival-info/useFestivalInfoMutation.ts b/src/hooks/queries/festival-info/useFestivalInfoMutation.ts new file mode 100644 index 0000000..5d3458d --- /dev/null +++ b/src/hooks/queries/festival-info/useFestivalInfoMutation.ts @@ -0,0 +1,40 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { supabase } from "@/integrations/supabase/client"; +import { Tables } from "@/integrations/supabase/types"; +import { festivalInfoKeys } from "./useFestivalInfo"; + +export function useFestivalInfoMutation(festivalId: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (data: Partial>) => { + // Check if festival info already exists + const { data: existingInfo } = await supabase + .from("festival_info") + .select("id") + .eq("festival_id", festivalId) + .single(); + + if (existingInfo) { + // Update existing record + const { error } = await supabase + .from("festival_info") + .update(data) + .eq("festival_id", festivalId); + if (error) throw error; + } else { + // Create new record + const { error } = await supabase.from("festival_info").insert({ + festival_id: festivalId, + ...data, + }); + if (error) throw error; + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: festivalInfoKeys.byFestival(festivalId), + }); + }, + }); +} diff --git a/src/hooks/queries/festivals/useCreateFestival.ts b/src/hooks/queries/festivals/useCreateFestival.ts index 32e75f0..05e1249 100644 --- a/src/hooks/queries/festivals/useCreateFestival.ts +++ b/src/hooks/queries/festivals/useCreateFestival.ts @@ -7,6 +7,7 @@ async function createFestival(festivalData: { name: string; slug: string; description?: string; + published?: boolean; logo_url?: string | null; }) { const { data, error } = await supabase diff --git a/src/hooks/queries/festivals/useUpdateFestival.ts b/src/hooks/queries/festivals/useUpdateFestival.ts index 8077d15..8f517e7 100644 --- a/src/hooks/queries/festivals/useUpdateFestival.ts +++ b/src/hooks/queries/festivals/useUpdateFestival.ts @@ -5,12 +5,13 @@ import { festivalsKeys } from "./types"; async function updateFestival( festivalId: string, - festivalData: { + festivalData: Partial<{ name: string; slug: string; description?: string; + published?: boolean; logo_url?: string | null; - }, + }>, ) { const { data, error } = await supabase .from("festivals") @@ -33,12 +34,13 @@ export function useUpdateFestivalMutation() { festivalData, }: { festivalId: string; - festivalData: { + festivalData: Partial<{ name: string; slug: string; description?: string; + published?: boolean; logo_url?: string | null; - }; + }>; }) => updateFestival(festivalId, festivalData), onSuccess: () => { queryClient.invalidateQueries({ queryKey: festivalsKeys.all() }); diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index b281e43..5c85de3 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -210,6 +210,44 @@ export type Database = { }; Relationships: []; }; + custom_links: { + Row: { + created_at: string | null; + display_order: number | null; + festival_id: string; + id: string; + title: string; + updated_at: string | null; + url: string; + }; + Insert: { + created_at?: string | null; + display_order?: number | null; + festival_id: string; + id?: string; + title: string; + updated_at?: string | null; + url: string; + }; + Update: { + created_at?: string | null; + display_order?: number | null; + festival_id?: string; + id?: string; + title?: string; + updated_at?: string | null; + url?: string; + }; + Relationships: [ + { + foreignKeyName: "custom_links_festival_id_fkey"; + columns: ["festival_id"]; + isOneToOne: false; + referencedRelation: "festivals"; + referencedColumns: ["id"]; + }, + ]; + }; festival_editions: { Row: { archived: boolean; @@ -269,6 +307,47 @@ export type Database = { }, ]; }; + festival_info: { + Row: { + created_at: string; + 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; + 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; + 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; @@ -280,7 +359,6 @@ export type Database = { published: boolean | null; slug: string; updated_at: string; - website_url: string | null; }; Insert: { archived?: boolean; @@ -292,7 +370,6 @@ export type Database = { published?: boolean | null; slug: string; updated_at?: string; - website_url?: string | null; }; Update: { archived?: boolean; @@ -304,7 +381,6 @@ export type Database = { published?: boolean | null; slug?: string; updated_at?: string; - website_url?: string | null; }; Relationships: []; }; @@ -631,6 +707,10 @@ export type Database = { [_ in never]: never; }; Functions: { + bootstrap_super_admin: { + Args: { user_email: string }; + Returns: boolean; + }; can_edit_artists: { Args: { check_user_id: string }; Returns: boolean; diff --git a/src/pages/EditionView/MainTabNavigation.tsx b/src/pages/EditionView/MainTabNavigation.tsx index 223900a..7f4c9aa 100644 --- a/src/pages/EditionView/MainTabNavigation.tsx +++ b/src/pages/EditionView/MainTabNavigation.tsx @@ -1,5 +1,5 @@ import { cn } from "@/lib/utils"; -import { Calendar, List, Map, Info, MessageSquare } from "lucide-react"; +import { Calendar, List, Map, Info } from "lucide-react"; import { NavLink } from "react-router-dom"; import { useFestivalEdition } from "@/contexts/FestivalEditionContext"; @@ -22,20 +22,20 @@ const TAB_CONFIG = { icon: Map, label: "Map", shortLabel: "Map", - disabled: true, + disabled: false, }, info: { icon: Info, label: "Info", shortLabel: "Info", - disabled: true, - }, - social: { - icon: MessageSquare, - label: "Social", - shortLabel: "Social", - disabled: true, + disabled: false, }, + // social: { + // icon: MessageSquare, + // label: "Social", + // shortLabel: "Social", + // disabled: true, + // }, } as const; export function MainTabNavigation() { diff --git a/src/pages/EditionView/tabs/InfoTab.tsx b/src/pages/EditionView/tabs/InfoTab.tsx index edb2c8d..84575b0 100644 --- a/src/pages/EditionView/tabs/InfoTab.tsx +++ b/src/pages/EditionView/tabs/InfoTab.tsx @@ -1,18 +1,53 @@ import { useFestivalEdition } from "@/contexts/FestivalEditionContext"; +import { useFestivalInfoQuery } from "@/hooks/queries/festival-info/useFestivalInfo"; +import { EditionTitle } from "./InfoTab/EditionTitle"; +import { InfoText } from "./InfoTab/InfoText"; +import { CustomLinks } from "./InfoTab/CustomLinks"; +import { NoInfo } from "./InfoTab/NoInfo"; +import { LoadingInfo } from "./InfoTab/LoadingInfo"; +import { SocialLinkItem } from "./InfoTab/SocialLinkItem"; +import { TitleSection } from "@/components/layout/AppHeader/TitleSection"; +import { useCustomLinksQuery } from "@/hooks/queries/custom-links/useCustomLinks"; export function InfoTab() { - const { edition } = useFestivalEdition(); + const { edition, festival } = useFestivalEdition(); + const { data: festivalInfo, isLoading } = useFestivalInfoQuery(festival?.id); + const customLinksQuery = useCustomLinksQuery(festival?.id || ""); + if (isLoading) { + return ; + } + + const customLinks = customLinksQuery.data || []; + const noInfoAvailable = + !festivalInfo?.info_text && + !festivalInfo?.facebook_url && + !festivalInfo?.instagram_url && + customLinks.length === 0; return ( - <> -
-

- {edition?.name} -

-
-
-

Festival info coming soon!

-
- +
+ + + + {festivalInfo?.info_text && ( + + )} + + {customLinks.length > 0 && } + + {festivalInfo?.facebook_url ? ( + + ) : null} + + {festivalInfo?.instagram_url ? ( + + ) : null} + + {noInfoAvailable && } +
); } diff --git a/src/pages/EditionView/tabs/InfoTab/CustomLinks.tsx b/src/pages/EditionView/tabs/InfoTab/CustomLinks.tsx new file mode 100644 index 0000000..2ca1b0f --- /dev/null +++ b/src/pages/EditionView/tabs/InfoTab/CustomLinks.tsx @@ -0,0 +1,23 @@ +import { SocialLinkItem } from "./SocialLinkItem"; + +export interface CustomLink { + title: string; + url: string; +} + +interface CustomLinksProps { + links: CustomLink[]; +} + +export function CustomLinks({ links }: CustomLinksProps) { + return ( +
+

Links

+
+ {links.map((link, index) => ( + + ))} +
+
+ ); +} diff --git a/src/pages/EditionView/tabs/InfoTab/EditionTitle.tsx b/src/pages/EditionView/tabs/InfoTab/EditionTitle.tsx new file mode 100644 index 0000000..4865156 --- /dev/null +++ b/src/pages/EditionView/tabs/InfoTab/EditionTitle.tsx @@ -0,0 +1,13 @@ +interface EditionTitleProps { + name?: string; +} + +export function EditionTitle({ name }: EditionTitleProps) { + return ( +
+

+ {name} +

+
+ ); +} diff --git a/src/pages/EditionView/tabs/InfoTab/InfoText.tsx b/src/pages/EditionView/tabs/InfoTab/InfoText.tsx new file mode 100644 index 0000000..107f786 --- /dev/null +++ b/src/pages/EditionView/tabs/InfoTab/InfoText.tsx @@ -0,0 +1,14 @@ +interface InfoTextProps { + infoText?: string; +} + +export function InfoText({ infoText }: InfoTextProps) { + return ( +
+
+
+ ); +} diff --git a/src/pages/EditionView/tabs/InfoTab/LoadingInfo.tsx b/src/pages/EditionView/tabs/InfoTab/LoadingInfo.tsx new file mode 100644 index 0000000..3b6740f --- /dev/null +++ b/src/pages/EditionView/tabs/InfoTab/LoadingInfo.tsx @@ -0,0 +1,7 @@ +export function LoadingInfo() { + return ( +
+

Loading festival information...

+
+ ); +} diff --git a/src/pages/EditionView/tabs/InfoTab/NoInfo.tsx b/src/pages/EditionView/tabs/InfoTab/NoInfo.tsx new file mode 100644 index 0000000..4ed52a6 --- /dev/null +++ b/src/pages/EditionView/tabs/InfoTab/NoInfo.tsx @@ -0,0 +1,7 @@ +export function NoInfo() { + return ( +
+

Festival information not available yet.

+
+ ); +} diff --git a/src/pages/EditionView/tabs/InfoTab/SocialLinkItem.tsx b/src/pages/EditionView/tabs/InfoTab/SocialLinkItem.tsx new file mode 100644 index 0000000..b1df1a9 --- /dev/null +++ b/src/pages/EditionView/tabs/InfoTab/SocialLinkItem.tsx @@ -0,0 +1,22 @@ +import { ExternalLink } from "lucide-react"; +import type { CustomLink } from "./CustomLinks"; + +interface SocialLinkItemProps { + link: CustomLink; +} + +export function SocialLinkItem({ link }: SocialLinkItemProps) { + return ( + +
+ {link.title} +
+ +
+ ); +} 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..c91992a 100644 --- a/src/pages/EditionView/tabs/SocialTab.tsx +++ b/src/pages/EditionView/tabs/SocialTab.tsx @@ -1,7 +1,141 @@ +import { useFestivalEdition } from "@/contexts/FestivalEditionContext"; +import { useFestivalInfoQuery } from "@/hooks/queries/festival-info/useFestivalInfo"; + +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() { - return ( -
-

Social feed coming soon!

-
- ); + 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 <>Unavailable; + + // return ( + //
+ //
+ //

Follow Us

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

Facebook

+ // + // View on Facebook + // + // + //
+ + //
+ //