diff --git a/frontend/package.json b/frontend/package.json index b33a72f..c63f927 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,36 +1,39 @@ -{ - "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": { - "@tailwindcss/vite": "^4.1.4", - "axios": "^1.8.4", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-router-dom": "^7.5.0", - "tailwindcss": "^4.1.4", - "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-router-dom": "^5.3.3", - "@vitejs/plugin-react": "^4.4.0", - "prettier": "^3.5.3", - "webpack": "^5.99.5", - "webpack-cli": "^6.0.1" - } -} +{ + "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": { + "@tailwindcss/vite": "^4.1.4", + "axios": "^1.8.4", + "framer-motion": "^12.7.3", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-force-graph-2d": "^1.27.1", + "react-icons": "^5.5.0", + "react-router-dom": "^7.5.0", + "tailwindcss": "^4.1.4", + "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-router-dom": "^5.3.3", + "@vitejs/plugin-react": "^4.4.0", + "prettier": "^3.5.3", + "webpack": "^5.99.5", + "webpack-cli": "^6.0.1" + } +} diff --git a/frontend/src/components/FriendshipNetwork.tsx b/frontend/src/components/FriendshipNetwork.tsx index 7c871f6..bd5045a 100644 --- a/frontend/src/components/FriendshipNetwork.tsx +++ b/frontend/src/components/FriendshipNetwork.tsx @@ -2,11 +2,18 @@ import React, { useState, useRef, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useFriendshipNetwork } from '../hooks/useFriendshipNetwork'; import { useNetworks } from '../context/NetworkContext'; +import { motion, AnimatePresence } from 'framer-motion'; +import ForceGraph2D from 'react-force-graph-2d'; +import { + FaUserPlus, FaUserFriends, FaTrash, FaTimes, FaCog, + FaSearch, FaSearchPlus, FaSearchMinus, FaRedo, FaCompress +} from 'react-icons/fa'; const FriendshipNetwork: React.FC = () => { const { id } = useParams<{ id: string }>(); const { networks } = useNetworks(); const navigate = useNavigate(); + const graphRef = useRef(null); const { people, @@ -19,11 +26,12 @@ const FriendshipNetwork: React.FC = () => { createRelationship, updateRelationship, deleteRelationship, + refreshNetwork, } = useFriendshipNetwork(id || null); // Local state for the UI - const [selectedNode, setSelectedNode] = useState(null); - const [popupInfo, setPopupInfo] = useState(null); + const [sidebarOpen, setSidebarOpen] = useState(true); + const [activeTab, setActiveTab] = useState('add'); const [newPerson, setNewPerson] = useState({ firstName: '', lastName: '', @@ -35,14 +43,19 @@ const FriendshipNetwork: React.FC = () => { type: 'freund', customType: '', }); - const svgRef = useRef(null); - const nodeRefs = useRef<{ [key: string]: SVGGElement | null }>({}); - const [dragging, setDragging] = useState(null); - const [dragStartPos, setDragStartPos] = useState({ x: 0, y: 0 }); - const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); const [showOverrideModal, setShowOverrideModal] = useState(false); const [overrideRelationship, setOverrideRelationship] = useState(null); - + const [graphSettings, setGraphSettings] = useState({ + chargeStrength: -150, + linkDistance: 100, + collideRadius: 50, + velocityDecay: 0.4, + showLabels: true, + nodeSize: 20, + }); + const [showSettings, setShowSettings] = useState(false); + const [graphData, setGraphData] = useState({ nodes: [], links: [] }); + // Get current network info const currentNetwork = networks.find(network => network._id === id); @@ -53,6 +66,55 @@ const FriendshipNetwork: React.FC = () => { } }, [currentNetwork, networks, loading, navigate]); + // Update graph data when people or relationships change + useEffect(() => { + if (people && relationships) { + const nodes = people.map(person => ({ + id: person.id, + nodeId: person._id, + firstName: person.firstName, + lastName: person.lastName, + label: `${person.firstName} ${person.lastName.charAt(0)}.`, + birthday: person.birthday, + x: person.position?.x, + y: person.position?.y, + // Dynamic size based on connection count + val: 1 + relationships.filter(r => r.source === person.id || r.target === person.id).length * 0.5 + })); + + const links = relationships.map(rel => { + // Different colors for different relationship types + let color = '#9CA3AF'; // Default gray + if (rel.type === 'freund') color = '#3B82F6'; // Blue + if (rel.type === 'partner') color = '#EC4899'; // Pink + if (rel.type === 'familie') color = '#10B981'; // Green + if (rel.type === 'arbeitskolleg') color = '#F59E0B'; // Yellow + + return { + source: rel.source, + target: rel.target, + id: rel.id, + relId: rel._id, + type: rel.type, + color, + // Visual elements + value: rel.type === 'partner' ? 3 : rel.type === 'familie' ? 2 : 1, + }; + }); + + setGraphData({ nodes, links }); + } + }, [people, relationships]); + + // Save node positions when they are dragged + const handleNodeDragEnd = (node: any) => { + if (node && node.x && node.y && node.nodeId) { + updatePerson(node.nodeId, { + position: { x: node.x, y: node.y } + }); + } + }; + // Add a new person to the network const handleAddPerson = async () => { if (newPerson.firstName.trim() === '' || newPerson.lastName.trim() === '') { @@ -66,6 +128,7 @@ const FriendshipNetwork: React.FC = () => { lastName: newPerson.lastName.trim(), birthday: newPerson.birthday || undefined, position: { + // Generate a random position within the viewport x: 100 + Math.random() * 400, y: 100 + Math.random() * 300, }, @@ -236,78 +299,6 @@ const FriendshipNetwork: React.FC = () => { setNewRelationship({ ...newRelationship, targets: selectedOptions }); }; - // Handle node drag start - const handleMouseDown = (e: React.MouseEvent, id: string) => { - if (svgRef.current) { - const node = people.find(n => n.id === id); - if (!node) return; - - setDragging(id); - setDragStartPos({ ...node.position }); - setMousePos({ x: e.clientX, y: e.clientY }); - - e.stopPropagation(); - e.preventDefault(); - } - }; - - // Handle node dragging - const handleMouseMove = (e: React.MouseEvent) => { - if (dragging && svgRef.current) { - const dx = e.clientX - mousePos.x; - const dy = e.clientY - mousePos.y; - - const newX = dragStartPos.x + dx; - const newY = dragStartPos.y + dy; - - // Update node position in the UI immediately - const updatedPeople = people.map(node => - node.id === dragging - ? { - ...node, - position: { x: newX, y: newY }, - } - : node - ); - - // We don't actually update the state here for performance reasons - // Instead, we update the DOM directly - const draggedNode = nodeRefs.current[dragging]; - if (draggedNode) { - draggedNode.setAttribute('transform', `translate(${newX}, ${newY})`); - } - } - }; - - // Handle node drag end - const handleMouseUp = async () => { - if (dragging) { - const node = people.find(n => n.id === dragging); - if (node) { - // Get the final position from the DOM - const draggedNode = nodeRefs.current[dragging]; - if (draggedNode) { - const transform = draggedNode.getAttribute('transform'); - if (transform) { - const match = transform.match(/translate\(([^,]+),\s*([^)]+)\)/); - if (match) { - const x = parseFloat(match[1]); - const y = parseFloat(match[2]); - - // Save the new position to the server - try { - await updatePerson(dragging, { position: { x, y } }); - } catch (error) { - console.error('Error updating position:', error); - } - } - } - } - } - setDragging(null); - } - }; - // Delete a node and its associated edges const handleDeleteNode = async (id: string) => { if ( @@ -317,8 +308,6 @@ const FriendshipNetwork: React.FC = () => { ) { try { await deletePerson(id); - setSelectedNode(null); - setPopupInfo(null); } catch (error) { console.error('Error deleting person:', error); alert('Failed to delete person.'); @@ -346,462 +335,617 @@ const FriendshipNetwork: React.FC = () => { const handleRemoveRelationship = async (edgeId: string) => { try { await deleteRelationship(edgeId); - - // Update popup info if it's open - if (popupInfo) { - const nodeId = popupInfo.node.id; - const nodeRelationships = relationships - .filter(edge => edge.id !== edgeId) - .filter(edge => edge.source === nodeId || edge.target === nodeId) - .map(edge => { - const otherId = edge.source === nodeId ? edge.target : edge.source; - const other = people.find(n => n.id === otherId); - return { - person: other ? `${other.firstName} ${other.lastName}` : otherId, - type: edge.type, - edgeId: edge.id, - }; - }); - - setPopupInfo({ - ...popupInfo, - relationships: nodeRelationships, - }); - } } catch (error) { console.error('Error removing relationship:', error); alert('Failed to remove relationship.'); } }; - // Show popup with person details and relationships - const showPersonDetails = (nodeId: string) => { - const node = people.find(n => n.id === nodeId); - if (!node) return; - - // Find all relationships - const nodeRelationships = relationships - .filter(edge => edge.source === nodeId || edge.target === nodeId) - .map(edge => { - const otherId = edge.source === nodeId ? edge.target : edge.source; - const other = people.find(n => n.id === otherId); - return { - person: other ? `${other.firstName} ${other.lastName}` : otherId, - type: edge.type, - edgeId: edge.id, - }; - }); - - setPopupInfo({ - node, - relationships: nodeRelationships, - position: { ...node.position }, - }); + // Graph control functions + const zoomIn = () => { + if (graphRef.current) { + graphRef.current.zoom(1.2); + } }; - // Close popup - const closePopup = () => { - setPopupInfo(null); + const zoomOut = () => { + if (graphRef.current) { + graphRef.current.zoom(0.8); + } }; - // Get abbreviated name for display in graph (first name + first letter of last name) - const getDisplayName = (node: any) => { - return `${node.firstName} ${node.lastName.charAt(0)}.`; + const centerGraph = () => { + if (graphRef.current) { + graphRef.current.zoomToFit(400); + } }; - // Close popup when clicking outside - useEffect(() => { - const handleClickOutside = (e: MouseEvent) => { - if (popupInfo && !(e.target as Element).closest('.popup') && !dragging) { - closePopup(); - } - }; + const refreshGraph = () => { + refreshNetwork(); + }; - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [popupInfo, dragging]); + // Toggle sidebar visibility + const toggleSidebar = () => { + setSidebarOpen(!sidebarOpen); + }; + + // Handle physics settings change + const handleSettingChange = (setting: string, value: number | boolean) => { + setGraphSettings(prev => ({ + ...prev, + [setting]: value + })); + }; if (loading) { - return
Loading network data...
; + return ( +
+
+
+ ); } if (error) { return ( -
{error}
+
+
+

Error

+

{error}

+ +
+
); } return ( -
- {/* Sidebar menu */} -
-

{currentNetwork?.name || 'Friend Network'}

+
+ {/* Mobile Toggle Button */} + - {/* Add Person Form */} -
-

Add Person

-
- setNewPerson({ ...newPerson, firstName: e.target.value })} - /> - setNewPerson({ ...newPerson, lastName: e.target.value })} - /> - setNewPerson({ ...newPerson, birthday: e.target.value })} - /> - -
-
+ {/* Sidebar */} + + {sidebarOpen && ( + +
+

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

- {/* Add Relationship Form */} -
-

Add Relationship

-
- - - -

Hold Ctrl/Cmd to select multiple people

- - - - {newRelationship.type === 'custom' && ( - - setNewRelationship({ ...newRelationship, customType: e.target.value }) - } - /> - )} - - -
-
- - {/* People List */} -
-

People ({people.length})

-
    - {people.map(node => ( -
  • - - {node.firstName} {node.lastName} - - -
  • - ))} -
+ + +
+ + {activeTab === 'add' && ( + <> + {/* Add Person Form */} +
+

+ Add Person +

+
+ setNewPerson({ ...newPerson, firstName: e.target.value })} + /> + setNewPerson({ ...newPerson, lastName: e.target.value })} + /> + setNewPerson({ ...newPerson, birthday: e.target.value })} + /> + +
+
+ + {/* Add Relationship Form */} +
+

+ Add Relationship +

+
+ + + +

Hold Ctrl/Cmd to select multiple people

+ + + + {newRelationship.type === 'custom' && ( + + setNewRelationship({ ...newRelationship, customType: e.target.value }) + } + /> + )} + + +
+
+ + )} + + {activeTab === 'view' && ( + <> + {/* People List */} +
+

+ People ({people.length}) +

+ {people.length > 0 ? ( +
+ {people.map(node => ( +
+ {node.firstName} {node.lastName} + +
+ ))} +
+ ) : ( +

+ No people added yet. +

+ )} +
+ + {/* Relationships List */} +
+

+ Relationships ({relationships.length}) +

+ {relationships.length > 0 ? ( +
+ {relationships.map(edge => { + const source = people.find(n => n.id === edge.source); + const target = people.find(n => n.id === edge.target); + if (!source || !target) return null; + + return ( +
+
+
+ {source.firstName} {source.lastName.charAt(0)}. + + {target.firstName} {target.lastName.charAt(0)}. +
+ +
+
+ {getRelationshipLabel(edge.type)} +
+
+ ); + })} +
+ ) : ( +

+ No relationships added yet. +

+ )} +
+ + )} + + {activeTab === 'settings' && ( +
+

Graph Physics Settings

+ +
+ + handleSettingChange('chargeStrength', parseInt(e.target.value))} + className="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer" + /> +

Stronger negative values push nodes apart more

+
+ +
+ + handleSettingChange('linkDistance', parseInt(e.target.value))} + className="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer" + /> +

Higher values increase distance between connected nodes

+
+ +
+ + handleSettingChange('collideRadius', parseInt(e.target.value))} + className="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer" + /> +

Higher values prevent node overlap more

+
+ +
+ + handleSettingChange('velocityDecay', parseFloat(e.target.value))} + className="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer" + /> +

Higher values make the graph settle faster

+
+ +
+ + handleSettingChange('nodeSize', parseInt(e.target.value))} + className="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer" + /> +
+ +
+ handleSettingChange('showLabels', e.target.checked)} + className="w-4 h-4 rounded bg-slate-700 border-slate-600 focus:ring-indigo-500 focus:ring-2" + /> + +
+ + +
+ )} +
+
+ )} +
+ + {/* Main Graph Area */} +
+
+ `${node.firstName} ${node.lastName}`} + nodeRelSize={graphSettings.nodeSize} + nodeVal={node => node.val * graphSettings.nodeSize / 10} + nodeColor={node => { + // Different colors for different node types or connections + const connCount = graphData.links.filter( + (link: any) => link.source.id === node.id || link.target.id === node.id + ).length; + + // Color scales from blue to purple based on number of connections + return connCount === 0 ? '#4B5563' : // Gray for isolated nodes + connCount < 3 ? '#3B82F6' : // Blue for few connections + connCount < 6 ? '#8B5CF6' : // Indigo for moderate + '#A855F7'; // Purple for many + }} + linkWidth={link => link.value} + linkColor={link => link.color} + nodeCanvasObjectMode={() => graphSettings.showLabels ? 'after' : undefined} + nodeCanvasObject={(node, ctx, globalScale) => { + if (!graphSettings.showLabels) return; + + const label = node.label; + const fontSize = 12/globalScale; + ctx.font = `${fontSize}px Sans-Serif`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; + + // Draw background for better readability + const textWidth = ctx.measureText(label).width; + ctx.fillStyle = 'rgba(0, 0, 0, 0.6)'; + ctx.fillRect( + node.x - textWidth/2 - 2, + node.y + graphSettings.nodeSize/globalScale + 2, + textWidth + 4, + fontSize + 2 + ); + + // Draw text + ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; + ctx.fillText( + label, + node.x, + node.y + graphSettings.nodeSize/globalScale + fontSize/2 + 3 + ); + }} + linkDirectionalParticles={3} + linkDirectionalParticleWidth={link => link.value} + linkDirectionalParticleSpeed={0.005} + d3AlphaDecay={0.02} + d3VelocityDecay={graphSettings.velocityDecay} + cooldownTicks={100} + onNodeDragEnd={handleNodeDragEnd} + // Physics settings + d3Force={(forceName, force) => { + if (forceName === 'charge') { + force.strength(graphSettings.chargeStrength); + } + if (forceName === 'link') { + force.distance(graphSettings.linkDistance); + } + if (forceName === 'collide') { + force.radius(graphSettings.collideRadius); + } + }} + enableNodeDrag={true} + enableZoomInteraction={true} + enablePanInteraction={true} + onNodeClick={(node) => { + // Center view on node when clicked + if (graphRef.current) { + graphRef.current.centerAt(node.x, node.y, 1000); + graphRef.current.zoom(1.5, 1000); + } + }} + />
- {/* Relationships List */} -
-

All Relationships ({relationships.length})

- {relationships.length === 0 ? ( -

No relationships yet

- ) : ( -
    - {relationships.map(edge => { - const source = people.find(n => n.id === edge.source); - const target = people.find(n => n.id === edge.target); - if (!source || !target) return null; + {/* Graph Controls */} +
    + + + + +
    - return ( -
  • -
    - - {source.firstName} {source.lastName.charAt(0)}. ↔ {target.firstName}{' '} - {target.lastName.charAt(0)}. - -
    - - {getRelationshipLabel(edge.type)} - - -
    -
    -
  • - ); - })} -
- )} + {/* Mobile Controls */} +
+
- {/* Visualization Area */} -
- - {/* Edges (Relationships) */} - {relationships.map(edge => { - const source = people.find(n => n.id === edge.source); - const target = people.find(n => n.id === edge.target); - - if (!source || !target) return null; - - // Determine the line color based on relationship type - let strokeColor = '#9CA3AF'; // Default gray - if (edge.type === 'freund') strokeColor = '#3B82F6'; // Blue - if (edge.type === 'partner') strokeColor = '#EC4899'; // Pink - if (edge.type === 'familie') strokeColor = '#10B981'; // Green - if (edge.type === 'arbeitskolleg') strokeColor = '#F59E0B'; // Yellow - - return ( - - - - {getRelationshipLabel(edge.type)} - - - ); - })} - - {/* Nodes (People) */} - {people.map(node => ( - handleMouseDown(e, node.id)} - ref={el => { - nodeRefs.current[node.id] = el; - }} - className="cursor-grab" - > - showPersonDetails(node.id)} - /> - - {getDisplayName(node)} - - - ))} - - - {/* Person Details Popup */} - {popupInfo && ( -
(svgRef.current?.clientWidth || 0) / 2 - ? popupInfo.position.x - 260 - : popupInfo.position.x + 40, - top: - popupInfo.position.y > (svgRef.current?.clientHeight || 0) / 2 - ? popupInfo.position.y - 200 - : popupInfo.position.y, - }} - > -
-

Person Details

- -
-
-

- Name: {popupInfo.node.firstName} {popupInfo.node.lastName} -

- {popupInfo.node.birthday && ( -

- Birthday: {new Date(popupInfo.node.birthday).toLocaleDateString()} -

- )} -
-
-

Relationships:

- {popupInfo.relationships.length === 0 ? ( -

No relationships yet

- ) : ( -
    - {popupInfo.relationships.map((rel: any, index: number) => ( -
  • -
    - {rel.person} - {getRelationshipLabel(rel.type)} -
    -
    - -
    -
  • - ))} -
- )} -
-
- )} - - {/* Override Confirmation Modal */} + {/* Override Confirmation Modal */} + {showOverrideModal && overrideRelationship && ( -
-
-

Existing Relationship(s)

-

+ + +

Existing Relationship(s)

+

{overrideRelationship.existingRelationships.length === 1 ? 'There is already a relationship between these people:' : 'There are already relationships between these people:'}

-
    - {overrideRelationship.existingRelationships.map((rel: any, index: number) => { - const source = people.find(n => n.id === rel.source); - const target = people.find(n => n.id === rel.target); - if (!source || !target) return null; +
    +
      + {overrideRelationship.existingRelationships.map((rel: any, index: number) => { + const source = people.find(n => n.id === rel.source); + const target = people.find(n => n.id === rel.target); + if (!source || !target) return null; - return ( -
    • -
      - - {source.firstName} {source.lastName} ↔ {target.firstName}{' '} - {target.lastName} - -
      -
      - - Current: {getRelationshipLabel(rel.existingType)} - - - New: {getRelationshipLabel(rel.newType)} - -
      -
    • - ); - })} -
    + return ( +
  • +
    + + {source.firstName} {source.lastName.charAt(0)}. ↔ {target.firstName}{' '} + {target.lastName.charAt(0)}. + +
    +
    + + Current: {getRelationshipLabel(rel.existingType)} + + + New: {getRelationshipLabel(rel.newType)} + +
    +
  • + ); + })} +
+
-

Do you want to override the existing relationship(s)?

+

Do you want to override the existing relationship(s)?

-
+
-
-
+ + )} - - {/* Instructions */} -
-

- Tip: Drag people to arrange them. Click on a person to view their - details and relationships. -

-
-
+
); }; -export default FriendshipNetwork; +export default FriendshipNetwork; \ No newline at end of file diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index aa88404..084742b 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -1,10 +1,12 @@ import React from 'react'; -import { Link, useNavigate } from 'react-router-dom'; +import { Link, useNavigate, useLocation } from 'react-router-dom'; import { useAuth } from '../../context/AuthContext'; +import { FaUser, FaSignOutAlt, FaNetworkWired } from 'react-icons/fa'; const Header: React.FC = () => { const { user, logout } = useAuth(); const navigate = useNavigate(); + const location = useLocation(); const handleLogout = async () => { try { @@ -15,44 +17,83 @@ const Header: React.FC = () => { } }; - return ( -
-
- - Friendship Network - + // Check if we're on the login or register page + const isAuthPage = location.pathname === '/login' || location.pathname === '/register'; - + if (isAuthPage) { + return null; // Don't show header on auth pages + } + + return ( +
+
+
+
+ + + RelNet + + + {user && ( + + )} +
+ +
+ {user ? ( +
+
+ Hello, {user.username} +
+
+ + +
+ +
+
+
+ ) : ( +
+ + Log in + + + Sign up + +
+ )} +
+
); }; -export default Header; +export default Header; \ No newline at end of file diff --git a/frontend/src/components/networks/NetworkList.tsx b/frontend/src/components/networks/NetworkList.tsx index 6a77115..3054a52 100644 --- a/frontend/src/components/networks/NetworkList.tsx +++ b/frontend/src/components/networks/NetworkList.tsx @@ -2,6 +2,8 @@ import React, { useState } from 'react'; import { useNetworks } from '../../context/NetworkContext'; import { useAuth } from '../../context/AuthContext'; import { useNavigate } from 'react-router-dom'; +import { motion, AnimatePresence } from 'framer-motion'; +import { FaPlus, FaNetworkWired, FaTrash, FaEye, FaGlobe, FaLock, FaTimes } from 'react-icons/fa'; const NetworkList: React.FC = () => { const { networks, loading, error, createNetwork, deleteNetwork } = useNetworks(); @@ -59,216 +61,285 @@ const NetworkList: React.FC = () => { } }; + // Filter networks by ownership + const myNetworks = networks.filter(network => { + if (!user) return false; + const ownerId = typeof network.owner === 'string' ? network.owner : network.owner._id; + return ownerId === user.id; + }); + + const publicNetworks = networks.filter(network => { + if (!user) return false; + const ownerId = typeof network.owner === 'string' ? network.owner : network.owner._id; + return ownerId !== user.id; + }); + if (loading) { - return
Loading networks...
; + return ( +
+
+
+ ); } return ( -
-
-

My Networks

- -
- - {error && ( -
- {error} +
+
+
+

+ + My Networks +

+ setShowCreateForm(!showCreateForm)} + > + {showCreateForm ? : } + {showCreateForm ? 'Cancel' : 'Create New Network'} +
- )} - {/* Create Network Form */} - {showCreateForm && ( -
-

Create New Network

- - {formError && ( -
- {formError} -
- )} - -
-
- - setNewNetworkName(e.target.value)} - required - /> -
- -
- -