relnet/frontend/src/components/NetworkSidebar.tsx
philipredstone 581433612b refactor ui
2025-04-17 14:14:01 +02:00

425 lines
16 KiB
TypeScript

import React from 'react';
import {
FaEdit,
FaHome,
FaSearch,
FaTrash,
FaUserCircle,
FaUserFriends,
FaUserPlus,
} from 'react-icons/fa';
import { Button, EmptyState, Tooltip, NetworkStats } from './FriendshipNetworkComponents';
import { PersonNode, RelationshipEdge } from '../types/network';
import {
getRelationshipColor,
getRelationshipLabel,
RELATIONSHIP_TYPES,
RELATIONSHIPS,
} from '../types/RelationShipTypes';
interface NetworkSidebarProps {
isOpen: boolean;
currentNetwork: any;
sidebarTab: string;
people: PersonNode[];
relationships: RelationshipEdge[];
selectedPersonId: string | null;
peopleFilter: string;
relationshipFilter: string;
relationshipTypeFilter: string;
onTabChange: (tab: string) => void;
onPeopleFilterChange: (filter: string) => void;
onRelationshipFilterChange: (filter: string) => void;
onRelationshipTypeFilterChange: (type: string) => void;
onAddPerson: () => void;
onAddRelationship: () => void;
onOpenSettings: () => void;
onOpenHelp: () => void;
onPersonDelete: (id: string) => void;
onRelationshipDelete: (id: string) => void;
onOpenPersonDetail: (person: PersonNode) => void;
onNavigateBack: () => void;
}
const NetworkSidebar: React.FC<NetworkSidebarProps> = ({
isOpen,
currentNetwork,
sidebarTab,
people,
relationships,
selectedPersonId,
peopleFilter,
relationshipFilter,
relationshipTypeFilter,
onTabChange,
onPeopleFilterChange,
onRelationshipFilterChange,
onRelationshipTypeFilterChange,
onAddPerson,
onAddRelationship,
onPersonDelete,
onRelationshipDelete,
onOpenPersonDetail,
onNavigateBack,
}) => {
// Filter logic for people and relationships
const filteredPeople = people.filter(person =>
`${person.firstName} ${person.lastName}`.toLowerCase().includes(peopleFilter.toLowerCase())
);
const filteredRelationships = relationships.filter(rel => {
const source = people.find(p => p._id === rel.source);
const target = people.find(p => p._id === rel.target);
if (!source || !target) return false;
const matchesFilter =
`${source.firstName} ${source.lastName} ${target.firstName} ${target.lastName}`
.toLowerCase()
.includes(relationshipFilter.toLowerCase());
const matchesType = relationshipTypeFilter === 'all' || rel.type === relationshipTypeFilter;
return matchesFilter && matchesType;
});
// Sort people alphabetically
const sortedPeople = [...filteredPeople].sort((a, b) => {
const nameA = `${a.firstName} ${a.lastName}`.toLowerCase();
const nameB = `${b.firstName} ${b.lastName}`.toLowerCase();
return nameA.localeCompare(nameB);
});
return (
<div
className={`bg-slate-800 border-r border-slate-700 h-full transition-all duration-300
ease-in-out z-30 ${isOpen ? 'w-100' : 'w-0'}`}
>
<div className="h-full overflow-y-auto p-4">
{/* Network Header */}
<div className="mb-6 mt-8">
<div className="flex items-center justify-between mb-1">
<h2 className="text-2xl font-bold text-white flex items-center">
<span className="truncate">{currentNetwork?.name || 'Relationship Network'}</span>
</h2>
<Tooltip text="Back to networks">
<button
onClick={onNavigateBack}
className="p-2 text-slate-400 hover:text-indigo-400 transition-colors"
>
<FaHome />
</button>
</Tooltip>
</div>
<p className="text-slate-400 text-sm">Visualize your connections</p>
</div>
{/* Network Stats */}
<NetworkStats people={people} relationships={relationships} />
{/* Action Buttons */}
<div className="flex space-x-2 mb-6">
<Button variant="primary" fullWidth onClick={onAddPerson} icon={<FaUserPlus />}>
Add Person
</Button>
<Button
variant="secondary"
fullWidth
onClick={onAddRelationship}
icon={<FaUserFriends />}
>
Add Relation
</Button>
</div>
{/* Sidebar Tabs */}
<div className="flex border-b border-slate-700 mb-4">
<button
className={`flex-1 py-2 font-medium flex items-center justify-center ${
sidebarTab === 'people'
? 'text-indigo-400 border-b-2 border-indigo-400'
: 'text-slate-400 hover:text-slate-300'
}`}
onClick={() => onTabChange('people')}
>
<FaUserCircle className="mr-2" /> People
</button>
<button
className={`flex-1 py-2 font-medium flex items-center justify-center ${
sidebarTab === 'relations'
? 'text-indigo-400 border-b-2 border-indigo-400'
: 'text-slate-400 hover:text-slate-300'
}`}
onClick={() => onTabChange('relations')}
>
<FaUserFriends className="mr-2" /> Relations
</button>
</div>
{/* Tab Content */}
{sidebarTab === 'people' && (
<div>
<div className="flex items-center mb-3">
<div className="relative flex-1">
<input
type="text"
className="w-full bg-slate-700 border border-slate-600 rounded-md py-2 pl-8 pr-3
text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white"
placeholder="Search people..."
value={peopleFilter}
onChange={e => onPeopleFilterChange(e.target.value)}
/>
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400" />
</div>
</div>
<div className="space-y-2 max-h-[calc(100vh-350px)] overflow-y-auto pr-1">
{sortedPeople.length > 0 ? (
sortedPeople.map(person => {
const connectionCount = relationships.filter(
r => r.source === person._id || r.target === person._id
).length;
return (
<div
key={person._id}
className={`bg-slate-700 rounded-lg p-3 group hover:bg-slate-600 transition-colors
cursor-pointer border-l-4 ${
selectedPersonId === person._id
? 'border-l-pink-500'
: connectionCount > 0
? 'border-l-indigo-500'
: 'border-l-slate-700'
}`}
onClick={() => {
onOpenPersonDetail(person);
}}
>
<div className="flex justify-between items-center">
<div>
<h4 className="font-medium">
{person.firstName} {person.lastName}
</h4>
<div className="flex items-center text-xs text-slate-400 mt-1">
<span
className="inline-block w-2 h-2 rounded-full mr-1"
style={{
backgroundColor: connectionCount > 0 ? '#60A5FA' : '#94A3B8',
}}
></span>
{connectionCount} connection{connectionCount !== 1 ? 's' : ''}
</div>
</div>
<div className="flex space-x-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Tooltip text="Edit">
<button
className="p-1 text-slate-400 hover:text-indigo-400 transition-colors"
onClick={e => {
e.stopPropagation();
onOpenPersonDetail(person);
}}
>
<FaEdit size={14} />
</button>
</Tooltip>
<Tooltip text="Delete">
<button
className="p-1 text-slate-400 hover:text-red-400 transition-colors"
onClick={e => {
e.stopPropagation();
onPersonDelete(person._id);
}}
>
<FaTrash size={14} />
</button>
</Tooltip>
</div>
</div>
</div>
);
})
) : (
<EmptyState
title={peopleFilter ? 'No matches found' : 'No people yet'}
description={
peopleFilter
? 'Try adjusting your search criteria'
: 'Add people to start building your network'
}
icon={<FaUserCircle className="text-2xl text-slate-400" />}
action={
!peopleFilter && (
<Button
variant="primary"
size="sm"
onClick={onAddPerson}
icon={<FaUserPlus />}
>
Add Person
</Button>
)
}
/>
)}
</div>
</div>
)}
{sidebarTab === 'relations' && (
<div>
<div className="flex items-center mb-3">
<div className="relative flex-1">
<input
type="text"
className="w-full bg-slate-700 border border-slate-600 rounded-md py-2 pl-8 pr-3
text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white"
placeholder="Search relationships..."
value={relationshipFilter}
onChange={e => onRelationshipFilterChange(e.target.value)}
/>
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400" />
</div>
</div>
<div className="flex mb-3 overflow-x-auto pb-2 space-x-1">
<button
className={`px-3 py-1 text-xs rounded-full whitespace-nowrap ${
relationshipTypeFilter === 'all'
? 'bg-indigo-600 text-white'
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
}`}
onClick={() => onRelationshipTypeFilterChange('all')}
>
All Types
</button>
{Object.entries(RELATIONSHIPS).map(([type, relationship]) => (
<button
key={type}
className={`px-3 py-1 text-xs rounded-full whitespace-nowrap flex items-center ${
relationshipTypeFilter === type
? 'bg-indigo-600 text-white'
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
}`}
onClick={() => onRelationshipTypeFilterChange(type as RELATIONSHIP_TYPES)}
>
<span
className="w-2 h-2 rounded-full mr-1"
style={{ backgroundColor: relationship.color }}
></span>
<span className="capitalize">
{getRelationshipLabel(type as RELATIONSHIP_TYPES)}
</span>
</button>
))}
</div>
<div className="space-y-2 max-h-[calc(100vh-390px)] overflow-y-auto pr-1">
{filteredRelationships.length > 0 ? (
filteredRelationships.map(rel => {
const source = people.find(p => p._id === rel.source);
const target = people.find(p => p._id === rel.target);
if (!source || !target) return null;
return (
<div
key={rel._id}
className={`bg-slate-700 rounded-lg p-3 group hover:bg-slate-600 transition-colors
border-l-4 ${
selectedPersonId === rel.source || selectedPersonId === rel.target
? 'border-l-pink-500'
: 'border-l-slate-700'
}`}
>
<div className="flex justify-between items-center">
<div>
<div className="flex items-center">
<span
className={`font-medium ${selectedPersonId === rel.source ? 'text-pink-400' : ''}`}
onClick={e => {
e.stopPropagation();
const sourcePerson = people.find(p => p._id === rel.source);
if (sourcePerson) onOpenPersonDetail(sourcePerson);
}}
>
{source.firstName} {source.lastName}
</span>
<span className="mx-2 text-slate-400"></span>
<span
className={`font-medium ${selectedPersonId === rel.target ? 'text-pink-400' : ''}`}
onClick={e => {
e.stopPropagation();
const targetPerson = people.find(p => p._id === rel.target);
if (targetPerson) onOpenPersonDetail(targetPerson);
}}
>
{target.firstName} {target.lastName}
</span>
</div>
<div className="flex items-center text-xs text-slate-400 mt-1">
<span
className="inline-block w-2 h-2 rounded-full mr-1"
style={{ backgroundColor: getRelationshipColor(rel.type) }}
></span>
<span className="capitalize">
{rel.type === 'custom'
? rel.customType
: getRelationshipLabel(rel.type)}
</span>
</div>
</div>
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
<Tooltip text="Delete">
<button
className="p-1 text-slate-400 hover:text-red-400 transition-colors"
onClick={() => onRelationshipDelete(rel._id)}
>
<FaTrash size={14} />
</button>
</Tooltip>
</div>
</div>
</div>
);
})
) : (
<EmptyState
title={
relationshipFilter || relationshipTypeFilter !== 'all'
? 'No matches found'
: 'No relationships yet'
}
description={
relationshipFilter || relationshipTypeFilter !== 'all'
? 'Try adjusting your search criteria'
: 'Create relationships between people to visualize connections'
}
icon={<FaUserFriends className="text-2xl text-slate-400" />}
action={
!relationshipFilter &&
relationshipTypeFilter === 'all' && (
<Button
variant="primary"
size="sm"
onClick={onAddRelationship}
icon={<FaUserFriends />}
>
Add Relationship
</Button>
)
}
/>
)}
</div>
</div>
)}
</div>
</div>
);
};
export default NetworkSidebar;