Files
tower/apps/web/app/_components/portal-nav.tsx
T
maaz519 d7b5988858 feat: portal redesign — shadcn/ui, React Query, Zod, isolated portals
Foundation:
- CLAUDE.md with full coding rules (tech stack, patterns, redirect rules)
- Install @tanstack/react-query, zod, react-hook-form, shadcn/ui deps
- CSS variables design system (Tailwind v4 + tw-animate-css)
- Shared components: Button, Input, Label, Card, Badge, Separator, Form
- PortalLoginShell: two-column login layout (branding left, form right)
- PortalNav: shared sidebar nav used by all portals
- ReactQueryProvider in root layout
- api-client.ts: typed fetch wrapper (no raw fetch in components)

Portal isolation:
- Sidebar now returns null for all non-chapter routes; portals own their layout
- Admin layout: auth guard + PortalNav (slate accent)
- Org layout: auth guard + PortalNav (violet accent)
- Member layout: server-side cookie check → MemberNav client component
- Chapter admin sidebar: PortalNav (blue accent)

Login pages (all use react-hook-form + Zod + PortalLoginShell):
- /login → defaults next to /search (was /) — fixes redirect bug
- /admin/login → replaces /admin
- /org/login → replaces /org
- /member-login → two-step phone+OTP with proper form validation

Portal selector (/) redesigned with accent border-left cards

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 18:01:27 +05:30

83 lines
2.6 KiB
TypeScript

'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { cn } from '../_lib/utils';
import { Button } from './ui/button';
import { Separator } from './ui/separator';
import { LogOut } from 'lucide-react';
interface NavItem {
href: string;
label: string;
icon?: React.ReactNode;
}
interface PortalNavProps {
portalName: string;
navItems: NavItem[];
user?: { name?: string | null; email?: string | null; role?: string };
onLogout: () => void;
accentClass?: string;
}
export function PortalNav({ portalName, navItems, user, onLogout, accentClass = 'text-primary' }: PortalNavProps) {
const pathname = usePathname();
return (
<nav className="w-56 shrink-0 flex flex-col h-full border-r bg-sidebar">
{/* Logo */}
<div className="px-4 py-5 flex items-center gap-2">
<span className={cn('font-bold text-base', accentClass)}>TOWER</span>
<Separator orientation="vertical" className="h-4" />
<span className="text-xs text-muted-foreground font-medium">{portalName}</span>
</div>
<Separator />
{/* Nav links */}
<div className="flex-1 overflow-y-auto py-3 px-2 space-y-0.5">
{navItems.map((item) => {
const active = item.href === '/'
? pathname === '/'
: pathname.startsWith(item.href);
return (
<Link
key={item.href}
href={item.href}
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors',
active
? 'bg-accent text-accent-foreground font-medium'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
)}
>
{item.icon && <span className="size-4 shrink-0">{item.icon}</span>}
{item.label}
</Link>
);
})}
</div>
<Separator />
{/* User footer */}
{user && (
<div className="p-3 space-y-2">
<div className="px-3 py-2">
<p className="text-sm font-medium truncate">{user.name ?? user.email}</p>
{user.name && <p className="text-xs text-muted-foreground truncate">{user.email}</p>}
{user.role && (
<p className="text-xs text-muted-foreground uppercase tracking-wide mt-0.5">{user.role.replace(/_/g, ' ')}</p>
)}
</div>
<Button variant="ghost" size="sm" className="w-full justify-start text-muted-foreground" onClick={onLogout}>
<LogOut className="size-4" />
Sign out
</Button>
</div>
)}
</nav>
);
}