Skip to main content

Component Patterns

The frontend includes a shared component library in apps/web/src/components/shared/. These components are designed for reuse across all feature modules.

DataTable

The primary table component used on all list pages. Supports sortable columns, pagination, column resizing, column visibility preferences, and row actions.

Location: apps/web/src/components/shared/data-table/DataTable.tsx

import { DataTable } from '../components/shared/data-table/DataTable';

function LeadsPage() {
const [data, setData] = useState([]);
const [meta, setMeta] = useState({ total: 0, page: 1, limit: 25, totalPages: 0 });
const { columns } = useTableColumns('leads');
const { preferences, updateColumnWidth } = useTablePreferences('leads');

return (
<DataTable
columns={columns}
data={data}
meta={meta}
preferences={preferences}
onPageChange={(page) => fetchData({ page })}
onSort={(column, direction) => fetchData({ sortBy: column, sortDir: direction })}
onColumnResize={updateColumnWidth}
actions={(row) => (
<div className="flex gap-1">
<button onClick={() => navigate(`/leads/${row.id}`)}>View</button>
<button onClick={() => handleDelete(row.id)}>Delete</button>
</div>
)}
loading={loading}
emptyMessage="No leads found"
/>
);
}

DataTable Props

PropTypeDescription
columnsColumn[]Column definitions (name, label, width, sortable)
dataany[]Row data array
metaPaginationMetaPagination info (total, page, limit, totalPages)
preferencesTablePreferenceUser column preferences
onPageChange(page: number) => voidPage change handler
onSort(column: string, dir: string) => voidSort handler
onColumnResize(column: string, width: number) => voidColumn resize handler
actions(row: any) => ReactNodeRow action buttons renderer
loadingbooleanShows loading skeleton
emptyMessagestringMessage when no data
selectablebooleanEnable row selection checkboxes
onSelectionChange(ids: string[]) => voidSelection change handler

useTableColumns Hook

const { columns, loading } = useTableColumns('leads');
// Returns column definitions configured for the 'leads' module

useTablePreferences Hook

const {
preferences, // Current user preferences
updateColumnVisibility, // Toggle column visibility
updateColumnWidth, // Resize column
updateSortOrder, // Change default sort
resetToDefault, // Reset all preferences
} = useTablePreferences('leads');

SearchableSelect

A dropdown select component with built-in search, supporting single and multi-select modes.

Location: apps/web/src/components/shared/SearchableSelect.tsx

import { SearchableSelect } from '../components/shared/SearchableSelect';

// Single select
<SearchableSelect
label="Assigned To"
options={users.map(u => ({ value: u.id, label: `${u.firstName} ${u.lastName}` }))}
value={selectedUserId}
onChange={setSelectedUserId}
placeholder="Select a user..."
searchable
/>

// Multi-select
<SearchableSelect
label="Teams"
options={teams.map(t => ({ value: t.id, label: t.name }))}
value={selectedTeamIds}
onChange={setSelectedTeamIds}
multiple
placeholder="Select teams..."
/>

SearchableSelect Props

PropTypeDescription
labelstringField label
options{ value: string; label: string }[]Available options
valuestring | string[]Selected value(s)
onChange(value: any) => voidChange handler
multiplebooleanEnable multi-select
searchablebooleanEnable search filtering
placeholderstringPlaceholder text
disabledbooleanDisable the input
errorstringError message to display

CustomFieldRenderer

Dynamically renders form fields based on their configured type (text, number, date, select, multi-select, checkbox, textarea, etc.).

Location: apps/web/src/components/shared/CustomFieldRenderer.tsx

import { CustomFieldRenderer } from '../components/shared/CustomFieldRenderer';

// Render a custom field
<CustomFieldRenderer
field={{
name: 'budget_range',
label: 'Budget Range',
type: 'select',
required: true,
options: ['< $10K', '$10K - $50K', '$50K - $100K', '> $100K'],
}}
value={formData.budget_range}
onChange={(value) => setFormData({ ...formData, budget_range: value })}
permission="editable" // 'editable' | 'read_only' | 'hidden'
/>

Supported Field Types

TypeRendered As
textText input
numberNumber input
dateDate picker
datetimeDate-time picker
emailEmail input
phonePhone input
urlURL input
textareaMulti-line textarea
selectDropdown select
multi_selectMulti-select with tags
checkboxCheckbox
currencyCurrency input with symbol
percentagePercentage input
user_lookupUser selector (SearchableSelect)

NotesPanel

Displays and manages notes for any entity. Includes note creation form and note list with timestamps.

Location: apps/web/src/components/shared/NotesPanel.tsx

import { NotesPanel } from '../components/shared/NotesPanel';

<NotesPanel
entityType="leads"
entityId={leadId}
/>

DocumentsPanel

Manages documents/files attached to any entity. Supports upload, download, and delete.

Location: apps/web/src/components/shared/DocumentsPanel.tsx

import { DocumentsPanel } from '../components/shared/DocumentsPanel';

<DocumentsPanel
entityType="leads"
entityId={leadId}
canUpload={canEdit}
canDelete={canDelete}
/>

AvatarUpload

Profile image upload component with preview and crop.

Location: apps/web/src/components/shared/AvatarUpload.tsx

import { AvatarUpload } from '../components/shared/AvatarUpload';

<AvatarUpload
currentAvatar={user.avatar}
onUpload={(url) => handleAvatarChange(url)}
size="lg" // 'sm' | 'md' | 'lg'
/>

Timeline

Displays an activity timeline for an entity (calls, emails, meetings, stage changes, etc.).

<Timeline
entityType="leads"
entityId={leadId}
/>

ChangeHistory

Shows the audit log as a chronological timeline of changes.

<ChangeHistory
entityType="leads"
entityId={leadId}
/>

StageFieldInput

Modal component that appears during stage transitions when the target stage has required fields.

<StageFieldInput
stageId={targetStageId}
fields={requiredFields}
onSubmit={(fieldValues) => handleStageChange(targetStageId, fieldValues)}
onCancel={() => setShowModal(false)}
/>

Building New Shared Components

When creating new shared components, follow these conventions:

File Structure

components/shared/
└── MyComponent.tsx

Component Template

import React from 'react';
import { Loader2 } from 'lucide-react';

interface MyComponentProps {
title: string;
loading?: boolean;
error?: string;
onRetry?: () => void;
children: React.ReactNode;
}

export function MyComponent({ title, loading, error, onRetry, children }: MyComponentProps) {
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-purple-600" />
</div>
);
}

if (error) {
return (
<div className="text-center py-12">
<p className="text-red-600 dark:text-red-400 mb-4">{error}</p>
{onRetry && (
<button
onClick={onRetry}
className="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-xl"
>
Retry
</button>
)}
</div>
);
}

return (
<div className="bg-white dark:bg-slate-800 rounded-xl border border-gray-200
dark:border-slate-700 p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{title}
</h3>
{children}
</div>
);
}
Checklist for New Components
  • Include TypeScript props interface
  • Support dark mode (dark: variants)
  • Handle loading state with <Loader2 className="animate-spin" />
  • Handle error state with retry option
  • Use Tailwind classes only (no inline styles)
  • Use rounded-xl for buttons and cards
  • Use purple-600 as the primary action color