# Frontend Components — Unit 4: Dashboard ## Component Hierarchy ```mermaid graph TD Main["main.tsx\n(App Entry)"] QCP["QueryClientProvider\n(TanStack Query root)"] AuthProv["AuthProvider"] RouterProv["RouterProvider"] DP["DashboardPage\n(/dashboard route)"] Hook["useAvailabilityStatus\n(useQuery hook)"] Welcome["Welcome Header\n(existing — unchanged)"] Widget["Availability Widget\nsection"] Badge["AvailabilityStatusBadge"] Skeleton["Skeleton loading\n(isLoading)"] ErrorBanner["Error Banner + Retry\n(isError)"] StaleIndicator["Stale Indicator\n(isError + data)"] Main --> QCP QCP --> AuthProv AuthProv --> RouterProv RouterProv --> DP DP --> Hook DP --> Welcome DP --> Widget Widget --> Skeleton Widget --> Badge Widget --> ErrorBanner Badge --> StaleIndicator classDef infra fill:#4CAF50,stroke:#2E7D32,color:#000 classDef page fill:#2196F3,stroke:#0D47A1,color:#000 classDef hook fill:#9C27B0,stroke:#4A148C,color:#000 classDef ui fill:#FF9800,stroke:#E65100,color:#000 classDef error fill:#F44336,stroke:#B71C1C,color:#000 class Main,QCP,AuthProv,RouterProv infra class DP page class Hook hook class Welcome,Widget,Badge,Skeleton,StaleIndicator ui class ErrorBanner error ``` Component hierarchy: main.tsx wraps app with QueryClientProvider; DashboardPage consumes availability hook and renders welcome header + availability widget with conditional states. --- ## New / Modified Files ### 1. `frontend/src/main.tsx` (modify) Add `QueryClientProvider` wrapping above `AuthProvider`. No other changes. ```tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: true }, }, }); // Wrap existing tree: ``` --- ### 2. `frontend/src/api/types.ts` (modify) Add two new types at the end of the file: ```ts export type AvailabilityStatus = 'Available' | 'Maintenance' | 'Unavailable'; export interface AvailabilityResponse { status: AvailabilityStatus; checkedAt: string; // ISO 8601 message: string; } ``` --- ### 3. `frontend/src/api/useAvailability.ts` (new) ```ts import { useQuery } from '@tanstack/react-query'; import { api } from '@/lib/api-client'; import type { AvailabilityResponse } from './types'; export function useAvailabilityStatus() { return useQuery({ queryKey: ['availability', 'status'], queryFn: () => api.get('/api/v1/Availability/status'), staleTime: 30_000, }); } ``` **Return**: standard TanStack Query result (`data`, `error`, `isLoading`, `isError`, `isFetching`, `refetch`). --- ### 4. `frontend/src/components/shared/AvailabilityStatusBadge.tsx` (new) **Props**: | Prop | Type | Required | Description | |---|---|---|---| | `status` | `AvailabilityStatus` | Yes | Current status value | | `message` | `string` | No | Subtitle text below badge (only shown when non-empty) | | `stale` | `boolean` | No | When true, renders a stale-data visual indicator (amber border, clock icon) | **Status → color mapping**: | Status | Badge color class | Icon | |---|---|---| | `Available` | `bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200` | `CheckCircle` | | `Maintenance` | `bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200` | `AlertTriangle` | | `Unavailable` | `bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200` | `XCircle` | **`stale` prop behavior**: When `stale={true}`, the badge wrapper gets an amber dashed border and a `Clock` icon + `t('availability.staleLabel')` label is rendered below the badge. **Test IDs**: | Element | `data-testid` | |---|---| | Badge wrapper | `availability-badge` | | Status label | `availability-status-label` | | Message subtitle | `availability-message` | | Stale indicator | `availability-stale-indicator` | --- ### 5. `frontend/src/pages/DashboardPage.tsx` (modify) Replace the generic placeholder `Card` with the availability widget section. **Conditional rendering logic**: ``` if (isLoading) → show Skeleton in widget area if (isError && !data) → show error card with Retry (no status to show) if (data) → show AvailabilityStatusBadge if (isError) → also show stale indicator on badge + error banner with Retry ``` **Layout sketch** (inside existing `space-y-6` wrapper): ``` [Welcome header — unchanged] [Card: title = t('availability.title')] [if isLoading] → skeleton placeholder [if !isLoading && data] → AvailabilityStatusBadge status={data.status} message={data.message} stale={isError} [if isError] → error banner (below badge if stale, full-card if no data) + Retry button ``` --- ### 6. `frontend/src/mocks/availability/handlers.ts` (new) MSW handler for tests: ```ts import { http, HttpResponse } from 'msw'; export const availabilityHandlers = [ http.get('*/Availability/status', () => HttpResponse.json({ status: 'Available', checkedAt: new Date().toISOString(), message: '', }) ), ]; ``` Register in `frontend/src/mocks/browser.ts` and `frontend/src/mocks/server.ts`. --- ## i18n Additions **`en/translation.json`**: ```json { "availability": { "title": "System Status", "available": "Available", "maintenance": "Maintenance", "unavailable": "Unavailable", "errorTitle": "Could not load status", "staleLabel": "Last known status", "retry": "Retry" } } ``` **`nl/translation.json`**: ```json { "availability": { "title": "Systeemstatus", "available": "Beschikbaar", "maintenance": "Onderhoud", "unavailable": "Niet beschikbaar", "errorTitle": "Status kon niet worden geladen", "staleLabel": "Laatste bekende status", "retry": "Opnieuw proberen" } } ``` --- ## Test IDs Summary | Element | `data-testid` | |---|---| | Availability badge wrapper | `availability-badge` | | Status label text | `availability-status-label` | | Message subtitle | `availability-message` | | Stale indicator | `availability-stale-indicator` | | Error banner | `availability-error` | | Retry button | `availability-retry` | | Widget skeleton | `availability-skeleton` |