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 type { Metadata, Viewport } from 'next';
|
||||||
import NextError from 'next/error';
|
import NextError from 'next/error';
|
||||||
import { Geist, Geist_Mono } from 'next/font/google';
|
import { Geist, Geist_Mono } from 'next/font/google';
|
||||||
|
|
||||||
import '@/app/(frontend)/styles.css';
|
import '@/app/(frontend)/styles.css';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import Footer from '@/components/layout/footer';
|
import Footer from '@/components/layout/footer';
|
||||||
import Header from '@/components/layout/header';
|
import Header from '@/components/layout/header';
|
||||||
@@ -14,7 +12,6 @@ import { env } from '@/env';
|
|||||||
import { generateMetadata } from '@/lib/metadata';
|
import { generateMetadata } from '@/lib/metadata';
|
||||||
import * as Sentry from '@sentry/nextjs';
|
import * as Sentry from '@sentry/nextjs';
|
||||||
import PlausibleProvider from 'next-plausible';
|
import PlausibleProvider from 'next-plausible';
|
||||||
|
|
||||||
import { Button, ThemeProvider, Toaster } from '@gib/ui';
|
import { Button, ThemeProvider, Toaster } from '@gib/ui';
|
||||||
|
|
||||||
export const metadata: Metadata = generateMetadata();
|
export const metadata: Metadata = generateMetadata();
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ import { generateMetadata } from '@/lib/metadata';
|
|||||||
import { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server';
|
import { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server';
|
||||||
import PlausibleProvider from 'next-plausible';
|
import PlausibleProvider from 'next-plausible';
|
||||||
import { ThemeProvider, Toaster } from '@gib/ui';
|
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();
|
export const metadata: Metadata = generateMetadata();
|
||||||
|
|
||||||
@@ -31,12 +28,11 @@ const geistMono = Geist_Mono({
|
|||||||
variable: '--font-geist-mono',
|
variable: '--font-geist-mono',
|
||||||
});
|
});
|
||||||
|
|
||||||
const RootLayout = async ({
|
const RootLayout = ({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) => {
|
}>) => {
|
||||||
const preloadedUser = await preloadQuery(api.auth.getUser, {});
|
|
||||||
return (
|
return (
|
||||||
<ConvexAuthNextjsServerProvider>
|
<ConvexAuthNextjsServerProvider>
|
||||||
<PlausibleProvider
|
<PlausibleProvider
|
||||||
@@ -55,9 +51,7 @@ const RootLayout = async ({
|
|||||||
>
|
>
|
||||||
<ConvexClientProvider>
|
<ConvexClientProvider>
|
||||||
<div className='flex min-h-screen flex-col'>
|
<div className='flex min-h-screen flex-col'>
|
||||||
<Header
|
<Header />
|
||||||
preloadedUser={preloadedUser}
|
|
||||||
/>
|
|
||||||
{children}
|
{children}
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
} from '@gib/ui';
|
} from '@gib/ui';
|
||||||
|
|
||||||
interface AvatarUploadProps {
|
type AvatarUploadProps = {
|
||||||
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,51 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { ComponentProps } from 'react';
|
import type { ComponentProps } from 'react';
|
||||||
import { type Preloaded, usePreloadedQuery } from 'convex/react';
|
import type { NavItem } from './navigation';
|
||||||
import { Kanit } from 'next/font/google';
|
import { Kanit } from 'next/font/google';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Coffee, Server, User, Wrench } from 'lucide-react';
|
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';
|
import { api } from '@gib/backend/convex/_generated/api.js';
|
||||||
|
import { Controls } from './controls';
|
||||||
type HeaderProps = {
|
import { DesktopNavigation, MobileNavigation } from './navigation';
|
||||||
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
|
||||||
headerProps?: ComponentProps<'header'>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const kanitSans = Kanit({
|
const kanitSans = Kanit({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
weight: ['400', '500', '600', '700'],
|
weight: ['400', '500', '600', '700'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function Header({preloadedUser, headerProps}: HeaderProps) {
|
const Header = (headerProps: ComponentProps<'header'>) => {
|
||||||
const user = usePreloadedQuery(preloadedUser);
|
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 (
|
return (
|
||||||
<header
|
<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'
|
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'
|
alt='Convex Monorepo'
|
||||||
width={50}
|
width={50}
|
||||||
height={50}
|
height={50}
|
||||||
className='w-10 lg:w-15 invert dark:invert-0'
|
className='w-10 invert lg:w-15 dark:invert-0'
|
||||||
/>
|
/>
|
||||||
<span
|
<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
|
convex-monorepo
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Navigation */}
|
<DesktopNavigation items={navItems} />
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Controls (Theme + Auth) */}
|
<div className='flex items-center gap-2'>
|
||||||
<Controls />
|
<Controls />
|
||||||
|
<MobileNavigation items={navItems} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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