Files
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

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

// 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 — boolean
  • login(...) — sets cookie via BFF, updates context
  • logout() — 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 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.