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>
7.1 KiB
7.1 KiB
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
fetchin 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
fetchin components. All API calls go through TanStack Query hooks. Define queries in a co-located_queries.tsor inlineuseQuery/useMutation. - No
useStatefor server data. Use React Query.useStateis 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 withz.infer, pass resolver touseForm. - 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
anytypes. Use proper types orunknown+ type narrowing.
TanStack React Query
// 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
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:
// 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— booleanlogin(...)— sets cookie via BFF, updates contextlogout()— clears cookie, redirects to portal login
Auth guard pattern (in portal layout, server component):
// 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 arbitraryp-[13px]). - Colors: use semantic tokens (
text-foreground,bg-muted,border) not raw colors. - Dark mode: set up via
classstrategy but only if explicitly requested.
NestJS API Rules
- All protected endpoints use a Guard decorator — never trust the request without it.
- DTOs use
class-validatordecorators.ValidationPipewithwhitelist: true, forbidNonWhitelisted: trueis global. - Always use
selectin Prisma queries to avoid leakingpasswordHashor other sensitive fields. - Org-scoped queries always include
organizationIdin thewhereclause. - Tenant-scoped queries always include
tenantIdin thewhereclause.
Redirect Rules (IMPORTANT)
After successful login, each portal redirects to its own home:
- Chapter admin:
/search(ornextparam 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.