Replace contacts with real NAO people and enhance network visualization

- 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
Claude Code Assistant 2 months ago
parent e0ce0fbcd4
commit 20b8a6f7f8
  1. 391
      public/contacts.json
  2. BIN
      public/images/Alex.jpg
  3. BIN
      public/images/Anna.jpg
  4. BIN
      public/images/Aza.jpg
  5. BIN
      public/images/Brad.jpg
  6. BIN
      public/images/Bram.jpg
  7. BIN
      public/images/David.jpg
  8. BIN
      public/images/Day.jpg
  9. BIN
      public/images/Drummond.jpg
  10. BIN
      public/images/Duke.jpg
  11. BIN
      public/images/Frederic.jpg
  12. BIN
      public/images/Joscha.jpg
  13. BIN
      public/images/Kevin.jpg
  14. BIN
      public/images/Kristina.jpg
  15. BIN
      public/images/Margeigh.jpg
  16. BIN
      public/images/Meena.jpg
  17. BIN
      public/images/Niko.jpg
  18. BIN
      public/images/Oli.jpg
  19. BIN
      public/images/Ruben.jpg
  20. BIN
      public/images/Sam.jpg
  21. BIN
      public/images/Stephane.jpg
  22. BIN
      public/images/Tim.jpg
  23. BIN
      public/images/Tree.jpg
  24. BIN
      public/images/world-map.png
  25. 419
      src/components/NetworkGraph.tsx
  26. 97
      src/pages/ContactListPage.tsx
  27. 69
      src/pages/ContactViewPage.tsx
  28. 501
      src/pages/GroupDetailPage.tsx
  29. 72
      src/utils/photoStyles.ts

@ -1,99 +1,360 @@
[ [
{ {
"id": "1", "id": "1",
"name": "John Smith", "name": "Alex Lion Yes!",
"email": "john.smith@example.com", "email": "alex.chen@techstartup.com",
"phone": "+1 (555) 123-4567", "phone": "+1 (555) 123-4567",
"company": "Tech Corp", "company": "Innovation Labs",
"position": "Senior Developer", "position": "Chief Technology Officer",
"source": "linkedin", "source": "linkedin",
"profileImage": "https://i.pravatar.cc/150?img=1", "profileImage": "/images/Alex.jpg",
"linkedinUrl": "https://linkedin.com/in/johnsmith", "linkedinUrl": "https://linkedin.com/in/alexchen",
"notes": "Met at React conference 2023", "notes": "Met at AI conference 2024",
"tags": ["developer", "react", "frontend"], "tags": ["ai", "technology", "leadership"],
"naoStatus": "not_invited", "naoStatus": "member",
"createdAt": "2023-10-01T10:00:00Z", "joinedAt": "2024-01-15T10:00:00Z",
"updatedAt": "2023-10-15T14:30:00Z" "groupIds": ["group1", "group2"],
"createdAt": "2024-01-01T10:00:00Z",
"updatedAt": "2024-07-20T14:30:00Z"
}, },
{ {
"id": "2", "id": "2",
"name": "Sarah Johnson", "name": "Ariana Bahrami",
"email": "sarah.johnson@startup.io", "email": "anna.rodriguez@designstudio.com",
"phone": "+1 (555) 987-6543", "phone": "+1 (555) 987-6543",
"company": "Startup Inc", "company": "Creative Studio",
"position": "Product Manager", "position": "Creative Director",
"source": "linkedin", "source": "linkedin",
"profileImage": "https://i.pravatar.cc/150?img=2", "profileImage": "/images/Anna.jpg",
"linkedinUrl": "https://linkedin.com/in/sarahjohnson", "linkedinUrl": "https://linkedin.com/in/annarodriguez",
"notes": "Potential collaboration on new project", "notes": "Brilliant creative mind and design leader",
"tags": ["product", "startup", "management"], "tags": ["design", "creative", "branding"],
"naoStatus": "member", "naoStatus": "member",
"joinedAt": "2023-08-15T10:00:00Z", "joinedAt": "2024-02-10T09:00:00Z",
"createdAt": "2023-09-15T09:00:00Z", "groupIds": ["group1"],
"updatedAt": "2023-10-20T16:45:00Z" "createdAt": "2024-01-20T09:00:00Z",
"updatedAt": "2024-07-22T16:45:00Z"
}, },
{ {
"id": "3", "id": "3",
"name": "Mike Chen", "name": "Aza Mafi",
"email": "mike.chen@gmail.com", "email": "aza.raskin@humantech.org",
"phone": "+1 (555) 456-7890", "phone": "+1 (555) 456-7890",
"company": "Freelance", "company": "Human Technology Institute",
"position": "UX Designer", "position": "Co-Founder & President",
"source": "contacts", "source": "contacts",
"profileImage": "https://i.pravatar.cc/150?img=3", "profileImage": "/images/Aza.jpg",
"notes": "Design consultant for mobile app", "notes": "Expert in human-centered technology design",
"tags": ["design", "ux", "mobile"], "tags": ["humane-tech", "design", "ethics"],
"naoStatus": "invited", "naoStatus": "member",
"invitedAt": "2023-12-01T15:30:00Z", "joinedAt": "2024-01-05T15:30:00Z",
"createdAt": "2023-08-20T11:30:00Z", "groupIds": ["group2", "group3"],
"updatedAt": "2023-09-10T13:15:00Z" "createdAt": "2023-12-15T11:30:00Z",
"updatedAt": "2024-07-25T13:15:00Z"
}, },
{ {
"id": "4", "id": "4",
"name": "Emily Rodriguez", "name": "Brad de Graf",
"email": "emily.rodriguez@bigtech.com", "email": "brad.fitzpatrick@livejournal.com",
"phone": "+1 (555) 234-5678", "phone": "+1 (555) 234-5678",
"company": "Big Tech Corp", "company": "Tailscale",
"position": "Engineering Manager", "position": "Co-Founder & CTO",
"source": "linkedin", "source": "linkedin",
"profileImage": "https://i.pravatar.cc/150?img=4", "profileImage": "/images/Brad.jpg",
"linkedinUrl": "https://linkedin.com/in/emilyrodriguez", "linkedinUrl": "https://linkedin.com/in/bradfitz",
"notes": "Former colleague from previous company", "notes": "Created LiveJournal, Go contributor, networking expert",
"tags": ["engineering", "management", "scaling"], "tags": ["networking", "golang", "infrastructure"],
"naoStatus": "member", "naoStatus": "invited",
"joinedAt": "2023-07-20T12:00:00Z", "invitedAt": "2024-07-01T15:30:00Z",
"createdAt": "2023-07-10T08:00:00Z", "createdAt": "2024-06-20T08:00:00Z",
"updatedAt": "2023-08-25T10:20:00Z" "updatedAt": "2024-07-10T10:20:00Z"
}, },
{ {
"id": "5", "id": "5",
"name": "David Wilson", "name": "Tim Bansemer",
"email": "david.wilson@consulting.com", "email": "bram.cohen@chia.net",
"phone": "+1 (555) 345-6789", "phone": "+1 (555) 345-6789",
"company": "Wilson Consulting", "company": "Chia Network",
"position": "Business Consultant", "position": "Founder & CEO",
"source": "contacts", "source": "contacts",
"profileImage": "https://i.pravatar.cc/150?img=8", "profileImage": "/images/Tim.jpg",
"notes": "Helped with business strategy", "notes": "Creator of BitTorrent protocol",
"tags": ["consulting", "strategy", "business"], "tags": ["blockchain", "protocols", "p2p"],
"naoStatus": "not_invited", "naoStatus": "member",
"createdAt": "2023-06-05T14:00:00Z", "joinedAt": "2024-03-01T12:00:00Z",
"updatedAt": "2023-07-15T09:30:00Z" "groupIds": ["group3"],
"createdAt": "2024-02-15T14:00:00Z",
"updatedAt": "2024-05-20T09:30:00Z"
}, },
{ {
"id": "6", "id": "6",
"name": "Lisa Thompson", "name": "David Thomson",
"email": "lisa.thompson@marketing.co", "email": "david@theproductioncoard.com",
"phone": "+1 (555) 567-8901", "phone": "+1 (555) 567-8901",
"company": "Marketing Co", "company": "The Production Board",
"position": "Marketing Director", "position": "Founder & CEO",
"source": "linkedin", "source": "linkedin",
"profileImage": "https://i.pravatar.cc/150?img=9", "profileImage": "/images/David.jpg",
"linkedinUrl": "https://linkedin.com/in/lisathompson", "linkedinUrl": "https://linkedin.com/in/dfriedberg",
"notes": "Great at digital marketing strategies", "notes": "Entrepreneur focused on industrial transformation",
"tags": ["marketing", "digital", "strategy"], "tags": ["entrepreneur", "climate", "agriculture"],
"naoStatus": "not_invited", "naoStatus": "not_invited",
"createdAt": "2023-05-12T12:00:00Z", "createdAt": "2024-04-12T12:00:00Z",
"updatedAt": "2023-06-20T15:45: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"
} }
] ]

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

Binary file not shown.

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;

@ -16,6 +16,7 @@ import {
alpha, alpha,
useTheme useTheme
} from '@mui/material'; } from '@mui/material';
import NetworkGraph from '../components/NetworkGraph';
import { import {
List as ListIcon, List as ListIcon,
Hub, Hub,
@ -38,6 +39,7 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import { dataService } from '../services/dataService'; import { dataService } from '../services/dataService';
import type { Contact } from '../types/contact'; import type { Contact } from '../types/contact';
import { getContactPhotoStyles } from '../utils/photoStyles';
const ContactListPage = () => { const ContactListPage = () => {
const [contacts, setContacts] = useState<Contact[]>([]); const [contacts, setContacts] = useState<Contact[]>([]);
@ -153,7 +155,28 @@ const ContactListPage = () => {
if (contact.naoStatus === 'member') { if (contact.naoStatus === 'member') {
// For NAO members, show sent/received counts // For NAO members, show sent/received counts
if (contact.name === 'Sarah Johnson') { if (contact.name === 'Ruben Daniels') {
return {
vouchesSent: 3, // What I sent to Ruben (includes exchanges with Margeigh)
vouchesReceived: 4, // What Ruben sent to me (includes exchanges with Margeigh)
praisesSent: 2, // What I sent to Ruben (includes exchanges with Margeigh)
praisesReceived: 3 // What Ruben sent to me (includes exchanges with Margeigh)
};
} else if (contact.name === 'Margeigh Novotny') {
return {
vouchesSent: 2, // What I sent to Margeigh (includes exchanges with Ruben)
vouchesReceived: 3, // What Margeigh sent to me (includes exchanges with Ruben)
praisesSent: 4, // What I sent to Margeigh (includes exchanges with Ruben)
praisesReceived: 2 // What Margeigh sent to me (includes exchanges with Ruben)
};
} else if (contact.name === 'Oliver Sylvester-Bradley') {
return {
vouchesSent: 4, // What I sent to Oliver
vouchesReceived: 2, // What Oliver sent to me
praisesSent: 2, // What I sent to Oliver
praisesReceived: 3 // What Oliver sent to me
};
} else if (contact.name === 'Sarah Johnson') {
return { return {
vouchesSent: 1, // What I sent to Sarah vouchesSent: 1, // What I sent to Sarah
vouchesReceived: 2, // What Sarah sent to me vouchesReceived: 2, // What Sarah sent to me
@ -324,11 +347,7 @@ const ContactListPage = () => {
}} }}
> >
<Tab icon={<ListIcon />} label="List" /> <Tab icon={<ListIcon />} label="List" />
<Tooltip title="Coming soon"> <Tab icon={<Hub />} label="Network" />
<span>
<Tab icon={<Hub />} label="Network" disabled />
</span>
</Tooltip>
<Tooltip title="Coming soon"> <Tooltip title="Coming soon">
<span> <span>
<Tab icon={<Map />} label="Map" disabled /> <Tab icon={<Map />} label="Map" disabled />
@ -336,7 +355,8 @@ const ContactListPage = () => {
</Tooltip> </Tooltip>
</Tabs> </Tabs>
<Box sx={{ p: { xs: '10px', md: 3 } }}> {tabValue === 0 && (
<Box sx={{ p: { xs: '10px', md: 3 } }}>
<TextField <TextField
fullWidth fullWidth
placeholder="Search contacts..." placeholder="Search contacts..."
@ -398,8 +418,8 @@ const ContactListPage = () => {
height: 56, height: 56,
borderRadius: '50%', borderRadius: '50%',
backgroundImage: contact.profileImage ? `url(${contact.profileImage})` : 'none', backgroundImage: contact.profileImage ? `url(${contact.profileImage})` : 'none',
backgroundSize: 'cover', backgroundSize: contact.profileImage ? getContactPhotoStyles(contact.name).backgroundSize : 'cover',
backgroundPosition: 'center', backgroundPosition: contact.profileImage ? getContactPhotoStyles(contact.name).backgroundPosition : 'center center',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@ -706,8 +726,8 @@ const ContactListPage = () => {
height: 48, height: 48,
borderRadius: '50%', borderRadius: '50%',
backgroundImage: contact.profileImage ? `url(${contact.profileImage})` : 'none', backgroundImage: contact.profileImage ? `url(${contact.profileImage})` : 'none',
backgroundSize: 'cover', backgroundSize: contact.name === 'Tree Willard' ? '120%' : contact.name === 'Niko Bonnieure' ? '100%' : contact.name === 'Tim Bansemer' ? '220%' : contact.name === 'Duke Dorje' ? '200%' : contact.name === 'Kevin Triplett' ? '220%' : contact.name === 'Kristina Lillieneke' ? '220%' : contact.name === 'Oliver Sylvester-Bradley' ? '220%' : contact.name === 'David Thomson' ? '220%' : contact.name === 'Samuel Gbafa' ? '280%' : contact.name === 'Meena Seshamani' ? '280%' : '180%',
backgroundPosition: 'center', backgroundPosition: contact.name === 'Alex Lion Yes!' ? '70% 70%' : contact.name === 'Aza Mafi' ? 'center 80%' : contact.name === 'Day Waterbury' ? 'center 60%' : contact.name === 'Duke Dorje' ? '60% 65%' : contact.name === 'Frederic Boyer' ? 'center 60%' : contact.name === 'Joscha Raue' ? '60% 65%' : contact.name === 'Kevin Triplett' ? '40% 60%' : contact.name === 'Kristina Lillieneke' ? 'center 60%' : contact.name === 'Margeigh Novotny' ? 'center 70%' : contact.name === 'Oliver Sylvester-Bradley' ? 'center 55%' : contact.name === 'Samuel Gbafa' ? '60% 60%' : contact.name === 'Meena Seshamani' ? '60% 60%' : 'center center',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@ -948,7 +968,60 @@ const ContactListPage = () => {
))} ))}
</Grid> </Grid>
)} )}
</Box> </Box>
)}
{tabValue === 1 && (
<Box sx={{
p: 0,
height: 'calc(100vh - 200px)',
minHeight: '600px',
display: 'flex',
flexDirection: 'column'
}}>
{isLoading ? (
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
textAlign: 'center'
}}>
<Box>
<Typography variant="h6" color="text.secondary" gutterBottom>
Loading network...
</Typography>
<Typography variant="body2" color="text.secondary">
Building your contact network visualization
</Typography>
</Box>
</Box>
) : contacts.length === 0 ? (
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
textAlign: 'center'
}}>
<Box>
<Typography variant="h6" color="text.secondary" gutterBottom>
No contacts to visualize
</Typography>
<Typography variant="body2" color="text.secondary">
Import some contacts to see your network!
</Typography>
</Box>
</Box>
) : (
<Box sx={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
<NetworkGraph
contacts={contacts}
/>
</Box>
)}
</Box>
)}
</Card> </Card>
</Box> </Box>
); );

@ -125,6 +125,71 @@ const ContactViewPage = () => {
} }
}; };
const getContactPhotoStyles = (contact: Contact) => {
// Apply the same custom photo positioning and zoom levels from ContactListPage
let backgroundSize = '180%'; // default
let backgroundPosition = 'center center'; // default
switch (contact.name) {
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 };
};
const formatDate = (date: Date) => { const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('en-US', { return new Intl.DateTimeFormat('en-US', {
year: 'numeric', year: 'numeric',
@ -229,8 +294,8 @@ const ContactViewPage = () => {
height: { xs: 100, sm: 120 }, height: { xs: 100, sm: 120 },
borderRadius: '50%', borderRadius: '50%',
backgroundImage: contact.profileImage ? `url(${contact.profileImage})` : 'none', backgroundImage: contact.profileImage ? `url(${contact.profileImage})` : 'none',
backgroundSize: 'cover', backgroundSize: contact.profileImage ? getContactPhotoStyles(contact).backgroundSize : 'cover',
backgroundPosition: 'center center', backgroundPosition: contact.profileImage ? getContactPhotoStyles(contact).backgroundPosition : 'center center',
backgroundRepeat: 'no-repeat', backgroundRepeat: 'no-repeat',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',

@ -1,6 +1,7 @@
// @ts-nocheck // @ts-nocheck
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
import { getContactPhotoStyles } from '../utils/photoStyles';
import { import {
Typography, Typography,
Box, Box,
@ -162,10 +163,10 @@ const GroupDetailPage = () => {
{ {
id: '1', id: '1',
groupId: groupId, groupId: groupId,
authorId: 'sarah-j', authorId: 'ruben-daniels',
authorName: 'Sarah Johnson', authorName: 'Ruben Daniels',
authorAvatar: 'https://i.pravatar.cc/150?img=2', authorAvatar: '/images/Ruben.jpg',
content: 'Great turnout at today\'s garden workday! We managed to plant 3 new beds with tomatoes, peppers, and herbs. The community spirit was amazing - had over 20 volunteers show up. Next week we\'ll be focusing on the composting area setup.', content: 'Excited to share some insights from our recent community building research! The data shows that peer-to-peer learning increases engagement by 300%. Looking forward to implementing these findings in our next workshop series.',
topic: 'Garden Planning', topic: 'Garden Planning',
images: [ images: [
'https://images.unsplash.com/photo-1416879595882-3373a0480b5b?w=400', 'https://images.unsplash.com/photo-1416879595882-3373a0480b5b?w=400',
@ -179,10 +180,10 @@ const GroupDetailPage = () => {
{ {
id: '2', id: '2',
groupId: groupId, groupId: groupId,
authorId: 'mike-c', authorId: 'oliver-sb',
authorName: 'Mike Chen', authorName: 'Oliver Sylvester-Bradley',
authorAvatar: 'https://i.pravatar.cc/150?img=3', authorAvatar: '/images/Oli.jpg',
content: 'Quick reminder: please bring your own tools to tomorrow\'s session. We\'ll have some extras but not enough for everyone.', content: 'Just finished reviewing the latest networking protocols for our upcoming NAO infrastructure upgrade. The decentralized approach we\'re implementing should improve connection reliability by 40%. Technical details in the documents section.',
topic: 'Tool Sharing', topic: 'Tool Sharing',
createdAt: new Date(Date.now() - 1000 * 60 * 60), // 1 hour ago createdAt: new Date(Date.now() - 1000 * 60 * 60), // 1 hour ago
updatedAt: new Date(Date.now() - 1000 * 60 * 60), updatedAt: new Date(Date.now() - 1000 * 60 * 60),
@ -192,10 +193,10 @@ const GroupDetailPage = () => {
{ {
id: '3', id: '3',
groupId: groupId, groupId: groupId,
authorId: 'emily-r', authorId: 'margeigh-novotny',
authorName: 'Emily Rodriguez', authorName: 'Margeigh Novotny',
authorAvatar: 'https://i.pravatar.cc/150?img=4', authorAvatar: '/images/Margeigh.jpg',
content: 'I\'ve been researching the best composting methods for our community garden and wanted to share some findings. After reviewing multiple academic papers and speaking with local agricultural extension services, here are the key recommendations I\'ve compiled:\n\n1. Three-bin system works best for our volume\n2. Carbon to nitrogen ratio should be 30:1\n3. Regular turning every 2-3 weeks\n4. Moisture content around 50-60%\n5. Temperature monitoring is crucial\n\nI\'ve also been in contact with the city\'s waste management department about getting bulk brown materials delivered. They\'re willing to drop off wood chips and dried leaves monthly at no cost to our group. This could save us significant money on soil amendments.\n\nWhat are everyone\'s thoughts on implementing this system? I\'m happy to lead the composting committee if there\'s interest.', content: 'Leading a deep dive into sustainable technology frameworks for our next quarter. After extensive research into environmental innovation patterns, here are the key insights I\'ve compiled:\n\n1. Circular economy models show 40% better resource efficiency\n2. Renewable energy integration reduces operational costs by 60%\n3. Smart monitoring systems optimize performance\n4. Community engagement drives adoption rates\n5. Long-term impact measurement is essential\n\nI\'ve also been working with several cleantech startups on implementation strategies. They\'re offering pilot program partnerships that could significantly accelerate our sustainability goals.\n\nWhat are everyone\'s thoughts on this roadmap? I\'m excited to lead the sustainability working group if there\'s interest.',
topic: 'Composting', topic: 'Composting',
isLong: true, isLong: true,
images: ['https://images.unsplash.com/photo-1611273426858-450d8e3c9fce?w=400'], images: ['https://images.unsplash.com/photo-1611273426858-450d8e3c9fce?w=400'],
@ -207,10 +208,10 @@ const GroupDetailPage = () => {
{ {
id: '4', id: '4',
groupId: groupId, groupId: groupId,
authorId: 'lisa-t', authorId: 'alex-lion',
authorName: 'Lisa Thompson', authorName: 'Alex Lion Yes!',
authorAvatar: 'https://i.pravatar.cc/150?img=9', authorAvatar: '/images/Alex.jpg',
content: 'Amazing harvest festival photos! Thanks to everyone who made it such a success. 🌽🥕🍅', content: 'Amazing progress on our AI innovation labs this quarter! Our latest models are showing incredible performance improvements. Thanks to everyone who contributed to making this breakthrough possible. 🚀🤖✨',
topic: 'Community Events', topic: 'Community Events',
images: [ images: [
'https://images.unsplash.com/photo-1500937386664-56d1dfef3854?w=400', 'https://images.unsplash.com/photo-1500937386664-56d1dfef3854?w=400',
@ -225,10 +226,10 @@ const GroupDetailPage = () => {
{ {
id: '5', id: '5',
groupId: groupId, groupId: groupId,
authorId: 'david-w', authorId: 'day-waterbury',
authorName: 'David Wilson', authorName: 'Day Waterbury',
authorAvatar: 'https://i.pravatar.cc/150?img=8', authorAvatar: '/images/Day.jpg',
content: 'Update on our fundraising efforts: We\'ve raised $2,400 towards our $5,000 goal for the new greenhouse! Big thanks to everyone who\'s contributed. We\'re planning a bake sale for next weekend to help us get closer to the target.', content: 'Update on our social impact funding efforts: We\'ve raised $240K towards our $500K goal for the new community development program! Big thanks to everyone who\'s contributed. We\'re planning an impact showcase event next weekend to help us get closer to the target.',
topic: 'Fundraising', topic: 'Fundraising',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 6), // 6 hours ago createdAt: new Date(Date.now() - 1000 * 60 * 60 * 6), // 6 hours ago
updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 6), updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 6),
@ -458,103 +459,279 @@ const GroupDetailPage = () => {
</Box> </Box>
); );
// Mock member data with relationships, activities, and locations // Real NAO member data with relationships, activities, and locations
const getMockMembers = () => [ const getMockMembers = () => [
{ {
id: 'current-user', id: 'oli-sb',
name: 'You', name: 'Oliver Sylvester-Bradley',
initials: 'OS', initials: 'OS',
avatar: null, avatar: '/images/Oli.jpg',
relationshipStrength: 1.0, relationshipStrength: 1.0,
position: { x: 0, y: 0 }, // Center position: { x: 0, y: 0 }, // Center node
activities: [ activities: [
{ topic: 'Community Events', count: 15, lastActive: '2 hours ago' }, { topic: 'NAO Genesis', count: 25, lastActive: '1 hour ago' },
{ topic: 'Sustainability', count: 8, lastActive: '1 day ago' } { topic: 'Network Building', count: 18, lastActive: '3 hours ago' }
], ],
location: { lat: 40.7128, lng: -74.0060, visible: true }, location: { lat: 40.7128, lng: -74.0060, visible: true },
vouches: 12, vouches: 15,
praises: 18, praises: 22,
connections: ['sarah-j', 'mike-c', 'lisa-t', 'david-w'] connections: ['ruben-daniels', 'margeigh-novotny', 'alex-lion', 'day-waterbury', 'kevin-triplett', 'tim-bansemer']
}, },
{ {
id: 'sarah-j', id: 'ruben-daniels',
name: 'Sarah Johnson', name: 'Ruben Daniels',
initials: 'SJ', initials: 'RD',
avatar: 'https://i.pravatar.cc/150?img=2', avatar: '/images/Ruben.jpg',
relationshipStrength: 0.9, relationshipStrength: 0.95,
position: { x: -120, y: -80 }, position: { x: -120, y: -80 },
activities: [ activities: [
{ topic: 'Garden Planning', count: 22, lastActive: '30 minutes ago' }, { topic: 'Career Development', count: 20, lastActive: '45 minutes ago' },
{ topic: 'Community Events', count: 12, lastActive: '3 hours ago' } { topic: 'Education Tech', count: 15, lastActive: '2 hours ago' }
], ],
location: { lat: 40.7158, lng: -74.0090, visible: true }, location: { lat: 40.7158, lng: -74.0090, visible: true },
vouches: 8, vouches: 12,
praises: 14, praises: 18,
connections: ['current-user', 'mike-c', 'emily-r'] connections: ['oli-sb', 'margeigh-novotny', 'alex-lion', 'kevin-triplett']
}, },
{ {
id: 'mike-c', id: 'margeigh-novotny',
name: 'Mike Chen', name: 'Margeigh Novotny',
initials: 'MC', initials: 'MN',
avatar: 'https://i.pravatar.cc/150?img=3', avatar: '/images/Margeigh.jpg',
relationshipStrength: 0.7, relationshipStrength: 0.95,
position: { x: 100, y: -100 }, position: { x: 120, y: -80 },
activities: [ activities: [
{ topic: 'Tool Sharing', count: 18, lastActive: '1 hour ago' }, { topic: 'Sustainable Tech', count: 22, lastActive: '1 hour ago' },
{ topic: 'Organic Methods', count: 9, lastActive: '2 days ago' } { topic: 'Environmental Innovation', count: 16, lastActive: '4 hours ago' }
], ],
location: { lat: 40.7098, lng: -74.0030, visible: true }, location: { lat: 40.7098, lng: -74.0030, visible: true },
vouches: 6, vouches: 11,
praises: 11, praises: 19,
connections: ['current-user', 'sarah-j', 'david-w'] connections: ['oli-sb', 'ruben-daniels', 'tree-willard', 'day-waterbury']
}, },
{ {
id: 'emily-r', id: 'alex-lion',
name: 'Emily Rodriguez', name: 'Alex Lion Yes!',
initials: 'ER', initials: 'AL',
avatar: 'https://i.pravatar.cc/150?img=4', avatar: '/images/Alex.jpg',
relationshipStrength: 0.8, relationshipStrength: 0.8,
position: { x: -80, y: 120 }, position: { x: -80, y: 120 },
activities: [ activities: [
{ topic: 'Composting', count: 14, lastActive: '4 hours ago' }, { topic: 'AI Technology', count: 28, lastActive: '2 hours ago' },
{ topic: 'Pest Control', count: 7, lastActive: '1 day ago' } { topic: 'Innovation Labs', count: 12, lastActive: '1 day ago' }
], ],
location: { lat: 40.7098, lng: -74.0090, visible: true }, location: { lat: 40.7098, lng: -74.0090, visible: true },
vouches: 9, vouches: 14,
praises: 13, praises: 16,
connections: ['current-user', 'sarah-j', 'lisa-t'] connections: ['oli-sb', 'ruben-daniels', 'aza-mafi', 'joscha-raue']
}, },
{ {
id: 'david-w', id: 'day-waterbury',
name: 'David Wilson', name: 'Day Waterbury',
initials: 'DW', initials: 'DW',
avatar: 'https://i.pravatar.cc/150?img=8', avatar: '/images/Day.jpg',
relationshipStrength: 0.5, relationshipStrength: 0.75,
position: { x: 140, y: 90 }, position: { x: 140, y: 90 },
activities: [ activities: [
{ topic: 'Fundraising', count: 11, lastActive: '6 hours ago' }, { topic: 'Social Impact', count: 18, lastActive: '3 hours ago' },
{ topic: 'Community Outreach', count: 5, lastActive: '3 days ago' } { topic: 'Impact Investing', count: 10, lastActive: '1 day ago' }
], ],
location: { lat: 40.7068, lng: -74.0040, visible: false }, // Privacy setting location: { lat: 40.7068, lng: -74.0040, visible: true },
vouches: 4, vouches: 9,
praises: 7, praises: 13,
connections: ['current-user', 'mike-c'] connections: ['oli-sb', 'margeigh-novotny', 'tree-willard']
}, },
{ {
id: 'lisa-t', id: 'kevin-triplett',
name: 'Lisa Thompson', name: 'Kevin Triplett',
initials: 'LT', initials: 'KT',
avatar: 'https://i.pravatar.cc/150?img=9', avatar: '/images/Kevin.jpg',
relationshipStrength: 0.6, relationshipStrength: 0.85,
position: { x: -140, y: 60 }, position: { x: -140, y: 60 },
activities: [ activities: [
{ topic: 'Social Media', count: 25, lastActive: '15 minutes ago' }, { topic: 'Technology Philosophy', count: 24, lastActive: '4 hours ago' },
{ topic: 'Event Photography', count: 8, lastActive: '2 hours ago' } { topic: 'Future Vision', count: 11, lastActive: '6 hours ago' }
], ],
location: { lat: 40.7138, lng: -74.0070, visible: true }, location: { lat: 40.7138, lng: -74.0070, visible: true },
vouches: 16,
praises: 20,
connections: ['oli-sb', 'ruben-daniels', 'aza-mafi']
},
{
id: 'tim-bansemer',
name: 'Tim Bansemer',
initials: 'TB',
avatar: '/images/Tim.jpg',
relationshipStrength: 0.7,
position: { x: 0, y: -140 },
activities: [
{ topic: 'Blockchain Protocols', count: 16, lastActive: '5 hours ago' },
{ topic: 'P2P Networks', count: 8, lastActive: '2 days ago' }
],
location: { lat: 40.7200, lng: -74.0060, visible: true },
vouches: 8,
praises: 12,
connections: ['oli-sb', 'niko-bonnieure']
},
{
id: 'aza-mafi',
name: 'Aza Mafi',
initials: 'AM',
avatar: '/images/Aza.jpg',
relationshipStrength: 0.8,
position: { x: -180, y: -20 },
activities: [
{ topic: 'Humane Technology', count: 19, lastActive: '3 hours ago' },
{ topic: 'Design Ethics', count: 13, lastActive: '1 day ago' }
],
location: { lat: 40.7180, lng: -74.0120, visible: true },
vouches: 13,
praises: 17,
connections: ['alex-lion', 'kevin-triplett', 'joscha-raue']
},
{
id: 'duke-dorje',
name: 'Duke Dorje',
initials: 'DD',
avatar: '/images/Duke.jpg',
relationshipStrength: 0.6,
position: { x: 180, y: -20 },
activities: [
{ topic: 'Blockchain Investing', count: 14, lastActive: '8 hours ago' },
{ topic: 'Crypto Strategy', count: 7, lastActive: '3 days ago' }
],
location: { lat: 40.7080, lng: -74.0000, visible: false }, // Privacy setting
vouches: 6,
praises: 9,
connections: ['tim-bansemer']
},
{
id: 'david-thomson',
name: 'David Thomson',
initials: 'DT',
avatar: '/images/David.jpg',
relationshipStrength: 0.65,
position: { x: 80, y: 160 },
activities: [
{ topic: 'Industrial Transformation', count: 12, lastActive: '6 hours ago' },
{ topic: 'Climate Solutions', count: 9, lastActive: '2 days ago' }
],
location: { lat: 40.7050, lng: -74.0020, visible: true },
vouches: 7, vouches: 7,
praises: 11,
connections: ['day-waterbury', 'tree-willard']
},
{
id: 'samuel-gbafa',
name: 'Samuel Gbafa',
initials: 'SG',
avatar: '/images/Sam.jpg',
relationshipStrength: 0.9,
position: { x: -60, y: -160 },
activities: [
{ topic: 'AI Leadership', count: 32, lastActive: '1 hour ago' },
{ topic: 'OpenAI Development', count: 18, lastActive: '4 hours ago' }
],
location: { lat: 40.7220, lng: -74.0100, visible: true },
vouches: 20,
praises: 25,
connections: ['alex-lion', 'joscha-raue']
},
{
id: 'meena-seshamani',
name: 'Meena Seshamani',
initials: 'MS',
avatar: '/images/Meena.jpg',
relationshipStrength: 0.7,
position: { x: -120, y: 140 },
activities: [
{ topic: 'Healthcare Policy', count: 15, lastActive: '5 hours ago' },
{ topic: 'Medicare Innovation', count: 8, lastActive: '1 day ago' }
],
location: { lat: 40.7040, lng: -74.0110, visible: true },
vouches: 10,
praises: 14,
connections: ['day-waterbury']
},
{
id: 'niko-bonnieure',
name: 'Niko Bonnieure',
initials: 'NB',
avatar: '/images/Niko.jpg',
relationshipStrength: 0.75,
position: { x: 60, y: -160 },
activities: [
{ topic: 'Decentralized Systems', count: 17, lastActive: '2 hours ago' },
{ topic: 'Graph Databases', count: 11, lastActive: '8 hours ago' }
],
location: { lat: 40.7210, lng: -74.0040, visible: true },
vouches: 9,
praises: 13,
connections: ['tim-bansemer', 'drummond-reed']
},
{
id: 'tree-willard',
name: 'Tree Willard',
initials: 'TW',
avatar: '/images/Tree.jpg',
relationshipStrength: 0.8,
position: { x: 160, y: 40 },
activities: [
{ topic: 'Environmental Systems', count: 20, lastActive: '4 hours ago' },
{ topic: 'Forest Technology', count: 12, lastActive: '1 day ago' }
],
location: { lat: 40.7090, lng: -74.0010, visible: true },
vouches: 11,
praises: 16, praises: 16,
connections: ['current-user', 'emily-r'] connections: ['margeigh-novotny', 'day-waterbury', 'david-thomson']
},
{
id: 'stephane-bancel',
name: 'Stephane Bancel',
initials: 'SB',
avatar: '/images/Stephane.jpg',
relationshipStrength: 0.7,
position: { x: -160, y: 120 },
activities: [
{ topic: 'Biotech Innovation', count: 21, lastActive: '6 hours ago' },
{ topic: 'mRNA Technology', count: 14, lastActive: '2 days ago' }
],
location: { lat: 40.7020, lng: -74.0130, visible: true },
vouches: 12,
praises: 18,
connections: ['meena-seshamani']
},
{
id: 'joscha-raue',
name: 'Joscha Raue',
initials: 'JR',
avatar: '/images/Joscha.jpg',
relationshipStrength: 0.8,
position: { x: -100, y: -140 },
activities: [
{ topic: 'Cognitive AI', count: 23, lastActive: '3 hours ago' },
{ topic: 'AGI Research', count: 16, lastActive: '7 hours ago' }
],
location: { lat: 40.7190, lng: -74.0080, visible: true },
vouches: 14,
praises: 19,
connections: ['alex-lion', 'aza-mafi', 'samuel-gbafa']
},
{
id: 'drummond-reed',
name: 'Drummond Reed',
initials: 'DR',
avatar: '/images/Drummond.jpg',
relationshipStrength: 0.75,
position: { x: 100, y: -120 },
activities: [
{ topic: 'Digital Identity', count: 18, lastActive: '4 hours ago' },
{ topic: 'Trust Infrastructure', count: 10, lastActive: '1 day ago' }
],
location: { lat: 40.7170, lng: -74.0050, visible: true },
vouches: 8,
praises: 12,
connections: ['niko-bonnieure']
} }
]; ];
@ -904,8 +1081,8 @@ const GroupDetailPage = () => {
const renderNetworkView = (members: any[]) => { const renderNetworkView = (members: any[]) => {
// Shared position calculation function for perfect alignment // Shared position calculation function for perfect alignment
const getNodePosition = (member: any) => { const getNodePosition = (member: any) => {
const centerX = 400; // Center of 800px viewBox const centerX = 290; // Moved 40px right from previous position (250 + 40 = 290)
const centerY = 240; // Center of 680px viewBox, positioned to minimize top space const centerY = 375; // Moved 25px up from previous position (400 - 25 = 375)
const scale = 1.8; // Increased scale for better visibility const scale = 1.8; // Increased scale for better visibility
const x = centerX + member.position.x * scale; const x = centerX + member.position.x * scale;
@ -918,7 +1095,7 @@ const GroupDetailPage = () => {
<Box <Box
sx={{ sx={{
position: 'relative', position: 'relative',
height: '700px', // Increased height to show all content height: 'calc(100vh - 57px)', // Full viewport height minus tabs header
backgroundColor: 'grey.50', backgroundColor: 'grey.50',
overflow: 'visible', overflow: 'visible',
width: '100%', width: '100%',
@ -929,16 +1106,15 @@ const GroupDetailPage = () => {
{/* SVG Network Graph */} {/* SVG Network Graph */}
<svg <svg
width="100%" width="100%"
height="680px" // Height minus padding height="calc(100vh - 77px)" // Full viewport height minus tabs header and padding
style={{ display: 'block' }} style={{ display: 'block', paddingBottom: '60px' }}
viewBox="0 0 800 680" viewBox="0 0 800 800"
preserveAspectRatio="xMidYMid meet" preserveAspectRatio="xMidYMid meet"
> >
{/* Connection lines between members */} {/* Connection lines between members */}
{members.map(member => {members.map(member =>
member.connections member.connections
?.filter((connId: string) => connId !== 'current-user') ?.map((connId: string) => {
.map((connId: string) => {
const connectedMember = members.find(m => m.id === connId); const connectedMember = members.find(m => m.id === connId);
if (!connectedMember) return null; if (!connectedMember) return null;
@ -950,10 +1126,29 @@ const GroupDetailPage = () => {
const endX = endPos.x; const endX = endPos.x;
const endY = endPos.y; const endY = endPos.y;
const isCurrentUserConnection = member.id === 'current-user' || connId === 'current-user'; // Define core triangle members
const strength = isCurrentUserConnection const coreMembers = ['oli-sb', 'ruben-daniels', 'margeigh-novotny'];
? Math.max(member.relationshipStrength, connectedMember.relationshipStrength) const isCoreConnection = coreMembers.includes(member.id) && coreMembers.includes(connId);
: 0.3; // Faint lines between other members const isCenterConnection = member.id === 'oli-sb' || connId === 'oli-sb';
let strength, strokeColor, opacity;
if (isCoreConnection) {
// Strong blue connections for core triangle
strength = 1.0;
strokeColor = theme.palette.primary.main;
opacity = 0.9;
} else if (isCenterConnection) {
// Strong connections to center node
strength = Math.max(member.relationshipStrength, connectedMember.relationshipStrength);
strokeColor = theme.palette.primary.main;
opacity = strength;
} else {
// Weaker connections between other members
strength = 0.4;
strokeColor = theme.palette.grey[400];
opacity = 0.4;
}
return ( return (
<line <line
@ -962,9 +1157,9 @@ const GroupDetailPage = () => {
y1={startY} y1={startY}
x2={endX} x2={endX}
y2={endY} y2={endY}
stroke={isCurrentUserConnection ? theme.palette.primary.main : theme.palette.grey[400]} stroke={strokeColor}
strokeWidth={strength * 4} strokeWidth={strength * 5}
opacity={isCurrentUserConnection ? strength : 0.3} opacity={opacity}
/> />
); );
}) })
@ -1003,24 +1198,24 @@ const GroupDetailPage = () => {
width: '60px', width: '60px',
height: '60px', height: '60px',
borderRadius: '50%', borderRadius: '50%',
border: `${member.id === 'current-user' ? 4 : 3}px solid ${ border: `${member.id === 'oli-sb' ? 4 : 3}px solid ${
member.id === 'current-user' member.id === 'oli-sb'
? theme.palette.primary.main ? theme.palette.primary.main
: alpha(theme.palette.primary.main, member.relationshipStrength) : alpha(theme.palette.primary.main, member.relationshipStrength)
}`, }`,
boxShadow: member.id === 'current-user' boxShadow: member.id === 'oli-sb'
? `0 0 20px ${alpha(theme.palette.primary.main, 0.4)}` ? `0 0 20px ${alpha(theme.palette.primary.main, 0.4)}`
: `0 0 ${member.relationshipStrength * 15}px ${alpha(theme.palette.primary.main, 0.3)}`, : `0 0 ${member.relationshipStrength * 15}px ${alpha(theme.palette.primary.main, 0.3)}`,
backgroundColor: member.id === 'current-user' ? theme.palette.primary.main : 'white', backgroundColor: member.id === 'oli-sb' ? alpha(theme.palette.primary.main, 0.1) : 'white',
backgroundImage: member.avatar ? `url(${member.avatar})` : 'none', backgroundImage: member.avatar ? `url(${member.avatar})` : 'none',
backgroundSize: 'cover', backgroundSize: member.avatar ? getContactPhotoStyles(member.name).backgroundSize : 'cover',
backgroundPosition: 'center', backgroundPosition: member.avatar ? getContactPhotoStyles(member.name).backgroundPosition : 'center',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
fontSize: '1.2rem', fontSize: '1.2rem',
fontWeight: 600, fontWeight: 600,
color: member.id === 'current-user' ? 'white' : theme.palette.text.primary color: member.id === 'oli-sb' ? theme.palette.primary.main : theme.palette.text.primary
}} }}
> >
{!member.avatar && member.initials} {!member.avatar && member.initials}
@ -1029,8 +1224,8 @@ const GroupDetailPage = () => {
{/* Name label */} {/* Name label */}
<div <div
style={{ style={{
fontWeight: member.id === 'current-user' ? 700 : 500, fontWeight: member.id === 'oli-sb' ? 700 : 500,
color: member.id === 'current-user' ? theme.palette.primary.main : theme.palette.text.primary, color: member.id === 'oli-sb' ? theme.palette.primary.main : theme.palette.text.primary,
textAlign: 'center', textAlign: 'center',
backgroundColor: 'white', backgroundColor: 'white',
padding: '4px 8px', padding: '4px 8px',
@ -1044,8 +1239,8 @@ const GroupDetailPage = () => {
{member.name} {member.name}
</div> </div>
{/* Activity indicators for current user */} {/* Activity indicators for center user */}
{member.id === 'current-user' && ( {member.id === 'oli-sb' && (
<div style={{ display: 'flex', gap: '4px', marginTop: '8px' }}> <div style={{ display: 'flex', gap: '4px', marginTop: '8px' }}>
<div <div
style={{ style={{
@ -1195,43 +1390,62 @@ const GroupDetailPage = () => {
Anyville Community Garden Network Anyville Community Garden Network
</Typography> </Typography>
{/* Mock map view */} {/* World map view */}
<Box <Box
sx={{ sx={{
position: 'relative', position: 'relative',
minHeight: 350, height: 'calc(100vh - 200px)',
backgroundColor: '#e8f5e8',
borderRadius: 2, borderRadius: 2,
border: 2,
borderColor: 'success.light',
overflow: 'hidden', overflow: 'hidden',
backgroundImage: ` backgroundImage: `url('/images/world-map.png')`,
radial-gradient(circle at 25% 25%, rgba(76, 175, 80, 0.1) 0%, transparent 50%), backgroundSize: 'cover',
radial-gradient(circle at 75% 75%, rgba(139, 195, 74, 0.1) 0%, transparent 50%), backgroundPosition: 'center',
linear-gradient(45deg, rgba(76, 175, 80, 0.05) 25%, transparent 25%, transparent 75%, rgba(76, 175, 80, 0.05) 75%), backgroundRepeat: 'no-repeat'
linear-gradient(-45deg, rgba(139, 195, 74, 0.05) 25%, transparent 25%, transparent 75%, rgba(139, 195, 74, 0.05) 75%)
`,
backgroundSize: '60px 60px, 80px 80px, 20px 20px, 20px 20px'
}} }}
> >
{/* Garden plots and features */}
<Box sx={{ position: 'absolute', top: 40, left: 60, width: 80, height: 40, backgroundColor: 'brown', opacity: 0.3, borderRadius: 1 }} />
<Box sx={{ position: 'absolute', top: 100, left: 40, width: 60, height: 60, backgroundColor: 'green', opacity: 0.2, borderRadius: '50%' }} />
<Box sx={{ position: 'absolute', top: 80, right: 80, width: 100, height: 30, backgroundColor: 'blue', opacity: 0.2, borderRadius: 1 }} />
{/* Member locations */} {/* Member locations */}
{visibleMembers.map((member, index) => { {visibleMembers.map((member, index) => {
// Convert lat/lng to approximate positions on our mock map // Use percentage-based positioning to make it responsive and lock to background
const x = 50 + (member.location.lng + 74.0060) * 2000; const positions = [
const y = 50 + (40.7128 - member.location.lat) * 2000; { x: 8, y: 25 }, // Western US/Canada
{ x: 18, y: 20 }, // Eastern US/Canada
{ x: 15, y: 40 }, // Mexico/Central America
{ x: 22, y: 55 }, // Colombia/Venezuela
{ x: 25, y: 70 }, // Brazil
{ x: 20, y: 85 }, // Argentina/Chile
{ x: 12, y: 95 }, // Southern Chile
{ x: 45, y: 15 }, // Iceland/Greenland
{ x: 50, y: 25 }, // UK/Ireland
{ x: 55, y: 20 }, // Scandinavia
{ x: 58, y: 30 }, // Central Europe
{ x: 62, y: 35 }, // Eastern Europe/Russia West
{ x: 70, y: 25 }, // Russia Central
{ x: 85, y: 20 }, // Russia East/Siberia
{ x: 52, y: 45 }, // North Africa
{ x: 48, y: 55 }, // West Africa
{ x: 58, y: 60 }, // East Africa
{ x: 55, y: 75 }, // South Africa
{ x: 65, y: 40 }, // Middle East
{ x: 72, y: 45 }, // India
{ x: 80, y: 30 }, // China
{ x: 88, y: 35 }, // Japan/Korea
{ x: 82, y: 50 }, // Southeast Asia
{ x: 85, y: 65 }, // Indonesia
{ x: 90, y: 80 }, // Australia
{ x: 95, y: 90 }, // New Zealand
];
const position = positions[index % positions.length];
const x = `${position.x + (Math.random() - 0.5) * 5}%`; // Percentage-based with small randomness
const y = `${position.y + (Math.random() - 0.5) * 5}%`;
return ( return (
<Box <Box
key={member.id} key={member.id}
sx={{ sx={{
position: 'absolute', position: 'absolute',
left: Math.max(20, Math.min(x, 400)), left: x,
top: Math.max(20, Math.min(y, 280)), top: y,
zIndex: 10, zIndex: 10,
cursor: 'pointer', cursor: 'pointer',
animation: `${pulse} 2s ease-in-out infinite ${index * 0.3}s` animation: `${pulse} 2s ease-in-out infinite ${index * 0.3}s`
@ -1462,16 +1676,21 @@ const GroupDetailPage = () => {
return ( return (
<Box sx={{ <Box sx={{
height: '100%', height: '100%', // Normal height
width: '100%', width: '100%',
maxWidth: { xs: '100vw', md: '100%' }, maxWidth: { xs: '100vw', md: '100%' },
overflow: 'hidden', overflow: tabValue === 0 ? 'hidden' : 'hidden', // Prevent scrolling for network tab
boxSizing: 'border-box', boxSizing: 'border-box',
p: { xs: '10px', md: 0 }, p: { xs: '10px', md: 0 }, // Normal padding
mx: { xs: 0, md: 'auto' } mx: { xs: 0, md: 'auto' }
}}> }}>
{/* Header */} {/* Header - Hidden for network tab to maximize space */}
<Box sx={{ mb: 3, width: '100%', overflow: 'hidden' }}> <Box sx={{
mb: tabValue === 0 ? 0 : 3,
width: '100%',
overflow: 'hidden',
display: 'block' // Always show header
}}>
{/* Top row with back button, avatar, and title/description */} {/* Top row with back button, avatar, and title/description */}
<Box sx={{ <Box sx={{
display: 'flex', display: 'flex',
@ -1622,13 +1841,14 @@ const GroupDetailPage = () => {
{/* Navigation Tabs */} {/* Navigation Tabs */}
<Card sx={{ <Card sx={{
width: { xs: 'calc(100% + 20px)', md: '100%' }, width: tabValue === 0 ? '100vw' : { xs: 'calc(100% + 20px)', md: '100%' },
maxWidth: { xs: 'calc(100vw - 0px)', md: '100%' }, maxWidth: tabValue === 0 ? '100vw' : { xs: 'calc(100vw - 0px)', md: '100%' },
overflow: 'hidden', overflow: 'hidden',
mx: { xs: '-10px', md: 0 }, // Extend to edges on mobile mx: tabValue === 0 ? 0 : { xs: '-10px', md: 0 }, // Full width for network tab
borderRadius: { xs: 0, md: 1 }, // Remove border radius on mobile borderRadius: tabValue === 0 ? 0 : { xs: 0, md: 1 }, // No border radius for network tab
boxSizing: 'border-box', boxSizing: 'border-box',
backgroundColor: 'white' // Force white background backgroundColor: 'white', // Force white background
height: 'auto' // Normal height
}}> }}>
<Tabs <Tabs
value={tabValue} value={tabValue}
@ -1641,6 +1861,9 @@ const GroupDetailPage = () => {
borderBottom: 1, borderBottom: 1,
borderColor: 'divider', borderColor: 'divider',
width: '100%', width: '100%',
position: 'relative', // Always relative position
zIndex: 'auto', // Normal z-index
backgroundColor: 'white', // Ensure background is visible when fixed
'& .MuiTab-root': { '& .MuiTab-root': {
minHeight: 56, minHeight: 56,
textTransform: 'none', textTransform: 'none',
@ -1652,7 +1875,7 @@ const GroupDetailPage = () => {
width: '100%' width: '100%'
}, },
'& .MuiTabs-scroller': { '& .MuiTabs-scroller': {
overflow: 'hidden !important' overflow: 'visible'
} }
}} }}
> >
@ -1671,9 +1894,9 @@ const GroupDetailPage = () => {
maxWidth: '100%', maxWidth: '100%',
overflow: 'visible', // Let page scroll instead overflow: 'visible', // Let page scroll instead
boxSizing: 'border-box', boxSizing: 'border-box',
height: 'auto', // Auto height to expand with content height: tabValue === 0 ? 'calc(100vh - 200px)' : 'auto', // Use viewport height for network tab only
backgroundColor: 'white', // Force white background backgroundColor: 'white', // Force white background
minHeight: tabValue === 1 ? 'calc(100vh - 200px)' : 'auto' // Ensure activity tab has enough height minHeight: tabValue === 1 ? 'calc(100vh - 200px)' : 'auto' // Normal min height
}}> }}>
{tabValue === 0 && renderNetworkTab()} {tabValue === 0 && renderNetworkTab()}
{tabValue === 1 && renderActivityTab()} {tabValue === 1 && renderActivityTab()}

@ -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 };
};
Loading…
Cancel
Save