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:
@@ -0,0 +1,171 @@
|
||||
# TOWER — Claude Code Rules
|
||||
|
||||
## Project Overview
|
||||
|
||||
TOWER is a multi-tenant WhatsApp community management platform. It has four portals served by a single Next.js app and a single NestJS API:
|
||||
|
||||
| Portal | Path prefix | Auth cookie | Guard |
|
||||
|--------|------------|-------------|-------|
|
||||
| Portal selector | `/` | — | public |
|
||||
| Chapter admin | `/`, `/search`, `/groups`, `/messages`, `/drafts`, `/settings`, `/threads`, `/claim-group` | `tower_token` | `JwtAuthGuard` |
|
||||
| Organisation admin | `/org/*` | `tower_org_token` | `OrgAdminGuard` |
|
||||
| Super admin | `/admin/*` | `tower_super_token` | `SuperAdminGuard` |
|
||||
| Member | `/my/*`, `/onboard`, `/member-login` | `tower_member_token` | `MemberAuthGuard` |
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Next.js 16** (App Router, TypeScript) — `apps/web`
|
||||
- **NestJS** (TypeScript) — `apps/api`
|
||||
- **Prisma** — ORM, PostgreSQL
|
||||
- **BullMQ** + **Redis** — job queues
|
||||
- **Tailwind CSS v4** — utility-first CSS
|
||||
- **shadcn/ui** — component library (new-york style, CSS variables)
|
||||
- **TanStack React Query** — all data fetching and mutations, no raw `fetch` in components
|
||||
- **Zod** — all schema validation (forms, API response shapes)
|
||||
- **react-hook-form** + **@hookform/resolvers/zod** — all forms
|
||||
- **lucide-react** — icons
|
||||
|
||||
## File & Folder Conventions
|
||||
|
||||
```
|
||||
apps/web/app/
|
||||
_lib/ # Shared utilities and contexts
|
||||
api.ts # getApiBaseUrl, jsonResponse, cookie helpers
|
||||
query-client.tsx # QueryClientProvider wrapper
|
||||
auth-context.tsx # Chapter admin auth (tower_token)
|
||||
super-admin-context.tsx # Super admin auth (tower_super_token)
|
||||
org-admin-context.tsx # Org admin auth (tower_org_token)
|
||||
utils.ts # cn() helper
|
||||
_components/ # Shared UI components (shadcn + custom)
|
||||
ui/ # Raw shadcn components (never edit directly)
|
||||
portal-shell.tsx # Shared portal layout wrapper
|
||||
nav-user.tsx # User dropdown in nav
|
||||
data-table.tsx # Shared table component
|
||||
api/ # Next.js BFF route handlers only
|
||||
auth/…
|
||||
admin/…
|
||||
org/…
|
||||
my/…
|
||||
(public)/ # Portal selector + all login pages (no auth)
|
||||
admin/ # Super admin portal
|
||||
org/ # Org admin portal
|
||||
my/ # Member portal
|
||||
[chapter pages]/ # Chapter admin pages at root level
|
||||
```
|
||||
|
||||
## Coding Rules
|
||||
|
||||
### General
|
||||
- **No raw `fetch` in components.** All API calls go through TanStack Query hooks. Define queries in a co-located `_queries.ts` or inline `useQuery`/`useMutation`.
|
||||
- **No `useState` for server data.** Use React Query. `useState` is for UI-only state (modal open, tab selection, etc.).
|
||||
- **All forms use react-hook-form + Zod.** Schema first: define a `z.object({...})` schema, derive the type with `z.infer`, pass resolver to `useForm`.
|
||||
- **Shared components live in `app/_components/`.** Never duplicate a button, input, or card across portals.
|
||||
- **Each portal has its own layout** with nav/sidebar. Do not put portal UI logic in the root layout.
|
||||
- **No `any` types.** Use proper types or `unknown` + type narrowing.
|
||||
|
||||
### TanStack React Query
|
||||
```tsx
|
||||
// Define query key factories
|
||||
export const tenantKeys = {
|
||||
all: ['tenants'] as const,
|
||||
list: () => [...tenantKeys.all, 'list'] as const,
|
||||
detail: (id: string) => [...tenantKeys.all, 'detail', id] as const,
|
||||
};
|
||||
|
||||
// In component
|
||||
const { data, isPending } = useQuery({
|
||||
queryKey: tenantKeys.list(),
|
||||
queryFn: () => api.get<Tenant[]>('/api/admin/tenants'),
|
||||
});
|
||||
|
||||
// Mutations always invalidate related queries
|
||||
const mutation = useMutation({
|
||||
mutationFn: (body: CreateTenantInput) => api.post('/api/admin/tenants', body),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: tenantKeys.all }),
|
||||
});
|
||||
```
|
||||
|
||||
### Forms
|
||||
```tsx
|
||||
const schema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
});
|
||||
type FormValues = z.infer<typeof schema>;
|
||||
|
||||
function MyForm() {
|
||||
const form = useForm<FormValues>({ resolver: zodResolver(schema) });
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FormField control={form.control} name="email" render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl><Input {...field} /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)} />
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### API Helper
|
||||
Use the typed `api` helper from `_lib/api-client.ts` — never call `fetch` directly in components or hooks:
|
||||
```ts
|
||||
// app/_lib/api-client.ts
|
||||
export const api = {
|
||||
get: <T>(url: string) => fetch(url, { cache: 'no-store' }).then(r => r.ok ? r.json() as T : Promise.reject(r)),
|
||||
post: <T>(url: string, body: unknown) => fetch(url, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body) }).then(r => r.ok ? r.json() as T : Promise.reject(r)),
|
||||
patch: <T>(url: string, body: unknown) => fetch(url, { method: 'PATCH', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body) }).then(r => r.ok ? r.json() as T : Promise.reject(r)),
|
||||
del: (url: string) => fetch(url, { method: 'DELETE' }).then(r => { if (!r.ok) return Promise.reject(r); }),
|
||||
};
|
||||
```
|
||||
|
||||
### Auth Contexts
|
||||
Each portal's auth context provides:
|
||||
- `user` / `admin` — the authenticated entity (undefined when loading)
|
||||
- `loading` — boolean
|
||||
- `login(...)` — sets cookie via BFF, updates context
|
||||
- `logout()` — clears cookie, redirects to portal login
|
||||
|
||||
Auth guard pattern (in portal layout, server component):
|
||||
```tsx
|
||||
// In a portal layout — always server component
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function PortalLayout({ children }) {
|
||||
const token = (await cookies()).get('tower_token')?.value;
|
||||
if (!token) redirect('/login');
|
||||
return <PortalShell>{children}</PortalShell>;
|
||||
}
|
||||
```
|
||||
|
||||
### Styling
|
||||
- Use shadcn/ui components as the base. Extend with Tailwind utilities.
|
||||
- **No inline styles.** All styles via Tailwind classes.
|
||||
- **No magic numbers in class names** — use design tokens (`text-sm`, `p-4`, `gap-2` — not arbitrary `p-[13px]`).
|
||||
- Colors: use semantic tokens (`text-foreground`, `bg-muted`, `border`) not raw colors.
|
||||
- Dark mode: set up via `class` strategy but only if explicitly requested.
|
||||
|
||||
### NestJS API Rules
|
||||
- All protected endpoints use a Guard decorator — never trust the request without it.
|
||||
- DTOs use `class-validator` decorators. `ValidationPipe` with `whitelist: true, forbidNonWhitelisted: true` is global.
|
||||
- Always use `select` in Prisma queries to avoid leaking `passwordHash` or other sensitive fields.
|
||||
- Org-scoped queries always include `organizationId` in the `where` clause.
|
||||
- Tenant-scoped queries always include `tenantId` in the `where` clause.
|
||||
|
||||
### Redirect Rules (IMPORTANT)
|
||||
After successful login, each portal redirects to its own home:
|
||||
- Chapter admin: `/search` (or `next` param if set)
|
||||
- Org admin: `/org`
|
||||
- Super admin: `/admin`
|
||||
- Member: `/my`
|
||||
|
||||
Never redirect to `/` after login — that is the public portal selector.
|
||||
|
||||
### Commit Style
|
||||
Conventional commits: `feat:`, `fix:`, `refactor:`, `chore:`
|
||||
Co-author every commit with Claude.
|
||||
Reference in New Issue
Block a user