Skip to content

Commit

Permalink
feat: add user informations & logout (#193)
Browse files Browse the repository at this point in the history
  • Loading branch information
foyarash authored Mar 21, 2024
1 parent 3fa5cd7 commit e8b0225
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/sixty-dingos-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@premieroctet/next-admin": minor
---

feat: add user informations & logout
11 changes: 11 additions & 0 deletions apps/docs/pages/docs/api-docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ import { Tabs } from "nextra/components";
- `options` used to customize the UI, like field formatters for example. Do not use with App router.
- `dashboard` used to customize the rendered dashboard
- `translations` used to customize some of the texts displayed in the UI. See [i18n](/docs/i18n) for more details.
- `user` used to add some user information at the bottom of the menu. See [user properties](#user-properties) for more details.

> ⚠️ : Do not override these `AdminComponentProps` props, they are used internally by Next Admin.
Expand Down Expand Up @@ -417,3 +418,13 @@ The edit page's form can display notice alerts. To do so, you can pass objects i
| title | The title of the notice. This is mandatory |
| id | A unique identifier for the notice that can be used to style it with the `styles` property. This is mandatory |
| description | The description of the notice. This is optional |

## User properties

The `user` property is an object that can take the following properties:

| Name | Description |
| ------------ | -------------------------------------------------------------------- |
| data.name | the name of the user displayed in the sidebar menu. This is required |
| data.picture | the URL of the user's avatar displayed in the sidebar menu |
| logoutUrl | an URL or path to logout the user. This is required. |
6 changes: 6 additions & 0 deletions apps/example/app/[locale]/admin/[[...nextadmin]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ export default async function AdminPage({
{...props}
locale={params.locale as string}
dashboard={Dashboard}
user={{
data: {
name: "Example User",
},
logoutUrl: "/",
}}
/>
);
}
10 changes: 9 additions & 1 deletion apps/example/app/[locale]/admin/custom/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@ const CustomPage = async () => {
const totalCategories = await prisma.category.count();

return (
<MainLayout {...mainLayoutProps}>
<MainLayout
{...mainLayoutProps}
user={{
data: {
name: "Example User",
},
logoutUrl: "/",
}}
>
<div>
<h1 className="text-xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight mb-4">
Custom page
Expand Down
14 changes: 13 additions & 1 deletion apps/example/pages/pagerouter/admin/[[...nextadmin]].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,19 @@ import "../../../styles.css";
const pageOptions = options;

export default function Admin(props: AdminComponentProps) {
return <NextAdmin {...props} dashboard={Dashboard} options={pageOptions} />;
return (
<NextAdmin
{...props}
dashboard={Dashboard}
options={pageOptions}
user={{
data: {
name: "Example User",
},
logoutUrl: "/",
}}
/>
);
}

export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
Expand Down
10 changes: 9 additions & 1 deletion apps/example/pages/pagerouter/admin/custom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@ const CustomPage = ({
...mainLayoutProps
}: Props) => {
return (
<MainLayout {...mainLayoutProps}>
<MainLayout
{...mainLayoutProps}
user={{
data: {
name: "Example User",
},
logoutUrl: "/",
}}
>
<div>
<h1 className="text-xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight mb-4">
Custom page
Expand Down
2 changes: 2 additions & 0 deletions packages/next-admin/src/components/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const MainLayout = ({
title = "Admin",
sidebar,
resourcesIcons,
user,
}: PropsWithChildren<Props>) => {
const mergedTranslations = merge({ ...defaultTranslations }, translations);
const localePath = locale ? `/${locale}` : "";
Expand All @@ -39,6 +40,7 @@ export const MainLayout = ({
customPages={customPages}
configuration={sidebar}
resourcesIcons={resourcesIcons}
user={user}
/>

<main className="py-10 lg:pl-72">
Expand Down
111 changes: 99 additions & 12 deletions packages/next-admin/src/components/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ import {
import { useConfig } from "../context/ConfigContext";
import { useRouterInternal } from "../hooks/useRouterInternal";
import ResourceIcon from "./common/ResourceIcon";
import {
Dropdown,
DropdownBody,
DropdownContent,
DropdownItem,
DropdownLabel,
DropdownSeparator,
DropdownTrigger,
} from "./radix/Dropdown";
import Button from "./radix/Button";
import { Cog6ToothIcon, PowerIcon } from "@heroicons/react/24/solid";

export type MenuProps = {
resource?: ModelName;
Expand All @@ -22,6 +33,7 @@ export type MenuProps = {
customPages?: AdminComponentProps["customPages"];
configuration?: SidebarConfiguration;
resourcesIcons: AdminComponentProps["resourcesIcons"];
user?: AdminComponentProps["user"];
};

export default function Menu({
Expand All @@ -31,6 +43,7 @@ export default function Menu({
customPages,
configuration,
resourcesIcons,
user,
}: MenuProps) {
const [sidebarOpen, setSidebarOpen] = useState(false);
const { basePath } = useConfig();
Expand Down Expand Up @@ -94,6 +107,74 @@ export default function Menu({
};
};

const getInitials = () => {
const username = user?.data.name;

if (username) {
const [firstName, lastName] = username.split(" ");

if (firstName && lastName) {
return `${firstName.charAt(0)}${lastName.charAt(0)}`;
}

return username.charAt(0);
}
};

const renderUser = () => {
if (!user) {
return null;
}

return (
<div className="flex px-2 py-3 leading-6 text-sm font-semibold text-gray-700 items-center justify-between">
<div className="flex gap-3 items-center">
{user.data.picture ? (
<img
className="h-8 w-8 rounded-full"
src={user.data.picture}
alt="User picture"
/>
) : (
<div className="h-8 w-8 flex items-center justify-center rounded-full bg-gray-200 text-gray-600 uppercase">
{getInitials()}
</div>
)}
<span className="sr-only">Logged in as</span>
<span aria-hidden="true">{user.data.name}</span>
</div>
<Dropdown>
<DropdownTrigger asChild>
<Button variant="ghost" size="sm" className="!px-2 py-2">
<Cog6ToothIcon className="w-6 h-6 text-gray-700" />
</Button>
</DropdownTrigger>
<DropdownBody>
<DropdownContent
side="top"
sideOffset={5}
className="z-50 px-1 py-2"
>
<DropdownLabel className="py-1 px-4 font-normal">
{user.data.name}
</DropdownLabel>
<DropdownSeparator />
<DropdownItem asChild>
<Link
href={user.logoutUrl}
className="flex items-center gap-2 hover:text-nextadmin-primary-600 hover:bg-gray-50 py-1 px-4 rounded font-medium"
>
<PowerIcon className="w-4 h-4" />
<span>Logout</span>
</Link>
</DropdownItem>
</DropdownContent>
</DropdownBody>
</Dropdown>
</div>
);
};

const renderNavigation = () => {
return (
<nav className="flex flex-1 flex-col">
Expand Down Expand Up @@ -188,13 +269,16 @@ export default function Menu({
</div>
</Transition.Child>
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 pb-2">
<div className="flex h-16 shrink-0 items-center">
<Link href={basePath}>
<HomeIcon className="h-6 w-6 text-nextadmin-primary--500" />
</Link>
<div className="flex grow flex-col justify-between bg-white">
<div className="flex flex-col gap-y-5 overflow-y-auto px-6 pb-2">
<div className="flex h-16 shrink-0 items-center">
<Link href={basePath}>
<HomeIcon className="h-6 w-6 text-nextadmin-primary--500" />
</Link>
</div>
{renderNavigation()}
</div>
{renderNavigation()}
{renderUser()}
</div>
</Dialog.Panel>
</Transition.Child>
Expand All @@ -205,13 +289,16 @@ export default function Menu({
{/* Static sidebar for desktop */}
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6">
<div className="flex h-16 shrink-0 items-center">
<Link href={basePath}>
<HomeIcon className="h-6 w-6 text-nextadmin-primary-600" />
</Link>
<div className="flex grow flex-col justify-between border-r border-gray-200 bg-white">
<div className="flex flex-col gap-y-5 overflow-y-auto px-6">
<div className="flex h-16 shrink-0 items-center">
<Link href={basePath}>
<HomeIcon className="h-6 w-6 text-nextadmin-primary-600" />
</Link>
</div>
{renderNavigation()}
</div>
{renderNavigation()}
{renderUser()}
</div>
</div>

Expand Down
2 changes: 2 additions & 0 deletions packages/next-admin/src/components/NextAdmin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function NextAdmin({
title,
sidebar,
resourcesIcons,
user,
}: AdminComponentProps & CustomUIProps) {
if (!isAppDir && !options) {
throw new Error(
Expand Down Expand Up @@ -119,6 +120,7 @@ export function NextAdmin({
title={title}
sidebar={sidebar}
resourcesIcons={resourcesIcons}
user={user}
>
{renderMainComponent()}
</MainLayout>
Expand Down
30 changes: 30 additions & 0 deletions packages/next-admin/src/components/radix/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,33 @@ export const DropdownItem = forwardRef<
});

DropdownItem.displayName = "DropdownItem";

export const DropdownLabel = forwardRef<
ElementRef<typeof DropdownMenu.Label>,
ComponentProps<typeof DropdownMenu.Label>
>(({ className, ...props }, ref) => {
return (
<DropdownMenu.Label
className={clsx("group text-sm font-medium text-gray-900", className)}
ref={ref}
{...props}
/>
);
});

DropdownLabel.displayName = "DropdownLabel";

export const DropdownSeparator = forwardRef<
ElementRef<typeof DropdownMenu.Separator>,
ComponentProps<typeof DropdownMenu.Separator>
>(({ className, ...props }, ref) => {
return (
<DropdownMenu.Separator
className={clsx("h-px bg-gray-200 m-1", className)}
ref={ref}
{...props}
/>
);
});

DropdownSeparator.displayName = "DropdownSeparator";
12 changes: 12 additions & 0 deletions packages/next-admin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,16 @@ export type ListDataFieldValue = ListDataFieldValueWithFormat &
}
);

export type UserData = {
name: string;
picture?: string;
};

export type AdminUser = {
data: UserData;
logoutUrl: string;
};

export type AdminComponentProps = {
basePath: string;
schema?: Schema;
Expand Down Expand Up @@ -333,6 +343,7 @@ export type AdminComponentProps = {
*/
title?: string;
sidebar?: SidebarConfiguration;
user?: AdminUser;
};

export type MainLayoutProps = Pick<
Expand All @@ -350,6 +361,7 @@ export type MainLayoutProps = Pick<
| "title"
| "sidebar"
| "resourcesIcons"
| "user"
>;

export type CustomUIProps = {
Expand Down

0 comments on commit e8b0225

Please sign in to comment.