e9914f61cb
- LinkWidget: single-link widget reusing FavoriteLink backend (D-06) - Single-link enforcement: add form hidden when link exists - List (row) and tile (grid) view modes, switchable in edit mode - Inline add/edit/delete forms in edit mode - Letter fallback span + icon with onError hide (T-08-11) - Links with target="_blank" rel="noreferrer" (T-08-12) - i18n keys added to de.json + en.json (link.*) - wireLinkWidget(LinkWidget) added to portal page.tsx - All 7 link-widget tests pass (GREEN); tsc --noEmit clean
126 lines
4.1 KiB
TypeScript
126 lines
4.1 KiB
TypeScript
'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, wireSearchWidget, wireCalendarWidget, wireNoteWidget, wireCalculatorWidget, wireStopwatchWidget, wireFavoritesWidget, wireLinkWidget } from '@/components/dashboard/widget-registry';
|
|
import { ClockWidget } from '@/components/dashboard/widgets/clock-widget';
|
|
import { SearchWidget } from '@/components/dashboard/widgets/search-widget';
|
|
import { CalendarWidget } from '@/components/dashboard/widgets/calendar-widget';
|
|
import { NoteWidget } from '@/components/dashboard/widgets/note-widget';
|
|
import { CalculatorWidget } from '@/components/dashboard/widgets/calculator-widget';
|
|
import { StopwatchWidget } from '@/components/dashboard/widgets/stopwatch-widget';
|
|
import { FavoritesWidget } from '@/components/dashboard/widgets/favorites-widget';
|
|
import { LinkWidget } from '@/components/dashboard/widgets/link-widget';
|
|
import { useDashboardStore } from '@/lib/stores/dashboard-store';
|
|
import type { WidgetType } from '@/components/dashboard/widget-registry';
|
|
|
|
// Wire widget components into the registry (deferred to avoid circular deps)
|
|
wireClockWidget(ClockWidget);
|
|
wireSearchWidget(SearchWidget);
|
|
wireCalendarWidget(CalendarWidget);
|
|
wireNoteWidget(NoteWidget);
|
|
wireCalculatorWidget(CalculatorWidget);
|
|
wireStopwatchWidget(StopwatchWidget);
|
|
wireFavoritesWidget(FavoritesWidget);
|
|
wireLinkWidget(LinkWidget);
|
|
|
|
export default function DashboardPage() {
|
|
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="relative p-4">
|
|
{/* Edit mode toggle — top right */}
|
|
<div className="absolute right-4 top-4 z-10">
|
|
<EditModeToggle
|
|
isEditMode={isEditMode}
|
|
onToggle={() => setEditMode(!isEditMode)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Dashboard grid */}
|
|
<div className="mt-8">
|
|
<DashboardGrid
|
|
layouts={layouts}
|
|
widgets={widgets}
|
|
isEditMode={isEditMode}
|
|
onLayoutChange={updateLayouts}
|
|
onRemoveWidget={removeWidget}
|
|
/>
|
|
</div>
|
|
|
|
{/* 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>
|
|
);
|
|
}
|