Merge branch 'main' into feat/add-runtime-env

This commit is contained in:
Lucas Smith
2024-02-08 22:06:59 +11:00
committed by GitHub
294 changed files with 15355 additions and 1451 deletions

View File

@ -5,6 +5,7 @@ import React from 'react';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
import type { Recipient } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
@ -48,8 +49,17 @@ export function AvatarWithRecipient({ recipient }: AvatarWithRecipientProps) {
type={getRecipientType(recipient)}
fallbackText={recipientAbbreviation(recipient)}
/>
<span className="text-muted-foreground text-sm">{recipient.email}</span>
<div>
<div
className="text-muted-foreground text-sm"
title="Click to copy signing link for sending to recipient"
>
<p>{recipient.email} </p>
<p className="text-muted-foreground/70 text-xs">
{RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName}
</p>
</div>
</div>
</div>
);
}

View File

@ -1,4 +1,5 @@
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
import type { Recipient } from '@documenso/prisma/client';
import {
@ -59,7 +60,12 @@ export const StackAvatarsWithTooltip = ({
type={getRecipientType(recipient)}
fallbackText={recipientAbbreviation(recipient)}
/>
<span className="text-muted-foreground text-sm">{recipient.email}</span>
<div className="">
<p className="text-muted-foreground text-sm">{recipient.email}</p>
<p className="text-muted-foreground/70 text-xs">
{RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName}
</p>
</div>
</div>
))}
</div>

View File

@ -197,20 +197,22 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
)}
{!currentPage && (
<>
<CommandGroup heading="Documents">
<CommandGroup className="mx-2 p-0 pb-2" heading="Documents">
<Commands push={push} pages={DOCUMENTS_PAGES} />
</CommandGroup>
<CommandGroup heading="Templates">
<CommandGroup className="mx-2 p-0 pb-2" heading="Templates">
<Commands push={push} pages={TEMPLATES_PAGES} />
</CommandGroup>
<CommandGroup heading="Settings">
<CommandGroup className="mx-2 p-0 pb-2" heading="Settings">
<Commands push={push} pages={SETTINGS_PAGES} />
</CommandGroup>
<CommandGroup heading="Preferences">
<CommandItem onSelect={() => addPage('theme')}>Change theme</CommandItem>
<CommandGroup className="mx-2 p-0 pb-2" heading="Preferences">
<CommandItem className="-mx-2 -my-1 rounded-lg" onSelect={() => addPage('theme')}>
Change theme
</CommandItem>
</CommandGroup>
{searchResults.length > 0 && (
<CommandGroup heading="Your documents">
<CommandGroup className="mx-2 p-0 pb-2" heading="Your documents">
<Commands push={push} pages={searchResults} />
</CommandGroup>
)}
@ -231,6 +233,7 @@ const Commands = ({
}) => {
return pages.map((page, idx) => (
<CommandItem
className="-mx-2 -my-1 rounded-lg"
key={page.path + idx}
value={page.value ?? page.label}
onSelect={() => push(page.path)}
@ -255,7 +258,7 @@ const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) =>
<CommandItem
key={theme.theme}
onSelect={() => setTheme(theme.theme)}
className="mx-2 first:mt-2 last:mb-2"
className="-my-1 mx-2 rounded-lg first:mt-2 last:mb-2"
>
<theme.icon className="mr-2" />
{theme.label}

View File

@ -4,10 +4,11 @@ import type { HTMLAttributes } from 'react';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useParams, usePathname } from 'next/navigation';
import { Search } from 'lucide-react';
import { getRootHref } from '@documenso/lib/utils/params';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@ -28,10 +29,13 @@ export type DesktopNavProps = HTMLAttributes<HTMLDivElement>;
export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
const pathname = usePathname();
const params = useParams();
const [open, setOpen] = useState(false);
const [modifierKey, setModifierKey] = useState(() => 'Ctrl');
const rootHref = getRootHref(params, { returnEmptyRootString: true });
useEffect(() => {
const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown';
const isMacOS = /Macintosh|Mac\s+OS\s+X/i.test(userAgent);
@ -51,11 +55,13 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
{navigationLinks.map(({ href, label }) => (
<Link
key={href}
href={href}
href={`${rootHref}${href}`}
className={cn(
'text-muted-foreground dark:text-muted focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2',
{
'text-foreground dark:text-muted-foreground': pathname?.startsWith(href),
'text-foreground dark:text-muted-foreground': pathname?.startsWith(
`${rootHref}${href}`,
),
},
)}
>

View File

@ -1,23 +1,40 @@
'use client';
import type { HTMLAttributes } from 'react';
import { useEffect, useState } from 'react';
import { type HTMLAttributes, useEffect, useState } from 'react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { MenuIcon, SearchIcon } from 'lucide-react';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import type { GetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
import { getRootHref } from '@documenso/lib/utils/params';
import type { User } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
import { Logo } from '~/components/branding/logo';
import { CommandMenu } from '../common/command-menu';
import { DesktopNav } from './desktop-nav';
import { MenuSwitcher } from './menu-switcher';
import { MobileNavigation } from './mobile-navigation';
import { ProfileDropdown } from './profile-dropdown';
export type HeaderProps = HTMLAttributes<HTMLDivElement> & {
user: User;
teams: GetTeamsResponse;
};
export const Header = ({ className, user, ...props }: HeaderProps) => {
export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
const params = useParams();
const { getFlag } = useFeatureFlags();
const isTeamsEnabled = getFlag('app_teams');
const [isCommandMenuOpen, setIsCommandMenuOpen] = useState(false);
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
@ -30,6 +47,34 @@ export const Header = ({ className, user, ...props }: HeaderProps) => {
return () => window.removeEventListener('scroll', onScroll);
}, []);
if (!isTeamsEnabled) {
return (
<header
className={cn(
'supports-backdrop-blur:bg-background/60 bg-background/95 sticky top-0 z-[60] flex h-16 w-full items-center border-b border-b-transparent backdrop-blur duration-200',
scrollY > 5 && 'border-b-border',
className,
)}
{...props}
>
<div className="mx-auto flex w-full max-w-screen-xl items-center justify-between gap-x-4 px-4 md:justify-normal md:px-8">
<Link
href="/"
className="focus-visible:ring-ring ring-offset-background rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Logo className="h-6 w-auto" />
</Link>
<DesktopNav />
<div className="flex gap-x-4 md:ml-8">
<ProfileDropdown user={user} />
</div>
</div>
</header>
);
}
return (
<header
className={cn(
@ -41,8 +86,8 @@ export const Header = ({ className, user, ...props }: HeaderProps) => {
>
<div className="mx-auto flex w-full max-w-screen-xl items-center justify-between gap-x-4 px-4 md:justify-normal md:px-8">
<Link
href="/"
className="focus-visible:ring-ring ring-offset-background rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
href={getRootHref(params)}
className="focus-visible:ring-ring ring-offset-background hidden rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 md:inline"
>
<Logo className="h-6 w-auto" />
</Link>
@ -50,11 +95,24 @@ export const Header = ({ className, user, ...props }: HeaderProps) => {
<DesktopNav />
<div className="flex gap-x-4 md:ml-8">
<ProfileDropdown user={user} />
<MenuSwitcher user={user} teams={teams} />
</div>
{/* <Button variant="outline" size="sm" className="h-10 w-10 p-0.5 md:hidden">
<Menu className="h-6 w-6" />
</Button> */}
<div className="flex flex-row items-center space-x-4 md:hidden">
<button onClick={() => setIsCommandMenuOpen(true)}>
<SearchIcon className="text-muted-foreground h-6 w-6" />
</button>
<button onClick={() => setIsHamburgerMenuOpen(true)}>
<MenuIcon className="text-muted-foreground h-6 w-6" />
</button>
<CommandMenu open={isCommandMenuOpen} onOpenChange={setIsCommandMenuOpen} />
<MobileNavigation
isMenuOpen={isHamburgerMenuOpen}
onMenuOpenChange={setIsHamburgerMenuOpen}
/>
</div>
</div>
</header>

View File

@ -0,0 +1,230 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { CheckCircle2, ChevronsUpDown, Plus, Settings2 } from 'lucide-react';
import { signOut } from 'next-auth/react';
import { TEAM_MEMBER_ROLE_MAP, TEAM_URL_REGEX } from '@documenso/lib/constants/teams';
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
import type { GetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
import type { User } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu';
export type MenuSwitcherProps = {
user: User;
teams: GetTeamsResponse;
};
export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProps) => {
const pathname = usePathname();
const isUserAdmin = isAdmin(user);
const { data: teamsQueryResult } = trpc.team.getTeams.useQuery(undefined, {
initialData: initialTeamsData,
});
const teams = teamsQueryResult && teamsQueryResult.length > 0 ? teamsQueryResult : null;
const isPathTeamUrl = (teamUrl: string) => {
if (!pathname || !pathname.startsWith(`/t/`)) {
return false;
}
return pathname.split('/')[2] === teamUrl;
};
const selectedTeam = teams?.find((team) => isPathTeamUrl(team.url));
const formatAvatarFallback = (teamName?: string) => {
if (teamName !== undefined) {
return teamName.slice(0, 1).toUpperCase();
}
return user.name ? extractInitials(user.name) : user.email.slice(0, 1).toUpperCase();
};
const formatSecondaryAvatarText = (team?: typeof selectedTeam) => {
if (!team) {
return 'Personal Account';
}
if (team.ownerUserId === user.id) {
return 'Owner';
}
return TEAM_MEMBER_ROLE_MAP[team.currentTeamMember.role];
};
/**
* Formats the redirect URL so we can switch between documents and templates page
* seemlessly between teams and personal accounts.
*/
const formatRedirectUrlOnSwitch = (teamUrl?: string) => {
const baseUrl = teamUrl ? `/t/${teamUrl}/` : '/';
const currentPathname = (pathname ?? '/').replace(TEAM_URL_REGEX, '');
if (currentPathname === '/templates') {
return `${baseUrl}templates`;
}
return baseUrl;
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
data-testid="menu-switcher"
variant="none"
className="relative flex h-12 flex-row items-center px-2 py-2 ring-0 focus-visible:border-0 focus-visible:ring-0"
>
<AvatarWithText
avatarFallback={formatAvatarFallback(selectedTeam?.name)}
primaryText={selectedTeam ? selectedTeam.name : user.name}
secondaryText={formatSecondaryAvatarText(selectedTeam)}
rightSideComponent={
<ChevronsUpDown className="text-muted-foreground ml-auto h-4 w-4" />
}
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className={cn('z-[60] ml-2 w-full md:ml-0', teams ? 'min-w-[20rem]' : 'min-w-[12rem]')}
align="end"
forceMount
>
{teams ? (
<>
<DropdownMenuLabel>Personal</DropdownMenuLabel>
<DropdownMenuItem asChild>
<Link href={formatRedirectUrlOnSwitch()}>
<AvatarWithText
avatarFallback={formatAvatarFallback()}
primaryText={user.name}
secondaryText={formatSecondaryAvatarText()}
rightSideComponent={
!pathname?.startsWith(`/t/`) && (
<CheckCircle2 className="ml-auto fill-black text-white dark:fill-white dark:text-black" />
)
}
/>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator className="mt-2" />
<DropdownMenuLabel>
<div className="flex flex-row items-center justify-between">
<p>Teams</p>
<div className="flex flex-row space-x-2">
<DropdownMenuItem asChild>
<Button
title="Manage teams"
variant="ghost"
className="text-muted-foreground flex h-5 w-5 items-center justify-center p-0"
asChild
>
<Link href="/settings/teams">
<Settings2 className="h-4 w-4" />
</Link>
</Button>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Button
title="Create team"
variant="ghost"
className="text-muted-foreground flex h-5 w-5 items-center justify-center p-0"
asChild
>
<Link href="/settings/teams?action=add-team">
<Plus className="h-4 w-4" />
</Link>
</Button>
</DropdownMenuItem>
</div>
</div>
</DropdownMenuLabel>
{teams.map((team) => (
<DropdownMenuItem asChild key={team.id}>
<Link href={formatRedirectUrlOnSwitch(team.url)}>
<AvatarWithText
avatarFallback={formatAvatarFallback(team.name)}
primaryText={team.name}
secondaryText={formatSecondaryAvatarText(team)}
rightSideComponent={
isPathTeamUrl(team.url) && (
<CheckCircle2 className="ml-auto fill-black text-white dark:fill-white dark:text-black" />
)
}
/>
</Link>
</DropdownMenuItem>
))}
</>
) : (
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
<Link
href="/settings/teams?action=add-team"
className="flex items-center justify-between"
>
Create team
<Plus className="ml-2 h-4 w-4" />
</Link>
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
{isUserAdmin && (
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
<Link href="/admin">Admin panel</Link>
</DropdownMenuItem>
)}
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
<Link href="/settings/profile">User settings</Link>
</DropdownMenuItem>
{selectedTeam &&
canExecuteTeamAction('MANAGE_TEAM', selectedTeam.currentTeamMember.role) && (
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
<Link href={`/t/${selectedTeam.url}/settings/`}>Team settings</Link>
</DropdownMenuItem>
)}
<DropdownMenuItem
className="text-destructive/90 hover:!text-destructive px-4 py-2"
onSelect={async () =>
signOut({
callbackUrl: '/',
})
}
>
Sign Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@ -0,0 +1,96 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { signOut } from 'next-auth/react';
import LogoImage from '@documenso/assets/logo.png';
import { getRootHref } from '@documenso/lib/utils/params';
import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
export type MobileNavigationProps = {
isMenuOpen: boolean;
onMenuOpenChange?: (_value: boolean) => void;
};
export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigationProps) => {
const params = useParams();
const handleMenuItemClick = () => {
onMenuOpenChange?.(false);
};
const rootHref = getRootHref(params, { returnEmptyRootString: true });
const menuNavigationLinks = [
{
href: `${rootHref}/documents`,
text: 'Documents',
},
{
href: `${rootHref}/templates`,
text: 'Templates',
},
{
href: '/settings/teams',
text: 'Teams',
},
{
href: '/settings/profile',
text: 'Settings',
},
];
return (
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
<SheetContent className="flex w-full max-w-[400px] flex-col">
<Link href="/" onClick={handleMenuItemClick}>
<Image
src={LogoImage}
alt="Documenso Logo"
className="dark:invert"
width={170}
height={25}
/>
</Link>
<div className="mt-8 flex w-full flex-col items-start gap-y-4">
{menuNavigationLinks.map(({ href, text }) => (
<Link
key={href}
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
href={href}
onClick={() => handleMenuItemClick()}
>
{text}
</Link>
))}
<button
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
onClick={async () =>
signOut({
callbackUrl: '/',
})
}
>
Sign Out
</button>
</div>
<div className="mt-auto flex w-full flex-col space-y-4 self-end">
<div className="w-fit">
<ThemeSwitcher />
</div>
<p className="text-muted-foreground text-sm">
© {new Date().getFullYear()} Documenso, Inc. All rights reserved.
</p>
</div>
</SheetContent>
</Sheet>
);
};

View File

@ -20,7 +20,7 @@ import { LuGithub } from 'react-icons/lu';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
import { recipientInitials } from '@documenso/lib/utils/recipient-formatter';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import type { User } from '@documenso/prisma/client';
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
@ -51,7 +51,7 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
const isBillingEnabled = getFlag('app_billing');
const avatarFallback = user.name
? recipientInitials(user.name)
? extractInitials(user.name)
: user.email.slice(0, 1).toUpperCase();
return (

View File

@ -21,9 +21,9 @@ export const PeriodSelector = () => {
const router = useRouter();
const period = useMemo(() => {
const p = searchParams?.get('period') ?? '';
const p = searchParams?.get('period') ?? 'all';
return isPeriodSelectorValue(p) ? p : '';
return isPeriodSelectorValue(p) ? p : 'all';
}, [searchParams]);
const onPeriodChange = (newPeriod: string) => {
@ -35,7 +35,7 @@ export const PeriodSelector = () => {
params.set('period', newPeriod);
if (newPeriod === '') {
if (newPeriod === '' || newPeriod === 'all') {
params.delete('period');
}
@ -49,7 +49,7 @@ export const PeriodSelector = () => {
</SelectTrigger>
<SelectContent position="popper">
<SelectItem value="">All Time</SelectItem>
<SelectItem value="all">All Time</SelectItem>
<SelectItem value="7d">Last 7 days</SelectItem>
<SelectItem value="14d">Last 14 days</SelectItem>
<SelectItem value="30d">Last 30 days</SelectItem>

View File

@ -1,4 +1,4 @@
export type PeriodSelectorValue = '' | '7d' | '14d' | '30d';
import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents';
export const isPeriodSelectorValue = (value: unknown): value is PeriodSelectorValue => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions

View File

@ -1,11 +1,11 @@
'use client';
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { CreditCard, Lock, User } from 'lucide-react';
import { CreditCard, Lock, User, Users } from 'lucide-react';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { cn } from '@documenso/ui/lib/utils';
@ -19,6 +19,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
const { getFlag } = useFeatureFlags();
const isBillingEnabled = getFlag('app_billing');
const isTeamsEnabled = getFlag('app_teams');
return (
<div className={cn('flex flex-col gap-y-2', className)} {...props}>
@ -35,6 +36,21 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
</Button>
</Link>
{isTeamsEnabled && (
<Link href="/settings/teams">
<Button
variant="ghost"
className={cn(
'w-full justify-start',
pathname?.startsWith('/settings/teams') && 'bg-secondary',
)}
>
<Users className="mr-2 h-5 w-5" />
Teams
</Button>
</Link>
)}
<Link href="/settings/security">
<Button
variant="ghost"

View File

@ -0,0 +1,25 @@
import React from 'react';
export type SettingsHeaderProps = {
title: string;
subtitle: string;
children?: React.ReactNode;
};
export const SettingsHeader = ({ children, title, subtitle }: SettingsHeaderProps) => {
return (
<>
<div className="flex flex-row items-center justify-between">
<div>
<h3 className="text-lg font-medium">{title}</h3>
<p className="text-muted-foreground text-sm md:mt-2">{subtitle}</p>
</div>
{children}
</div>
<hr className="my-4" />
</>
);
};

View File

@ -1,11 +1,11 @@
'use client';
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { CreditCard, Lock, User } from 'lucide-react';
import { CreditCard, Lock, User, Users } from 'lucide-react';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { cn } from '@documenso/ui/lib/utils';
@ -19,6 +19,7 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
const { getFlag } = useFeatureFlags();
const isBillingEnabled = getFlag('app_billing');
const isTeamsEnabled = getFlag('app_teams');
return (
<div
@ -38,6 +39,21 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
</Button>
</Link>
{isTeamsEnabled && (
<Link href="/settings/teams">
<Button
variant="ghost"
className={cn(
'w-full justify-start',
pathname?.startsWith('/settings/teams') && 'bg-secondary',
)}
>
<Users className="mr-2 h-5 w-5" />
Teams
</Button>
</Link>
)}
<Link href="/settings/security">
<Button
variant="ghost"