d7b5988858
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>
172 lines
7.1 KiB
Markdown
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.
|