diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 7b429aa..2976cd7 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import cytoscape, { Core, ElementDefinition } from 'cytoscape'; import { loadLandkarte, Landkarte, VaultNode } from './landkarte'; import { loadNoteContent } from './note-loader'; +import ClusterMap from './ClusterMap'; const TYPE_COLORS: Record = { inbox: '#5B8FF9', @@ -11,6 +12,8 @@ const TYPE_COLORS: Record = { unknown: '#999999', }; +type ViewMode = 'graph' | 'cluster'; + export default function App() { const containerRef = useRef(null); const cyRef = useRef(null); @@ -18,6 +21,7 @@ export default function App() { const [error, setError] = useState(null); const [selected, setSelected] = useState(null); const [noteContent, setNoteContent] = useState(null); + const [view, setView] = useState('graph'); useEffect(() => { loadLandkarte() @@ -26,7 +30,7 @@ export default function App() { }, []); useEffect(() => { - if (!landkarte || !containerRef.current) return; + if (view !== 'graph' || !landkarte || !containerRef.current) return; const elements: ElementDefinition[] = [ ...landkarte.nodes.map((n) => ({ @@ -90,7 +94,6 @@ export default function App() { const node = evt.target.data('node') as VaultNode; setSelected(node); setNoteContent(null); - // Versuche, Inhalt zu laden try { const content = await loadNoteContent(node.id); setNoteContent(content); @@ -110,62 +113,106 @@ export default function App() { cyRef.current?.destroy(); cyRef.current = null; }; - }, [landkarte]); + }, [landkarte, view]); return ( -
-
- +
); } +function tabStyle(active: boolean): React.CSSProperties { + return { + padding: '4px 12px', + background: active ? '#fff' : 'transparent', + color: active ? '#222' : '#aaa', + border: 'none', + borderRadius: 3, + cursor: 'pointer', + fontSize: 13, + fontFamily: 'inherit', + }; +} + function Legend() { return (
diff --git a/webapp/src/ClusterMap.tsx b/webapp/src/ClusterMap.tsx new file mode 100644 index 0000000..ade26a1 --- /dev/null +++ b/webapp/src/ClusterMap.tsx @@ -0,0 +1,107 @@ +import { useMemo } from 'react'; +import type { Landkarte, VaultNode } from './landkarte'; + +const TYPE_COLORS: Record = { + inbox: '#5B8FF9', + daily: '#5AD8A6', + wiki: '#F6BD16', + project: '#E8684A', + unknown: '#999999', +}; + +interface Props { + landkarte: Landkarte; + onSelect: (node: VaultNode) => void; +} + +/** + * Phase 3.1: Cluster-Karte. + * Zeigt Notizen gruppiert nach Cluster als HTML-Grid. + * Jeder Cluster ist eine Spalte (oder Reihe bei vielen Clustern). + */ +export default function ClusterMap({ landkarte, onSelect }: Props) { + // Gruppierung nach Cluster + const groups = useMemo(() => { + const byCluster = new Map(); + for (const n of landkarte.nodes) { + const key = n.cluster ?? '(ohne Cluster)'; + const list = byCluster.get(key) ?? []; + list.push(n); + byCluster.set(key, list); + } + // Sortierung: Cluster-Name alphabetisch, Notizen nach Typ + ID + const sorted: Array<[string, VaultNode[]]> = [...byCluster.entries()] + .sort((a, b) => a[0].localeCompare(b[0])); + for (const [, list] of sorted) { + list.sort((a, b) => { + const t = a.type.localeCompare(b.type); + return t !== 0 ? t : a.id.localeCompare(b.id); + }); + } + return sorted; + }, [landkarte]); + + return ( +
+ {groups.map(([cluster, nodes]) => ( +
+

+ {cluster} ({nodes.length}) +

+
+ {nodes.map((n) => ( + + ))} +
+
+ ))} +
+ ); +}