UI Components
DBS ships a set of shared UI components that standardize loading, empty, and error states across all pages. These live in src/components/common/ and are used everywhere a data fetch might return no results, fail, or be in progress.
DataLoader
The DataLoader component is the recommended wrapper for any section that fetches remote data. It handles the three non-happy states so your page component only needs to render the happy path.
import { DataLoader } from '@/components/common/DataLoader';
import { useData } from '@/hooks/use-data';
import { teamsApi } from '@/services/teams/api';
export function TeamList() {
const { data: teams, isLoading, error } = useData(
'teams-list',
() => teamsApi.getTeams()
);
return (
<DataLoader
isLoading={isLoading}
error={error}
isEmpty={!teams || teams.length === 0}
emptyMessage="No teams found"
emptyDescription="Create your first team to get started."
>
{teams?.map(team => <TeamCard key={team.id} team={team} />)}
</DataLoader>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
isLoading | boolean | — | Shows a skeleton/spinner when true |
error | Error | undefined | — | Shows ErrorState when truthy |
isEmpty | boolean | — | Shows EmptyState when true (and not loading/error) |
emptyMessage | string | 'No data found' | Title for the empty state |
emptyDescription | string | — | Subtitle for the empty state |
children | ReactNode | — | Content rendered in the happy path |
loadingRows | number | 5 | Number of skeleton rows to display |
PageHeader
Provides a consistent page title + description at the top of every dashboard page:
import { PageHeader } from '@/components/common/PageHeader';
export default function UsersPage() {
return (
<div>
<PageHeader
title="Users"
description="Manage all user accounts and their roles."
action={
<Button>
<Plus className="mr-2 h-4 w-4" />
Add User
</Button>
}
/>
{/* page content */}
</div>
);
}Props
| Prop | Type | Description |
|---|---|---|
title | string | Page heading (renders as <h1>) |
description | string | Subtitle paragraph |
action | ReactNode | Optional button or element aligned to the right |
EmptyState
Displayed when a list or table has no data:
import { EmptyState } from '@/components/common/EmptyState';
<EmptyState
icon={<Users className="h-8 w-8 text-muted-foreground" />}
title="No users yet"
description="Invite your first team member to get started."
action={<Button>Invite User</Button>}
/>ErrorState
Displayed when a data fetch returns an error:
import { ErrorState } from '@/components/common/ErrorState';
<ErrorState
title="Failed to load users"
message={error.message}
retry={mutate} // SWR mutate function to retry
/>PermissionAlert
A standardized “access denied” message for when a client-side permission check fails:
import { PermissionAlert } from '@/components/common/PermissionAlert';
import { usePermission } from '@/lib/rbac/hooks/usePermission';
export function AdminSection() {
const { allowed } = usePermission('settings.manage');
if (!allowed) {
return <PermissionAlert />;
}
return <SettingsPanel />;
}AppTable
The primary data table component used throughout DBS. Built on top of TanStack Table with shadcn/ui styling:
import { AppTable } from '@/components/common/AppTable';
const columns = [
{ key: 'name', header: 'Name', sortable: true },
{ key: 'email', header: 'Email', sortable: true },
{ key: 'role', header: 'Role', render: (row) => <RoleBadge role={row.role} /> },
];
export function UsersTable({ data, isLoading }) {
return (
<AppTable
data={data}
columns={columns}
isLoading={isLoading}
searchable
searchPlaceholder="Search users..."
/>
);
}For full table functionality including server-side pagination, sorting, and filtering, pair AppTable with the useDataTable hook documented in Data Fetching.
email-template.tsx
DBS uses a React Email template for all transactional emails. The template is located at src/components/email-template.tsx and rendered server-side via the Resend API.
// src/components/email-template.tsx
import { Html, Body, Container, Heading, Link, Text } from '@react-email/components';
interface EmailTemplateProps {
type: 'verification' | 'reset-password' | 'invite';
actionUrl: string;
userName?: string;
}
export function EmailTemplate({ type, actionUrl, userName }: EmailTemplateProps) {
return (
<Html>
<Body>
<Container>
<Heading>DBS — {titles[type]}</Heading>
<Text>Hi {userName ?? 'there'},</Text>
<Text>{messages[type]}</Text>
<Link href={actionUrl}>Click here to proceed →</Link>
</Container>
</Body>
</Html>
);
}See the Email System docs for usage details.