From e60ec9248d968c37f3b1a361b74dcabc165c0ac2 Mon Sep 17 00:00:00 2001 From: philipredstone Date: Thu, 17 Apr 2025 13:06:50 +0200 Subject: [PATCH] =?UTF-8?q?ach=20ich=20wei=C3=9F=20nicht?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.lockb | Bin 0 -> 148662 bytes frontend/.dockerignore | 1 - frontend/.prettierignore | 16 - frontend/.prettierrc | 12 - frontend/package.json | 44 - frontend/src/app.css | 1 - frontend/src/components/FriendshipNetwork.tsx | 1991 ----------------- frontend/tsconfig.json | 113 - frontend/vite.config.mjs | 13 - frontend/index.html => index.html | 0 package.json | 104 +- postcss.config.js | 8 + {src => server}/config/db.ts | 0 .../controllers/auth.controller.ts | 2 +- .../controllers/network.controller.ts | 2 +- .../controllers/people.controller.ts | 2 +- .../controllers/relationship.controller.ts | 2 +- server/dev.ts | 7 + src/app.ts => server/index.ts | 28 +- {src => server}/middleware/auth.middleware.ts | 2 +- .../middleware/network-access.middleware.ts | 2 +- {src => server}/models/network.model.ts | 0 {src => server}/models/person.model.ts | 0 {src => server}/models/relationship.model.ts | 0 {src => server}/models/user.model.ts | 0 {src => server}/routes/auth.routes.ts | 0 {src => server}/routes/network.routes.ts | 0 {src => server}/routes/people.routes.ts | 0 {src => server}/routes/relationship.routes.ts | 0 {frontend/src => src}/App.tsx | 17 +- src/api/api.ts | 14 + {frontend/src => src}/api/auth.ts | 7 +- {frontend/src => src}/api/network.ts | 7 +- {frontend/src => src}/api/people.ts | 7 +- {frontend/src => src}/api/relationships.ts | 7 +- src/app.css | 9 + .../src => src}/components/CanvasGraph.tsx | 0 .../FriendshipNetworkComponents.tsx | 0 src/components/Modals.tsx | 575 +++++ src/components/NetworkSidebar.tsx | 460 ++++ src/components/UIComponents.tsx | 230 ++ .../src => src}/components/auth/Login.tsx | 0 .../src => src}/components/auth/Register.tsx | 0 .../src => src}/components/layout/Header.tsx | 12 +- .../components/networks/NetworkList.tsx | 0 {frontend/src => src}/context/AuthContext.tsx | 0 .../src => src}/context/NetworkContext.tsx | 0 .../src => src}/hooks/useFriendshipNetwork.ts | 0 src/hooks/useGraphDimensions.ts | 63 + src/hooks/useKeyboardShortcuts.ts | 78 + src/hooks/useSmartNodePositioning.ts | 60 + src/hooks/useToastNotifications.ts | 30 + {frontend/src => src}/main.tsx | 0 src/pages/FriendshipNetwork.tsx | 729 ++++++ src/server.ts | 15 - src/types/express.d.ts | 8 - src/types/types.ts | 85 + tailwind.config.js | 8 + tsconfig.json | 41 +- tsconfig.node.json | 10 + vite.config.ts | 39 + 61 files changed, 2538 insertions(+), 2323 deletions(-) create mode 100755 bun.lockb delete mode 100644 frontend/.dockerignore delete mode 100644 frontend/.prettierignore delete mode 100644 frontend/.prettierrc delete mode 100644 frontend/package.json delete mode 100644 frontend/src/app.css delete mode 100644 frontend/src/components/FriendshipNetwork.tsx delete mode 100644 frontend/tsconfig.json delete mode 100644 frontend/vite.config.mjs rename frontend/index.html => index.html (100%) create mode 100644 postcss.config.js rename {src => server}/config/db.ts (100%) rename {src => server}/controllers/auth.controller.ts (99%) rename {src => server}/controllers/network.controller.ts (98%) rename {src => server}/controllers/people.controller.ts (98%) rename {src => server}/controllers/relationship.controller.ts (98%) create mode 100644 server/dev.ts rename src/app.ts => server/index.ts (65%) rename {src => server}/middleware/auth.middleware.ts (95%) rename {src => server}/middleware/network-access.middleware.ts (94%) rename {src => server}/models/network.model.ts (100%) rename {src => server}/models/person.model.ts (100%) rename {src => server}/models/relationship.model.ts (100%) rename {src => server}/models/user.model.ts (100%) rename {src => server}/routes/auth.routes.ts (100%) rename {src => server}/routes/network.routes.ts (100%) rename {src => server}/routes/people.routes.ts (100%) rename {src => server}/routes/relationship.routes.ts (100%) rename {frontend/src => src}/App.tsx (82%) create mode 100644 src/api/api.ts rename {frontend/src => src}/api/auth.ts (85%) rename {frontend/src => src}/api/network.ts (89%) rename {frontend/src => src}/api/people.ts (89%) rename {frontend/src => src}/api/relationships.ts (90%) create mode 100644 src/app.css rename {frontend/src => src}/components/CanvasGraph.tsx (100%) rename {frontend/src => src}/components/FriendshipNetworkComponents.tsx (100%) create mode 100644 src/components/Modals.tsx create mode 100644 src/components/NetworkSidebar.tsx create mode 100644 src/components/UIComponents.tsx rename {frontend/src => src}/components/auth/Login.tsx (100%) rename {frontend/src => src}/components/auth/Register.tsx (100%) rename {frontend/src => src}/components/layout/Header.tsx (90%) rename {frontend/src => src}/components/networks/NetworkList.tsx (100%) rename {frontend/src => src}/context/AuthContext.tsx (100%) rename {frontend/src => src}/context/NetworkContext.tsx (100%) rename {frontend/src => src}/hooks/useFriendshipNetwork.ts (100%) create mode 100644 src/hooks/useGraphDimensions.ts create mode 100644 src/hooks/useKeyboardShortcuts.ts create mode 100644 src/hooks/useSmartNodePositioning.ts create mode 100644 src/hooks/useToastNotifications.ts rename {frontend/src => src}/main.tsx (100%) create mode 100644 src/pages/FriendshipNetwork.tsx delete mode 100644 src/server.ts delete mode 100644 src/types/express.d.ts create mode 100644 src/types/types.ts create mode 100644 tailwind.config.js create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..e23380c5aded46ae9bc3545f175f8fa64426c07e GIT binary patch literal 148662 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 N&QyG=`v1d!{vSkX?cD$X literal 0 HcmV?d00001 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" - /> - -
-
- - -