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,
|
||||
Cog6ToothIcon,
|
||||
FireIcon,
|
||||
PencilSquareIcon,
|
||||
WrenchIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { useOrganizationContext } from "@/app/provider/organization-provider";
|
||||
@ -37,6 +38,11 @@ const links: SidebarLink[] = [
|
||||
icon: <ChartBarSquareIcon />,
|
||||
href: "/insights",
|
||||
},
|
||||
{
|
||||
name: "Audit Logs",
|
||||
icon: <PencilSquareIcon />,
|
||||
href: "/audit",
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
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">
|
||||
{/* Header */}
|
||||
<Link
|
||||
className="flex gap-3 items-center select-none group"
|
||||
className="flex gap-4 items-center select-none group"
|
||||
href="/dashboard"
|
||||
>
|
||||
<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
|
||||
</h1>
|
||||
</Link>
|
||||
|
@ -61,19 +61,19 @@ const MyAccount = (): ReactElement => (
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<Link href="/dashboard/user/profile">
|
||||
<DropdownMenuItem className="gap-2.5">
|
||||
<DropdownMenuItem className="gap-2.5 cursor-pointer">
|
||||
<UserIcon className="w-5 h-5" />
|
||||
<span>Profile</span>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link href="/dashboard/user/billing">
|
||||
<DropdownMenuItem className="gap-2.5">
|
||||
<DropdownMenuItem className="gap-2.5 cursor-pointer">
|
||||
<CreditCardIcon className="w-5 h-5" />
|
||||
<span>Billing</span>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link href="/dashboard/user/settings">
|
||||
<DropdownMenuItem className="gap-2.5">
|
||||
<DropdownMenuItem className="gap-2.5 cursor-pointer">
|
||||
<Cog6ToothIcon className="w-5 h-5" />
|
||||
<span>Settings</span>
|
||||
</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: {
|
||||
size: {
|
||||
sm: "w-6 h-6",
|
||||
default: "w-10 h-10",
|
||||
default: "w-11 h-11",
|
||||
},
|
||||
},
|
||||
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