feat: add shadcn admin workspace
This commit is contained in:
171
admin/src/components/app-shell.tsx
Normal file
171
admin/src/components/app-shell.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import { ExternalLink, LayoutDashboard, LogOut, Orbit, Settings, Sparkles } from 'lucide-react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const primaryNav = [
|
||||
{
|
||||
to: '/',
|
||||
label: 'Overview',
|
||||
description: 'Live operational dashboard',
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
{
|
||||
to: '/settings',
|
||||
label: 'Site settings',
|
||||
description: 'Brand, profile, and AI config',
|
||||
icon: Settings,
|
||||
},
|
||||
]
|
||||
|
||||
const nextNav = ['Posts editor', 'Comments moderation', 'Friend links queue', 'Review library']
|
||||
|
||||
export function AppShell({
|
||||
children,
|
||||
username,
|
||||
loggingOut,
|
||||
onLogout,
|
||||
}: {
|
||||
children: ReactNode
|
||||
username: string | null
|
||||
loggingOut: boolean
|
||||
onLogout: () => Promise<void>
|
||||
}) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<div className="mx-auto flex min-h-screen w-full max-w-[1600px] gap-6 px-4 py-4 lg:px-6 lg:py-6">
|
||||
<aside className="hidden w-[310px] shrink-0 lg:block">
|
||||
<div className="sticky top-6 overflow-hidden rounded-[2rem] border border-border/70 bg-card/90 shadow-[0_32px_90px_rgba(15,23,42,0.14)] backdrop-blur">
|
||||
<div className="space-y-5 p-6">
|
||||
<div className="space-y-3">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-primary/20 bg-primary/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.28em] text-primary">
|
||||
<Orbit className="h-3.5 w-3.5" />
|
||||
Termi admin
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Control room for the blog system
|
||||
</h1>
|
||||
<p className="text-sm leading-6 text-muted-foreground">
|
||||
A separate React workspace for operations, moderation, and AI-related site
|
||||
controls.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<nav className="space-y-2">
|
||||
{primaryNav.map((item) => {
|
||||
const Icon = item.icon
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
end={item.to === '/'}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'group flex items-start gap-3 rounded-2xl border px-4 py-3 transition-all',
|
||||
isActive
|
||||
? 'border-primary/30 bg-primary/10 shadow-[0_12px_30px_rgba(37,99,235,0.14)]'
|
||||
: 'border-transparent bg-background/50 hover:border-border/80 hover:bg-accent/55',
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'mt-0.5 flex h-10 w-10 items-center justify-center rounded-xl border',
|
||||
isActive
|
||||
? 'border-primary/25 bg-primary/12 text-primary'
|
||||
: 'border-border/80 bg-secondary text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-medium">{item.label}</div>
|
||||
<p className="mt-1 text-sm leading-6 text-muted-foreground">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.24em] text-muted-foreground">
|
||||
Migration queue
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Legacy Tera screens that move here next.
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="outline">phase 1</Badge>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{nextNav.map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
className="flex items-center justify-between rounded-2xl border border-border/60 bg-background/60 px-4 py-3"
|
||||
>
|
||||
<span className="text-sm text-muted-foreground">{item}</span>
|
||||
<Badge variant="secondary">next</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div className="min-w-0 flex-1 space-y-6">
|
||||
<header className="sticky top-4 z-20 rounded-[1.8rem] border border-border/70 bg-card/80 px-5 py-4 shadow-[0_20px_60px_rgba(15,23,42,0.1)] backdrop-blur">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-secondary px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-secondary-foreground">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
New admin workspace
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Signed in as {username ?? 'admin'}</p>
|
||||
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
|
||||
React + shadcn/ui foundation
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button variant="outline" asChild>
|
||||
<a href="http://localhost:4321" target="_blank" rel="noreferrer">
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
Open site
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={() => void onLogout()} disabled={loggingOut}>
|
||||
<LogOut className="h-4 w-4" />
|
||||
{loggingOut ? 'Signing out...' : 'Sign out'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user