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:
2026-06-22 13:39:29 +02:00
co-authored by Claude Haiku 4.5
parent f476e06691
commit 3c6a06028e
23 changed files with 1219 additions and 22 deletions
+8
View File
@@ -60,3 +60,11 @@ export interface InviteCompleteRequest {
name: string;
password: string;
}
export type AvailabilityStatus = 'Available' | 'Maintenance' | 'Unavailable';
export interface AvailabilityResponse {
status: AvailabilityStatus;
checkedAt: string; // ISO 8601
message: string;
}
+73
View File
@@ -0,0 +1,73 @@
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { http, HttpResponse } from 'msw';
import { createElement } from 'react';
import { describe, expect, it } from 'vitest';
import { server } from '@/mocks/server';
import { API_BASE } from '@/mocks/auth/fixtures';
import { useAvailabilityStatus } from './useAvailability';
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return ({ children }: { children: React.ReactNode }) =>
createElement(QueryClientProvider, { client: queryClient }, children);
}
const AVAILABILITY_URL = `${API_BASE}/api/v1/Availability/status`;
describe('useAvailabilityStatus', () => {
it('starts in loading state', () => {
const { result } = renderHook(() => useAvailabilityStatus(), {
wrapper: createWrapper(),
});
expect(result.current.isLoading).toBe(true);
expect(result.current.data).toBeUndefined();
});
it('returns data on successful fetch', async () => {
const { result } = renderHook(() => useAvailabilityStatus(), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.isError).toBe(false);
expect(result.current.data).toMatchObject({
status: 'Available',
message: '',
});
expect(typeof result.current.data?.checkedAt).toBe('string');
});
it('enters error state when the server returns 5xx', async () => {
server.use(
http.get(AVAILABILITY_URL, () =>
HttpResponse.json(
{ title: 'Internal Server Error', status: 500 },
{ status: 500 },
),
),
);
const { result } = renderHook(() => useAvailabilityStatus(), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.isError).toBe(true);
expect(result.current.data).toBeUndefined();
});
it('exposes a refetch function', async () => {
const { result } = renderHook(() => useAvailabilityStatus(), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(typeof result.current.refetch).toBe('function');
});
});
+11
View File
@@ -0,0 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '@/lib/api-client';
import type { AvailabilityResponse } from './types';
export function useAvailabilityStatus() {
return useQuery<AvailabilityResponse, Error>({
queryKey: ['availability', 'status'],
queryFn: () => api.get<AvailabilityResponse>('/api/v1/Availability/status'),
staleTime: 30_000,
});
}