feat(05-01): dashboard grid, clock widget, widget registry, store, and tests

- Install react-grid-layout@2.2.3 and react-resizable
- Create widget-registry.tsx with all 4 widget types, WIDGET_CONSTRAINTS, WidgetProps
- Create dashboard-api.ts with fetch/save layout and widget CRUD functions
- Create dashboard-store.ts (Zustand, NO persist — D-05) with edit mode and auto-save on exit
- Create DashboardGrid with react-grid-layout v2 Responsive, ResizeObserver width
- Create ClockWidget using Intl.DateTimeFormat (no manual UTC offsets)
- Create WidgetWrapper with drag handle and delete button in edit mode
- Create EditModeToggle (pencil/checkmark), WidgetCatalogModal (2x2 grid)
- Rewrite portal page.tsx as dashboard with grid, edit toggle, widget catalog
- Add ResizeObserver polyfill in test setup, CSS mock support in vitest config
- All 5 tests green (dashboard grid + clock widget)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 11:21:43 +02:00
parent 792b7661d8
commit d5e1c42e64
14 changed files with 1115 additions and 29 deletions
+70
View File
@@ -0,0 +1,70 @@
/**
* Dashboard API client functions.
* All calls use credentials: 'include' for cookie-based auth.
*/
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
export async function fetchLayout(): Promise<Record<string, unknown>> {
const res = await fetch(`${API_URL}/dashboard/layout`, {
credentials: 'include',
});
if (!res.ok) throw new Error('Failed to fetch layout');
return res.json();
}
export async function saveLayout(
layouts: Record<string, unknown>,
): Promise<void> {
const res = await fetch(`${API_URL}/dashboard/layout`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ layouts }),
});
if (!res.ok) throw new Error('Failed to save layout');
}
export async function fetchWidgets(): Promise<
Array<{ id: string; widgetType: string; config: Record<string, unknown> }>
> {
const res = await fetch(`${API_URL}/dashboard/widgets`, {
credentials: 'include',
});
if (!res.ok) throw new Error('Failed to fetch widgets');
return res.json();
}
export async function addWidget(
widgetType: string,
): Promise<{ id: string; widgetType: string; config: Record<string, unknown> }> {
const res = await fetch(`${API_URL}/dashboard/widgets`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ widgetType }),
});
if (!res.ok) throw new Error('Failed to add widget');
return res.json();
}
export async function removeWidget(id: string): Promise<void> {
const res = await fetch(`${API_URL}/dashboard/widgets/${id}`, {
method: 'DELETE',
credentials: 'include',
});
if (!res.ok) throw new Error('Failed to remove widget');
}
export async function updateWidgetConfig(
id: string,
config: Record<string, unknown>,
): Promise<void> {
const res = await fetch(`${API_URL}/dashboard/widgets/${id}/config`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ config }),
});
if (!res.ok) throw new Error('Failed to update widget config');
}