# Frontend Components — Unit 3: frontend-cms-page ## Component Hierarchy ``` CmsPage (src/pages/CmsPage.tsx) ├── AddCmsInstanceDialog (src/components/cms/AddCmsInstanceDialog.tsx) ├── SetStatusDialog (src/components/cms/SetStatusDialog.tsx) └── CmsInstanceList (src/components/cms/CmsInstanceList.tsx) ``` --- ## CmsPage **File**: `src/pages/CmsPage.tsx` **State**: | State | Type | Initial | Description | |-------|------|---------|-------------| | `addDialogOpen` | `boolean` | `false` | Controls AddCmsInstanceDialog visibility | | `statusTarget` | `CmsInstance \| null` | `null` | Instance passed to SetStatusDialog; null = closed | **Hooks**: `useCmsInstances()` **Render logic**: ``` Header row: title + "Add CMS" button (always visible) isPending → loading text isError → error text data === [] → EmptyState (icon + heading + description + Add button) data.length > 0 → CmsInstanceList AddCmsInstanceDialog (controlled by addDialogOpen) SetStatusDialog (controlled by statusTarget !== null) ``` --- ## CmsInstanceList **File**: `src/components/cms/CmsInstanceList.tsx` **Props**: ```typescript interface CmsInstanceListProps { instances: CmsInstance[]; onSetStatus: (instance: CmsInstance) => void; } ``` **Columns** (BR-FE-01): | Column | Source field | Notes | |--------|-------------|-------| | Name | `instance.name` | — | | URL | `instance.url` | — | | Status | `instance.status` | Rendered as badge (BR-FE-05..07) | | Last Contact | `instance.lastContactedAt` | Formatted date; "—" when null (BR-FE-02) | | Disable Message | `instance.disableMessage` | "—" when null (BR-FE-03) | | Actions | — | Dropdown with "Set Status" item | **Row styling**: `` (BR-FE-04) **Interactions**: Clicking "Set Status" in the actions dropdown calls `onSetStatus(instance)`. --- ## AddCmsInstanceDialog **File**: `src/components/cms/AddCmsInstanceDialog.tsx` **Props**: ```typescript interface AddCmsInstanceDialogProps { open: boolean; onOpenChange: (open: boolean) => void; } ``` **Form fields**: | Field | Type | Validation | |-------|------|-----------| | Name | text | Required, non-empty | | URL | text | Required, valid URL format | | ApiKey | password | Required, non-empty | **Hooks**: `useAddCmsInstance()` **Submit behaviour**: - Success (HTTP 201): call `onOpenChange(false)`, show success toast - HTTP 400: show `FormBannerError` inside dialog, keep open (BR-FE-15) - Network error: show `FormBannerError` inside dialog, keep open (BR-FE-16) --- ## SetStatusDialog **File**: `src/components/cms/SetStatusDialog.tsx` **Props**: ```typescript interface SetStatusDialogProps { instance: CmsInstance | null; open: boolean; onOpenChange: (open: boolean) => void; } ``` **Form fields**: | Field | Type | Condition | Validation | |-------|------|-----------|-----------| | Status | select (`Available` \| `NotAvailable` \| `Inactive`) | always | Required | | DisableMessage | text | only when `status === 'NotAvailable'` | Required when visible | **Hooks**: `useUpdateCmsInstanceStatus()` **Submit behaviour**: - HTTP 200 + `slaveContactSuccess=true`: close dialog, toast "Status updated — cliënt confirmed" (BR-FE-12) - HTTP 200 + `slaveContactSuccess=false`: close dialog, toast "Status saved — cliënt unreachable" (BR-FE-13) - HTTP 404: toast error, keep open - HTTP 400 / network error: toast error, keep open **DisableMessage field logic** (BR-FE-08..11): - Rendered only when selected status is `'NotAvailable'` - On status change away from `'NotAvailable'`: clear field value - Value sent to API: `null` unless status is `'NotAvailable'` --- ## Hooks ### useCmsInstances **File**: `src/api/useCmsInstances.ts` ```typescript useQuery({ queryKey: ['cmsInstances'], queryFn: () => api.get('/api/v1/CmsInstances'), staleTime: 30_000, }) ``` ### useAddCmsInstance **File**: `src/api/useAddCmsInstance.ts` ```typescript useMutation({ mutationFn: (data) => api.post('/api/v1/CmsInstances', data), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['cmsInstances'] }), }) ``` ### useUpdateCmsInstanceStatus **File**: `src/api/useUpdateCmsInstanceStatus.ts` ```typescript useMutation({ mutationFn: ({ id, ...body }) => api.put(`/api/v1/CmsInstances/${id}/status`, body), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['cmsInstances'] }), }) ``` --- ## i18n Keys (additions to `cms` namespace) ```json "cms": { "title": "CMS Instances", "addButton": "Add CMS", "emptyState": { "heading": "No CMS instances yet", "description": "Add your first CMS instance to get started." }, "table": { "name": "Name", "url": "URL", "status": "Status", "lastContact": "Last Contact", "disableMessage": "Disable Message", "actions": "Actions", "never": "—" }, "status": { "Available": "Available", "NotAvailable": "Unavailable", "Inactive": "Inactive" }, "actions": { "setStatus": "Set Status" }, "add": { "title": "Add CMS Instance", "nameLabel": "Name", "urlLabel": "URL", "apiKeyLabel": "API Key", "submitButton": "Add", "successToast": "CMS instance added successfully" }, "setStatus": { "title": "Set Status", "statusLabel": "Status", "disableMessageLabel": "Disable Message", "disableMessagePlaceholder": "Reason for disabling…", "submitButton": "Save", "successContactedToast": "Status updated — cliënt confirmed", "successUnreachableToast": "Status saved — cliënt unreachable" } } ```