diff --git a/bun.lockb b/bun.lockb
index d6784b2..dfdd62b 100644
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index 86ee8d7..c3618b6 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/app/types/user/user.ts b/src/app/types/user/user.ts
index 4e14f7e..ce5cb85 100644
--- a/src/app/types/user/user.ts
+++ b/src/app/types/user/user.ts
@@ -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.
*/
diff --git a/src/components/branding.tsx b/src/components/branding.tsx
index 8681e49..af11234 100644
--- a/src/components/branding.tsx
+++ b/src/components/branding.tsx
@@ -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 ?? "/"}
>
-
+
);
export default Branding;
diff --git a/src/components/dashboard/sidebar/links.tsx b/src/components/dashboard/sidebar/links.tsx
index e1c62f3..97a213f 100644
--- a/src/components/dashboard/sidebar/links.tsx
+++ b/src/components/dashboard/sidebar/links.tsx
@@ -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 (
-
+
{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 (
{
"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}
>
{link.icon}
{link.name}
diff --git a/src/components/dashboard/sidebar/organization-selector.tsx b/src/components/dashboard/sidebar/organization-selector.tsx
index 5b2b641..adb9c86 100644
--- a/src/components/dashboard/sidebar/organization-selector.tsx
+++ b/src/components/dashboard/sidebar/organization-selector.tsx
@@ -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 ? (
-
- {selected.logo ? (
-
- ) : (
-
- )}
-
+
{selected.name}
) : (
@@ -114,7 +105,7 @@ const OrganizationSelector = (): ReactElement => {
(organization: Organization, index: number) => (
selectOrganization(
@@ -126,10 +117,14 @@ const OrganizationSelector = (): ReactElement => {
)
}
>
+
{organization.name}
{organization.snowflake ===
selectedOrganization && (
-
+
)}
)
diff --git a/src/components/dashboard/sidebar/sidebar.tsx b/src/components/dashboard/sidebar/sidebar.tsx
index d8326b7..1e32e90 100644
--- a/src/components/dashboard/sidebar/sidebar.tsx
+++ b/src/components/dashboard/sidebar/sidebar.tsx
@@ -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) ? (
) : (
diff --git a/src/components/dashboard/sidebar/user-menu.tsx b/src/components/dashboard/sidebar/user-menu.tsx
new file mode 100644
index 0000000..a1d5f05
--- /dev/null
+++ b/src/components/dashboard/sidebar/user-menu.tsx
@@ -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 (
+
+
+
+ @
+ {user?.username}
+
+
+
+ {/* Content */}
+
+
+ {/* Logout */}
+
+
+
+ Logout
+
+
+
+ );
+};
+
+/**
+ * The my account section.
+ *
+ * @return the section jsx
+ */
+const MyAccount = (): ReactElement => (
+
+
+ My Account
+
+
+
+
+
+ Profile
+
+
+
+
+
+ Billing
+
+
+
+
+
+ Settings
+
+
+
+);
+
+export default UserMenu;
diff --git a/src/components/org/organization-logo.tsx b/src/components/org/organization-logo.tsx
new file mode 100644
index 0000000..4f62896
--- /dev/null
+++ b/src/components/org/organization-logo.tsx
@@ -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 => (
+
+ {organization.logo ? (
+
+ ) : (
+
+ )}
+
+);
+export default OrganizationLogo;
diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..a838229
--- /dev/null
+++ b/src/components/ui/dropdown-menu.tsx
@@ -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,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+));
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName;
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName;
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName;
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ );
+};
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+};
diff --git a/src/components/user/user-avatar.tsx b/src/components/user/user-avatar.tsx
new file mode 100644
index 0000000..08baa9e
--- /dev/null
+++ b/src/components/user/user-avatar.tsx
@@ -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 => (
+
+ {user.avatar ? (
+
+ ) : (
+
+ )}
+
+);
+export default UserAvatar;