user profile page base

This commit is contained in:
Braydon 2024-09-19 03:18:16 -04:00
parent 5b6ae80948
commit d20f5a8274
12 changed files with 365 additions and 6 deletions

BIN
bun.lockb

Binary file not shown.

@ -0,0 +1,71 @@
"use client";
import { ReactElement } from "react";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { User } from "@/app/types/user/user";
import { useUserContext } from "@/app/provider/user-provider";
import { UserState } from "@/app/store/user-store";
import { Separator } from "@/components/ui/separator";
import AvatarSetting from "@/components/dashboard/user/avatar-setting";
import UsernameSetting from "@/components/dashboard/user/username-setting";
import EmailSetting from "@/components/dashboard/user/email-setting";
import TierSetting from "@/components/dashboard/user/tier-setting";
/**
* The user profile page.
*
* @return the page jsx
*/
const UserProfilePage = (): ReactElement => (
<main className="w-[47rem] p-10 flex flex-col gap-5">
<Header />
{/* Content */}
<div className="flex flex-col gap-5">
<Separator className="opacity-65" />
<AvatarSetting />
<Separator className="opacity-65" />
<EmailSetting />
<Separator className="opacity-65" />
<UsernameSetting />
<Separator className="opacity-65" />
<TierSetting />
</div>
</main>
);
const Header = (): ReactElement => {
const user: User | undefined = useUserContext(
(state: UserState) => state.user
);
return (
<div className="flex flex-col gap-1.5 select-none">
<h1 className="text-2xl font-bold">My Profile</h1>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/dashboard">
Dashboard
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>{user?.username}</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>My Profile</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
);
};
export default UserProfilePage;

@ -10,6 +10,7 @@ import {
ClipboardIcon, ClipboardIcon,
Cog6ToothIcon, Cog6ToothIcon,
FireIcon, FireIcon,
PencilSquareIcon,
WrenchIcon, WrenchIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { useOrganizationContext } from "@/app/provider/organization-provider"; import { useOrganizationContext } from "@/app/provider/organization-provider";
@ -37,6 +38,11 @@ const links: SidebarLink[] = [
icon: <ChartBarSquareIcon />, icon: <ChartBarSquareIcon />,
href: "/insights", href: "/insights",
}, },
{
name: "Audit Logs",
icon: <PencilSquareIcon />,
href: "/audit",
},
{ {
name: "Settings", name: "Settings",
icon: <Cog6ToothIcon />, icon: <Cog6ToothIcon />,

@ -26,11 +26,11 @@ const Sidebar = (): ReactElement => {
<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 <Link
className="flex gap-3 items-center select-none group" className="flex gap-4 items-center select-none group"
href="/dashboard" 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-2xl font-bold group-hover:opacity-75 transition-all transform-gpu">
Pulse App Pulse App
</h1> </h1>
</Link> </Link>

@ -61,19 +61,19 @@ const MyAccount = (): ReactElement => (
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<Link href="/dashboard/user/profile"> <Link href="/dashboard/user/profile">
<DropdownMenuItem className="gap-2.5"> <DropdownMenuItem className="gap-2.5 cursor-pointer">
<UserIcon className="w-5 h-5" /> <UserIcon className="w-5 h-5" />
<span>Profile</span> <span>Profile</span>
</DropdownMenuItem> </DropdownMenuItem>
</Link> </Link>
<Link href="/dashboard/user/billing"> <Link href="/dashboard/user/billing">
<DropdownMenuItem className="gap-2.5"> <DropdownMenuItem className="gap-2.5 cursor-pointer">
<CreditCardIcon className="w-5 h-5" /> <CreditCardIcon className="w-5 h-5" />
<span>Billing</span> <span>Billing</span>
</DropdownMenuItem> </DropdownMenuItem>
</Link> </Link>
<Link href="/dashboard/user/settings"> <Link href="/dashboard/user/settings">
<DropdownMenuItem className="gap-2.5"> <DropdownMenuItem className="gap-2.5 cursor-pointer">
<Cog6ToothIcon className="w-5 h-5" /> <Cog6ToothIcon className="w-5 h-5" />
<span>Settings</span> <span>Settings</span>
</DropdownMenuItem> </DropdownMenuItem>

@ -0,0 +1,34 @@
import { ReactElement } from "react";
import UserAvatar from "@/components/user/user-avatar";
import { User } from "@/app/types/user/user";
import { useUserContext } from "@/app/provider/user-provider";
import { UserState } from "@/app/store/user-store";
/**
* The setting that allows a
* {@link User} to change their
* avatar.
*
* @return the setting jsx
*/
const AvatarSetting = (): ReactElement => {
const user: User | undefined = useUserContext(
(state: UserState) => state.user
);
return (
<div className="px-5 flex items-center">
{/* Name & Description */}
<div className="w-96 flex flex-col gap-0.5 select-none pointer-events-none">
<h1 className="text-lg font-bold">Avatar</h1>
<p className="max-w-64 text-sm opacity-75">
Set a profile picture for your account. This can be seen by
other users.
</p>
</div>
{/* Setting */}
<UserAvatar user={user as User} />
</div>
);
};
export default AvatarSetting;

@ -0,0 +1,37 @@
import { ReactElement } from "react";
import { User } from "@/app/types/user/user";
import { useUserContext } from "@/app/provider/user-provider";
import { UserState } from "@/app/store/user-store";
import { Input } from "@/components/ui/input";
/**
* The setting that allows a
* {@link User} to view their
* email.
*
* @return the setting jsx
*/
const EmailSetting = (): ReactElement => {
const user: User | undefined = useUserContext(
(state: UserState) => state.user
);
return (
<div className="px-5 flex items-center">
{/* Name & Description */}
<div className="w-96 flex flex-col gap-0.5 select-none pointer-events-none">
<h1 className="text-lg font-bold">Email</h1>
<p className="max-w-64 text-sm opacity-75">
The email you use to login to this account.
</p>
</div>
{/* Setting */}
<Input
className="w-60 rounded-lg select-none"
value={user?.email}
disabled
/>
</div>
);
};
export default EmailSetting;

@ -0,0 +1,49 @@
import { ReactElement } from "react";
import { User } from "@/app/types/user/user";
import { useUserContext } from "@/app/provider/user-provider";
import { UserState } from "@/app/store/user-store";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { capitalizeWords } from "@/lib/string";
/**
* The setting that allows a
* {@link User} to view their
* tier.
*
* @return the setting jsx
*/
const TierSetting = (): ReactElement => {
const user: User | undefined = useUserContext(
(state: UserState) => state.user
);
return (
<div className="px-5 flex items-center">
{/* Name & Description */}
<div className="w-96 flex flex-col gap-0.5 select-none pointer-events-none">
<h1 className="text-lg font-bold">Tier</h1>
<p className="max-w-64 text-sm opacity-75">
The tier of your account.
</p>
</div>
{/* Setting */}
<div className="flex gap-10 items-center">
<span className="font-medium">
{capitalizeWords(user?.tier)}
</span>
<Link href="/#pricing">
<Button
className="bg-background/30"
size="sm"
variant="outline"
>
View Pricing
</Button>
</Link>
</div>
</div>
);
};
export default TierSetting;

@ -0,0 +1,37 @@
import { ReactElement } from "react";
import { User } from "@/app/types/user/user";
import { useUserContext } from "@/app/provider/user-provider";
import { UserState } from "@/app/store/user-store";
import { Input } from "@/components/ui/input";
/**
* The setting that allows a
* {@link User} to view their
* username.
*
* @return the setting jsx
*/
const UsernameSetting = (): ReactElement => {
const user: User | undefined = useUserContext(
(state: UserState) => state.user
);
return (
<div className="px-5 flex items-center">
{/* Name & Description */}
<div className="w-96 flex flex-col gap-0.5 select-none pointer-events-none">
<h1 className="text-lg font-bold">Username</h1>
<p className="max-w-64 text-sm opacity-75">
The username used to identify you on the app.
</p>
</div>
{/* Setting */}
<Input
className="w-60 rounded-lg select-none"
value={user?.username}
disabled
/>
</div>
);
};
export default UsernameSetting;

@ -0,0 +1,115 @@
import * as React from "react";
import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons";
import { Slot } from "@radix-ui/react-slot";
import { cn } from "@/lib/utils";
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
));
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
));
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a";
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
);
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
));
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRightIcon />}
</li>
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<DotsHorizontalIcon className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};

@ -13,7 +13,7 @@ const avatarVariants = cva("relative rounded-full", {
variants: { variants: {
size: { size: {
sm: "w-6 h-6", sm: "w-6 h-6",
default: "w-10 h-10", default: "w-11 h-11",
}, },
}, },
defaultVariants: { defaultVariants: {

10
src/lib/string.ts Normal file

@ -0,0 +1,10 @@
/**
* Capitalize the first letter of
* each word in the given string.
*
* @param str the string to capitalize
* @return the capitalized string
*/
export const capitalizeWords = (str: string | undefined): string | undefined =>
str &&
str.toLowerCase().replace(/\b\w/g, (char: string) => char.toUpperCase());