suna/frontend/src/components/sidebar/nav-user-with-teams.tsx

337 lines
12 KiB
TypeScript

'use client';
import * as React from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import {
BadgeCheck,
Bell,
ChevronDown,
ChevronsUpDown,
Command,
CreditCard,
LogOut,
Plus,
Settings,
User,
AudioWaveform,
Sun,
Moon,
KeyRound,
} from 'lucide-react';
import { useAccounts } from '@/hooks/use-accounts';
import NewTeamForm from '@/components/basejump/new-team-form';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from '@/components/ui/sidebar';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { createClient } from '@/lib/supabase/client';
import { useTheme } from 'next-themes';
import { isLocalMode } from '@/lib/config';
export function NavUserWithTeams({
user,
}: {
user: {
name: string;
email: string;
avatar: string;
};
}) {
const router = useRouter();
const { isMobile } = useSidebar();
const { data: accounts } = useAccounts();
const [showNewTeamDialog, setShowNewTeamDialog] = React.useState(false);
const { theme, setTheme } = useTheme();
// Prepare personal account and team accounts
const personalAccount = React.useMemo(
() => accounts?.find((account) => account.personal_account),
[accounts],
);
const teamAccounts = React.useMemo(
() => accounts?.filter((account) => !account.personal_account),
[accounts],
);
// Create a default list of teams with logos for the UI (will show until real data loads)
const defaultTeams = [
{
name: personalAccount?.name || 'Personal Account',
logo: Command,
plan: 'Personal',
account_id: personalAccount?.account_id,
slug: personalAccount?.slug,
personal_account: true,
},
...(teamAccounts?.map((team) => ({
name: team.name,
logo: AudioWaveform,
plan: 'Team',
account_id: team.account_id,
slug: team.slug,
personal_account: false,
})) || []),
];
// Use the first team or first entry in defaultTeams as activeTeam
const [activeTeam, setActiveTeam] = React.useState(defaultTeams[0]);
// Update active team when accounts load
React.useEffect(() => {
if (accounts?.length) {
const currentTeam = accounts.find(
(account) => account.account_id === activeTeam.account_id,
);
if (currentTeam) {
setActiveTeam({
name: currentTeam.name,
logo: currentTeam.personal_account ? Command : AudioWaveform,
plan: currentTeam.personal_account ? 'Personal' : 'Team',
account_id: currentTeam.account_id,
slug: currentTeam.slug,
personal_account: currentTeam.personal_account,
});
} else {
// If current team not found, set first available account as active
const firstAccount = accounts[0];
setActiveTeam({
name: firstAccount.name,
logo: firstAccount.personal_account ? Command : AudioWaveform,
plan: firstAccount.personal_account ? 'Personal' : 'Team',
account_id: firstAccount.account_id,
slug: firstAccount.slug,
personal_account: firstAccount.personal_account,
});
}
}
}, [accounts, activeTeam.account_id]);
// Handle team selection
const handleTeamSelect = (team) => {
setActiveTeam(team);
// Navigate to the appropriate dashboard
if (team.personal_account) {
router.push('/dashboard');
} else {
router.push(`/${team.slug}`);
}
};
const handleLogout = async () => {
const supabase = createClient();
await supabase.auth.signOut();
router.push('/auth');
};
const getInitials = (name: string) => {
return name
.split(' ')
.map((part) => part.charAt(0))
.join('')
.toUpperCase()
.substring(0, 2);
};
if (!activeTeam) {
return null;
}
return (
<Dialog open={showNewTeamDialog} onOpenChange={setShowNewTeamDialog}>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">
{getInitials(user.name)}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56"
side={isMobile ? 'bottom' : 'top'}
align="start"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1.5 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">
{getInitials(user.name)}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{/* Teams Section */}
{personalAccount && (
<>
<DropdownMenuLabel className="text-muted-foreground text-xs">
Personal Account
</DropdownMenuLabel>
<DropdownMenuItem
key={personalAccount.account_id}
onClick={() =>
handleTeamSelect({
name: personalAccount.name,
logo: Command,
plan: 'Personal',
account_id: personalAccount.account_id,
slug: personalAccount.slug,
personal_account: true,
})
}
className="gap-2 p-2"
>
<div className="flex size-6 items-center justify-center rounded-xs border">
<Command className="size-4 shrink-0" />
</div>
{personalAccount.name}
<DropdownMenuShortcut>1</DropdownMenuShortcut>
</DropdownMenuItem>
</>
)}
{teamAccounts?.length > 0 && (
<>
<DropdownMenuLabel className="text-muted-foreground text-xs mt-2">
Teams
</DropdownMenuLabel>
{teamAccounts.map((team, index) => (
<DropdownMenuItem
key={team.account_id}
onClick={() =>
handleTeamSelect({
name: team.name,
logo: AudioWaveform,
plan: 'Team',
account_id: team.account_id,
slug: team.slug,
personal_account: false,
})
}
className="gap-2 p-2"
>
<div className="flex size-6 items-center justify-center rounded-xs border">
<AudioWaveform className="size-4 shrink-0" />
</div>
{team.name}
<DropdownMenuShortcut>{index + 2}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
</>
)}
{/* <DropdownMenuSeparator />
<DialogTrigger asChild>
<DropdownMenuItem
className="gap-2 p-2"
onClick={() => {
setShowNewTeamDialog(true)
}}
>
<div className="bg-background flex size-6 items-center justify-center rounded-md border">
<Plus className="size-4" />
</div>
<div className="text-muted-foreground font-medium">Add team</div>
</DropdownMenuItem>
</DialogTrigger> */}
<DropdownMenuSeparator />
{/* User Settings Section */}
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<Link href="/settings/billing">
<CreditCard className="h-4 w-4" />
Billing
</Link>
</DropdownMenuItem>
{isLocalMode() && <DropdownMenuItem asChild>
<Link href="/settings/env-manager">
<KeyRound className="h-4 w-4" />
Local .Env Manager
</Link>
</DropdownMenuItem>}
{/* <DropdownMenuItem asChild>
<Link href="/settings">
<Settings className="mr-2 h-4 w-4" />
Settings
</Link>
</DropdownMenuItem> */}
<DropdownMenuItem
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
>
<div className="flex items-center gap-2">
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span>Theme</span>
</div>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem className='text-destructive focus:text-destructive focus:bg-destructive/10' onClick={handleLogout}>
<LogOut className="h-4 w-4 text-destructive" />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
<DialogContent className="sm:max-w-[425px] border-subtle dark:border-white/10 bg-card-bg dark:bg-background-secondary rounded-2xl shadow-custom">
<DialogHeader>
<DialogTitle className="text-foreground">
Create a new team
</DialogTitle>
<DialogDescription className="text-foreground/70">
Create a team to collaborate with others.
</DialogDescription>
</DialogHeader>
<NewTeamForm />
</DialogContent>
</Dialog>
);
}