Skip to main content

Code Conventions

Coding standards and patterns used across the Client Portal codebase.

File Naming

TypeConventionExample
React componentsPascalCaseWorkspaceDashboard.tsx, SalesKPIs.tsx
HookscamelCase with use prefixuseSalesProspects.ts, useAuth.tsx
Utility filescamelCaseutils.ts, sales-types.ts
Edge functionskebab-case directorysupabase/functions/summarize-sales-call/index.ts
Scriptskebab-casescripts/enrich-attorneys.ts
Migration filesTimestamped snake_case20260210100000_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.

caution

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 TypeSolution
Server state (database data)TanStack React Query
Auth stateReact 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 of process.env
  • Import from URLs or import maps, not node_modules
  • Use serve() from https://deno.land/[email protected]/http/server.ts
  • Each function has its own index.ts in supabase/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' },
});
});