diff --git a/src/components/layout/AppHeader/Navigation.tsx b/src/components/layout/AppHeader/Navigation.tsx index 26572e76..7df7ddf9 100644 --- a/src/components/layout/AppHeader/Navigation.tsx +++ b/src/components/layout/AppHeader/Navigation.tsx @@ -1,5 +1,5 @@ import { Link, useRouter } from "@tanstack/react-router"; -import { ArrowLeft, Users } from "lucide-react"; +import { ArrowLeft, UserPlus, Users } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Tooltip, @@ -7,6 +7,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { useAuth } from "@/contexts/AuthContext"; +import { useActiveGroup } from "@/hooks/useActiveGroup"; interface NavigationProps { showBackButton?: boolean; @@ -50,6 +51,10 @@ export function Navigation({ }: NavigationProps) { const { user } = useAuth(); const router = useRouter(); + const { activeGroup, hasGroups, isLoading: isGroupsLoading } = useActiveGroup(); + + const groupsButtonClassName = + "border-purple-400/50 text-purple-300 hover:bg-purple-600 hover:text-white hover:border-purple-600 transition-colors"; return (
@@ -68,19 +73,34 @@ export function Navigation({ )} - {/* Groups Button */} + {/* Groups / Active Group Indicator */} {showGroupsButton && user && ( - - - {!isMobile && Groups} - + {hasGroups || isGroupsLoading ? ( + + + {!isMobile && ( + {activeGroup?.name || "Groups"} + )} + + ) : ( + + + {!isMobile && Create/Join a Group} + + )} )}
diff --git a/src/hooks/useActiveGroup.ts b/src/hooks/useActiveGroup.ts new file mode 100644 index 00000000..b128ebcf --- /dev/null +++ b/src/hooks/useActiveGroup.ts @@ -0,0 +1,31 @@ +import { useAuth } from "@/contexts/AuthContext"; +import { useUserGroupsQuery } from "@/api/groups/useUserGroups"; +import { resolveActiveGroupId } from "@/lib/activeGroup"; +import type { Group } from "@/api/groups/types"; + +interface ActiveGroupState { + activeGroupId: string | undefined; + activeGroup: Group | undefined; + groups: Group[]; + hasGroups: boolean; + isLoading: boolean; +} + +export function useActiveGroup(): ActiveGroupState { + const { user, profile } = useAuth(); + const groupsQuery = useUserGroupsQuery(user?.id); + const groups = groupsQuery.data ?? []; + + const activeGroupId = resolveActiveGroupId({ + profileActiveGroupId: profile?.active_group_id, + groupIds: groups.map((group) => group.id), + }); + + return { + activeGroupId, + activeGroup: groups.find((group) => group.id === activeGroupId), + groups, + hasGroups: groups.length > 0, + isLoading: groupsQuery.isLoading, + }; +} diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index ab971e76..953a6dd7 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -519,6 +519,7 @@ export type Database = { }; profiles: { Row: { + active_group_id: string | null; completed_onboarding: boolean | null; created_at: string; email: string | null; @@ -526,6 +527,7 @@ export type Database = { username: string | null; }; Insert: { + active_group_id?: string | null; completed_onboarding?: boolean | null; created_at?: string; email?: string | null; @@ -533,13 +535,22 @@ export type Database = { username?: string | null; }; Update: { + active_group_id?: string | null; completed_onboarding?: boolean | null; created_at?: string; email?: string | null; id?: string; username?: string | null; }; - Relationships: []; + Relationships: [ + { + foreignKeyName: "profiles_active_group_id_fkey"; + columns: ["active_group_id"]; + isOneToOne: false; + referencedRelation: "groups"; + referencedColumns: ["id"]; + }, + ]; }; set_artists: { Row: { diff --git a/src/lib/activeGroup.test.ts b/src/lib/activeGroup.test.ts new file mode 100644 index 00000000..c8b0b676 --- /dev/null +++ b/src/lib/activeGroup.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from "vitest"; +import { resolveActiveGroupId } from "./activeGroup"; + +describe("resolveActiveGroupId", () => { + it("returns undefined when the user has no groups", () => { + expect( + resolveActiveGroupId({ profileActiveGroupId: null, groupIds: [] }), + ).toBeUndefined(); + }); + + it("auto-activates the single group when no active group is set", () => { + expect( + resolveActiveGroupId({ + profileActiveGroupId: null, + groupIds: ["group-1"], + }), + ).toBe("group-1"); + }); + + it("does not auto-activate when the user belongs to multiple groups", () => { + expect( + resolveActiveGroupId({ + profileActiveGroupId: null, + groupIds: ["group-1", "group-2"], + }), + ).toBeUndefined(); + }); + + it("returns the persisted active group when it is still a membership", () => { + expect( + resolveActiveGroupId({ + profileActiveGroupId: "group-2", + groupIds: ["group-1", "group-2"], + }), + ).toBe("group-2"); + }); + + it("ignores a persisted active group the user is no longer a member of", () => { + expect( + resolveActiveGroupId({ + profileActiveGroupId: "group-3", + groupIds: ["group-1", "group-2"], + }), + ).toBeUndefined(); + }); + + it("falls back to auto-activation when the stale active group leaves exactly one membership", () => { + expect( + resolveActiveGroupId({ + profileActiveGroupId: "group-3", + groupIds: ["group-1"], + }), + ).toBe("group-1"); + }); + + it("prefers the persisted active group over auto-activation when only one group remains", () => { + expect( + resolveActiveGroupId({ + profileActiveGroupId: "group-1", + groupIds: ["group-1"], + }), + ).toBe("group-1"); + }); +}); diff --git a/src/lib/activeGroup.ts b/src/lib/activeGroup.ts new file mode 100644 index 00000000..c10198e8 --- /dev/null +++ b/src/lib/activeGroup.ts @@ -0,0 +1,19 @@ +interface ResolveActiveGroupIdParams { + profileActiveGroupId: string | null | undefined; + groupIds: string[]; +} + +export function resolveActiveGroupId({ + profileActiveGroupId, + groupIds, +}: ResolveActiveGroupIdParams): string | undefined { + if (profileActiveGroupId && groupIds.includes(profileActiveGroupId)) { + return profileActiveGroupId; + } + + if (groupIds.length === 1) { + return groupIds[0]; + } + + return undefined; +} diff --git a/supabase/migrations/20260710154342_add_active_group_id_to_profiles.sql b/supabase/migrations/20260710154342_add_active_group_id_to_profiles.sql new file mode 100644 index 00000000..62d0766b --- /dev/null +++ b/supabase/migrations/20260710154342_add_active_group_id_to_profiles.sql @@ -0,0 +1,7 @@ +-- Add active_group_id to profiles: the Group a user is currently viewing the +-- app "as". Global across festivals/editions. Nullable; cleared automatically +-- if the referenced group is deleted. +ALTER TABLE public.profiles +ADD COLUMN active_group_id UUID REFERENCES public.groups(id) ON DELETE SET NULL; + +CREATE INDEX idx_profiles_active_group_id ON public.profiles(active_group_id);