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>
This commit is contained in:
2026-06-20 18:01:27 +05:30
parent f8b7afcc38
commit d7b5988858
27 changed files with 2306 additions and 521 deletions
+59
View File
@@ -0,0 +1,59 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { cn } from '../_lib/utils';
import { Separator } from '../_components/ui/separator';
import { Button } from '../_components/ui/button';
import { LayoutDashboard, Settings, Users, LogOut } from 'lucide-react';
const NAV = [
{ href: '/my', label: 'Dashboard', icon: <LayoutDashboard /> },
{ href: '/my/groups', label: 'My Groups', icon: <Users /> },
{ href: '/my/settings', label: 'Settings', icon: <Settings /> },
];
export function MemberNav() {
const pathname = usePathname();
async function logout() {
await fetch('/api/my/logout', { method: 'POST' });
window.location.href = '/member-login';
}
return (
<nav className="w-56 shrink-0 flex flex-col h-full border-r bg-sidebar">
<div className="px-4 py-5 flex items-center gap-2">
<span className="font-bold text-base text-emerald-600">TOWER</span>
<Separator orientation="vertical" className="h-4" />
<span className="text-xs text-muted-foreground font-medium">Member</span>
</div>
<Separator />
<div className="flex-1 overflow-y-auto py-3 px-2 space-y-0.5">
{NAV.map((item) => {
const active = item.href === '/my' ? pathname === '/my' : 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',
)}
>
<span className="size-4 shrink-0">{item.icon}</span>
{item.label}
</Link>
);
})}
</div>
<Separator />
<div className="p-3">
<Button variant="ghost" size="sm" className="w-full justify-start text-muted-foreground" onClick={() => void logout()}>
<LogOut className="size-4" />
Sign out
</Button>
</div>
</nav>
);
}