af87c2c112
Je Reiter wird die Flaeche des ersten Desktop-Bildschirms als __canvas im Layout-JSON gespeichert (ohne API/DB-Aenderung, GRID_VERSION bleibt 3). Das Raster rendert in Leinwandbreite und wird per transform: scale(min(bw/cw, bh/ch)) eingepasst, Schrift eingeschlossen, ohne Scrollen; unter 768 px wie bisher. Ziehen/Groesse aendern unter Skalierung ueber eine eigene Positionsstrategie (createScaledStrategy aus react-grid-layout 2.2.3 rechnet den Rasterversatz falsch). Im Browser nachgewiesen: 1920x1080 -> 1366x768 (Faktor 0,66), Ziehen +200 px folgt der Maus, 700 px ohne Skalierung. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
226 lines
8.5 KiB
TypeScript
226 lines
8.5 KiB
TypeScript
'use client';
|
|
|
|
import { useTranslations } from 'next-intl';
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import {
|
|
BackgroundPicker,
|
|
DashboardBackdrop,
|
|
useDashboardBackground,
|
|
} from '@/components/dashboard/dashboard-background';
|
|
import { DashboardGrid } from '@/components/dashboard/dashboard-grid';
|
|
import { DashboardTabs } from '@/components/dashboard/dashboard-tabs';
|
|
import { EditModeToggle } from '@/components/dashboard/edit-mode-toggle';
|
|
import { WidgetCatalogModal } from '@/components/dashboard/widget-catalog-modal';
|
|
import type { WidgetType } from '@/components/dashboard/widget-registry';
|
|
import { registerWidget } from '@/components/dashboard/widget-registry';
|
|
import { CalculatorWidget } from '@/components/dashboard/widgets/calculator-widget';
|
|
import { CalendarWidget } from '@/components/dashboard/widgets/calendar-widget';
|
|
import { ClockWidget } from '@/components/dashboard/widgets/clock-widget';
|
|
import { FavoritesWidget } from '@/components/dashboard/widgets/favorites-widget';
|
|
import { NoteWidget } from '@/components/dashboard/widgets/note-widget';
|
|
import { PictureFrameWidget } from '@/components/dashboard/widgets/picture-frame-widget';
|
|
import { ProxmoxWidget } from '@/components/dashboard/widgets/proxmox-widget';
|
|
import { ReminderWidget } from '@/components/dashboard/widgets/reminder-widget';
|
|
import { SearchWidget } from '@/components/dashboard/widgets/search-widget';
|
|
import { StopwatchWidget } from '@/components/dashboard/widgets/stopwatch-widget';
|
|
import { XframeWidget } from '@/components/dashboard/widgets/xframe-widget';
|
|
import { HEADER_ACTIONS_SLOT_ID } from '@/components/layout/header-slot';
|
|
import { useDashboardStore } from '@/lib/stores/dashboard-store';
|
|
|
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
|
|
|
// Anmeldung der Kachel-Komponenten an der Registry. Steht hier und nicht in
|
|
// der Registry selbst, weil die Komponenten ueber den Wrapper wieder die
|
|
// Registry importieren — ein Import aus der Registry heraus waere ein
|
|
// Zirkelimport. Seit quick-260922-m1h EINE Funktion statt neun `wireXWidget`.
|
|
registerWidget('clock', ClockWidget);
|
|
registerWidget('search', SearchWidget);
|
|
registerWidget('calendar', CalendarWidget);
|
|
registerWidget('note', NoteWidget);
|
|
registerWidget('calculator', CalculatorWidget);
|
|
registerWidget('stopwatch', StopwatchWidget);
|
|
registerWidget('favorites', FavoritesWidget);
|
|
registerWidget('picture-frame', PictureFrameWidget);
|
|
registerWidget('xframe', XframeWidget);
|
|
registerWidget('proxmox', ProxmoxWidget);
|
|
registerWidget('reminder', ReminderWidget);
|
|
|
|
/** Modul-Eintrag aus `GET /modules/active` — hier zaehlt nur der Slug. */
|
|
interface ActiveModule {
|
|
slug: string;
|
|
}
|
|
|
|
export default function DashboardPage() {
|
|
const t = useTranslations('widgets');
|
|
const [catalogOpen, setCatalogOpen] = useState(false);
|
|
// Seitlicher Versatz der zentrierten Kacheln — die Zeile mit Begruessung
|
|
// und Befehlsleiste rueckt mit, damit sie buendig ueber den Kacheln steht.
|
|
// Einhaengepunkt der Kopfzeile fuer die Dashboard-Aktionen (quick-260928-vxe)
|
|
const [actionsSlot, setActionsSlot] = useState<HTMLElement | null>(null);
|
|
useEffect(() => {
|
|
setActionsSlot(document.getElementById(HEADER_ACTIONS_SLOT_ID));
|
|
}, []);
|
|
// Hintergrund je Benutzer (Design „Mosaik“), seit quick-260928-ujj in der
|
|
// Datenbank gespeichert und mit der Sitzung geladen.
|
|
const { background, choose: chooseBackground } = useDashboardBackground();
|
|
const hasBackground = background.kind !== 'none';
|
|
// quick-260922-m1h: Slugs der Module, die dieser Benutzer nutzen darf —
|
|
// der Katalog blendet Kacheln gesperrter Module damit aus. `null` heisst
|
|
// "noch unbekannt oder Abruf fehlgeschlagen" und ist fail-closed.
|
|
const [accessibleModuleSlugs, setAccessibleModuleSlugs] = useState<string[] | null>(null);
|
|
|
|
const {
|
|
dashboards,
|
|
activeDashboardId,
|
|
isSwitchingDashboard,
|
|
layouts,
|
|
canvas,
|
|
widgets,
|
|
isEditMode,
|
|
isLoading,
|
|
error,
|
|
setEditMode,
|
|
updateLayouts,
|
|
addWidget,
|
|
removeWidget,
|
|
loadDashboard,
|
|
selectDashboard,
|
|
createDashboard,
|
|
renameDashboard,
|
|
deleteDashboard,
|
|
reorderDashboards,
|
|
captureCanvas,
|
|
} = useDashboardStore();
|
|
|
|
// Load dashboard data on mount
|
|
useEffect(() => {
|
|
loadDashboard();
|
|
}, [loadDashboard]);
|
|
|
|
// Zugaengliche Module holen — gleiches Muster wie die Seitenleiste
|
|
// (`components/layout/sidebar.tsx`): derselbe Endpunkt, derselbe
|
|
// Sitzungs-Keks, Fehler still. Der Abruf steht hier und nicht im Dialog,
|
|
// damit der Dialog ein reines Anzeige-Bauteil bleibt.
|
|
const fetchAccessibleModules = useCallback(async () => {
|
|
try {
|
|
const res = await fetch(`${API_URL}/modules/active`, {
|
|
credentials: 'include',
|
|
});
|
|
if (!res.ok) return;
|
|
const modules: ActiveModule[] = await res.json();
|
|
setAccessibleModuleSlugs(modules.map((m) => m.slug));
|
|
} catch {
|
|
// still: die Liste bleibt null, der Katalog zeigt dann nur
|
|
// Plattform-Kacheln (fail-closed).
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
fetchAccessibleModules();
|
|
}, [fetchAccessibleModules]);
|
|
|
|
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>
|
|
);
|
|
}
|
|
|
|
const actions = (
|
|
<div role="toolbar" aria-label={t('toolbarLabel')} className="flex items-center gap-1">
|
|
{isEditMode && <BackgroundPicker value={background} onChange={chooseBackground} />}
|
|
{isEditMode && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setCatalogOpen(true)}
|
|
aria-label={t('addWidget')}
|
|
title={t('addWidget')}
|
|
className="btn btn-appbar"
|
|
>
|
|
<svg
|
|
aria-hidden="true"
|
|
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>
|
|
<span className="hidden lg:inline">{t('addWidget')}</span>
|
|
</button>
|
|
)}
|
|
<EditModeToggle isEditMode={isEditMode} onToggle={() => setEditMode(!isEditMode)} />
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className={`relative p-2 ${hasBackground ? 'dashboard-has-bg' : ''}`}>
|
|
<DashboardBackdrop background={background} />
|
|
{/* Reiterleiste (quick-260923-ad9) — seit quick-260924-h7x per Portal
|
|
in der Kopfzeile, hier nimmt sie keinen Platz mehr ein. Sie bleibt
|
|
waehrend eines Reiterwechsels stehen, nur das Raster zeigt eine
|
|
kurze Ladezeile. */}
|
|
<DashboardTabs
|
|
dashboards={dashboards}
|
|
activeDashboardId={activeDashboardId}
|
|
isEditMode={isEditMode}
|
|
onSelect={selectDashboard}
|
|
onCreate={createDashboard}
|
|
onRename={renameDashboard}
|
|
onDelete={deleteDashboard}
|
|
onReorder={reorderDashboards}
|
|
/>
|
|
|
|
{/* Dashboard-Aktionen (quick-260928-vxe): per Portal rechts in der
|
|
App-Leiste statt als eigene Zeile ueber dem Raster. Fehlt der
|
|
Einhaengepunkt (etwa in Tests ohne Kopfzeile), stehen sie hier. */}
|
|
{actionsSlot ? createPortal(actions, actionsSlot) : actions}
|
|
|
|
{isSwitchingDashboard ? (
|
|
<div className="flex min-h-[40vh] items-center justify-center">
|
|
<p className="text-sm text-muted-foreground">{t('tabs.switching')}</p>
|
|
</div>
|
|
) : (
|
|
/* Dashboard grid — direkt im Container, ohne Abstands-Wrapper. */
|
|
<DashboardGrid
|
|
layouts={layouts}
|
|
widgets={widgets}
|
|
isEditMode={isEditMode}
|
|
onLayoutChange={updateLayouts}
|
|
onRemoveWidget={removeWidget}
|
|
onOpenCatalog={() => setCatalogOpen(true)}
|
|
onQuickAdd={(type) => addWidget(type)}
|
|
canvas={canvas}
|
|
onCaptureCanvas={captureCanvas}
|
|
/>
|
|
)}
|
|
|
|
{/* Widget catalog modal */}
|
|
<WidgetCatalogModal
|
|
isOpen={catalogOpen}
|
|
onClose={() => setCatalogOpen(false)}
|
|
accessibleModuleSlugs={accessibleModuleSlugs}
|
|
onAddWidget={(type: WidgetType) => {
|
|
addWidget(type);
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|