user profile page base
This commit is contained in:
parent
5b6ae80948
commit
d20f5a8274
71
src/app/(pages)/dashboard/user/profile/page.tsx
Normal file
71
src/app/(pages)/dashboard/user/profile/page.tsx
Normal file
@ -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>
|
||||||
|
34
src/components/dashboard/user/avatar-setting.tsx
Normal file
34
src/components/dashboard/user/avatar-setting.tsx
Normal file
@ -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;
|
37
src/components/dashboard/user/email-setting.tsx
Normal file
37
src/components/dashboard/user/email-setting.tsx
Normal file
@ -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;
|
49
src/components/dashboard/user/tier-setting.tsx
Normal file
49
src/components/dashboard/user/tier-setting.tsx
Normal file
@ -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;
|
37
src/components/dashboard/user/username-setting.tsx
Normal file
37
src/components/dashboard/user/username-setting.tsx
Normal file
@ -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;
|
115
src/components/ui/breadcrumb.tsx
Normal file
115
src/components/ui/breadcrumb.tsx
Normal file
@ -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
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());
|
Loading…
x
Reference in New Issue
Block a user