feat(unit-4): Dashboard — availability widget with TanStack Query
- Add @tanstack/react-query 5.101.0; wrap app with QueryClientProvider - Add AvailabilityStatus type and AvailabilityResponse to api/types.ts - Implement useAvailabilityStatus (staleTime 30s, stale-on-error preserved) - Add AvailabilityStatusBadge with green/amber/red states and stale indicator - Replace DashboardPage placeholder card with live availability widget - Add MSW availability handler; update test/utils with QueryClientProvider - 55/55 tests pass (FR-05) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
+231
@@ -0,0 +1,231 @@
|
||||
# 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:
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<RouterProvider router={router} context={{ auth }} />
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 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<AvailabilityResponse>({
|
||||
queryKey: ['availability', 'status'],
|
||||
queryFn: () => api.get<AvailabilityResponse>('/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` |
|
||||
Reference in New Issue
Block a user