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:
@@ -1,42 +1,111 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { DashboardGrid } from '@/components/dashboard/dashboard-grid';
|
||||
import { EditModeToggle } from '@/components/dashboard/edit-mode-toggle';
|
||||
import { WidgetCatalogModal } from '@/components/dashboard/widget-catalog-modal';
|
||||
import { wireClockWidget } from '@/components/dashboard/widget-registry';
|
||||
import { ClockWidget } from '@/components/dashboard/widgets/clock-widget';
|
||||
import { useDashboardStore } from '@/lib/stores/dashboard-store';
|
||||
import type { WidgetType } from '@/components/dashboard/widget-registry';
|
||||
|
||||
// Wire ClockWidget into the registry (deferred to avoid circular deps)
|
||||
wireClockWidget(ClockWidget);
|
||||
|
||||
export default function DashboardPage() {
|
||||
const t = useTranslations('dashboard');
|
||||
const t = useTranslations('widgets');
|
||||
const [catalogOpen, setCatalogOpen] = useState(false);
|
||||
|
||||
const {
|
||||
layouts,
|
||||
widgets,
|
||||
isEditMode,
|
||||
isLoading,
|
||||
error,
|
||||
setEditMode,
|
||||
updateLayouts,
|
||||
addWidget,
|
||||
removeWidget,
|
||||
loadDashboard,
|
||||
} = useDashboardStore();
|
||||
|
||||
// Load dashboard data on mount
|
||||
useEffect(() => {
|
||||
loadDashboard();
|
||||
}, [loadDashboard]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex min-h-[60vh] items-center justify-center">
|
||||
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex min-h-[60vh] items-center justify-center">
|
||||
<p className="text-sm text-destructive">{t('layoutLoadError')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[60vh] gap-4">
|
||||
{/* Grid icon */}
|
||||
<div className="rounded-lg bg-muted p-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<rect x="3" y="3" width="7" height="7" rx="1" />
|
||||
<rect x="14" y="3" width="7" height="7" rx="1" />
|
||||
<rect x="14" y="14" width="7" height="7" rx="1" />
|
||||
<rect x="3" y="14" width="7" height="7" rx="1" />
|
||||
</svg>
|
||||
<div className="relative p-4">
|
||||
{/* Edit mode toggle — top right */}
|
||||
<div className="absolute right-4 top-4 z-10">
|
||||
<EditModeToggle
|
||||
isEditMode={isEditMode}
|
||||
onToggle={() => setEditMode(!isEditMode)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Empty state text (D-16) */}
|
||||
<p className="text-lg text-muted-foreground">
|
||||
{t('empty')}
|
||||
</p>
|
||||
{/* Dashboard grid */}
|
||||
<div className="mt-8">
|
||||
<DashboardGrid
|
||||
layouts={layouts}
|
||||
widgets={widgets}
|
||||
isEditMode={isEditMode}
|
||||
onLayoutChange={updateLayouts}
|
||||
onRemoveWidget={removeWidget}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Add widget button */}
|
||||
<button className="mt-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity">
|
||||
{t('addWidget')}
|
||||
</button>
|
||||
{/* Add widget button (edit mode only) */}
|
||||
{isEditMode && (
|
||||
<div className="fixed bottom-6 right-6 z-20">
|
||||
<button
|
||||
onClick={() => setCatalogOpen(true)}
|
||||
className="flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow-lg hover:opacity-90 transition-opacity"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
{t('addWidget')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Widget catalog modal */}
|
||||
<WidgetCatalogModal
|
||||
isOpen={catalogOpen}
|
||||
onClose={() => setCatalogOpen(false)}
|
||||
onAddWidget={(type: WidgetType) => {
|
||||
addWidget(type);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user