diff options
| author | Bertrand Yuan <bert.yuan@outlook.com> | 2025-12-15 23:48:10 +0800 |
|---|---|---|
| committer | Bertrand Yuan <bert.yuan@outlook.com> | 2025-12-15 23:48:10 +0800 |
| commit | 5b7ccf0b671e2999b62befc729a3e517a0433728 (patch) | |
| tree | 8bf476dc7c75914c221042546840dc76267366df /src/components/auth | |
initial commit -- the front-end prototype
The initial code is base on Anirudh's work.
More to see at:
https://github.com/techwithanirudh/shadcn-blog
Therefore, the code in this commit is under MIT license.
Diffstat (limited to 'src/components/auth')
| -rw-r--r-- | src/components/auth/user-avatar.tsx | 56 | ||||
| -rw-r--r-- | src/components/auth/user-button.tsx | 127 |
2 files changed, 183 insertions, 0 deletions
diff --git a/src/components/auth/user-avatar.tsx b/src/components/auth/user-avatar.tsx new file mode 100644 index 0000000..53b20b5 --- /dev/null +++ b/src/components/auth/user-avatar.tsx @@ -0,0 +1,56 @@ +import type { ComponentProps } from 'react'; + +import { Icons } from '@/components/icons/icons'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import type { User } from '@/lib/auth-client'; +import { cn } from '@/lib/utils'; + +export interface UserAvatarClassNames { + base?: string; + image?: string; + fallback?: string; + fallbackIcon?: string; +} + +export interface UserAvatarProps { + user?: User | null; + classNames?: UserAvatarClassNames; +} + +export function UserAvatar({ + user, + classNames, + className, + ...props +}: UserAvatarProps & ComponentProps<typeof Avatar>) { + const name = user?.name || user?.email; + const src = user?.image; + + return ( + <Avatar + key={src} + className={cn('rounded-md', className, classNames?.base)} + {...props} + > + <AvatarImage + alt={name ?? 'Avatar'} + className={cn('rounded-md', classNames?.image)} + src={src ?? undefined} + /> + + <AvatarFallback + className={cn( + 'rounded-md bg-transparent uppercase', + classNames?.fallback, + )} + delayMs={src ? 200 : 0} + > + {firstTwoCharacters(name) ?? ( + <Icons.user className={cn('w-[55%]', classNames?.fallbackIcon)} /> + )} + </AvatarFallback> + </Avatar> + ); +} + +const firstTwoCharacters = (name?: string | null) => name?.slice(0, 2); diff --git a/src/components/auth/user-button.tsx b/src/components/auth/user-button.tsx new file mode 100644 index 0000000..7caea4c --- /dev/null +++ b/src/components/auth/user-button.tsx @@ -0,0 +1,127 @@ +'use client'; + +import Link from 'next/link'; + +import { Icons } from '@/components/icons/icons'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Skeleton } from '@/components/ui/skeleton'; +import { signOut, useSession } from '@/lib/auth-client'; +import { cn } from '@/lib/utils'; + +import type { UserAvatarClassNames } from './user-avatar'; +import { UserAvatar } from './user-avatar'; + +export interface UserButtonClassNames { + base?: string; + skeleton?: string; + trigger?: { + base?: string; + avatar?: UserAvatarClassNames; + skeleton?: string; + }; + content?: { + base?: string; + avatar?: UserAvatarClassNames; + menuItem?: string; + separator?: string; + }; +} + +export interface UserButtonProps { + className?: string; + classNames?: UserButtonClassNames; +} + +export function UserButton({ className, classNames }: UserButtonProps) { + const { data: sessionData, isPending } = useSession(); + const user = sessionData?.user ?? null; + + return ( + <DropdownMenu> + <DropdownMenuTrigger + className={cn('rounded-md bg-transparent', classNames?.trigger?.base)} + asChild + > + <Button + variant='ghost' + className='size-auto rounded-md border-none bg-transparent p-1.5 hover:bg-accent dark:hover:bg-accent' + disabled={isPending} + > + {isPending ? ( + <Skeleton + className={cn( + 'size-8 rounded-md', + className, + classNames?.base, + classNames?.skeleton, + classNames?.trigger?.skeleton, + )} + /> + ) : ( + <UserAvatar + className={cn('size-5', className, classNames?.base)} + classNames={classNames?.trigger?.avatar} + user={user} + /> + )} + </Button> + </DropdownMenuTrigger> + + <DropdownMenuContent + className={cn('max-w-64', classNames?.content?.base)} + onCloseAutoFocus={(e) => e.preventDefault()} + align='end' + > + {user ? ( + <div className='flex items-center gap-2 p-2'> + <UserAvatar classNames={classNames?.content?.avatar} user={user} /> + + <div className='flex flex-col truncate'> + <div className='truncate font-medium text-sm'> + {user.name || user.email} + </div> + + {user.name && ( + <div className='truncate text-muted-foreground text-xs'> + {user.email} + </div> + )} + </div> + </div> + ) : ( + <div className='px-2 py-1 text-muted-foreground text-xs'>Account</div> + )} + + <DropdownMenuSeparator className={classNames?.content?.separator} /> + + {!user ? ( + <> + <DropdownMenuItem className={classNames?.content?.menuItem} asChild> + <Link href={'/login'}> + <Icons.logIn className='size-4' /> + Sign In + </Link> + </DropdownMenuItem> + </> + ) : ( + <> + <DropdownMenuItem + className={classNames?.content?.menuItem} + onClick={() => signOut()} + > + <Icons.logOut className='size-4' /> + Log Out + </DropdownMenuItem> + </> + )} + </DropdownMenuContent> + </DropdownMenu> + ); +} |
