Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 32 additions & 12 deletions src/components/layout/AppHeader/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
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,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useAuth } from "@/contexts/AuthContext";
import { useActiveGroup } from "@/hooks/useActiveGroup";

interface NavigationProps {
showBackButton?: boolean;
Expand Down Expand Up @@ -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 (
<div className="flex items-center gap-3">
Expand All @@ -68,19 +73,34 @@ export function Navigation({
</TooltipButton>
)}

{/* Groups Button */}
{/* Groups / Active Group Indicator */}
{showGroupsButton && user && (
<Link to="/groups">
<TooltipButton
variant="outline"
size={isMobile ? "sm" : "default"}
className="border-purple-400/50 text-purple-300 hover:bg-purple-600 hover:text-white hover:border-purple-600 transition-colors"
tooltip="View Your Groups"
isMobile={isMobile}
>
<Users className="h-4 w-4" />
{!isMobile && <span className="ml-2">Groups</span>}
</TooltipButton>
{hasGroups || isGroupsLoading ? (
<TooltipButton
variant="outline"
size={isMobile ? "sm" : "default"}
className={groupsButtonClassName}
tooltip={activeGroup ? `Active group: ${activeGroup.name}` : "View Your Groups"}
isMobile={isMobile}
>
<Users className="h-4 w-4" />
{!isMobile && (
<span className="ml-2">{activeGroup?.name || "Groups"}</span>
)}
</TooltipButton>
) : (
<TooltipButton
variant="outline"
size={isMobile ? "sm" : "default"}
className={groupsButtonClassName}
tooltip="Create or join a group to start sharing votes"
isMobile={isMobile}
>
<UserPlus className="h-4 w-4" />
{!isMobile && <span className="ml-2">Create/Join a Group</span>}
</TooltipButton>
)}
</Link>
)}
</div>
Expand Down
31 changes: 31 additions & 0 deletions src/hooks/useActiveGroup.ts
Original file line number Diff line number Diff line change
@@ -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),
});
Comment on lines +19 to +22

return {
activeGroupId,
activeGroup: groups.find((group) => group.id === activeGroupId),
groups,
hasGroups: groups.length > 0,
isLoading: groupsQuery.isLoading,
};
}
13 changes: 12 additions & 1 deletion src/integrations/supabase/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,27 +519,38 @@ export type Database = {
};
profiles: {
Row: {
active_group_id: string | null;
completed_onboarding: boolean | null;
created_at: string;
email: string | null;
id: string;
username: string | null;
};
Insert: {
active_group_id?: string | null;
completed_onboarding?: boolean | null;
created_at?: string;
email?: string | null;
id: string;
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: {
Expand Down
64 changes: 64 additions & 0 deletions src/lib/activeGroup.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
19 changes: 19 additions & 0 deletions src/lib/activeGroup.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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);
Loading