diff --git a/package.json b/package.json index 7289b9ab..46513050 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "docs:api:generate": "node scripts/generate-api-docs.mjs" }, "dependencies": { - "@tensorflow/tfjs-node": "^5.0.0", + "@tensorflow/tfjs-node": "^4.17.0", "express": "^4.18.2", "@axe-core/react": "^4.11.3", "@stellar/stellar-sdk": "^12.3.0", diff --git a/src/components/dashboard/LayoutManager.tsx b/src/components/dashboard/LayoutManager.tsx new file mode 100644 index 00000000..72e9925d --- /dev/null +++ b/src/components/dashboard/LayoutManager.tsx @@ -0,0 +1,698 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + loadAllLayouts, + saveLayout, + deleteLayout, + getActiveLayoutId, + setActiveLayout, + getActiveLayout, + duplicateLayout, + exportLayout, + importLayout, + PRESET_LAYOUTS, + generateLayoutId, + createEmptyLayout, + type DashboardLayout +} from '../../lib/dashboardLayouts'; +import { useResponsive } from '../../hooks/useResponsive'; +import { addBreadcrumb } from '../../lib/errorReporting'; +import { + Plus, + Copy, + Trash2, + Download, + Upload, + X, + Check, + LayoutTemplate, + Share2, + Settings +} from 'lucide-react'; + +interface LayoutManagerProps { + isOpen: boolean; + currentWidgets: any[]; + onLayoutChange: (widgets: any[]) => void; + onLayoutsChange?: () => Promise; + onClose: () => void; +} + +export default function LayoutManager({ isOpen, currentWidgets, onLayoutChange, onLayoutsChange, onClose }: LayoutManagerProps) { + const { isMobile } = useResponsive() as { isMobile: boolean }; + const [layouts, setLayouts] = useState([]); + const [activeLayoutId, setActiveLayoutId] = useState(null); + const [showCreateModal, setShowCreateModal] = useState(false); + const [showImportModal, setShowImportModal] = useState(false); + const [newLayoutName, setNewLayoutName] = useState(''); + const [importData, setImportData] = useState(''); + const [selectedPreset, setSelectedPreset] = useState(null); + + useEffect(() => { + if (isOpen) { + loadLayouts(); + } + }, [isOpen]); + + const loadLayouts = async () => { + const allLayouts = await loadAllLayouts(); + const activeId = await getActiveLayoutId(); + setLayouts(allLayouts); + setActiveLayoutId(activeId); + }; + + const handleCreateLayout = async () => { + if (!newLayoutName.trim()) return; + + const newLayout = createEmptyLayout(newLayoutName); + newLayout.widgets = currentWidgets.map(w => ({ + id: w.id, + type: w.type, + height: w.height, + span: w.span, + })); + + await saveLayout(newLayout); + await setActiveLayout(newLayout.id); + await loadLayouts(); + setShowCreateModal(false); + setNewLayoutName(''); + addBreadcrumb('Layout created', 'user_action', { layoutId: newLayout.id, name: newLayout.name }); + }; + + const handleSwitchLayout = async (layout: DashboardLayout) => { + await setActiveLayout(layout.id); + setActiveLayoutId(layout.id); + onLayoutChange(layout.widgets); + addBreadcrumb('Layout switched', 'user_action', { layoutId: layout.id, name: layout.name }); + }; + + const handleDeleteLayout = async (layoutId: string) => { + if (!confirm('Are you sure you want to delete this layout?')) return; + await deleteLayout(layoutId); + await loadLayouts(); + addBreadcrumb('Layout deleted', 'user_action', { layoutId }); + }; + + const handleDuplicateLayout = async (layoutId: string) => { + const duplicated = await duplicateLayout(layoutId); + await loadLayouts(); + addBreadcrumb('Layout duplicated', 'user_action', { + originalId: layoutId, + newId: duplicated.id + }); + }; + + const handleExportLayout = (layout: DashboardLayout) => { + const exported = exportLayout(layout); + const blob = new Blob([exported], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${layout.name.replace(/\s+/g, '-').toLowerCase()}-layout.json`; + a.click(); + URL.revokeObjectURL(url); + addBreadcrumb('Layout exported', 'user_action', { layoutId: layout.id }); + }; + + const handleImportLayout = async () => { + if (!importData.trim()) return; + + try { + const imported = importLayout(importData); + await saveLayout(imported); + await loadLayouts(); + setShowImportModal(false); + setImportData(''); + addBreadcrumb('Layout imported', 'user_action', { layoutId: imported.id }); + } catch (error) { + alert(`Failed to import layout: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + }; + + const handleApplyPreset = async (presetId: string) => { + const preset = PRESET_LAYOUTS.find(p => p.id === presetId); + if (!preset) return; + + const newLayout: DashboardLayout = { + id: generateLayoutId(), + name: preset.name, + description: preset.description, + widgets: preset.widgets, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + isPreset: true, + }; + + await saveLayout(newLayout); + await setActiveLayout(newLayout.id); + onLayoutChange(preset.widgets); + await loadLayouts(); + addBreadcrumb('Preset layout applied', 'user_action', { presetId: preset.id }); + }; + + const handleShareLayout = (layout: DashboardLayout) => { + const exported = exportLayout(layout); + const shareToken = btoa(exported); + navigator.clipboard.writeText(shareToken); + alert('Layout share code copied to clipboard!'); + addBreadcrumb('Layout shared', 'user_action', { layoutId: layout.id }); + }; + + const overlayStyles: React.CSSProperties = { + position: 'fixed', + inset: 0, + background: 'rgba(0, 0, 0, 0.6)', + backdropFilter: 'blur(4px)', + zIndex: 2000, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: isMobile ? '16px' : '32px', + }; + + const modalStyles: React.CSSProperties = { + background: 'var(--bg-surface)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-lg)', + width: '100%', + maxWidth: isMobile ? '100%' : '900px', + maxHeight: '90vh', + overflow: 'hidden', + display: 'flex', + flexDirection: 'column', + }; + + return ( +
+
e.stopPropagation()}> + {/* Header */} +
+
+ +
+

+ Dashboard Layouts +

+

+ Manage and switch between custom layouts +

+
+
+ +
+ + {/* Content */} +
+ {/* Action Buttons */} +
+ + + +
+ + {/* Preset Layouts */} +
+

+ + Preset Layouts +

+
+ {PRESET_LAYOUTS.map(preset => ( +
handleApplyPreset(preset.id)} + onMouseEnter={e => { + e.currentTarget.style.borderColor = 'var(--cyan)'; + e.currentTarget.style.boxShadow = '0 4px 12px var(--cyan-glow-sm)'; + }} + onMouseLeave={e => { + e.currentTarget.style.borderColor = 'var(--border)'; + e.currentTarget.style.boxShadow = 'none'; + }} + > +
{preset.icon}
+
+ {preset.name} +
+
+ {preset.description} +
+
+ ))} +
+
+ + {/* Saved Layouts */} +
+

+ + Your Layouts ({layouts.length}) +

+ + {layouts.length === 0 ? ( +
+
📭
+
+ No saved layouts +
+
+ Save your current dashboard or apply a preset to get started +
+
+ ) : ( +
+ {layouts.map(layout => ( +
+
+
+
+ {layout.name} + {activeLayoutId === layout.id && ( + + )} +
+ {layout.description && ( +
+ {layout.description} +
+ )} +
+ {layout.widgets.length} widgets +
+
+
+ +
+ + + + + + + + + +
+
+ ))} +
+ )} +
+
+ + {/* Create Layout Modal */} + {showCreateModal && ( +
setShowCreateModal(false)}> +
e.stopPropagation()}> +

+ Save Current Layout +

+ setNewLayoutName(e.target.value)} + style={{ + width: '100%', + padding: '10px 12px', + border: '1px solid var(--border)', + borderRadius: 'var(--radius)', + background: 'var(--bg-card)', + color: 'var(--text-primary)', + fontSize: '13px', + marginBottom: '16px', + }} + onKeyDown={e => e.key === 'Enter' && handleCreateLayout()} + /> +
+ + +
+
+
+ )} + + {/* Import Modal */} + {showImportModal && ( +
setShowImportModal(false)}> +
e.stopPropagation()}> +

+ Import Layout +

+