diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..e23380c Binary files /dev/null and b/bun.lockb differ diff --git a/frontend/.dockerignore b/frontend/.dockerignore deleted file mode 100644 index 3c3629e..0000000 --- a/frontend/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/frontend/.prettierignore b/frontend/.prettierignore deleted file mode 100644 index b161fc0..0000000 --- a/frontend/.prettierignore +++ /dev/null @@ -1,16 +0,0 @@ -# Ignore build outputs -/dist -/build - -# Ignore dependencies -/node_modules - -# Ignore coverage reports -/coverage - -# Ignore logs -*.log - -# Ignore configuration files -.env -.env.* \ No newline at end of file diff --git a/frontend/.prettierrc b/frontend/.prettierrc deleted file mode 100644 index 95fce3f..0000000 --- a/frontend/.prettierrc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "semi": true, - "singleQuote": true, - "tabWidth": 2, - "printWidth": 100, - "trailingComma": "es5", - "arrowParens": "avoid", - "endOfLine": "lf", - "bracketSpacing": true, - "jsxSingleQuote": false, - "bracketSameLine": false -} \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 8c447ef..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "frontend", - "version": "1.0.0", - "main": "index.js", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview", - "format": "prettier --write \"src/**/*.{tsx,ts,js,jsx,json,css,html}\"", - "format:check": "prettier --check \"src/**/*.{tsx,ts,js,jsx,json,css,html}\"" - }, - "author": "", - "license": "ISC", - "description": "", - "dependencies": { - "@headlessui/react": "^2.2.1", - "@tailwindcss/vite": "^4.1.4", - "axios": "^1.8.4", - "framer-motion": "^12.7.3", - "react": "^19.1.0", - "react-datepicker": "^8.3.0", - "react-dom": "^19.1.0", - "react-force-graph-2d": "^1.27.1", - "react-icons": "^5.5.0", - "react-router-dom": "^7.5.0", - "ts-node": "^10.9.2", - "typescript": "^5.8.3", - "vite": "^6.2.6" - }, - "devDependencies": { - "@types/axios": "^0.14.4", - "@types/node": "^22.14.1", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.2", - "@types/react-router-dom": "^5.3.3", - "@vitejs/plugin-react": "^4.4.0", - "autoprefixer": "^10.4.21", - "postcss": "^8.4.32", - "prettier": "^3.5.3", - "tailwindcss": "^4.1.4", - "webpack": "^5.99.5", - "webpack-cli": "^6.0.1" - } -} diff --git a/frontend/src/app.css b/frontend/src/app.css deleted file mode 100644 index d4b5078..0000000 --- a/frontend/src/app.css +++ /dev/null @@ -1 +0,0 @@ -@import 'tailwindcss'; diff --git a/frontend/src/components/FriendshipNetwork.tsx b/frontend/src/components/FriendshipNetwork.tsx deleted file mode 100644 index f27da40..0000000 --- a/frontend/src/components/FriendshipNetwork.tsx +++ /dev/null @@ -1,1991 +0,0 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { useFriendshipNetwork } from '../hooks/useFriendshipNetwork'; -import { useNetworks } from '../context/NetworkContext'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; -import { Transition } from '@headlessui/react'; -import { - FaArrowLeft, - FaChevronLeft, - FaChevronRight, - FaCog, - FaCompress, - FaEdit, - FaExclamationTriangle, - FaHome, - FaInfo, - FaPlus, - FaRedo, - FaRegCalendarAlt, - FaSave, - FaSearch, - FaSearchMinus, - FaSearchPlus, - FaStar, - FaTimes, - FaTrash, - FaUserCircle, - FaUserFriends, - FaUserPlus, -} from 'react-icons/fa'; - -// Import custom UI components -import { - Button, - Card, - CardBody, - ConfirmDialog, - EmptyState, - FormField, - Modal, - NetworkStats, - Toast, - ToastItem, - Tooltip, -} from './FriendshipNetworkComponents'; - -// Import visible canvas graph component -import CanvasGraph from './CanvasGraph'; - -// Define types -type RelationshipType = 'freund' | 'partner' | 'familie' | 'arbeitskolleg' | 'custom'; - -interface PersonNode { - _id: string; - firstName: string; - lastName: string; - birthday?: Date | string | null; - notes?: string; - position?: { - x: number; - y: number; - }; -} - -interface RelationshipEdge { - _id: string; - source: string; - target: string; - type: RelationshipType; - customType?: string; - notes?: string; -} - -interface CanvasGraphProps { - data: { - nodes: { - id: string; - firstName: string; - lastName: string; - connectionCount: number; - bgColor: string; - x: number; - y: number; - showLabel: boolean; - }[]; - edges: { - id: string; - source: string; - target: string; - color: string; - width: number; - type: RelationshipType; - customType?: string; - }[]; - }; - width: number; - height: number; - zoomLevel: number; - onNodeClick: (nodeId: string) => void; - onNodeDrag: (nodeId: string, x: number, y: number) => void; -} - -// Type for form errors -interface FormErrors { - [key: string]: string; -} - -// Graph appearance constants -const RELATIONSHIP_COLORS = { - freund: '#60A5FA', // Light blue - partner: '#F472B6', // Pink - familie: '#34D399', // Green - arbeitskolleg: '#FBBF24', // Yellow - custom: '#9CA3AF', // Gray -}; - -const RELATIONSHIP_LABELS = { - freund: 'Friend', - partner: 'Partner', - familie: 'Family', - arbeitskolleg: 'Colleague', - custom: 'Custom', -}; - -// Main FriendshipNetwork component -const FriendshipNetwork: React.FC = () => { - const { id } = useParams<{ id: string }>(); - const { networks } = useNetworks(); - const navigate = useNavigate(); - const graphContainerRef = useRef(null); - const [graphDimensions, setGraphDimensions] = useState({ width: 0, height: 0 }); - - // Network data state from custom hook - const { - people, - relationships, - loading, - error, - createPerson, - updatePerson, - deletePerson, - createRelationship, - deleteRelationship, - refreshNetwork, - updatePersonPosition: updatePersonPositionImpl = ( - id: string, - position: { x: number; y: number }, - ) => { - console.warn('updatePersonPosition not implemented'); - return Promise.resolve(); - }, - } = useFriendshipNetwork(id || null) as any; - - // Create a type-safe wrapper for updatePersonPosition - const updatePersonPosition = (id: string, position: { x: number; y: number }) => { - return updatePersonPositionImpl(id, position); - }; - - // Local UI state - const [sidebarOpen, setSidebarOpen] = useState(true); - const [sidebarTab, setSidebarTab] = useState('overview'); - const [zoomLevel, setZoomLevel] = useState(1); - const [toasts, setToasts] = useState([]); - const [interactionHint, setInteractionHint] = useState(true); - - // Modal states - const [personModalOpen, setPersonModalOpen] = useState(false); - const [relationshipModalOpen, setRelationshipModalOpen] = useState(false); - const [personDetailModalOpen, setPersonDetailModalOpen] = useState(false); - const [settingsModalOpen, setSettingsModalOpen] = useState(false); - const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); - const [helpModalOpen, setHelpModalOpen] = useState(false); - const [itemToDelete, setItemToDelete] = useState<{ type: string; id: string }>({ - type: '', - id: '', - }); - - // Form errors - const [personFormErrors, setPersonFormErrors] = useState({}); - const [relationshipFormErrors, setRelationshipFormErrors] = useState({}); - - // Form states - const [newPerson, setNewPerson] = useState({ - firstName: '', - lastName: '', - birthday: null as Date | null, - notes: '', - }); - - const [editPerson, setEditPerson] = useState(null); - - const [newRelationship, setNewRelationship] = useState({ - source: '', - target: '', - type: 'freund' as RelationshipType, - customType: '', - notes: '', - bidirectional: true, - }); - - // Filter states - const [peopleFilter, setPeopleFilter] = useState(''); - const [relationshipFilter, setRelationshipFilter] = useState(''); - const [relationshipTypeFilter, setRelationshipTypeFilter] = useState('all'); - - // Settings state - const [settings, setSettings] = useState({ - darkMode: true, - autoLayout: true, - showLabels: true, - animationSpeed: 'medium', - highlightConnections: true, - nodeSize: 'medium', - }); - - // Selected person state for highlighting - const [selectedPersonId, setSelectedPersonId] = useState(null); - - // Get current network info - const currentNetwork = networks.find(network => network._id === id); - - // Effect for graph container dimensions - useEffect(() => { - if (!graphContainerRef.current) return; - - const updateDimensions = () => { - if (graphContainerRef.current) { - const { width, height } = graphContainerRef.current.getBoundingClientRect(); - - setGraphDimensions(prev => { - if (prev.width !== width || prev.height !== height) { - return { width, height }; - } - return prev; - }); - } - }; - - // Initial measurement - updateDimensions(); - - // Set up resize observer - const resizeObserver = new ResizeObserver(updateDimensions); - if (graphContainerRef.current) { - resizeObserver.observe(graphContainerRef.current); - } - - // Set up window resize listener - window.addEventListener('resize', updateDimensions); - - // Clean up - return () => { - if (graphContainerRef.current) { - resizeObserver.unobserve(graphContainerRef.current); - } - window.removeEventListener('resize', updateDimensions); - }; - }, []); - - // Update dimensions when sidebar is toggled - useEffect(() => { - const timeoutId = setTimeout(() => { - if (graphContainerRef.current) { - const { width, height } = graphContainerRef.current.getBoundingClientRect(); - setGraphDimensions({ width, height }); - } - }, 300); - - return () => clearTimeout(timeoutId); - }, [sidebarOpen]); - - // Dismiss interaction hint after 10 seconds - useEffect(() => { - if (interactionHint) { - const timer = setTimeout(() => { - setInteractionHint(false); - }, 10000); - return () => clearTimeout(timer); - } - }, [interactionHint]); - - // Keyboard shortcuts - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - // Only apply shortcuts when not in an input field - const target = e.target as HTMLElement; - if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return; - - // Ctrl/Cmd + / to open help modal - if ((e.ctrlKey || e.metaKey) && e.key === '/') { - e.preventDefault(); - setHelpModalOpen(true); - } - - // + for zoom in - if (e.key === '+' || e.key === '=') { - e.preventDefault(); - handleZoomIn(); - } - - // - for zoom out - if (e.key === '-' || e.key === '_') { - e.preventDefault(); - handleZoomOut(); - } - - // 0 for reset zoom - if (e.key === '0') { - e.preventDefault(); - handleResetZoom(); - } - - // n for new person - if (e.key === 'n' && !e.ctrlKey && !e.metaKey) { - e.preventDefault(); - setPersonModalOpen(true); - } - - // r for new relationship - if (e.key === 'r' && !e.ctrlKey && !e.metaKey) { - e.preventDefault(); - setRelationshipModalOpen(true); - } - - // s for toggle sidebar - if (e.key === 's' && !e.ctrlKey && !e.metaKey) { - e.preventDefault(); - toggleSidebar(); - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, []); - - // Filtered people and relationships - const filteredPeople = people.filter(person => - `${person.firstName} ${person.lastName}`.toLowerCase().includes(peopleFilter.toLowerCase()), - ); - - const filteredRelationships = relationships.filter(rel => { - const source = people.find(p => p._id === rel.source); - const target = people.find(p => p._id === rel.target); - - if (!source || !target) return false; - - const matchesFilter = - `${source.firstName} ${source.lastName} ${target.firstName} ${target.lastName}` - .toLowerCase() - .includes(relationshipFilter.toLowerCase()); - - const matchesType = relationshipTypeFilter === 'all' || rel.type === relationshipTypeFilter; - - return matchesFilter && matchesType; - }); - - // Add toast notification - const addToast = (message: string, type: 'error' | 'success' | 'warning' | 'info' = 'success') => { - const id = Date.now(); - setToasts(prevToasts => [...prevToasts, { id, message, type, onClose: () => removeToast(id) }]); - - // Auto-remove after 3 seconds - setTimeout(() => { - removeToast(id); - }, 3000); - }; - - // Remove toast notification - const removeToast = (id: number) => { - setToasts(prevToasts => prevToasts.filter(toast => toast.id !== id)); - }; - - // Smart node placement for new people - const getSmartNodePosition = useCallback(() => { - const centerX = graphDimensions.width / 2; - const centerY = graphDimensions.height / 2; - const maxRadius = Math.min(graphDimensions.width, graphDimensions.height) * 0.4; - const totalNodes = people.length; - const index = totalNodes; - - if (totalNodes <= 0) { - return { x: centerX, y: centerY }; - } else if (totalNodes <= 4) { - const theta = index * 2.399; - const radius = maxRadius * 0.5 * Math.sqrt(index / (totalNodes + 1)); - return { - x: centerX + radius * Math.cos(theta), - y: centerY + radius * Math.sin(theta), - }; - } else if (totalNodes <= 11) { - const isOuterRing = index >= Math.floor(totalNodes / 2); - const ringIndex = isOuterRing ? index - Math.floor(totalNodes / 2) : index; - const ringTotal = isOuterRing - ? totalNodes - Math.floor(totalNodes / 2) + 1 - : Math.floor(totalNodes / 2); - const ringRadius = isOuterRing ? maxRadius * 0.8 : maxRadius * 0.4; - - const angle = (ringIndex / ringTotal) * 2 * Math.PI + (isOuterRing ? 0 : Math.PI / ringTotal); - return { - x: centerX + ringRadius * Math.cos(angle), - y: centerY + ringRadius * Math.sin(angle), - }; - } else { - const clusterCount = Math.max(3, Math.floor(Math.sqrt(totalNodes))); - const clusterIndex = index % clusterCount; - - const clusterAngle = (clusterIndex / clusterCount) * 2 * Math.PI; - const clusterDistance = maxRadius * 0.6; - const clusterX = centerX + clusterDistance * Math.cos(clusterAngle); - const clusterY = centerY + clusterDistance * Math.sin(clusterAngle); - - const clusterRadius = maxRadius * 0.3; - const randomAngle = Math.random() * 2 * Math.PI; - const randomDistance = Math.random() * clusterRadius; - - return { - x: clusterX + randomDistance * Math.cos(randomAngle), - y: clusterY + randomDistance * Math.sin(randomAngle), - }; - } - }, [graphDimensions.width, graphDimensions.height, people.length]); - - // Transform API data to graph format - const getGraphData = useCallback(() => { - if (!people || !relationships) { - return { nodes: [], edges: [] }; - } - - // Create nodes - const graphNodes = people.map(person => { - const connectionCount = relationships.filter( - r => r.source === person._id || r.target === person._id, - ).length; - - // Determine if node should be highlighted - const isSelected = person._id === selectedPersonId; - const isConnected = selectedPersonId - ? relationships.some( - r => - (r.source === selectedPersonId && r.target === person._id) || - (r.target === selectedPersonId && r.source === person._id), - ) - : false; - - // Determine background color based on connection count or highlight state - let bgColor; - if (isSelected) { - bgColor = '#F472B6'; // Pink-400 for selected - } else if (isConnected && settings.highlightConnections) { - bgColor = '#A78BFA'; // Violet-400 for connected - } else if (connectionCount === 0) { - bgColor = '#94A3B8'; // Slate-400 - } else if (connectionCount === 1) { - bgColor = '#38BDF8'; // Sky-400 - } else if (connectionCount <= 3) { - bgColor = '#818CF8'; // Indigo-400 - } else if (connectionCount <= 5) { - bgColor = '#A78BFA'; // Violet-400 - } else { - bgColor = '#F472B6'; // Pink-400 - } - - return { - id: person._id, - firstName: person.firstName, - lastName: person.lastName, - connectionCount, - bgColor, - x: person.position?.x || 0, - y: person.position?.y || 0, - showLabel: settings.showLabels, - }; - }); - - // Create edges - const graphEdges = relationships.map(rel => { - const color = RELATIONSHIP_COLORS[rel.type] || RELATIONSHIP_COLORS.custom; - const width = rel.type === 'partner' ? 4 : rel.type === 'familie' ? 3 : 2; - - // Highlight edges connected to selected node - const isHighlighted = - selectedPersonId && - settings.highlightConnections && - (rel.source === selectedPersonId || rel.target === selectedPersonId); - - return { - id: rel._id, - source: rel.source, - target: rel.target, - color: isHighlighted ? '#F472B6' : color, // Pink color for highlighted edges - width: isHighlighted ? width + 1 : width, // Slightly thicker for highlighted - type: rel.type, - customType: rel.customType, - }; - }); - - return { nodes: graphNodes, edges: graphEdges }; - }, [people, relationships, settings.showLabels, settings.highlightConnections, selectedPersonId]); - - // Validate person form - const validatePersonForm = (person: typeof newPerson): FormErrors => { - const errors: FormErrors = {}; - - if (!person.firstName.trim()) { - errors.firstName = 'First name is required'; - } - - if (!person.lastName.trim()) { - errors.lastName = 'Last name is required'; - } - - return errors; - }; - - // Validate relationship form - const validateRelationshipForm = (relationship: typeof newRelationship): FormErrors => { - const errors: FormErrors = {}; - - if (!relationship.source) { - errors.source = 'Source person is required'; - } - - if (!relationship.target) { - errors.target = 'Target person is required'; - } - - if (relationship.source === relationship.target) { - errors.target = 'Source and target cannot be the same person'; - } - - if (relationship.type === 'custom' && !relationship.customType.trim()) { - errors.customType = 'Custom relationship type is required'; - } - - // Check if relationship already exists - if (relationship.source && relationship.target) { - const existingRelationship = relationships.find( - r => - (r.source === relationship.source && r.target === relationship.target) || - (relationship.bidirectional && - r.source === relationship.target && - r.target === relationship.source), - ); - - if (existingRelationship) { - errors.general = 'This relationship already exists'; - } - } - - return errors; - }; - - // Handle person form submission - const handlePersonSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - const errors = validatePersonForm(newPerson); - setPersonFormErrors(errors); - - if (Object.keys(errors).length > 0) return; - - // Create person with smart positioning - const position = getSmartNodePosition(); - - createPerson({ - firstName: newPerson.firstName.trim(), - lastName: newPerson.lastName.trim(), - birthday: newPerson.birthday?.toISOString() || undefined, - notes: newPerson.notes, - position, - }); - - // Reset form and close modal - setNewPerson({ - firstName: '', - lastName: '', - birthday: null, - notes: '', - }); - - setPersonModalOpen(false); - addToast('Person added successfully'); - }; - - // Handle person update - const handleUpdatePerson = (e: React.FormEvent) => { - e.preventDefault(); - - if (!editPerson) return; - - const errors = validatePersonForm(editPerson as any); - setPersonFormErrors(errors); - - if (Object.keys(errors).length > 0) return; - - updatePerson(editPerson._id, { - firstName: editPerson.firstName, - lastName: editPerson.lastName, - birthday: editPerson.birthday ? new Date(editPerson.birthday).toISOString() : undefined, - notes: editPerson.notes, - }); - - setEditPerson(null); - setPersonDetailModalOpen(false); - addToast('Person updated successfully'); - }; - - // Handle relationship form submission - const handleRelationshipSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - const errors = validateRelationshipForm(newRelationship); - setRelationshipFormErrors(errors); - - if (Object.keys(errors).length > 0) return; - - const { source, target, type, customType, notes, bidirectional } = newRelationship; - - // Create the relationship - createRelationship({ - source, - target, - type, - customType: type === 'custom' ? customType : undefined, - notes, - }); - - // Create bidirectional relationship if selected - if (bidirectional && source !== target) { - createRelationship({ - source: target, - target: source, - type, - customType: type === 'custom' ? customType : undefined, - notes, - }); - } - - // Reset form and close modal - setNewRelationship({ - source: '', - target: '', - type: 'freund', - customType: '', - notes: '', - bidirectional: true, - }); - - setRelationshipModalOpen(false); - addToast(`Relationship${bidirectional ? 's' : ''} created successfully`); - }; - - // Handle deletion confirmation - const confirmDelete = (type: string, id: string) => { - setItemToDelete({ type, id }); - setDeleteConfirmOpen(true); - }; - - // Execute deletion - const executeDelete = () => { - const { type, id } = itemToDelete; - - if (type === 'person') { - deletePerson(id); - addToast('Person deleted'); - } else if (type === 'relationship') { - deleteRelationship(id); - addToast('Relationship deleted'); - } - }; - - // Open person detail modal - const openPersonDetail = (person: PersonNode) => { - setEditPerson({ ...person }); - setPersonDetailModalOpen(true); - }; - - // Handle zoom controls - const handleZoomIn = () => { - setZoomLevel(prev => Math.min(prev + 0.2, 2.5)); - }; - - const handleZoomOut = () => { - setZoomLevel(prev => Math.max(prev - 0.2, 0.5)); - }; - - const handleResetZoom = () => { - setZoomLevel(1); - }; - - // Toggle sidebar - const toggleSidebar = () => { - setSidebarOpen(!sidebarOpen); - }; - - // Handle refresh network - const handleRefreshNetwork = () => { - refreshNetwork(); - addToast('Network refreshed'); - }; - - // Handle node click to select and highlight - const handleNodeClick = (nodeId: string) => { - // Toggle selection - if (selectedPersonId === nodeId) { - setSelectedPersonId(null); - } else { - setSelectedPersonId(nodeId); - } - - // Open person details - const person = people.find(p => p._id === nodeId); - if (person) { - openPersonDetail(person); - } - }; - - // Sort people alphabetically - const sortedPeople = [...filteredPeople].sort((a, b) => { - const nameA = `${a.firstName} ${a.lastName}`.toLowerCase(); - const nameB = `${b.firstName} ${b.lastName}`.toLowerCase(); - return nameA.localeCompare(nameB); - }); - - // Loading state - if (loading) { - return ( -
-
-
-

Loading your network...

-
-
- ); - } - - // Error state - if (error) { - return ( -
-
-

- Error -

-

{error}

- -
-
- ); - } - - // Generate graph data - const graphData = getGraphData(); - - return ( -
- {/* Sidebar Toggle Button */} - - - {/* Sidebar */} -
- -
- {/* Network Header */} -
-
-

- {currentNetwork?.name || 'Relationship Network'} -

- - - -
-

Visualize your connections

-
- - {/* Network Stats */} - - - {/* Action Buttons */} -
- - -
- - {/* Sidebar Tabs */} -
- - - -
- - {/* Tab Content */} - {sidebarTab === 'overview' && ( -
- - -

About This Network

-

- This interactive visualization shows relationships between people in your - network. -

-
    -
  • - - Drag nodes to rearrange the network -
  • -
  • - - Click on people for more details -
  • -
  • - - Hover over connections to see relationship types -
  • -
  • - - Use the controls to zoom in/out and center the view -
  • -
-
-
- - - -

Legend

-
- {Object.entries(RELATIONSHIP_COLORS).map(([type, color]) => ( -
-
- - {RELATIONSHIP_LABELS[type as RelationshipType]} - -
- ))} -
-
-
- -
- - -
-
- )} - - {sidebarTab === 'people' && ( -
-
-
- setPeopleFilter(e.target.value)} - /> - -
-
- -
- {sortedPeople.length > 0 ? ( - sortedPeople.map(person => { - const connectionCount = relationships.filter( - r => r.source === person._id || r.target === person._id, - ).length; - - return ( -
0 - ? 'border-l-indigo-500' - : 'border-l-slate-700' - }`} - onClick={() => { - openPersonDetail(person); - setSelectedPersonId(person._id); - }} - > -
-
-

- {person.firstName} {person.lastName} -

-
- 0 ? '#60A5FA' : '#94A3B8', - }} - > - {connectionCount} connection{connectionCount !== 1 ? 's' : ''} -
-
-
- - - - - - -
-
-
- ); - }) - ) : ( - } - action={ - !peopleFilter && ( - - ) - } - /> - )} -
-
- )} - - {sidebarTab === 'relations' && ( -
-
-
- setRelationshipFilter(e.target.value)} - /> - -
-
- -
- - {Object.entries(RELATIONSHIP_COLORS).map(([type, color]) => ( - - ))} -
- -
- {filteredRelationships.length > 0 ? ( - filteredRelationships.map(rel => { - const source = people.find(p => p._id === rel.source); - const target = people.find(p => p._id === rel.target); - if (!source || !target) return null; - - return ( -
-
-
-
- { - e.stopPropagation(); - setSelectedPersonId(rel.source); - openPersonDetail(source); - }} - > - {source.firstName} {source.lastName} - - - { - e.stopPropagation(); - setSelectedPersonId(rel.target); - const targetPerson = people.find(p => p._id === rel.target); - if (targetPerson) openPersonDetail(targetPerson); - }} - > - {target.firstName} {target.lastName} - -
-
- - - {rel.type === 'custom' - ? rel.customType - : RELATIONSHIP_LABELS[rel.type]} - -
-
-
- - - -
-
-
- ); - }) - ) : ( - } - action={ - !relationshipFilter && - relationshipTypeFilter === 'all' && ( - - ) - } - /> - )} -
-
- )} -
-
-
- - {/* Main Graph Area */} -
- {graphDimensions.width <= 0 || graphDimensions.height <= 0 ? ( -
-
-
- ) : ( - { - updatePersonPosition(nodeId, { x, y }).then(); - }} - /> - )} - - {/* Empty state overlay */} - {people.length === 0 && ( -
-
-
- -
-

Start Building Your Network

-

- Add people and create relationships between them to visualize your network -

- -
-
- )} - - {/* Interaction hint */} - {people.length > 0 && interactionHint && ( -
- - Click on a person to see details, drag to reposition - -
- )} - - {/* Graph controls */} -
- - - - - - - - - - - - -
- - {/* Quick action buttons */} -
- - - - - - -
-
- - {/* Add Person Modal */} - { - setPersonModalOpen(false); - setPersonFormErrors({}); - }} - title="Add New Person" - > -
- {personFormErrors.general && ( -
- {personFormErrors.general} -
- )} - - - setNewPerson({ ...newPerson, firstName: e.target.value })} - /> - - - - setNewPerson({ ...newPerson, lastName: e.target.value })} - /> - - - -
- setNewPerson({ ...newPerson, birthday: date })} - dateFormat="MMMM d, yyyy" - placeholderText="Select birthday" - className="w-full bg-slate-700 border border-slate-600 rounded-md p-2 - focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white" - showYearDropdown - dropdownMode="select" - wrapperClassName="w-full" - /> - -
-
- - -