--- name: react-web-saas description: Add new pages, features, or API integrations to a React (Vite + TypeScript + Tailwind) web app. Use when the user asks to add a page, React component, web UI feature, form, table, or dashboard section. license: CC-BY-ND-4.0 --- # React Web SaaS Page/Feature ## New page checklist 1. Create page in `src/pages/` 2. Add route in router config (`src/router.tsx` or `App.tsx`) 3. Add API functions in `src/api/{domain}.ts` 4. Add TanStack Query hooks 5. Add navigation link to sidebar/nav ## Page template ```typescript // src/pages/InvoicesPage.tsx import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { api } from '@/api/client'; import { useOrganization } from '@/auth/organization'; export function InvoicesPage() { const { orgId } = useOrganization(); const queryClient = useQueryClient(); const { data: invoices, isLoading } = useQuery({ queryKey: ['invoices', orgId], queryFn: () => api.get('/api/invoices').then(r => r.data), }); const createMutation = useMutation({ mutationFn: (data: CreateInvoiceDto) => api.post('/api/invoices', data), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['invoices', orgId] }), }); if (isLoading) return ; return (

Invoices

{/* content */}
); } ``` ## OTP Auth pages ### Login page (`src/pages/LoginPage.tsx`) ```typescript // 1. Email input form // 2. Submit → POST /api/auth/otp/send // 3. On success → navigate to /verify?email={email} ``` ### Verify page (`src/pages/VerifyPage.tsx`) ```typescript // 1. 6-digit OTP input (auto-focus, paste support) // 2. Submit → POST /api/auth/otp/verify // 3. On success → store JWT via token.ts → navigate to /dashboard ``` ## API client setup ```typescript // src/api/client.ts import axios from 'axios'; import { getToken } from '@/auth/token'; import { getOrgId } from '@/auth/organization'; export const api = axios.create({ baseURL: import.meta.env.VITE_API_URL, }); api.interceptors.request.use(config => { const token = getToken(); const orgId = getOrgId(); if (token) config.headers.Authorization = `Bearer ${token}`; if (orgId) config.headers['X-Organization-Id'] = orgId; return config; }); api.interceptors.response.use( r => r, err => { if (err.response?.status === 401) { clearToken(); window.location.href = '/login'; } return Promise.reject(err); } ); ``` ## Layout / Navigation pattern ```typescript // src/components/Layout.tsx // Sidebar with nav links, top bar with org switcher + user menu // All authenticated pages render inside ``` ## Profile page essentials - Display user name, email - Organization name + switch org - Update profile form (name, etc.) - Sign out: `clearToken()` + `navigate('/login')` ## Form pattern (with React Hook Form + Zod) ```typescript import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; const schema = z.object({ name: z.string().min(1) }); type FormData = z.infer; const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(schema), }); ``` ## Table pattern ```typescript // Use TanStack Table for sortable/filterable data tables import { useReactTable, getCoreRowModel, flexRender } from '@tanstack/react-table'; ``` ## Tailwind class conventions - Layout: `flex`, `grid`, `p-{n}`, `gap-{n}`, `max-w-{n}` - Cards: `bg-white rounded-lg shadow-sm border border-gray-200 p-6` - Buttons: `px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700` - Text: `text-gray-900` (primary), `text-gray-600` (secondary), `text-sm` (small) ## Build verification ```bash pnpm run build # always run after substantive changes pnpm run type-check # TypeScript only, no emit ```