user menu on the sidebar
This commit is contained in:
parent
77910a05ae
commit
8d7c41bac4
@ -17,6 +17,7 @@
|
||||
"@heroicons/react": "^2.1.5",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-popover": "^1.1.1",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
|
@ -14,6 +14,11 @@ export type User = {
|
||||
*/
|
||||
username: string;
|
||||
|
||||
/**
|
||||
* The hash to the avatar of this user, if any.
|
||||
*/
|
||||
avatar: string | undefined;
|
||||
|
||||
/**
|
||||
* The tier of this user.
|
||||
*/
|
||||
|
@ -3,6 +3,9 @@ import Image from "next/image";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* The variants of the branding.
|
||||
*/
|
||||
const brandingVariants = cva(
|
||||
"relative group-hover:opacity-75 hover:opacity-75 select-none transition-all transform-gpu",
|
||||
{
|
||||
@ -45,7 +48,7 @@ const Branding = ({ href, size, className }: BrandingProps) => (
|
||||
className={cn(brandingVariants({ size, className }))}
|
||||
href={href ?? "/"}
|
||||
>
|
||||
<Image src="/media/logo.png" alt="PulseApp Logo" fill priority />
|
||||
<Image src="/media/logo.png" alt="Pulse App Logo" fill priority />
|
||||
</Link>
|
||||
);
|
||||
export default Branding;
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { useOrganizationContext } from "@/app/provider/organization-provider";
|
||||
import { OrganizationState } from "@/app/store/organization-store";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
const links: SidebarLink[] = [
|
||||
{
|
||||
@ -53,10 +54,12 @@ const Links = (): ReactElement => {
|
||||
const selectedOrganization: string | undefined = useOrganizationContext(
|
||||
(state: OrganizationState) => state.selected
|
||||
);
|
||||
const path: string = usePathname();
|
||||
return (
|
||||
<div className="mt-3.5 w-full flex flex-col gap-0.5">
|
||||
<div className="mt-3.5 w-full flex flex-col gap-0.5 select-none">
|
||||
{links.map((link: SidebarLink, index: number) => {
|
||||
const active: boolean = index === 0;
|
||||
const href: string = `/dashboard/org/${selectedOrganization}${link.href}`;
|
||||
const active: boolean = path.startsWith(href);
|
||||
return (
|
||||
<SimpleTooltip
|
||||
key={index}
|
||||
@ -68,7 +71,7 @@ const Links = (): ReactElement => {
|
||||
"px-3 py-2 flex gap-2 items-center text-sm rounded-lg hover:bg-zinc-800 transition-all transform-gpu",
|
||||
active && "font-medium bg-zinc-800"
|
||||
)}
|
||||
href={`/dashboard/org/${selectedOrganization}${link.href}`}
|
||||
href={href}
|
||||
>
|
||||
<div className="relative w-5 h-5">{link.icon}</div>
|
||||
{link.name}
|
||||
|
@ -4,7 +4,6 @@ import * as React from "react";
|
||||
import { ReactElement, useEffect, useState } from "react";
|
||||
import { ChevronsUpDownIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import InitialsAvatar from "react-initials-avatar";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@ -22,7 +21,7 @@ import { useOrganizationContext } from "@/app/provider/organization-provider";
|
||||
import { OrganizationState } from "@/app/store/organization-store";
|
||||
import { Organization } from "@/app/types/org/organization";
|
||||
import { CheckIcon } from "@heroicons/react/24/outline";
|
||||
import Image from "next/image";
|
||||
import OrganizationLogo from "@/components/org/organization-logo";
|
||||
|
||||
/**
|
||||
* The organization selector.
|
||||
@ -84,18 +83,10 @@ const OrganizationSelector = (): ReactElement => {
|
||||
>
|
||||
{selected ? (
|
||||
<div className="flex gap-2.5 items-center">
|
||||
<div className="relative p-0.5 w-5 h-5 rounded-full">
|
||||
{selected.logo ? (
|
||||
<Image
|
||||
className="rounded-full"
|
||||
src={`${process.env.NEXT_PUBLIC_CDN_ENDPOINT}/organizations/${selected.logo}.webp`}
|
||||
alt={`${selected.name}'s Logo`}
|
||||
fill
|
||||
/>
|
||||
) : (
|
||||
<InitialsAvatar name={selected.name} />
|
||||
)}
|
||||
</div>
|
||||
<OrganizationLogo
|
||||
organization={selected}
|
||||
size="sm"
|
||||
/>
|
||||
{selected.name}
|
||||
</div>
|
||||
) : (
|
||||
@ -114,7 +105,7 @@ const OrganizationSelector = (): ReactElement => {
|
||||
(organization: Organization, index: number) => (
|
||||
<CommandItem
|
||||
key={index}
|
||||
className="flex justify-between"
|
||||
className="flex gap-2 items-center"
|
||||
value={organization.name}
|
||||
onSelect={(currentValue: string) =>
|
||||
selectOrganization(
|
||||
@ -126,10 +117,14 @@ const OrganizationSelector = (): ReactElement => {
|
||||
)
|
||||
}
|
||||
>
|
||||
<OrganizationLogo
|
||||
organization={organization}
|
||||
size="sm"
|
||||
/>
|
||||
{organization.name}
|
||||
{organization.snowflake ===
|
||||
selectedOrganization && (
|
||||
<CheckIcon className="mr-2 w-4 h-4" />
|
||||
<CheckIcon className="mx-auto w-4 h-4" />
|
||||
)}
|
||||
</CommandItem>
|
||||
)
|
||||
|
@ -11,7 +11,13 @@ import { useUserContext } from "@/app/provider/user-provider";
|
||||
import { UserState } from "@/app/store/user-store";
|
||||
import { hasFlag } from "@/lib/user";
|
||||
import { UserFlag } from "@/app/types/user/user-flag";
|
||||
import UserMenu from "@/components/dashboard/sidebar/user-menu";
|
||||
|
||||
/**
|
||||
* The sidebar to display on the dashboard.
|
||||
*
|
||||
* @return the sidebar jsx
|
||||
*/
|
||||
const Sidebar = (): ReactElement => {
|
||||
const user: User | undefined = useUserContext(
|
||||
(state: UserState) => state.user
|
||||
@ -19,7 +25,10 @@ const Sidebar = (): ReactElement => {
|
||||
return hasFlag(user as User, UserFlag.COMPLETED_ONBOARDING) ? (
|
||||
<nav className="w-56 px-3 py-4 h-screen flex flex-col items-center bg-zinc-900 border-r">
|
||||
{/* Header */}
|
||||
<Link className="flex gap-3 items-center group" href="/dashboard">
|
||||
<Link
|
||||
className="flex gap-3 items-center select-none group"
|
||||
href="/dashboard"
|
||||
>
|
||||
<Branding size="xs" />
|
||||
<h1 className="text-xl font-bold group-hover:opacity-75 transition-all transform-gpu">
|
||||
Pulse App
|
||||
@ -30,7 +39,9 @@ const Sidebar = (): ReactElement => {
|
||||
{/* Content */}
|
||||
<OrganizationSelector />
|
||||
<Links />
|
||||
<div className="mt-auto">USER</div>
|
||||
<div className="mt-auto">
|
||||
<UserMenu />
|
||||
</div>
|
||||
</nav>
|
||||
) : (
|
||||
<div />
|
||||
|
84
src/components/dashboard/sidebar/user-menu.tsx
Normal file
84
src/components/dashboard/sidebar/user-menu.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { ReactElement } from "react";
|
||||
import { useUserContext } from "@/app/provider/user-provider";
|
||||
import { UserState } from "@/app/store/user-store";
|
||||
import { User } from "@/app/types/user/user";
|
||||
import UserAvatar from "@/components/user/user-avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
ArrowLeftEndOnRectangleIcon,
|
||||
Cog6ToothIcon,
|
||||
CreditCardIcon,
|
||||
UserIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import Link from "next/link";
|
||||
|
||||
const UserMenu = (): ReactElement => {
|
||||
const user: User | undefined = useUserContext(
|
||||
(state: UserState) => state.user
|
||||
);
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<div className="px-5 py-2 flex gap-3 items-center font-medium bg-background/30 border hover:opacity-75 rounded-lg transition-all transform-gpu">
|
||||
<UserAvatar user={user as User} size="sm" />@
|
||||
{user?.username}
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-44">
|
||||
{/* Content */}
|
||||
<MyAccount />
|
||||
|
||||
{/* Logout */}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="gap-2.5 text-red-500">
|
||||
<ArrowLeftEndOnRectangleIcon className="w-5 h-5" />
|
||||
<span>Logout</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* The my account section.
|
||||
*
|
||||
* @return the section jsx
|
||||
*/
|
||||
const MyAccount = (): ReactElement => (
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="select-none pointer-events-none">
|
||||
My Account
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<Link href="/dashboard/user/profile">
|
||||
<DropdownMenuItem className="gap-2.5">
|
||||
<UserIcon className="w-5 h-5" />
|
||||
<span>Profile</span>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link href="/dashboard/user/billing">
|
||||
<DropdownMenuItem className="gap-2.5">
|
||||
<CreditCardIcon className="w-5 h-5" />
|
||||
<span>Billing</span>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link href="/dashboard/user/settings">
|
||||
<DropdownMenuItem className="gap-2.5">
|
||||
<Cog6ToothIcon className="w-5 h-5" />
|
||||
<span>Settings</span>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
</DropdownMenuGroup>
|
||||
);
|
||||
|
||||
export default UserMenu;
|
70
src/components/org/organization-logo.tsx
Normal file
70
src/components/org/organization-logo.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import * as React from "react";
|
||||
import { ReactElement } from "react";
|
||||
import { cva } from "class-variance-authority";
|
||||
import Image from "next/image";
|
||||
import InitialsAvatar from "react-initials-avatar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Organization } from "@/app/types/org/organization";
|
||||
|
||||
/**
|
||||
* The variants of the logo.
|
||||
*/
|
||||
const logoVariants = cva("relative rounded-full", {
|
||||
variants: {
|
||||
size: {
|
||||
sm: "w-5 h-5",
|
||||
default: "w-10 h-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* The props for this component.
|
||||
*/
|
||||
type OrganizationLogoProps = {
|
||||
/**
|
||||
* The organization to show the logo for.
|
||||
*/
|
||||
organization: Organization;
|
||||
|
||||
/**
|
||||
* The size of the logo.
|
||||
*/
|
||||
size?: "sm" | "default";
|
||||
|
||||
/**
|
||||
* The optional class name to apply to the logo.
|
||||
*/
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A logo for an organization.
|
||||
*
|
||||
* @param organization the organization
|
||||
* @param size the size
|
||||
* @param className additional class names
|
||||
* @return the organization jsx
|
||||
*/
|
||||
const OrganizationLogo = ({
|
||||
organization,
|
||||
size,
|
||||
className,
|
||||
}: OrganizationLogoProps): ReactElement => (
|
||||
<div className={cn(logoVariants({ size, className }))}>
|
||||
{organization.logo ? (
|
||||
<Image
|
||||
className="rounded-full"
|
||||
src={`${process.env.NEXT_PUBLIC_CDN_ENDPOINT}/organizations/${organization.logo}.webp`}
|
||||
alt={`${organization.name}'s Logo`}
|
||||
fill
|
||||
/>
|
||||
) : (
|
||||
<InitialsAvatar name={organization.name} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
export default OrganizationLogo;
|
208
src/components/ui/dropdown-menu.tsx
Normal file
208
src/components/ui/dropdown-menu.tsx
Normal file
@ -0,0 +1,208 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronRightIcon,
|
||||
DotFilledIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<DotFilledIcon className="h-4 w-4 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest opacity-60",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
};
|
70
src/components/user/user-avatar.tsx
Normal file
70
src/components/user/user-avatar.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import * as React from "react";
|
||||
import { ReactElement } from "react";
|
||||
import { cva } from "class-variance-authority";
|
||||
import Image from "next/image";
|
||||
import InitialsAvatar from "react-initials-avatar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { User } from "@/app/types/user/user";
|
||||
|
||||
/**
|
||||
* The variants of the avatar.
|
||||
*/
|
||||
const avatarVariants = cva("relative rounded-full", {
|
||||
variants: {
|
||||
size: {
|
||||
sm: "w-6 h-6",
|
||||
default: "w-10 h-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* The props for this component.
|
||||
*/
|
||||
type UserAvatarProps = {
|
||||
/**
|
||||
* The user to show the avatar for.
|
||||
*/
|
||||
user: User;
|
||||
|
||||
/**
|
||||
* The size of the avatar.
|
||||
*/
|
||||
size?: "sm" | "default";
|
||||
|
||||
/**
|
||||
* The optional class name to apply to the logo.
|
||||
*/
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* An avatar for a user.
|
||||
*
|
||||
* @param user the user
|
||||
* @param size the size
|
||||
* @param className additional class names
|
||||
* @return the avatar jsx
|
||||
*/
|
||||
const UserAvatar = ({
|
||||
user,
|
||||
size,
|
||||
className,
|
||||
}: UserAvatarProps): ReactElement => (
|
||||
<div className={cn(avatarVariants({ size, className }))}>
|
||||
{user.avatar ? (
|
||||
<Image
|
||||
className="rounded-full"
|
||||
src={`${process.env.NEXT_PUBLIC_CDN_ENDPOINT}/avatars/${user.avatar}.webp`}
|
||||
alt={`${user.username}'s Avatar`}
|
||||
fill
|
||||
/>
|
||||
) : (
|
||||
<InitialsAvatar name={user.username} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
export default UserAvatar;
|
Loading…
x
Reference in New Issue
Block a user