Conditionally render admin link for admin users
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -3,9 +3,7 @@
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import NextError from 'next/error';
|
||||
import { Geist, Geist_Mono } from 'next/font/google';
|
||||
|
||||
import '@/app/(frontend)/styles.css';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import Footer from '@/components/layout/footer';
|
||||
import Header from '@/components/layout/header';
|
||||
@@ -14,7 +12,6 @@ import { env } from '@/env';
|
||||
import { generateMetadata } from '@/lib/metadata';
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import PlausibleProvider from 'next-plausible';
|
||||
|
||||
import { Button, ThemeProvider, Toaster } from '@gib/ui';
|
||||
|
||||
export const metadata: Metadata = generateMetadata();
|
||||
|
||||
@@ -9,9 +9,6 @@ import { generateMetadata } from '@/lib/metadata';
|
||||
import { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server';
|
||||
import PlausibleProvider from 'next-plausible';
|
||||
import { ThemeProvider, Toaster } from '@gib/ui';
|
||||
import { preloadQuery } from 'convex/nextjs';
|
||||
import { api } from '@gib/backend/convex/_generated/api.js';
|
||||
|
||||
|
||||
export const metadata: Metadata = generateMetadata();
|
||||
|
||||
@@ -31,12 +28,11 @@ const geistMono = Geist_Mono({
|
||||
variable: '--font-geist-mono',
|
||||
});
|
||||
|
||||
const RootLayout = async ({
|
||||
const RootLayout = ({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) => {
|
||||
const preloadedUser = await preloadQuery(api.auth.getUser, {});
|
||||
return (
|
||||
<ConvexAuthNextjsServerProvider>
|
||||
<PlausibleProvider
|
||||
@@ -55,9 +51,7 @@ const RootLayout = async ({
|
||||
>
|
||||
<ConvexClientProvider>
|
||||
<div className='flex min-h-screen flex-col'>
|
||||
<Header
|
||||
preloadedUser={preloadedUser}
|
||||
/>
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
Input,
|
||||
} from '@gib/ui';
|
||||
|
||||
interface AvatarUploadProps {
|
||||
type AvatarUploadProps = {
|
||||
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import { type Preloaded, usePreloadedQuery } from 'convex/react';
|
||||
import type { NavItem } from './navigation';
|
||||
import { Kanit } from 'next/font/google';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { Coffee, Server, User, Wrench } from 'lucide-react';
|
||||
import { Controls } from './controls';
|
||||
import { useQuery } from 'convex/react';
|
||||
import { api } from '@gib/backend/convex/_generated/api.js';
|
||||
|
||||
type HeaderProps = {
|
||||
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
||||
headerProps?: ComponentProps<'header'>;
|
||||
};
|
||||
import { Controls } from './controls';
|
||||
import { DesktopNavigation, MobileNavigation } from './navigation';
|
||||
|
||||
const kanitSans = Kanit({
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '500', '600', '700'],
|
||||
});
|
||||
|
||||
export default function Header({preloadedUser, headerProps}: HeaderProps) {
|
||||
const user = usePreloadedQuery(preloadedUser);
|
||||
const Header = (headerProps: ComponentProps<'header'>) => {
|
||||
const user = useQuery(api.auth.getUser, {});
|
||||
const navItems: NavItem[] = [
|
||||
{
|
||||
href: '/#features',
|
||||
icon: Wrench,
|
||||
label: 'Features',
|
||||
},
|
||||
{
|
||||
href: '/#tech-stack',
|
||||
icon: Server,
|
||||
label: 'Stack',
|
||||
},
|
||||
{
|
||||
href: 'https://git.gbrown.org/gib/convex-monorepo',
|
||||
icon: Coffee,
|
||||
label: 'Repository',
|
||||
external: true,
|
||||
},
|
||||
];
|
||||
|
||||
if (user?.isAdmin) {
|
||||
navItems.push({
|
||||
href: '/admin',
|
||||
icon: User,
|
||||
label: 'Admin',
|
||||
external: true,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<header
|
||||
className='border-border/40 bg-background/95 supports-backdrop-filter:bg-background/60 sticky top-0 z-50 w-full border-b backdrop-blur'
|
||||
@@ -37,57 +62,24 @@ export default function Header({preloadedUser, headerProps}: HeaderProps) {
|
||||
alt='Convex Monorepo'
|
||||
width={50}
|
||||
height={50}
|
||||
className='w-10 lg:w-15 invert dark:invert-0'
|
||||
className='w-10 invert lg:w-15 dark:invert-0'
|
||||
/>
|
||||
<span
|
||||
className={`font-extrabold hidden md:inline sm:text-lg sm:mb-1 lg:mb-3 lg:text-4xl xl:text-5xl ${kanitSans.className}`}
|
||||
className={`hidden font-extrabold sm:mb-1 sm:text-lg md:inline lg:mb-3 lg:text-4xl xl:text-5xl ${kanitSans.className}`}
|
||||
>
|
||||
convex-monorepo
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className='hidden items-center gap-4 md:gap-6 text-xs lg:text-base font-medium sm:flex'>
|
||||
<Link
|
||||
href='/#features'
|
||||
className='text-foreground/60 hover:text-foreground flex items-center gap-2 transition-colors'
|
||||
>
|
||||
<Wrench width={18} height={18} />
|
||||
Features
|
||||
</Link>
|
||||
<Link
|
||||
href='/#tech-stack'
|
||||
className='text-foreground/60 hover:text-foreground flex items-center gap-2 transition-colors'
|
||||
>
|
||||
<Server width={18} height={18} />
|
||||
Stack
|
||||
</Link>
|
||||
<Link
|
||||
href='https://git.gbrown.org/gib/convex-monorepo'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-foreground/60 hover:text-foreground flex items-center gap-2 transition-colors'
|
||||
>
|
||||
<Coffee width={20} height={20} />
|
||||
Repository
|
||||
</Link>
|
||||
{user?.isAdmin && (
|
||||
<Link
|
||||
href='/admin'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-foreground/60 hover:text-foreground
|
||||
flex items-center gap-2 transition-colors'
|
||||
>
|
||||
<User width={18} height={18} />
|
||||
Admin
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
<DesktopNavigation items={navItems} />
|
||||
|
||||
{/* Controls (Theme + Auth) */}
|
||||
<Controls />
|
||||
<div className='flex items-center gap-2'>
|
||||
<Controls />
|
||||
<MobileNavigation items={navItems} />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
||||
97
apps/next/src/components/layout/header/navigation.tsx
Normal file
97
apps/next/src/components/layout/header/navigation.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
'use client';
|
||||
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { ExternalLink, Menu } from 'lucide-react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@gib/ui';
|
||||
|
||||
export type NavItem = {
|
||||
href: string;
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
external?: boolean;
|
||||
};
|
||||
|
||||
type NavigationProps = {
|
||||
items: NavItem[];
|
||||
};
|
||||
|
||||
const DesktopNavigation = ({ items }: NavigationProps) => {
|
||||
return (
|
||||
<nav className='hidden items-center gap-4 text-xs font-medium sm:flex md:gap-6 lg:text-base'>
|
||||
{items.map(({ href, icon: Icon, label, external }) => (
|
||||
<Link
|
||||
key={label}
|
||||
href={href}
|
||||
target={external ? '_blank' : undefined}
|
||||
rel={external ? 'noopener noreferrer' : undefined}
|
||||
className='text-foreground/60 hover:text-foreground flex items-center gap-2 transition-colors'
|
||||
>
|
||||
<Icon width={18} height={18} />
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
const MobileNavigation = ({ items }: NavigationProps) => {
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='icon-sm'
|
||||
className='sm:hidden'
|
||||
aria-label='Open navigation menu'
|
||||
>
|
||||
<Menu className='size-4.5' />
|
||||
<span className='sr-only'>Open navigation menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side='right' className='w-[min(88vw,22rem)] px-0'>
|
||||
<SheetHeader className='border-border/60 from-background to-muted/40 border-b bg-linear-to-br from-35% px-5 py-5 text-left'>
|
||||
<SheetTitle className='text-left text-lg'>Navigation</SheetTitle>
|
||||
<SheetDescription className='text-left'>
|
||||
Quick access to the links that collapse out of the header.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<div className='flex flex-col gap-3 px-4 py-5'>
|
||||
{items.map(({ href, icon: Icon, label, external }) => (
|
||||
<SheetClose asChild key={label}>
|
||||
<Link
|
||||
href={href}
|
||||
target={external ? '_blank' : undefined}
|
||||
rel={external ? 'noopener noreferrer' : undefined}
|
||||
className='bg-card hover:bg-muted/70 border-border/60 text-card-foreground flex items-center justify-between rounded-2xl border px-4 py-3 transition-colors'
|
||||
>
|
||||
<span className='flex items-center gap-3'>
|
||||
<span className='bg-muted text-foreground flex h-9 w-9 items-center justify-center rounded-xl'>
|
||||
<Icon className='size-4.5' />
|
||||
</span>
|
||||
<span className='text-sm font-medium'>{label}</span>
|
||||
</span>
|
||||
{external ? (
|
||||
<ExternalLink className='text-muted-foreground size-4' />
|
||||
) : null}
|
||||
</Link>
|
||||
</SheetClose>
|
||||
))}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
|
||||
export { DesktopNavigation, MobileNavigation };
|
||||
Reference in New Issue
Block a user