Code Conventions
Coding standards and patterns used across the Client Portal codebase.
File Naming
| Type | Convention | Example |
|---|---|---|
| React components | PascalCase | WorkspaceDashboard.tsx, SalesKPIs.tsx |
| Hooks | camelCase with use prefix | useSalesProspects.ts, useAuth.tsx |
| Utility files | camelCase | utils.ts, sales-types.ts |
| Edge functions | kebab-case directory | supabase/functions/summarize-sales-call/index.ts |
| Scripts | kebab-case | scripts/enrich-attorneys.ts |
| Migration files | Timestamped snake_case | 20260210100000_add_ghl_integrations.sql |
Component Pattern
All components are function components with TypeScript:
import { useState } from 'react';
import { Button } from '@/components/ui/button';
interface ProspectCardProps {
prospect: SalesProspect;
onEdit: (id: string) => void;
}
export function ProspectCard({ prospect, onEdit }: ProspectCardProps) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className="rounded-lg border p-4">
<h3 className="font-semibold">{prospect.company_name}</h3>
<Button onClick={() => onEdit(prospect.id)}>Edit</Button>
</div>
);
}
Conventions:
- Named exports (not default exports)
- Props defined as an interface above the component
- Destructure props in the function signature
- No class components
Data Fetching
All server state is fetched and cached using TanStack React Query with the Supabase client. There is no custom API layer.
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
// Query
const { data, isLoading, error } = useQuery({
queryKey: ['sales-prospects', filters],
queryFn: async () => {
const { data, error } = await supabase
.from('sales_prospects')
.select('*')
.order('created_at', { ascending: false });
if (error) throw error;
return data;
},
});
// Mutation
const queryClient = useQueryClient();
const updateProspect = useMutation({
mutationFn: async (updates: Partial<SalesProspect>) => {
const { error } = await supabase
.from('sales_prospects')
.update(updates)
.eq('id', updates.id);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['sales-prospects'] });
},
});
Conventions:
- Query keys are descriptive arrays:
['table-name', ...filters] - Always check for Supabase errors and throw them
- Invalidate related queries on mutation success
- Wrap reusable queries in custom hooks (e.g.,
useSalesProspects)
TypeScript Types
Supabase Types
Types for database tables are manually maintained in src/integrations/supabase/types.ts. They are NOT auto-generated.
The attorneys table was created via the Supabase dashboard and is not in the migration files. It will not appear in auto-generated types. If you regenerate types, you must manually re-add the attorneys table definition.
Sales Types
Sales-specific types are defined in src/lib/sales-types.ts:
// Example from sales-types.ts
interface SalesProspect {
id: string;
company_name: string;
contact_name: string | null;
contact_email: string | null;
engagement_level: EngagementLevel;
// ... etc
}
Always import from the correct type file:
- Database table types:
@/integrations/supabase/types - Sales domain types:
@/lib/sales-types
UI Components
Use shadcn/ui components from @/components/ui/. These are built on Radix UI primitives and styled with Tailwind.
import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
There are 51 shadcn/ui component files in src/components/ui/. Check there before building custom UI primitives.
Styling
Use Tailwind CSS utility classes. No custom CSS files except for global styles in src/css/custom.css.
// Good
<div className="flex items-center gap-2 rounded-lg border p-4 shadow-sm">
// Avoid -- no inline styles
<div style={{ display: 'flex', padding: '16px' }}>
Use the cn() utility from @/lib/utils for conditional classes:
import { cn } from '@/lib/utils';
<div className={cn(
'rounded-lg border p-4',
isActive && 'border-blue-500 bg-blue-50',
isDisabled && 'opacity-50 cursor-not-allowed'
)} />
Forms
Use React Hook Form with Zod validation:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const prospectSchema = z.object({
company_name: z.string().min(1, 'Company name is required'),
contact_email: z.string().email('Invalid email').optional(),
contact_phone: z.string().optional(),
});
type ProspectFormData = z.infer<typeof prospectSchema>;
function AddProspectForm() {
const form = useForm<ProspectFormData>({
resolver: zodResolver(prospectSchema),
});
const onSubmit = (data: ProspectFormData) => {
// Handle submission
};
return <form onSubmit={form.handleSubmit(onSubmit)}>...</form>;
}
State Management
| State Type | Solution |
|---|---|
| Server state (database data) | TanStack React Query |
| Auth state | React Context (useAuth hook) |
| UI state (modals, tabs) | React useState / useReducer |
| URL state (filters, pagination) | React Router search params |
There is no Redux, Zustand, or other global state library.
Routing
Routes are defined in src/App.tsx using React Router v6:
<Route path="/admin/sales" element={
<ProtectedRoute>
<RoleGuard allowedRoles={['admin']}>
<SalesDashboard />
</RoleGuard>
</ProtectedRoute>
} />
Route protection:
<ProtectedRoute>-- Requires authenticated user<RoleGuard allowedRoles={[...]}>-- Requires specific role(s)
Edge Functions (Deno)
Edge functions run in Deno, not Node. Key differences:
- Use
Deno.env.get()instead ofprocess.env - Import from URLs or import maps, not
node_modules - Use
serve()fromhttps://deno.land/[email protected]/http/server.ts - Each function has its own
index.tsinsupabase/functions/<name>/
import { serve } from 'https://deno.land/[email protected]/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
serve(async (req) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
// Handle request...
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' },
});
});