user menu on the sidebar
This commit is contained in:
parent
77910a05ae
commit
8d7c41bac4
@ -17,6 +17,7 @@
|
|||||||
"@heroicons/react": "^2.1.5",
|
"@heroicons/react": "^2.1.5",
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@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-icons": "^1.3.0",
|
||||||
"@radix-ui/react-popover": "^1.1.1",
|
"@radix-ui/react-popover": "^1.1.1",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
|
@ -14,6 +14,11 @@ export type User = {
|
|||||||
*/
|
*/
|
||||||
username: string;
|
username: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hash to the avatar of this user, if any.
|
||||||
|
*/
|
||||||
|
avatar: string | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The tier of this user.
|
* The tier of this user.
|
||||||
*/
|
*/
|
||||||
|
@ -3,6 +3,9 @@ import Image from "next/image";
|
|||||||
import { cva } from "class-variance-authority";
|
import { cva } from "class-variance-authority";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The variants of the branding.
|
||||||
|
*/
|
||||||
const brandingVariants = cva(
|
const brandingVariants = cva(
|
||||||
"relative group-hover:opacity-75 hover:opacity-75 select-none transition-all transform-gpu",
|
"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 }))}
|
className={cn(brandingVariants({ size, className }))}
|
||||||
href={href ?? "/"}
|
href={href ?? "/"}
|
||||||
>
|
>
|
||||||
<Image src="/media/logo.png" alt="PulseApp Logo" fill priority />
|
<Image src="/media/logo.png" alt="Pulse App Logo" fill priority />
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
export default Branding;
|
export default Branding;
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { useOrganizationContext } from "@/app/provider/organization-provider";
|
import { useOrganizationContext } from "@/app/provider/organization-provider";
|
||||||
import { OrganizationState } from "@/app/store/organization-store";
|
import { OrganizationState } from "@/app/store/organization-store";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
const links: SidebarLink[] = [
|
const links: SidebarLink[] = [
|
||||||
{
|
{
|
||||||
@ -53,10 +54,12 @@ const Links = (): ReactElement => {
|
|||||||
const selectedOrganization: string | undefined = useOrganizationContext(
|
const selectedOrganization: string | undefined = useOrganizationContext(
|
||||||
(state: OrganizationState) => state.selected
|
(state: OrganizationState) => state.selected
|
||||||
);
|
);
|
||||||
|
const path: string = usePathname();
|
||||||
return (
|
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) => {
|
{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 (
|
return (
|
||||||
<SimpleTooltip
|
<SimpleTooltip
|
||||||
key={index}
|
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",
|
"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"
|
active && "font-medium bg-zinc-800"
|
||||||
)}
|
)}
|
||||||
href={`/dashboard/org/${selectedOrganization}${link.href}`}
|
href={href}
|
||||||
>
|
>
|
||||||
<div className="relative w-5 h-5">{link.icon}</div>
|
<div className="relative w-5 h-5">{link.icon}</div>
|
||||||
{link.name}
|
{link.name}
|
||||||
|
@ -4,7 +4,6 @@ import * as React from "react";
|
|||||||
import { ReactElement, useEffect, useState } from "react";
|
import { ReactElement, useEffect, useState } from "react";
|
||||||
import { ChevronsUpDownIcon } from "lucide-react";
|
import { ChevronsUpDownIcon } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import InitialsAvatar from "react-initials-avatar";
|
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@ -22,7 +21,7 @@ import { useOrganizationContext } from "@/app/provider/organization-provider";
|
|||||||
import { OrganizationState } from "@/app/store/organization-store";
|
import { OrganizationState } from "@/app/store/organization-store";
|
||||||
import { Organization } from "@/app/types/org/organization";
|
import { Organization } from "@/app/types/org/organization";
|
||||||
import { CheckIcon } from "@heroicons/react/24/outline";
|
import { CheckIcon } from "@heroicons/react/24/outline";
|
||||||
import Image from "next/image";
|
import OrganizationLogo from "@/components/org/organization-logo";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The organization selector.
|
* The organization selector.
|
||||||
@ -84,18 +83,10 @@ const OrganizationSelector = (): ReactElement => {
|
|||||||
>
|
>
|
||||||
{selected ? (
|
{selected ? (
|
||||||
<div className="flex gap-2.5 items-center">
|
<div className="flex gap-2.5 items-center">
|
||||||
<div className="relative p-0.5 w-5 h-5 rounded-full">
|
<OrganizationLogo
|
||||||
{selected.logo ? (
|
organization={selected}
|
||||||
<Image
|
size="sm"
|
||||||
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>
|
|
||||||
{selected.name}
|
{selected.name}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -114,7 +105,7 @@ const OrganizationSelector = (): ReactElement => {
|
|||||||
(organization: Organization, index: number) => (
|
(organization: Organization, index: number) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={index}
|
key={index}
|
||||||
className="flex justify-between"
|
className="flex gap-2 items-center"
|
||||||
value={organization.name}
|
value={organization.name}
|
||||||
onSelect={(currentValue: string) =>
|
onSelect={(currentValue: string) =>
|
||||||
selectOrganization(
|
selectOrganization(
|
||||||
@ -126,10 +117,14 @@ const OrganizationSelector = (): ReactElement => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<OrganizationLogo
|
||||||
|
organization={organization}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
{organization.name}
|
{organization.name}
|
||||||
{organization.snowflake ===
|
{organization.snowflake ===
|
||||||
selectedOrganization && (
|
selectedOrganization && (
|
||||||
<CheckIcon className="mr-2 w-4 h-4" />
|
<CheckIcon className="mx-auto w-4 h-4" />
|
||||||
)}
|
)}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
)
|
)
|
||||||
|
@ -11,7 +11,13 @@ import { useUserContext } from "@/app/provider/user-provider";
|
|||||||
import { UserState } from "@/app/store/user-store";
|
import { UserState } from "@/app/store/user-store";
|
||||||
import { hasFlag } from "@/lib/user";
|
import { hasFlag } from "@/lib/user";
|
||||||
import { UserFlag } from "@/app/types/user/user-flag";
|
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 Sidebar = (): ReactElement => {
|
||||||
const user: User | undefined = useUserContext(
|
const user: User | undefined = useUserContext(
|
||||||
(state: UserState) => state.user
|
(state: UserState) => state.user
|
||||||
@ -19,7 +25,10 @@ const Sidebar = (): ReactElement => {
|
|||||||
return hasFlag(user as User, UserFlag.COMPLETED_ONBOARDING) ? (
|
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">
|
<nav className="w-56 px-3 py-4 h-screen flex flex-col items-center bg-zinc-900 border-r">
|
||||||
{/* Header */}
|
{/* 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" />
|
<Branding size="xs" />
|
||||||
<h1 className="text-xl font-bold group-hover:opacity-75 transition-all transform-gpu">
|
<h1 className="text-xl font-bold group-hover:opacity-75 transition-all transform-gpu">
|
||||||
Pulse App
|
Pulse App
|
||||||
@ -30,7 +39,9 @@ const Sidebar = (): ReactElement => {
|
|||||||
{/* Content */}
|
{/* Content */}
|
||||||
<OrganizationSelector />
|
<OrganizationSelector />
|
||||||
<Links />
|
<Links />
|
||||||
<div className="mt-auto">USER</div>
|
<div className="mt-auto">
|
||||||
|
<UserMenu />
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
) : (
|
) : (
|
||||||
<div />
|
<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