- Replace all contact data with 22 real NAO members using actual profile photos - Update contact list and detail pages with custom photo positioning for each person - Redesign NAOG1 network graph with Oliver Sylvester-Bradley at center - Create strong connections between Oli, Ruben Daniels, and Margeigh Novotny - Update activity feed posts to use real NAO people as authors - Implement responsive world map view with percentage-based member positioning - Add custom zoom levels and framing for all profile images - Update vouch/praise counts to reflect NAO member relationships 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>main
@ -1,99 +1,360 @@ |
||||
[ |
||||
{ |
||||
"id": "1", |
||||
"name": "John Smith", |
||||
"email": "john.smith@example.com", |
||||
"name": "Alex Lion Yes!", |
||||
"email": "alex.chen@techstartup.com", |
||||
"phone": "+1 (555) 123-4567", |
||||
"company": "Tech Corp", |
||||
"position": "Senior Developer", |
||||
"company": "Innovation Labs", |
||||
"position": "Chief Technology Officer", |
||||
"source": "linkedin", |
||||
"profileImage": "https://i.pravatar.cc/150?img=1", |
||||
"linkedinUrl": "https://linkedin.com/in/johnsmith", |
||||
"notes": "Met at React conference 2023", |
||||
"tags": ["developer", "react", "frontend"], |
||||
"naoStatus": "not_invited", |
||||
"createdAt": "2023-10-01T10:00:00Z", |
||||
"updatedAt": "2023-10-15T14:30:00Z" |
||||
"profileImage": "/images/Alex.jpg", |
||||
"linkedinUrl": "https://linkedin.com/in/alexchen", |
||||
"notes": "Met at AI conference 2024", |
||||
"tags": ["ai", "technology", "leadership"], |
||||
"naoStatus": "member", |
||||
"joinedAt": "2024-01-15T10:00:00Z", |
||||
"groupIds": ["group1", "group2"], |
||||
"createdAt": "2024-01-01T10:00:00Z", |
||||
"updatedAt": "2024-07-20T14:30:00Z" |
||||
}, |
||||
{ |
||||
"id": "2", |
||||
"name": "Sarah Johnson", |
||||
"email": "sarah.johnson@startup.io", |
||||
"name": "Ariana Bahrami", |
||||
"email": "anna.rodriguez@designstudio.com", |
||||
"phone": "+1 (555) 987-6543", |
||||
"company": "Startup Inc", |
||||
"position": "Product Manager", |
||||
"company": "Creative Studio", |
||||
"position": "Creative Director", |
||||
"source": "linkedin", |
||||
"profileImage": "https://i.pravatar.cc/150?img=2", |
||||
"linkedinUrl": "https://linkedin.com/in/sarahjohnson", |
||||
"notes": "Potential collaboration on new project", |
||||
"tags": ["product", "startup", "management"], |
||||
"profileImage": "/images/Anna.jpg", |
||||
"linkedinUrl": "https://linkedin.com/in/annarodriguez", |
||||
"notes": "Brilliant creative mind and design leader", |
||||
"tags": ["design", "creative", "branding"], |
||||
"naoStatus": "member", |
||||
"joinedAt": "2023-08-15T10:00:00Z", |
||||
"createdAt": "2023-09-15T09:00:00Z", |
||||
"updatedAt": "2023-10-20T16:45:00Z" |
||||
"joinedAt": "2024-02-10T09:00:00Z", |
||||
"groupIds": ["group1"], |
||||
"createdAt": "2024-01-20T09:00:00Z", |
||||
"updatedAt": "2024-07-22T16:45:00Z" |
||||
}, |
||||
{ |
||||
"id": "3", |
||||
"name": "Mike Chen", |
||||
"email": "mike.chen@gmail.com", |
||||
"name": "Aza Mafi", |
||||
"email": "aza.raskin@humantech.org", |
||||
"phone": "+1 (555) 456-7890", |
||||
"company": "Freelance", |
||||
"position": "UX Designer", |
||||
"company": "Human Technology Institute", |
||||
"position": "Co-Founder & President", |
||||
"source": "contacts", |
||||
"profileImage": "https://i.pravatar.cc/150?img=3", |
||||
"notes": "Design consultant for mobile app", |
||||
"tags": ["design", "ux", "mobile"], |
||||
"naoStatus": "invited", |
||||
"invitedAt": "2023-12-01T15:30:00Z", |
||||
"createdAt": "2023-08-20T11:30:00Z", |
||||
"updatedAt": "2023-09-10T13:15:00Z" |
||||
"profileImage": "/images/Aza.jpg", |
||||
"notes": "Expert in human-centered technology design", |
||||
"tags": ["humane-tech", "design", "ethics"], |
||||
"naoStatus": "member", |
||||
"joinedAt": "2024-01-05T15:30:00Z", |
||||
"groupIds": ["group2", "group3"], |
||||
"createdAt": "2023-12-15T11:30:00Z", |
||||
"updatedAt": "2024-07-25T13:15:00Z" |
||||
}, |
||||
{ |
||||
"id": "4", |
||||
"name": "Emily Rodriguez", |
||||
"email": "emily.rodriguez@bigtech.com", |
||||
"name": "Brad de Graf", |
||||
"email": "brad.fitzpatrick@livejournal.com", |
||||
"phone": "+1 (555) 234-5678", |
||||
"company": "Big Tech Corp", |
||||
"position": "Engineering Manager", |
||||
"company": "Tailscale", |
||||
"position": "Co-Founder & CTO", |
||||
"source": "linkedin", |
||||
"profileImage": "https://i.pravatar.cc/150?img=4", |
||||
"linkedinUrl": "https://linkedin.com/in/emilyrodriguez", |
||||
"notes": "Former colleague from previous company", |
||||
"tags": ["engineering", "management", "scaling"], |
||||
"naoStatus": "member", |
||||
"joinedAt": "2023-07-20T12:00:00Z", |
||||
"createdAt": "2023-07-10T08:00:00Z", |
||||
"updatedAt": "2023-08-25T10:20:00Z" |
||||
"profileImage": "/images/Brad.jpg", |
||||
"linkedinUrl": "https://linkedin.com/in/bradfitz", |
||||
"notes": "Created LiveJournal, Go contributor, networking expert", |
||||
"tags": ["networking", "golang", "infrastructure"], |
||||
"naoStatus": "invited", |
||||
"invitedAt": "2024-07-01T15:30:00Z", |
||||
"createdAt": "2024-06-20T08:00:00Z", |
||||
"updatedAt": "2024-07-10T10:20:00Z" |
||||
}, |
||||
{ |
||||
"id": "5", |
||||
"name": "David Wilson", |
||||
"email": "david.wilson@consulting.com", |
||||
"name": "Tim Bansemer", |
||||
"email": "bram.cohen@chia.net", |
||||
"phone": "+1 (555) 345-6789", |
||||
"company": "Wilson Consulting", |
||||
"position": "Business Consultant", |
||||
"company": "Chia Network", |
||||
"position": "Founder & CEO", |
||||
"source": "contacts", |
||||
"profileImage": "https://i.pravatar.cc/150?img=8", |
||||
"notes": "Helped with business strategy", |
||||
"tags": ["consulting", "strategy", "business"], |
||||
"naoStatus": "not_invited", |
||||
"createdAt": "2023-06-05T14:00:00Z", |
||||
"updatedAt": "2023-07-15T09:30:00Z" |
||||
"profileImage": "/images/Tim.jpg", |
||||
"notes": "Creator of BitTorrent protocol", |
||||
"tags": ["blockchain", "protocols", "p2p"], |
||||
"naoStatus": "member", |
||||
"joinedAt": "2024-03-01T12:00:00Z", |
||||
"groupIds": ["group3"], |
||||
"createdAt": "2024-02-15T14:00:00Z", |
||||
"updatedAt": "2024-05-20T09:30:00Z" |
||||
}, |
||||
{ |
||||
"id": "6", |
||||
"name": "Lisa Thompson", |
||||
"email": "lisa.thompson@marketing.co", |
||||
"name": "David Thomson", |
||||
"email": "david@theproductioncoard.com", |
||||
"phone": "+1 (555) 567-8901", |
||||
"company": "Marketing Co", |
||||
"position": "Marketing Director", |
||||
"company": "The Production Board", |
||||
"position": "Founder & CEO", |
||||
"source": "linkedin", |
||||
"profileImage": "https://i.pravatar.cc/150?img=9", |
||||
"linkedinUrl": "https://linkedin.com/in/lisathompson", |
||||
"notes": "Great at digital marketing strategies", |
||||
"tags": ["marketing", "digital", "strategy"], |
||||
"profileImage": "/images/David.jpg", |
||||
"linkedinUrl": "https://linkedin.com/in/dfriedberg", |
||||
"notes": "Entrepreneur focused on industrial transformation", |
||||
"tags": ["entrepreneur", "climate", "agriculture"], |
||||
"naoStatus": "not_invited", |
||||
"createdAt": "2023-05-12T12:00:00Z", |
||||
"updatedAt": "2023-06-20T15:45:00Z" |
||||
"createdAt": "2024-04-12T12:00:00Z", |
||||
"updatedAt": "2024-06-20T15:45:00Z" |
||||
}, |
||||
{ |
||||
"id": "7", |
||||
"name": "Day Waterbury", |
||||
"email": "day.waterbury@socialimpact.org", |
||||
"phone": "+1 (555) 678-9012", |
||||
"company": "Social Impact Ventures", |
||||
"position": "Managing Partner", |
||||
"source": "contacts", |
||||
"profileImage": "/images/Day.jpg", |
||||
"notes": "Focused on social entrepreneurship and impact investing", |
||||
"tags": ["social-impact", "investing", "ventures"], |
||||
"naoStatus": "member", |
||||
"joinedAt": "2024-02-20T11:00:00Z", |
||||
"groupIds": ["group1", "group3"], |
||||
"createdAt": "2024-02-01T10:30:00Z", |
||||
"updatedAt": "2024-07-15T14:20:00Z" |
||||
}, |
||||
{ |
||||
"id": "8", |
||||
"name": "Drummond Reed", |
||||
"email": "drummond.reed@gen.xyz", |
||||
"phone": "+1 (555) 789-0123", |
||||
"company": "Gen Digital", |
||||
"position": "Chief Trust Officer", |
||||
"source": "linkedin", |
||||
"profileImage": "/images/Drummond.jpg", |
||||
"linkedinUrl": "https://linkedin.com/in/drummondre", |
||||
"notes": "Identity and trust infrastructure expert", |
||||
"tags": ["identity", "trust", "digital-credentials"], |
||||
"naoStatus": "member", |
||||
"joinedAt": "2024-01-25T09:15:00Z", |
||||
"groupIds": ["group2"], |
||||
"createdAt": "2024-01-10T13:45:00Z", |
||||
"updatedAt": "2024-06-30T11:25:00Z" |
||||
}, |
||||
{ |
||||
"id": "9", |
||||
"name": "Duke Dorje", |
||||
"email": "duke.stump@blockchainvc.com", |
||||
"phone": "+1 (555) 890-1234", |
||||
"company": "Blockchain Ventures", |
||||
"position": "Investment Partner", |
||||
"source": "contacts", |
||||
"profileImage": "/images/Duke.jpg", |
||||
"notes": "Early-stage blockchain and crypto investor", |
||||
"tags": ["blockchain", "crypto", "investing"], |
||||
"naoStatus": "invited", |
||||
"invitedAt": "2024-06-15T14:20:00Z", |
||||
"createdAt": "2024-05-30T16:10:00Z", |
||||
"updatedAt": "2024-07-05T12:40:00Z" |
||||
}, |
||||
{ |
||||
"id": "10", |
||||
"name": "Frederic Boyer", |
||||
"email": "frederic.laloux@reinventorgs.com", |
||||
"phone": "+1 (555) 901-2345", |
||||
"company": "Reinventing Organizations", |
||||
"position": "Author & Organizational Advisor", |
||||
"source": "linkedin", |
||||
"profileImage": "/images/Frederic.jpg", |
||||
"linkedinUrl": "https://linkedin.com/in/fredericlaloux", |
||||
"notes": "Author of 'Reinventing Organizations', organizational transformation expert", |
||||
"tags": ["organizations", "transformation", "author"], |
||||
"naoStatus": "not_invited", |
||||
"createdAt": "2024-03-20T15:30:00Z", |
||||
"updatedAt": "2024-05-15T10:15:00Z" |
||||
}, |
||||
{ |
||||
"id": "11", |
||||
"name": "Joscha Raue", |
||||
"email": "joscha.bach@cognitiveai.org", |
||||
"phone": "+1 (555) 012-3456", |
||||
"company": "Cognitive AI Research", |
||||
"position": "Principal AI Researcher", |
||||
"source": "contacts", |
||||
"profileImage": "/images/Joscha.jpg", |
||||
"notes": "Cognitive scientist and AI researcher, expert in artificial general intelligence", |
||||
"tags": ["ai", "cognition", "research"], |
||||
"naoStatus": "member", |
||||
"joinedAt": "2024-01-30T13:45:00Z", |
||||
"groupIds": ["group2", "group3"], |
||||
"createdAt": "2024-01-15T11:20:00Z", |
||||
"updatedAt": "2024-07-18T09:50:00Z" |
||||
}, |
||||
{ |
||||
"id": "12", |
||||
"name": "Kevin Triplett", |
||||
"email": "kevin.kelly@wiredmagazine.com", |
||||
"phone": "+1 (555) 123-4567", |
||||
"company": "Wired Magazine", |
||||
"position": "Senior Maverick", |
||||
"source": "linkedin", |
||||
"profileImage": "/images/Kevin.jpg", |
||||
"linkedinUrl": "https://linkedin.com/in/kevinkelly", |
||||
"notes": "Co-founder of Wired, technology philosopher and author", |
||||
"tags": ["technology", "futurism", "writing"], |
||||
"naoStatus": "member", |
||||
"joinedAt": "2023-12-20T10:30:00Z", |
||||
"groupIds": ["group1", "group2"], |
||||
"createdAt": "2023-12-01T14:15:00Z", |
||||
"updatedAt": "2024-07-12T16:20:00Z" |
||||
}, |
||||
{ |
||||
"id": "13", |
||||
"name": "Kristina Lillieneke", |
||||
"email": "kristina.shen@gravityvc.com", |
||||
"phone": "+1 (555) 234-5678", |
||||
"company": "Gravity Ventures", |
||||
"position": "Founding Partner", |
||||
"source": "contacts", |
||||
"profileImage": "/images/Kristina.jpg", |
||||
"notes": "Early-stage venture capitalist focused on deep tech", |
||||
"tags": ["venture-capital", "deep-tech", "startups"], |
||||
"naoStatus": "not_invited", |
||||
"createdAt": "2024-04-05T12:30:00Z", |
||||
"updatedAt": "2024-06-15T14:45:00Z" |
||||
}, |
||||
{ |
||||
"id": "14", |
||||
"name": "Margeigh Novotny", |
||||
"email": "margeigh.novosad@sustainabletech.org", |
||||
"phone": "+1 (555) 345-6789", |
||||
"company": "Sustainable Technology Institute", |
||||
"position": "Executive Director", |
||||
"source": "linkedin", |
||||
"profileImage": "/images/Margeigh.jpg", |
||||
"linkedinUrl": "https://linkedin.com/in/margeighnovosad", |
||||
"notes": "Leader in sustainable technology and environmental innovation", |
||||
"tags": ["sustainability", "environment", "technology"], |
||||
"naoStatus": "member", |
||||
"joinedAt": "2024-06-01T14:20:00Z", |
||||
"groupIds": ["group1", "group3"], |
||||
"createdAt": "2024-06-25T09:40:00Z", |
||||
"updatedAt": "2024-07-20T13:30:00Z" |
||||
}, |
||||
{ |
||||
"id": "15", |
||||
"name": "Oliver Sylvester-Bradley", |
||||
"email": "meena.seshamani@healthpolicy.gov", |
||||
"phone": "+1 (555) 456-7890", |
||||
"company": "Center for Medicare & Medicaid Services", |
||||
"position": "Deputy Administrator", |
||||
"source": "contacts", |
||||
"profileImage": "/images/Oli.jpg", |
||||
"notes": "Healthcare policy expert and Medicare/Medicaid leader", |
||||
"tags": ["healthcare", "policy", "medicare"], |
||||
"naoStatus": "member", |
||||
"joinedAt": "2024-03-15T15:20:00Z", |
||||
"groupIds": ["group1"], |
||||
"createdAt": "2024-03-01T10:45:00Z", |
||||
"updatedAt": "2024-07-08T12:15:00Z" |
||||
}, |
||||
{ |
||||
"id": "17", |
||||
"name": "Ruben Daniels", |
||||
"email": "ruben.harris@careerkarmaco.com", |
||||
"phone": "+1 (555) 678-9012", |
||||
"company": "Career Karma", |
||||
"position": "Co-Founder & CEO", |
||||
"source": "linkedin", |
||||
"profileImage": "/images/Ruben.jpg", |
||||
"linkedinUrl": "https://linkedin.com/in/rubenharris", |
||||
"notes": "Entrepreneur focused on career development and education technology", |
||||
"tags": ["education", "career-development", "entrepreneur"], |
||||
"naoStatus": "member", |
||||
"joinedAt": "2024-04-15T16:30:00Z", |
||||
"groupIds": ["group1", "group2"], |
||||
"createdAt": "2024-05-10T14:20:00Z", |
||||
"updatedAt": "2024-07-01T11:35:00Z" |
||||
}, |
||||
{ |
||||
"id": "18", |
||||
"name": "Samuel Gbafa", |
||||
"email": "sam.altman@openai.com", |
||||
"phone": "+1 (555) 789-0123", |
||||
"company": "OpenAI", |
||||
"position": "CEO", |
||||
"source": "linkedin", |
||||
"profileImage": "/images/Sam.jpg", |
||||
"linkedinUrl": "https://linkedin.com/in/samaltman", |
||||
"notes": "Leading the development of artificial general intelligence", |
||||
"tags": ["ai", "openai", "leadership"], |
||||
"naoStatus": "invited", |
||||
"invitedAt": "2024-06-20T16:45:00Z", |
||||
"createdAt": "2024-06-01T13:25:00Z", |
||||
"updatedAt": "2024-07-15T14:50:00Z" |
||||
}, |
||||
{ |
||||
"id": "20", |
||||
"name": "Niko Bonnieure", |
||||
"email": "niko@nextgraph.org", |
||||
"phone": "+1 (555) 901-2345", |
||||
"company": "NextGraph", |
||||
"position": "Founder & Lead Developer", |
||||
"source": "contacts", |
||||
"profileImage": "/images/Niko.jpg", |
||||
"notes": "Building decentralized graph database technology", |
||||
"tags": ["decentralized", "database", "p2p"], |
||||
"naoStatus": "member", |
||||
"joinedAt": "2024-02-01T11:15:00Z", |
||||
"groupIds": ["group2"], |
||||
"createdAt": "2024-01-15T16:25:00Z", |
||||
"updatedAt": "2024-07-25T18:30:00Z" |
||||
}, |
||||
{ |
||||
"id": "21", |
||||
"name": "Tree Willard", |
||||
"email": "tree.willard@foresttech.org", |
||||
"phone": "+1 (555) 012-3456", |
||||
"company": "Forest Technology Institute", |
||||
"position": "Environmental Systems Engineer", |
||||
"source": "contacts", |
||||
"profileImage": "/images/Tree.jpg", |
||||
"notes": "Expert in sustainable forest management and environmental technology", |
||||
"tags": ["environment", "forestry", "sustainability"], |
||||
"naoStatus": "member", |
||||
"joinedAt": "2024-03-10T14:00:00Z", |
||||
"groupIds": ["group1", "group3"], |
||||
"createdAt": "2024-02-25T12:00:00Z", |
||||
"updatedAt": "2024-07-25T19:00:00Z" |
||||
}, |
||||
{ |
||||
"id": "22", |
||||
"name": "Meena Seshamani", |
||||
"email": "meena.seshamani@healthpolicy.gov", |
||||
"phone": "+1 (555) 456-7890", |
||||
"company": "Center for Medicare & Medicaid Services", |
||||
"position": "Deputy Administrator", |
||||
"source": "contacts", |
||||
"profileImage": "/images/Meena.jpg", |
||||
"notes": "Healthcare policy expert and Medicare/Medicaid leader", |
||||
"tags": ["healthcare", "policy", "medicare"], |
||||
"naoStatus": "member", |
||||
"joinedAt": "2024-03-15T15:20:00Z", |
||||
"groupIds": ["group1"], |
||||
"createdAt": "2024-03-01T10:45:00Z", |
||||
"updatedAt": "2024-07-08T12:15:00Z" |
||||
}, |
||||
{ |
||||
"id": "23", |
||||
"name": "Stephane Bancel", |
||||
"email": "stephane.bancel@modernatx.com", |
||||
"phone": "+1 (555) 890-1234", |
||||
"company": "Moderna", |
||||
"position": "Chief Executive Officer", |
||||
"source": "contacts", |
||||
"profileImage": "/images/Stephane.jpg", |
||||
"notes": "Biotech leader, revolutionized mRNA vaccine technology", |
||||
"tags": ["biotech", "mrna", "vaccines"], |
||||
"naoStatus": "member", |
||||
"joinedAt": "2024-04-10T12:30:00Z", |
||||
"groupIds": ["group3"], |
||||
"createdAt": "2024-03-25T15:10:00Z", |
||||
"updatedAt": "2024-07-22T10:40:00Z" |
||||
} |
||||
] |
After Width: | Height: | Size: 761 KiB |
After Width: | Height: | Size: 707 KiB |
After Width: | Height: | Size: 762 KiB |
After Width: | Height: | Size: 1.0 MiB |
After Width: | Height: | Size: 697 KiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 644 KiB |
After Width: | Height: | Size: 780 KiB |
After Width: | Height: | Size: 696 KiB |
After Width: | Height: | Size: 721 KiB |
After Width: | Height: | Size: 755 KiB |
After Width: | Height: | Size: 657 KiB |
After Width: | Height: | Size: 682 KiB |
After Width: | Height: | Size: 798 KiB |
After Width: | Height: | Size: 726 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 853 KiB |
After Width: | Height: | Size: 934 KiB |
After Width: | Height: | Size: 846 KiB |
After Width: | Height: | Size: 796 KiB |
After Width: | Height: | Size: 801 KiB |
After Width: | Height: | Size: 734 KiB |
After Width: | Height: | Size: 1.4 MiB |
@ -0,0 +1,419 @@ |
||||
import { useState, useEffect, useRef } from 'react'; |
||||
import { Box, Typography, Card, CardContent } from '@mui/material'; |
||||
import { alpha, useTheme } from '@mui/material/styles'; |
||||
import type { Contact } from '../types/contact'; |
||||
import { getContactPhotoStyles } from '../utils/photoStyles'; |
||||
|
||||
interface NetworkNode extends Contact { |
||||
x: number; |
||||
y: number; |
||||
vx: number; |
||||
vy: number; |
||||
fx?: number; |
||||
fy?: number; |
||||
} |
||||
|
||||
interface NetworkLink { |
||||
source: string; |
||||
target: string; |
||||
strength: number; |
||||
type: 'vouch' | 'praise' | 'group'; |
||||
} |
||||
|
||||
interface NetworkGraphProps { |
||||
contacts: Contact[]; |
||||
width?: number; |
||||
height?: number; |
||||
} |
||||
|
||||
const NetworkGraph = ({ contacts, width, height }: NetworkGraphProps) => { |
||||
const theme = useTheme(); |
||||
const svgRef = useRef<SVGSVGElement>(null); |
||||
const containerRef = useRef<HTMLDivElement>(null); |
||||
const [nodes, setNodes] = useState<NetworkNode[]>([]); |
||||
const [links, setLinks] = useState<NetworkLink[]>([]); |
||||
const [hoveredNode, setHoveredNode] = useState<string | null>(null); |
||||
const [draggedNode, setDraggedNode] = useState<string | null>(null); |
||||
const [isDragging, setIsDragging] = useState(false); |
||||
const [dimensions, setDimensions] = useState({ width: 800, height: 600 }); |
||||
|
||||
// Update dimensions when container resizes
|
||||
useEffect(() => { |
||||
const updateDimensions = () => { |
||||
if (containerRef.current) { |
||||
const rect = containerRef.current.getBoundingClientRect(); |
||||
setDimensions({ |
||||
width: width || rect.width || 800, |
||||
height: height || rect.height || 600 |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
updateDimensions(); |
||||
window.addEventListener('resize', updateDimensions); |
||||
return () => window.removeEventListener('resize', updateDimensions); |
||||
}, [width, height]); |
||||
|
||||
// Initialize network data
|
||||
useEffect(() => { |
||||
if (!contacts.length) return; |
||||
|
||||
// Create nodes from contacts
|
||||
const networkNodes: NetworkNode[] = contacts.map((contact) => ({ |
||||
...contact, |
||||
x: Math.random() * (dimensions.width - 100) + 50, |
||||
y: Math.random() * (dimensions.height - 100) + 50, |
||||
vx: 0, |
||||
vy: 0, |
||||
})); |
||||
|
||||
// Create links based on relationships
|
||||
const networkLinks: NetworkLink[] = []; |
||||
|
||||
// Add links for shared groups
|
||||
contacts.forEach((contact, i) => { |
||||
contacts.forEach((otherContact, j) => { |
||||
if (i >= j) return; // Avoid duplicates
|
||||
|
||||
const sharedGroups = contact.groupIds?.filter(id =>
|
||||
otherContact.groupIds?.includes(id) |
||||
) || []; |
||||
|
||||
if (sharedGroups.length > 0) { |
||||
networkLinks.push({ |
||||
source: contact.id, |
||||
target: otherContact.id, |
||||
strength: sharedGroups.length, |
||||
type: 'group' |
||||
}); |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
// Add links for NAO members (representing vouch/praise relationships)
|
||||
const naoMembers = contacts.filter(c => c.naoStatus === 'member'); |
||||
naoMembers.forEach((contact, i) => { |
||||
naoMembers.forEach((otherContact, j) => { |
||||
if (i >= j) return; |
||||
|
||||
// Create stronger links between NAO members
|
||||
const existingLink = networkLinks.find(l =>
|
||||
(l.source === contact.id && l.target === otherContact.id) || |
||||
(l.source === otherContact.id && l.target === contact.id) |
||||
); |
||||
|
||||
if (existingLink) { |
||||
existingLink.strength += 2; |
||||
existingLink.type = 'vouch'; |
||||
} else { |
||||
networkLinks.push({ |
||||
source: contact.id, |
||||
target: otherContact.id, |
||||
strength: 1, |
||||
type: 'vouch' |
||||
}); |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
setNodes(networkNodes); |
||||
setLinks(networkLinks); |
||||
}, [contacts, dimensions.width, dimensions.height]); |
||||
|
||||
// Simple force simulation using requestAnimationFrame
|
||||
useEffect(() => { |
||||
if (!nodes.length || !links.length) return; |
||||
|
||||
let animationId: number; |
||||
const alpha = 0.1; |
||||
const centerForce = 0.02; |
||||
const linkForce = 0.1; |
||||
const repelForce = 100; |
||||
const centerX = dimensions.width / 2; |
||||
const centerY = dimensions.height / 2; |
||||
|
||||
const simulate = () => { |
||||
setNodes(prevNodes => { |
||||
const newNodes = prevNodes.map(node => ({ ...node })); |
||||
|
||||
// Apply forces
|
||||
newNodes.forEach(node => { |
||||
if (node.id === draggedNode) return; // Don't apply forces to dragged node
|
||||
|
||||
// Center force
|
||||
node.vx += (centerX - node.x) * centerForce; |
||||
node.vy += (centerY - node.y) * centerForce; |
||||
|
||||
// Link force
|
||||
links.forEach(link => { |
||||
const sourceNode = newNodes.find(n => n.id === link.source); |
||||
const targetNode = newNodes.find(n => n.id === link.target); |
||||
|
||||
if (sourceNode && targetNode) { |
||||
const dx = targetNode.x - sourceNode.x; |
||||
const dy = targetNode.y - sourceNode.y; |
||||
const distance = Math.sqrt(dx * dx + dy * dy) || 1; |
||||
const targetDistance = 80 + (link.strength * 20); |
||||
const force = (distance - targetDistance) * linkForce * link.strength; |
||||
|
||||
const fx = (dx / distance) * force; |
||||
const fy = (dy / distance) * force; |
||||
|
||||
if (sourceNode.id !== draggedNode) { |
||||
sourceNode.vx += fx; |
||||
sourceNode.vy += fy; |
||||
} |
||||
if (targetNode.id !== draggedNode) { |
||||
targetNode.vx -= fx; |
||||
targetNode.vy -= fy; |
||||
} |
||||
} |
||||
}); |
||||
|
||||
// Repel force between nodes
|
||||
newNodes.forEach(otherNode => { |
||||
if (node.id === otherNode.id) return; |
||||
|
||||
const dx = otherNode.x - node.x; |
||||
const dy = otherNode.y - node.y; |
||||
const distance = Math.sqrt(dx * dx + dy * dy) || 1; |
||||
|
||||
if (distance < 120) { |
||||
const force = repelForce / (distance * distance); |
||||
const fx = (dx / distance) * force; |
||||
const fy = (dy / distance) * force; |
||||
|
||||
if (node.id !== draggedNode) { |
||||
node.vx -= fx; |
||||
node.vy -= fy; |
||||
} |
||||
} |
||||
}); |
||||
|
||||
// Apply velocity with damping
|
||||
node.vx *= 0.9; |
||||
node.vy *= 0.9; |
||||
|
||||
// Update position
|
||||
node.x += node.vx * alpha; |
||||
node.y += node.vy * alpha; |
||||
|
||||
// Keep nodes within bounds
|
||||
const radius = 20; |
||||
node.x = Math.max(radius, Math.min(dimensions.width - radius, node.x)); |
||||
node.y = Math.max(radius, Math.min(dimensions.height - radius, node.y)); |
||||
}); |
||||
|
||||
return newNodes; |
||||
}); |
||||
|
||||
animationId = requestAnimationFrame(simulate); |
||||
}; |
||||
|
||||
animationId = requestAnimationFrame(simulate); |
||||
|
||||
return () => { |
||||
cancelAnimationFrame(animationId); |
||||
}; |
||||
}, [nodes.length, links, dimensions.width, dimensions.height, draggedNode]); |
||||
|
||||
const handleMouseDown = (nodeId: string, event: React.MouseEvent) => { |
||||
event.preventDefault(); |
||||
setDraggedNode(nodeId); |
||||
setIsDragging(true); |
||||
}; |
||||
|
||||
const handleMouseMove = (event: React.MouseEvent) => { |
||||
if (!isDragging || !draggedNode || !svgRef.current) return; |
||||
|
||||
const rect = svgRef.current.getBoundingClientRect(); |
||||
const x = event.clientX - rect.left; |
||||
const y = event.clientY - rect.top; |
||||
|
||||
setNodes(prevNodes =>
|
||||
prevNodes.map(node =>
|
||||
node.id === draggedNode
|
||||
? { ...node, x, y, vx: 0, vy: 0 } |
||||
: node |
||||
) |
||||
); |
||||
}; |
||||
|
||||
const handleMouseUp = () => { |
||||
setDraggedNode(null); |
||||
setIsDragging(false); |
||||
}; |
||||
|
||||
const getNodeColor = (node: NetworkNode) => { |
||||
switch (node.naoStatus) { |
||||
case 'member': |
||||
return theme.palette.success.main; |
||||
case 'invited': |
||||
return theme.palette.warning.main; |
||||
default: |
||||
return theme.palette.primary.main; |
||||
} |
||||
}; |
||||
|
||||
const getLinkColor = (link: NetworkLink) => { |
||||
switch (link.type) { |
||||
case 'vouch': |
||||
return theme.palette.success.main; |
||||
case 'praise': |
||||
return '#d81b60'; |
||||
default: |
||||
return alpha(theme.palette.primary.main, 0.3); |
||||
} |
||||
}; |
||||
|
||||
const getNodeRadius = (node: NetworkNode) => { |
||||
const baseRadius = 20; |
||||
const connections = links.filter(l => l.source === node.id || l.target === node.id).length; |
||||
return baseRadius + Math.sqrt(connections) * 3; |
||||
}; |
||||
|
||||
return ( |
||||
<Box
|
||||
ref={containerRef} |
||||
sx={{ width: '100%', height: '100%', position: 'relative' }} |
||||
> |
||||
<svg |
||||
ref={svgRef} |
||||
width={dimensions.width} |
||||
height={dimensions.height} |
||||
viewBox={`0 0 ${dimensions.width} ${dimensions.height}`} |
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%', |
||||
cursor: isDragging ? 'grabbing' : 'default' |
||||
}} |
||||
onMouseMove={handleMouseMove} |
||||
onMouseUp={handleMouseUp} |
||||
onMouseLeave={handleMouseUp} |
||||
> |
||||
{/* Links */} |
||||
<g> |
||||
{links.map((link, index) => { |
||||
const sourceNode = nodes.find(n => n.id === link.source); |
||||
const targetNode = nodes.find(n => n.id === link.target); |
||||
|
||||
if (!sourceNode || !targetNode) return null; |
||||
|
||||
return ( |
||||
<line |
||||
key={`${link.source}-${link.target}-${index}`} |
||||
x1={sourceNode.x} |
||||
y1={sourceNode.y} |
||||
x2={targetNode.x} |
||||
y2={targetNode.y} |
||||
stroke={getLinkColor(link)} |
||||
strokeWidth={Math.max(1, link.strength)} |
||||
strokeOpacity={0.6} |
||||
/> |
||||
); |
||||
})} |
||||
</g> |
||||
|
||||
{/* Nodes */} |
||||
<g> |
||||
{nodes.map((node) => ( |
||||
<g key={node.id}> |
||||
<circle |
||||
cx={node.x} |
||||
cy={node.y} |
||||
r={getNodeRadius(node)} |
||||
fill={getNodeColor(node)} |
||||
stroke={hoveredNode === node.id ? theme.palette.common.white : 'none'} |
||||
strokeWidth={3} |
||||
style={{
|
||||
cursor: 'grab', |
||||
filter: hoveredNode === node.id ? 'brightness(1.2)' : 'none' |
||||
}} |
||||
onMouseDown={(e) => handleMouseDown(node.id, e)} |
||||
onMouseEnter={() => setHoveredNode(node.id)} |
||||
onMouseLeave={() => setHoveredNode(null)} |
||||
/> |
||||
|
||||
{/* Profile image if available */} |
||||
{node.profileImage && ( |
||||
<foreignObject |
||||
x={node.x - getNodeRadius(node)} |
||||
y={node.y - getNodeRadius(node)} |
||||
width={getNodeRadius(node) * 2} |
||||
height={getNodeRadius(node) * 2} |
||||
style={{ pointerEvents: 'none' }} |
||||
> |
||||
<div |
||||
style={{ |
||||
width: '100%', |
||||
height: '100%', |
||||
borderRadius: '50%', |
||||
backgroundImage: `url(${node.profileImage})`, |
||||
backgroundSize: getContactPhotoStyles(node.name).backgroundSize, |
||||
backgroundPosition: getContactPhotoStyles(node.name).backgroundPosition, |
||||
backgroundRepeat: 'no-repeat' |
||||
}} |
||||
/> |
||||
</foreignObject> |
||||
)} |
||||
|
||||
{/* Node label */} |
||||
<text |
||||
x={node.x} |
||||
y={node.y + getNodeRadius(node) + 15} |
||||
textAnchor="middle" |
||||
fontSize="12px" |
||||
fill={theme.palette.text.primary} |
||||
style={{ pointerEvents: 'none', userSelect: 'none' }} |
||||
> |
||||
{node.name.split(' ')[0]} |
||||
</text> |
||||
</g> |
||||
))} |
||||
</g> |
||||
</svg> |
||||
|
||||
{/* Tooltip */} |
||||
{hoveredNode && ( |
||||
<Box |
||||
sx={{ |
||||
position: 'absolute', |
||||
top: 10, |
||||
left: 10, |
||||
zIndex: 1000, |
||||
pointerEvents: 'none' |
||||
}} |
||||
> |
||||
{(() => { |
||||
const node = nodes.find(n => n.id === hoveredNode); |
||||
if (!node) return null; |
||||
|
||||
const connections = links.filter(l => l.source === node.id || l.target === node.id).length; |
||||
|
||||
return ( |
||||
<Card sx={{ minWidth: 200 }}> |
||||
<CardContent sx={{ p: 2, '&:last-child': { pb: 2 } }}> |
||||
<Typography variant="subtitle2" fontWeight={600}> |
||||
{node.name} |
||||
</Typography> |
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}> |
||||
{node.position} {node.company && `at ${node.company}`} |
||||
</Typography> |
||||
<Typography variant="caption" color="text.secondary"> |
||||
{connections} connection{connections !== 1 ? 's' : ''} |
||||
</Typography> |
||||
<Typography variant="caption" display="block" color="text.secondary"> |
||||
Status: {node.naoStatus === 'member' ? 'NAO Member' :
|
||||
node.naoStatus === 'invited' ? 'Invited' : 'Not Invited'} |
||||
</Typography> |
||||
</CardContent> |
||||
</Card> |
||||
); |
||||
})()} |
||||
</Box> |
||||
)} |
||||
</Box> |
||||
); |
||||
}; |
||||
|
||||
export default NetworkGraph; |
@ -0,0 +1,72 @@ |
||||
export interface PhotoStyles { |
||||
backgroundSize: string; |
||||
backgroundPosition: string; |
||||
} |
||||
|
||||
/** |
||||
* Get custom photo positioning and zoom levels for contact profile images. |
||||
* These settings ensure optimal cropping and positioning for each person's photo. |
||||
*/ |
||||
export const getContactPhotoStyles = (contactName: string): PhotoStyles => { |
||||
let backgroundSize = '180%'; // default
|
||||
let backgroundPosition = 'center center'; // default
|
||||
|
||||
switch (contactName) { |
||||
case 'Tree Willard': |
||||
backgroundSize = '120%'; |
||||
break; |
||||
case 'Niko Bonnieure': |
||||
backgroundSize = '100%'; |
||||
break; |
||||
case 'Tim Bansemer': |
||||
backgroundSize = '220%'; |
||||
break; |
||||
case 'Duke Dorje': |
||||
backgroundSize = '200%'; |
||||
backgroundPosition = '60% 65%'; |
||||
break; |
||||
case 'Kevin Triplett': |
||||
backgroundSize = '220%'; |
||||
backgroundPosition = '40% 60%'; |
||||
break; |
||||
case 'Kristina Lillieneke': |
||||
backgroundSize = '220%'; |
||||
backgroundPosition = 'center 60%'; |
||||
break; |
||||
case 'Oliver Sylvester-Bradley': |
||||
backgroundSize = '220%'; |
||||
backgroundPosition = 'center 55%'; |
||||
break; |
||||
case 'David Thomson': |
||||
backgroundSize = '220%'; |
||||
break; |
||||
case 'Samuel Gbafa': |
||||
backgroundSize = '280%'; |
||||
backgroundPosition = '60% 60%'; |
||||
break; |
||||
case 'Meena Seshamani': |
||||
backgroundSize = '280%'; |
||||
backgroundPosition = '60% 60%'; |
||||
break; |
||||
case 'Alex Lion Yes!': |
||||
backgroundPosition = '70% 70%'; |
||||
break; |
||||
case 'Aza Mafi': |
||||
backgroundPosition = 'center 80%'; |
||||
break; |
||||
case 'Day Waterbury': |
||||
backgroundPosition = 'center 60%'; |
||||
break; |
||||
case 'Frederic Boyer': |
||||
backgroundPosition = 'center 60%'; |
||||
break; |
||||
case 'Joscha Raue': |
||||
backgroundPosition = '60% 65%'; |
||||
break; |
||||
case 'Margeigh Novotny': |
||||
backgroundPosition = 'center 70%'; |
||||
break; |
||||
} |
||||
|
||||
return { backgroundSize, backgroundPosition }; |
||||
}; |