header/sidebar layout
This commit is contained in:
parent
ec582f2eef
commit
8f9fe5278b
3
src/app/(with-header)/dashboard/page.tsx
Normal file
3
src/app/(with-header)/dashboard/page.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default function DashboardPage() {
|
||||||
|
return <div>dashboard</div>;
|
||||||
|
}
|
11
src/app/(with-header)/layout.tsx
Normal file
11
src/app/(with-header)/layout.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { LayoutProps } from '@/types/layout';
|
||||||
|
import { ClientProviders } from '@/components/client-providers';
|
||||||
|
import { HeaderSidebar } from '@/components/header-sidebar';
|
||||||
|
|
||||||
|
export default async function HeaderLayout({children}: LayoutProps) {
|
||||||
|
return (<ClientProviders>
|
||||||
|
<HeaderSidebar>
|
||||||
|
{ children }
|
||||||
|
</HeaderSidebar>
|
||||||
|
</ClientProviders>)
|
||||||
|
};
|
3
src/app/(with-header)/settings/page.tsx
Normal file
3
src/app/(with-header)/settings/page.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default function SettingsPage() {
|
||||||
|
return (<div>settings</div>);
|
||||||
|
}
|
122
src/components/header-sidebar.tsx
Normal file
122
src/components/header-sidebar.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button, Divider } from '@nextui-org/react';
|
||||||
|
import { Bars3Icon, ChevronLeftIcon, XMarkIcon } from '@heroicons/react/20/solid';
|
||||||
|
import { Fragment, useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { ThemeSwitcherDropdown, ThemeSwitcherSwitch } from '@/components/theme-switcher';
|
||||||
|
import { AdjustmentsHorizontalIcon } from '@heroicons/react/24/solid';
|
||||||
|
import { login, logout } from '@/actions/auth';
|
||||||
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import { UserPayload } from '@/types/user';
|
||||||
|
import { MAIN_ROUTES, ROUTES, UserOnly } from '@/routes';
|
||||||
|
import { useUser } from '@/helpers/use-user';
|
||||||
|
|
||||||
|
export type HeaderSidebarProps = {
|
||||||
|
children?: React.ReactNode
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterUserOnly = (user: UserPayload | null | undefined, { userOnly }: { userOnly?: UserOnly }) => {
|
||||||
|
if (!userOnly) return true;
|
||||||
|
if (typeof userOnly === 'string') return user?.[userOnly];
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HeaderSidebar = ({ children }: HeaderSidebarProps) => {
|
||||||
|
const user = useUser();
|
||||||
|
const path = usePathname();
|
||||||
|
const params = useSearchParams();
|
||||||
|
const [isMenuOpen, setMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const from = params.get('from');
|
||||||
|
const filter = filterUserOnly.bind(null, user);
|
||||||
|
const routeGroup = ROUTES.find(route => route.title === from || path.startsWith(route.url))!;
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<div className={`fixed inset-0 w-full h-full z-[9999] ${isMenuOpen ? '' : 'pointer-events-none'}`}>
|
||||||
|
<div className={`transition bg-black z-40 absolute inset-0 w-full h-full ${isMenuOpen ? 'bg-opacity-25' : 'bg-opacity-0 pointer-events-none'}`} onClick={() => setMenuOpen(false)} />
|
||||||
|
<div className={`dark flex flex-col text-white absolute p-6 top-0 h-full max-w-full w-96 bg-gray-950 z-[9999] transition-all ${isMenuOpen ? 'left-0 shadow-2xl' : '-left-full'}`}>
|
||||||
|
<div className="flex">
|
||||||
|
<Button className="text-2xl mb-6 font-bold cursor-pointer flex items-center ps-1.5 pe-2" variant="light"
|
||||||
|
onClick={() => setMenuOpen(false)}>
|
||||||
|
<ChevronLeftIcon className="h-6 mt-0.5" />
|
||||||
|
<span>{ routeGroup.title }</span>
|
||||||
|
</Button>
|
||||||
|
<Button className="ml-auto" isIconOnly color="danger" onClick={() => setMenuOpen(false)}>
|
||||||
|
<XMarkIcon className="w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-y-auto">
|
||||||
|
{ROUTES.map((route, i) => <Fragment key={i}>
|
||||||
|
<div>
|
||||||
|
{!filter(route) ? <div className={`select-none text-2xl ${route === routeGroup ? 'font-bold' : 'font-semibold'}`}>
|
||||||
|
{route.name}
|
||||||
|
</div> : <Link href={route.url} onClick={() => setMenuOpen(false)}
|
||||||
|
className={`text-2xl transition hover:text-secondary ${route === routeGroup ? 'font-bold' : 'font-semibold'}`}>
|
||||||
|
{route.name}
|
||||||
|
</Link>}
|
||||||
|
<div className="ml-2 mt-2">
|
||||||
|
{route.routes?.filter(filter)?.map(subroute => <div className="mb-1" key={subroute.url}>
|
||||||
|
<Link href={subroute.url} onClick={() => setMenuOpen(false)}
|
||||||
|
className={`text-xl transition hover:text-secondary ${path.startsWith(subroute.url) ? 'font-semibold' : ''}`}>
|
||||||
|
{subroute.name}
|
||||||
|
</Link>
|
||||||
|
</div>)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{i < ROUTES.length - 1 && <Divider className="my-5" />}
|
||||||
|
</Fragment>)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-auto mb-4 flex items-baseline">
|
||||||
|
<ThemeSwitcherSwitch />
|
||||||
|
|
||||||
|
{user && <Link href="/settings" className="ml-auto">
|
||||||
|
<Button isIconOnly variant="bordered" size="lg">
|
||||||
|
<AdjustmentsHorizontalIcon className="w-8" />
|
||||||
|
</Button>
|
||||||
|
</Link>}
|
||||||
|
</div>
|
||||||
|
<Button color="primary" className="w-full" onClick={() => user ? logout({ redirectTo: '/' }) : login()}>
|
||||||
|
{user ? 'Logout' : 'Login'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex p-6 items-center flex-shrink-0 sticky top-0 bg-background z-[9998] shadow-lg">
|
||||||
|
<Button className="text-2xl font-bold cursor-pointer flex items-center m-0 ps-1.5 pe-2 mr-6" variant="light"
|
||||||
|
onClick={() => setMenuOpen(true)}>
|
||||||
|
<Bars3Icon className="h-6 mt-0.5" />
|
||||||
|
<span>{ routeGroup.title }</span>
|
||||||
|
</Button>
|
||||||
|
<div className="mr-auto mt-1 hidden md:flex text-lg">
|
||||||
|
{routeGroup.routes?.filter(filter).map(route =>
|
||||||
|
<Link href={route.url} key={route.url} className={`mr-4 transition ${path.startsWith(route.url) ? 'font-semibold text-primary' : 'hover:text-secondary'}`}>
|
||||||
|
{route.name}
|
||||||
|
</Link>)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{routeGroup !== MAIN_ROUTES && <div className="mr-4 mt-1 hidden lg:flex text-lg transition hover:text-secondary">
|
||||||
|
{MAIN_ROUTES.routes.filter(filter).map(route => <Link
|
||||||
|
href={`${route.url}?from=${encodeURIComponent(routeGroup.title)}`} key={route.url}
|
||||||
|
className={`mr-4 transition ${path.startsWith(route.url) ? 'font-semibold text-primary' : 'hover:text-secondary'}`}>
|
||||||
|
{route.name}
|
||||||
|
</Link>)}
|
||||||
|
</div>}
|
||||||
|
<div className="hidden md:flex">
|
||||||
|
<Link href={routeGroup === MAIN_ROUTES ? '/settings' : `/settings?from=${encodeURIComponent(routeGroup.title)}`}>
|
||||||
|
{user && <Button isIconOnly variant="bordered" size="sm" className="mr-2">
|
||||||
|
<AdjustmentsHorizontalIcon className="w-6" />
|
||||||
|
</Button>}
|
||||||
|
</Link>
|
||||||
|
<ThemeSwitcherDropdown />
|
||||||
|
<Button size="sm" className="ml-2" color="primary" onClick={() => user ? logout({ redirectTo: '/' }) : login()}>
|
||||||
|
{user ? 'Logout' : 'Login'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="sm:px-5 flex-grow">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>)
|
||||||
|
};
|
50
src/routes.ts
Normal file
50
src/routes.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { UserPayload } from '@/types/user';
|
||||||
|
|
||||||
|
export type UserOnly = boolean | keyof UserPayload;
|
||||||
|
|
||||||
|
type Subroute = {
|
||||||
|
url: string,
|
||||||
|
name: string,
|
||||||
|
userOnly?: UserOnly
|
||||||
|
};
|
||||||
|
|
||||||
|
type Route = {
|
||||||
|
url: string,
|
||||||
|
name: string,
|
||||||
|
title: string,
|
||||||
|
userOnly?: UserOnly,
|
||||||
|
routes: Subroute[]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MAIN_ROUTES: Route = {
|
||||||
|
url: '/',
|
||||||
|
name: "Actaeon",
|
||||||
|
title: 'Actaeon',
|
||||||
|
routes: [{
|
||||||
|
url: '/dashboard',
|
||||||
|
name: 'Overview'
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ROUTES: Route[] = [{
|
||||||
|
url: '/chuni',
|
||||||
|
name: 'Chunithm',
|
||||||
|
title: 'Chunithm',
|
||||||
|
userOnly: 'chuni',
|
||||||
|
routes: [{
|
||||||
|
url: '/chuni/dashboard',
|
||||||
|
name: 'Dashboard',
|
||||||
|
userOnly: 'chuni'
|
||||||
|
}, {
|
||||||
|
url: '/chuni/music',
|
||||||
|
name: 'Music List'
|
||||||
|
}, {
|
||||||
|
url: '/chuni/playlog',
|
||||||
|
name: 'Playlog',
|
||||||
|
userOnly: 'chuni'
|
||||||
|
}, {
|
||||||
|
url: '/chuni/userbox',
|
||||||
|
name: 'Userbox',
|
||||||
|
userOnly: 'chuni'
|
||||||
|
}]
|
||||||
|
}, MAIN_ROUTES];
|
Loading…
Reference in New Issue
Block a user