Files
tower/CLAUDE.md
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

172 lines
7.1 KiB
Markdown

# 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.