Files
spoon/apps/next/src/components/layout/header/controls/AvatarDropdown.tsx
T
Gabriel Brown 348c2d3d62
Build and Deploy Spoon / quality (push) Successful in 1m40s
Build and Deploy Spoon / build-images (push) Successful in 12m23s
feat: add PostHog, move Plausible to v4, and drop Sentry session replay
PostHog loads after the page is idle through a same-origin /ingest proxy and follows the signed-in Convex user. Plausible uses the per-site script through a /pa proxy. Sentry keeps errors and traces only. Pages load only the @spoon/ui modules they import, and client code reads public env from publicEnv.ts so zod stays out of the browser. Both analytics integrations stay off until their env is set in Infisical prod.
2026-10-01 17:17:21 -04:00

101 lines
3.0 KiB
TypeScript

'use client';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { resetAnalyticsIdentity, useAnalyticsIdentity } from '@/lib/analytics';
import { isRemoteImageUrl } from '@/lib/avatar';
import { useAuthActions } from '@convex-dev/auth/react';
import { useConvexAuth, useQuery } from 'convex/react';
import { api } from '@spoon/backend/convex/_generated/api.js';
import {
BasedAvatar,
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@spoon/ui';
export const AvatarDropdown = () => {
const router = useRouter();
const { isLoading, isAuthenticated } = useConvexAuth();
const { signOut } = useAuthActions();
const user = useQuery(api.auth.getUser, {});
useAnalyticsIdentity(user?._id);
const remoteImageUrl = isRemoteImageUrl(user?.image) ? user?.image : null;
const currentImageUrl = useQuery(
api.files.getImageUrl,
user?.image && !remoteImageUrl ? { storageId: user.image } : 'skip',
);
const avatarUrl = remoteImageUrl ?? currentImageUrl;
// Empty/whitespace names must fall through to the email, so intentionally
// treat a blank string as "no name" (nullish coalescing would keep it).
const trimmedName = user?.name?.trim();
const displayName = trimmedName?.length ? trimmedName : user?.email?.trim();
if (isLoading) {
return (
<div className='flex items-center gap-2'>
<div className='bg-muted h-8 w-16 animate-pulse rounded-md' />
<div className='bg-muted h-9 w-9 animate-pulse rounded-full' />
</div>
);
}
if (!isAuthenticated) {
return (
<div className='flex items-center gap-2'>
<Button size='sm' asChild>
<Link href='/sign-in'>Sign In</Link>
</Button>
</div>
);
}
return (
<DropdownMenu>
<DropdownMenuTrigger>
<BasedAvatar
src={avatarUrl}
fullName={user?.name}
className='h-9 w-9'
fallbackProps={{ className: 'text-sm font-semibold' }}
userIconProps={{ size: 20 }}
/>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
{displayName && (
<>
<DropdownMenuLabel className='text-center font-bold'>
{displayName}
</DropdownMenuLabel>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem asChild>
<Link href='/settings/profile' className='w-full cursor-pointer'>
Edit Profile
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<button
onClick={() =>
void signOut().then(() => {
resetAnalyticsIdentity();
router.push('/');
})
}
className='w-full cursor-pointer'
>
Sign Out
</button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};