Fix birthdate input

This commit is contained in:
Tobias Hopp 2025-04-16 11:17:50 +02:00
parent 9ce80b4c59
commit bbb3645d99

View File

@ -32,17 +32,7 @@ import {
// Import custom UI components // Import custom UI components
import { import {
Button, Button, Card, CardBody, ConfirmDialog, EmptyState, FormField, Modal, NetworkStats, Toast, ToastItem, Tooltip,
Card,
CardBody,
ConfirmDialog,
EmptyState,
FormField,
Modal,
NetworkStats,
Toast,
ToastItem,
Tooltip,
} from './FriendshipNetworkComponents'; } from './FriendshipNetworkComponents';
// Import visible canvas graph component // Import visible canvas graph component
@ -58,8 +48,7 @@ interface PersonNode {
birthday?: Date | string | null; birthday?: Date | string | null;
notes?: string; notes?: string;
position?: { position?: {
x: number; x: number; y: number;
y: number;
}; };
} }
@ -71,36 +60,6 @@ interface RelationshipEdge {
customType?: string; customType?: string;
notes?: string; notes?: string;
} }
interface CanvasGraphProps {
data: {
nodes: {
id: string;
firstName: string;
lastName: string;
connectionCount: number;
bgColor: string;
x: number;
y: number;
showLabel: boolean;
}[];
edges: {
id: string;
source: string;
target: string;
color: string;
width: number;
type: RelationshipType;
customType?: string;
}[];
};
width: number;
height: number;
zoomLevel: number;
onNodeClick: (nodeId: string) => void;
onNodeDrag: (nodeId: string, x: number, y: number) => void;
}
// Type for form errors // Type for form errors
interface FormErrors { interface FormErrors {
[key: string]: string; [key: string]: string;
@ -116,11 +75,7 @@ const RELATIONSHIP_COLORS = {
}; };
const RELATIONSHIP_LABELS = { const RELATIONSHIP_LABELS = {
freund: 'Friend', freund: 'Friend', partner: 'Partner', familie: 'Family', arbeitskolleg: 'Colleague', custom: 'Custom',
partner: 'Partner',
familie: 'Family',
arbeitskolleg: 'Colleague',
custom: 'Custom',
}; };
// Main FriendshipNetwork component // Main FriendshipNetwork component
@ -143,10 +98,7 @@ const FriendshipNetwork: React.FC = () => {
createRelationship, createRelationship,
deleteRelationship, deleteRelationship,
refreshNetwork, refreshNetwork,
updatePersonPosition: updatePersonPositionImpl = ( updatePersonPosition: updatePersonPositionImpl = (id: string, position: { x: number; y: number }) => {
id: string,
position: { x: number; y: number },
) => {
console.warn('updatePersonPosition not implemented'); console.warn('updatePersonPosition not implemented');
return Promise.resolve(); return Promise.resolve();
}, },
@ -172,8 +124,7 @@ const FriendshipNetwork: React.FC = () => {
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [helpModalOpen, setHelpModalOpen] = useState(false); const [helpModalOpen, setHelpModalOpen] = useState(false);
const [itemToDelete, setItemToDelete] = useState<{ type: string; id: string }>({ const [itemToDelete, setItemToDelete] = useState<{ type: string; id: string }>({
type: '', type: '', id: '',
id: '',
}); });
// Form errors // Form errors
@ -182,21 +133,13 @@ const FriendshipNetwork: React.FC = () => {
// Form states // Form states
const [newPerson, setNewPerson] = useState({ const [newPerson, setNewPerson] = useState({
firstName: '', firstName: '', lastName: '', birthday: null as Date | null, notes: '',
lastName: '',
birthday: null as Date | null,
notes: '',
}); });
const [editPerson, setEditPerson] = useState<PersonNode | null>(null); const [editPerson, setEditPerson] = useState<PersonNode | null>(null);
const [newRelationship, setNewRelationship] = useState({ const [newRelationship, setNewRelationship] = useState({
source: '', source: '', target: '', type: 'freund' as RelationshipType, customType: '', notes: '', bidirectional: true,
target: '',
type: 'freund' as RelationshipType,
customType: '',
notes: '',
bidirectional: true,
}); });
// Filter states // Filter states
@ -335,9 +278,7 @@ const FriendshipNetwork: React.FC = () => {
}, []); }, []);
// Filtered people and relationships // Filtered people and relationships
const filteredPeople = people.filter(person => const filteredPeople = people.filter(person => `${person.firstName} ${person.lastName}`.toLowerCase().includes(peopleFilter.toLowerCase()));
`${person.firstName} ${person.lastName}`.toLowerCase().includes(peopleFilter.toLowerCase()),
);
const filteredRelationships = relationships.filter(rel => { const filteredRelationships = relationships.filter(rel => {
const source = people.find(p => p._id === rel.source); const source = people.find(p => p._id === rel.source);
@ -345,10 +286,9 @@ const FriendshipNetwork: React.FC = () => {
if (!source || !target) return false; if (!source || !target) return false;
const matchesFilter = const matchesFilter = `${source.firstName} ${source.lastName} ${target.firstName} ${target.lastName}`
`${source.firstName} ${source.lastName} ${target.firstName} ${target.lastName}` .toLowerCase()
.toLowerCase() .includes(relationshipFilter.toLowerCase());
.includes(relationshipFilter.toLowerCase());
const matchesType = relationshipTypeFilter === 'all' || rel.type === relationshipTypeFilter; const matchesType = relationshipTypeFilter === 'all' || rel.type === relationshipTypeFilter;
@ -385,21 +325,17 @@ const FriendshipNetwork: React.FC = () => {
const theta = index * 2.399; const theta = index * 2.399;
const radius = maxRadius * 0.5 * Math.sqrt(index / (totalNodes + 1)); const radius = maxRadius * 0.5 * Math.sqrt(index / (totalNodes + 1));
return { return {
x: centerX + radius * Math.cos(theta), x: centerX + radius * Math.cos(theta), y: centerY + radius * Math.sin(theta),
y: centerY + radius * Math.sin(theta),
}; };
} else if (totalNodes <= 11) { } else if (totalNodes <= 11) {
const isOuterRing = index >= Math.floor(totalNodes / 2); const isOuterRing = index >= Math.floor(totalNodes / 2);
const ringIndex = isOuterRing ? index - Math.floor(totalNodes / 2) : index; const ringIndex = isOuterRing ? index - Math.floor(totalNodes / 2) : index;
const ringTotal = isOuterRing const ringTotal = isOuterRing ? totalNodes - Math.floor(totalNodes / 2) + 1 : Math.floor(totalNodes / 2);
? totalNodes - Math.floor(totalNodes / 2) + 1
: Math.floor(totalNodes / 2);
const ringRadius = isOuterRing ? maxRadius * 0.8 : maxRadius * 0.4; const ringRadius = isOuterRing ? maxRadius * 0.8 : maxRadius * 0.4;
const angle = (ringIndex / ringTotal) * 2 * Math.PI + (isOuterRing ? 0 : Math.PI / ringTotal); const angle = (ringIndex / ringTotal) * 2 * Math.PI + (isOuterRing ? 0 : Math.PI / ringTotal);
return { return {
x: centerX + ringRadius * Math.cos(angle), x: centerX + ringRadius * Math.cos(angle), y: centerY + ringRadius * Math.sin(angle),
y: centerY + ringRadius * Math.sin(angle),
}; };
} else { } else {
const clusterCount = Math.max(3, Math.floor(Math.sqrt(totalNodes))); const clusterCount = Math.max(3, Math.floor(Math.sqrt(totalNodes)));
@ -415,8 +351,7 @@ const FriendshipNetwork: React.FC = () => {
const randomDistance = Math.random() * clusterRadius; const randomDistance = Math.random() * clusterRadius;
return { return {
x: clusterX + randomDistance * Math.cos(randomAngle), x: clusterX + randomDistance * Math.cos(randomAngle), y: clusterY + randomDistance * Math.sin(randomAngle),
y: clusterY + randomDistance * Math.sin(randomAngle),
}; };
} }
}, [graphDimensions.width, graphDimensions.height, people.length]); }, [graphDimensions.width, graphDimensions.height, people.length]);
@ -424,24 +359,16 @@ const FriendshipNetwork: React.FC = () => {
// Transform API data to graph format // Transform API data to graph format
const getGraphData = useCallback(() => { const getGraphData = useCallback(() => {
if (!people || !relationships) { if (!people || !relationships) {
return { nodes: [], edges: [] }; return { nodes: [], edges: [], links: [] };
} }
// Create nodes // Create nodes
const graphNodes = people.map(person => { const graphNodes = people.map(person => {
const connectionCount = relationships.filter( const connectionCount = relationships.filter(r => r.source === person._id || r.target === person._id).length;
r => r.source === person._id || r.target === person._id,
).length;
// Determine if node should be highlighted // Determine if node should be highlighted
const isSelected = person._id === selectedPersonId; const isSelected = person._id === selectedPersonId;
const isConnected = selectedPersonId const isConnected = selectedPersonId ? relationships.some(r => (r.source === selectedPersonId && r.target === person._id) || (r.target === selectedPersonId && r.source === person._id)) : false;
? relationships.some(
r =>
(r.source === selectedPersonId && r.target === person._id) ||
(r.target === selectedPersonId && r.source === person._id),
)
: false;
// Determine background color based on connection count or highlight state // Determine background color based on connection count or highlight state
let bgColor; let bgColor;
@ -479,23 +406,16 @@ const FriendshipNetwork: React.FC = () => {
const width = rel.type === 'partner' ? 4 : rel.type === 'familie' ? 3 : 2; const width = rel.type === 'partner' ? 4 : rel.type === 'familie' ? 3 : 2;
// Highlight edges connected to selected node // Highlight edges connected to selected node
const isHighlighted = const isHighlighted = selectedPersonId && settings.highlightConnections && (rel.source === selectedPersonId || rel.target === selectedPersonId);
selectedPersonId &&
settings.highlightConnections &&
(rel.source === selectedPersonId || rel.target === selectedPersonId);
return { return {
id: rel._id, id: rel._id, source: rel.source, target: rel.target, color: isHighlighted ? '#F472B6' : color, // Pink color for highlighted edges
source: rel.source,
target: rel.target,
color: isHighlighted ? '#F472B6' : color, // Pink color for highlighted edges
width: isHighlighted ? width + 1 : width, // Slightly thicker for highlighted width: isHighlighted ? width + 1 : width, // Slightly thicker for highlighted
type: rel.type, type: rel.type, customType: rel.customType,
customType: rel.customType,
}; };
}); });
return { nodes: graphNodes, edges: graphEdges }; return { nodes: graphNodes, edges: graphEdges, links: [] };
}, [people, relationships, settings.showLabels, settings.highlightConnections, selectedPersonId]); }, [people, relationships, settings.showLabels, settings.highlightConnections, selectedPersonId]);
// Validate person form // Validate person form
@ -535,13 +455,7 @@ const FriendshipNetwork: React.FC = () => {
// Check if relationship already exists // Check if relationship already exists
if (relationship.source && relationship.target) { if (relationship.source && relationship.target) {
const existingRelationship = relationships.find( const existingRelationship = relationships.find(r => (r.source === relationship.source && r.target === relationship.target) || (relationship.bidirectional && r.source === relationship.target && r.target === relationship.source));
r =>
(r.source === relationship.source && r.target === relationship.target) ||
(relationship.bidirectional &&
r.source === relationship.target &&
r.target === relationship.source),
);
if (existingRelationship) { if (existingRelationship) {
errors.general = 'This relationship already exists'; errors.general = 'This relationship already exists';
@ -573,10 +487,7 @@ const FriendshipNetwork: React.FC = () => {
// Reset form and close modal // Reset form and close modal
setNewPerson({ setNewPerson({
firstName: '', firstName: '', lastName: '', birthday: null, notes: '',
lastName: '',
birthday: null,
notes: '',
}); });
setPersonModalOpen(false); setPersonModalOpen(false);
@ -619,32 +530,19 @@ const FriendshipNetwork: React.FC = () => {
// Create the relationship // Create the relationship
createRelationship({ createRelationship({
source, source, target, type, customType: type === 'custom' ? customType : undefined, notes,
target,
type,
customType: type === 'custom' ? customType : undefined,
notes,
}); });
// Create bidirectional relationship if selected // Create bidirectional relationship if selected
if (bidirectional && source !== target) { if (bidirectional && source !== target) {
createRelationship({ createRelationship({
source: target, source: target, target: source, type, customType: type === 'custom' ? customType : undefined, notes,
target: source,
type,
customType: type === 'custom' ? customType : undefined,
notes,
}); });
} }
// Reset form and close modal // Reset form and close modal
setNewRelationship({ setNewRelationship({
source: '', source: '', target: '', type: 'freund', customType: '', notes: '', bidirectional: true,
target: '',
type: 'freund',
customType: '',
notes: '',
bidirectional: true,
}); });
setRelationshipModalOpen(false); setRelationshipModalOpen(false);
@ -725,21 +623,18 @@ const FriendshipNetwork: React.FC = () => {
// Loading state // Loading state
if (loading) { if (loading) {
return ( return (<div className="flex justify-center items-center h-screen bg-slate-900">
<div className="flex justify-center items-center h-screen bg-slate-900">
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<div <div
className="w-16 h-16 border-t-4 border-b-4 border-indigo-500 border-solid rounded-full animate-spin"></div> className="w-16 h-16 border-t-4 border-b-4 border-indigo-500 border-solid rounded-full animate-spin"></div>
<p className="text-white text-lg">Loading your network...</p> <p className="text-white text-lg">Loading your network...</p>
</div> </div>
</div> </div>);
);
} }
// Error state // Error state
if (error) { if (error) {
return ( return (<div className="flex justify-center items-center h-screen bg-slate-900">
<div className="flex justify-center items-center h-screen bg-slate-900">
<div className="bg-red-500/20 border border-red-500 text-white p-6 rounded-lg shadow-lg max-w-md"> <div className="bg-red-500/20 border border-red-500 text-white p-6 rounded-lg shadow-lg max-w-md">
<h3 className="text-lg font-bold mb-3 flex items-center"> <h3 className="text-lg font-bold mb-3 flex items-center">
<FaExclamationTriangle className="mr-2 text-red-500" /> Error <FaExclamationTriangle className="mr-2 text-red-500" /> Error
@ -754,15 +649,13 @@ const FriendshipNetwork: React.FC = () => {
Back to Networks Back to Networks
</Button> </Button>
</div> </div>
</div> </div>);
);
} }
// Generate graph data // Generate graph data
const graphData = getGraphData(); const graphData = getGraphData();
return ( return (<div className="flex h-screen bg-slate-900 text-white overflow-hidden">
<div className="flex h-screen bg-slate-900 text-white overflow-hidden">
{/* Sidebar Toggle Button */} {/* Sidebar Toggle Button */}
<button <button
onClick={toggleSidebar} onClick={toggleSidebar}
@ -832,31 +725,19 @@ const FriendshipNetwork: React.FC = () => {
{/* Sidebar Tabs */} {/* Sidebar Tabs */}
<div className="flex border-b border-slate-700 mb-4"> <div className="flex border-b border-slate-700 mb-4">
<button <button
className={`flex-1 py-2 font-medium flex items-center justify-center ${ className={`flex-1 py-2 font-medium flex items-center justify-center ${sidebarTab === 'overview' ? 'text-indigo-400 border-b-2 border-indigo-400' : 'text-slate-400 hover:text-slate-300'}`}
sidebarTab === 'overview'
? 'text-indigo-400 border-b-2 border-indigo-400'
: 'text-slate-400 hover:text-slate-300'
}`}
onClick={() => setSidebarTab('overview')} onClick={() => setSidebarTab('overview')}
> >
<FaInfo className="mr-2" /> Overview <FaInfo className="mr-2" /> Overview
</button> </button>
<button <button
className={`flex-1 py-2 font-medium flex items-center justify-center ${ 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'}`}
sidebarTab === 'people'
? 'text-indigo-400 border-b-2 border-indigo-400'
: 'text-slate-400 hover:text-slate-300'
}`}
onClick={() => setSidebarTab('people')} onClick={() => setSidebarTab('people')}
> >
<FaUserCircle className="mr-2" /> People <FaUserCircle className="mr-2" /> People
</button> </button>
<button <button
className={`flex-1 py-2 font-medium flex items-center justify-center ${ 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'}`}
sidebarTab === 'relations'
? 'text-indigo-400 border-b-2 border-indigo-400'
: 'text-slate-400 hover:text-slate-300'
}`}
onClick={() => setSidebarTab('relations')} onClick={() => setSidebarTab('relations')}
> >
<FaUserFriends className="mr-2" /> Relations <FaUserFriends className="mr-2" /> Relations
@ -864,8 +745,7 @@ const FriendshipNetwork: React.FC = () => {
</div> </div>
{/* Tab Content */} {/* Tab Content */}
{sidebarTab === 'overview' && ( {sidebarTab === 'overview' && (<div className="space-y-4">
<div className="space-y-4">
<Card> <Card>
<CardBody> <CardBody>
<h3 className="font-medium mb-2 text-indigo-400">About This Network</h3> <h3 className="font-medium mb-2 text-indigo-400">About This Network</h3>
@ -907,8 +787,7 @@ const FriendshipNetwork: React.FC = () => {
<span className="capitalize"> <span className="capitalize">
{RELATIONSHIP_LABELS[type as RelationshipType]} {RELATIONSHIP_LABELS[type as RelationshipType]}
</span> </span>
</div> </div>))}
))}
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
@ -931,11 +810,9 @@ const FriendshipNetwork: React.FC = () => {
Help Help
</Button> </Button>
</div> </div>
</div> </div>)}
)}
{sidebarTab === 'people' && ( {sidebarTab === 'people' && (<div>
<div>
<div className="flex items-center mb-3"> <div className="flex items-center mb-3">
<div className="relative flex-1"> <div className="relative flex-1">
<input <input
@ -951,23 +828,13 @@ const FriendshipNetwork: React.FC = () => {
</div> </div>
<div className="space-y-2 max-h-[calc(100vh-350px)] overflow-y-auto pr-1"> <div className="space-y-2 max-h-[calc(100vh-350px)] overflow-y-auto pr-1">
{sortedPeople.length > 0 ? ( {sortedPeople.length > 0 ? (sortedPeople.map(person => {
sortedPeople.map(person => { const connectionCount = relationships.filter(r => r.source === person._id || r.target === person._id).length;
const connectionCount = relationships.filter(
r => r.source === person._id || r.target === person._id,
).length;
return ( return (<div
<div
key={person._id} key={person._id}
className={`bg-slate-700 rounded-lg p-3 group hover:bg-slate-600 transition-colors className={`bg-slate-700 rounded-lg p-3 group hover:bg-slate-600 transition-colors
cursor-pointer border-l-4 ${ cursor-pointer border-l-4 ${selectedPersonId === person._id ? 'border-l-pink-500' : connectionCount > 0 ? 'border-l-indigo-500' : 'border-l-slate-700'}`}
selectedPersonId === person._id
? 'border-l-pink-500'
: connectionCount > 0
? 'border-l-indigo-500'
: 'border-l-slate-700'
}`}
onClick={() => { onClick={() => {
openPersonDetail(person); openPersonDetail(person);
setSelectedPersonId(person._id); setSelectedPersonId(person._id);
@ -1013,38 +880,24 @@ const FriendshipNetwork: React.FC = () => {
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
</div> </div>);
); })) : (<EmptyState
})
) : (
<EmptyState
title={peopleFilter ? 'No matches found' : 'No people yet'} title={peopleFilter ? 'No matches found' : 'No people yet'}
description={ description={peopleFilter ? 'Try adjusting your search criteria' : 'Add people to start building your network'}
peopleFilter
? 'Try adjusting your search criteria'
: 'Add people to start building your network'
}
icon={<FaUserCircle className="text-2xl text-slate-400" />} icon={<FaUserCircle className="text-2xl text-slate-400" />}
action={ action={!peopleFilter && (<Button
!peopleFilter && ( variant="primary"
<Button size="sm"
variant="primary" onClick={() => setPersonModalOpen(true)}
size="sm" icon={<FaUserPlus />}
onClick={() => setPersonModalOpen(true)} >
icon={<FaUserPlus />} Add Person
> </Button>)}
Add Person />)}
</Button>
)
}
/>
)}
</div> </div>
</div> </div>)}
)}
{sidebarTab === 'relations' && ( {sidebarTab === 'relations' && (<div>
<div>
<div className="flex items-center mb-3"> <div className="flex items-center mb-3">
<div className="relative flex-1"> <div className="relative flex-1">
<input <input
@ -1061,23 +914,14 @@ const FriendshipNetwork: React.FC = () => {
<div className="flex mb-3 overflow-x-auto pb-2 space-x-1"> <div className="flex mb-3 overflow-x-auto pb-2 space-x-1">
<button <button
className={`px-3 py-1 text-xs rounded-full whitespace-nowrap ${ 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'}`}
relationshipTypeFilter === 'all'
? 'bg-indigo-600 text-white'
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
}`}
onClick={() => setRelationshipTypeFilter('all')} onClick={() => setRelationshipTypeFilter('all')}
> >
All Types All Types
</button> </button>
{Object.entries(RELATIONSHIP_COLORS).map(([type, color]) => ( {Object.entries(RELATIONSHIP_COLORS).map(([type, color]) => (<button
<button
key={type} key={type}
className={`px-3 py-1 text-xs rounded-full whitespace-nowrap flex items-center ${ 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'}`}
relationshipTypeFilter === type
? 'bg-indigo-600 text-white'
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
}`}
onClick={() => setRelationshipTypeFilter(type as RelationshipType)} onClick={() => setRelationshipTypeFilter(type as RelationshipType)}
> >
<span <span
@ -1087,26 +931,19 @@ const FriendshipNetwork: React.FC = () => {
<span className="capitalize"> <span className="capitalize">
{RELATIONSHIP_LABELS[type as RelationshipType]} {RELATIONSHIP_LABELS[type as RelationshipType]}
</span> </span>
</button> </button>))}
))}
</div> </div>
<div className="space-y-2 max-h-[calc(100vh-390px)] overflow-y-auto pr-1"> <div className="space-y-2 max-h-[calc(100vh-390px)] overflow-y-auto pr-1">
{filteredRelationships.length > 0 ? ( {filteredRelationships.length > 0 ? (filteredRelationships.map(rel => {
filteredRelationships.map(rel => {
const source = people.find(p => p._id === rel.source); const source = people.find(p => p._id === rel.source);
const target = people.find(p => p._id === rel.target); const target = people.find(p => p._id === rel.target);
if (!source || !target) return null; if (!source || !target) return null;
return ( return (<div
<div
key={rel._id} key={rel._id}
className={`bg-slate-700 rounded-lg p-3 group hover:bg-slate-600 transition-colors className={`bg-slate-700 rounded-lg p-3 group hover:bg-slate-600 transition-colors
border-l-4 ${ border-l-4 ${selectedPersonId === rel.source || selectedPersonId === rel.target ? 'border-l-pink-500' : 'border-l-slate-700'}`}
selectedPersonId === rel.source || selectedPersonId === rel.target
? 'border-l-pink-500'
: 'border-l-slate-700'
}`}
> >
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
@ -1140,9 +977,7 @@ const FriendshipNetwork: React.FC = () => {
style={{ backgroundColor: RELATIONSHIP_COLORS[rel.type] }} style={{ backgroundColor: RELATIONSHIP_COLORS[rel.type] }}
></span> ></span>
<span className="capitalize"> <span className="capitalize">
{rel.type === 'custom' {rel.type === 'custom' ? rel.customType : RELATIONSHIP_LABELS[rel.type]}
? rel.customType
: RELATIONSHIP_LABELS[rel.type]}
</span> </span>
</div> </div>
</div> </div>
@ -1157,40 +992,22 @@ const FriendshipNetwork: React.FC = () => {
</Tooltip> </Tooltip>
</div> </div>
</div> </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'}
<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" />} icon={<FaUserFriends className="text-2xl text-slate-400" />}
action={ action={!relationshipFilter && relationshipTypeFilter === 'all' && (<Button
!relationshipFilter && variant="primary"
relationshipTypeFilter === 'all' && ( size="sm"
<Button onClick={() => setRelationshipModalOpen(true)}
variant="primary" icon={<FaUserFriends />}
size="sm" >
onClick={() => setRelationshipModalOpen(true)} Add Relationship
icon={<FaUserFriends />} </Button>)}
> />)}
Add Relationship
</Button>
)
}
/>
)}
</div> </div>
</div> </div>)}
)}
</div> </div>
</Transition> </Transition>
</div> </div>
@ -1200,9 +1017,7 @@ const FriendshipNetwork: React.FC = () => {
{graphDimensions.width <= 0 || graphDimensions.height <= 0 ? ( {graphDimensions.width <= 0 || graphDimensions.height <= 0 ? (
<div className="w-full h-full flex justify-center items-center"> <div className="w-full h-full flex justify-center items-center">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500"></div> <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500"></div>
</div> </div>) : (<CanvasGraph
) : (
<CanvasGraph
data={graphData} data={graphData}
width={graphDimensions.width} width={graphDimensions.width}
height={graphDimensions.height} height={graphDimensions.height}
@ -1211,8 +1026,7 @@ const FriendshipNetwork: React.FC = () => {
onNodeDrag={(nodeId, x, y) => { onNodeDrag={(nodeId, x, y) => {
updatePersonPosition(nodeId, { x, y }).then(); updatePersonPosition(nodeId, { x, y }).then();
}} }}
/> />)}
)}
{/* Empty state overlay */} {/* Empty state overlay */}
{people.length === 0 && ( {people.length === 0 && (
@ -1234,12 +1048,10 @@ const FriendshipNetwork: React.FC = () => {
Add Your First Person Add Your First Person
</Button> </Button>
</div> </div>
</div> </div>)}
)}
{/* Interaction hint */} {/* Interaction hint */}
{people.length > 0 && interactionHint && ( {people.length > 0 && interactionHint && (<div
<div
className="absolute bottom-20 left-1/2 transform -translate-x-1/2 bg-indigo-900/90 className="absolute bottom-20 left-1/2 transform -translate-x-1/2 bg-indigo-900/90
text-white px-4 py-2 rounded-lg shadow-lg text-sm flex items-center space-x-2 animate-pulse" text-white px-4 py-2 rounded-lg shadow-lg text-sm flex items-center space-x-2 animate-pulse"
> >
@ -1251,8 +1063,7 @@ const FriendshipNetwork: React.FC = () => {
> >
<FaTimes /> <FaTimes />
</button> </button>
</div> </div>)}
)}
{/* Graph controls */} {/* Graph controls */}
<div className="absolute bottom-6 right-6 flex flex-col space-y-3"> <div className="absolute bottom-6 right-6 flex flex-col space-y-3">
@ -1330,8 +1141,7 @@ const FriendshipNetwork: React.FC = () => {
{personFormErrors.general && ( {personFormErrors.general && (
<div className="bg-red-500/20 border border-red-500 text-white p-3 rounded-lg text-sm mb-4"> <div className="bg-red-500/20 border border-red-500 text-white p-3 rounded-lg text-sm mb-4">
{personFormErrors.general} {personFormErrors.general}
</div> </div>)}
)}
<FormField label="First Name" id="firstName" required error={personFormErrors.firstName}> <FormField label="First Name" id="firstName" required error={personFormErrors.firstName}>
<input <input
@ -1363,7 +1173,7 @@ const FriendshipNetwork: React.FC = () => {
id="birthday" id="birthday"
selected={newPerson.birthday} selected={newPerson.birthday}
onChange={date => setNewPerson({ ...newPerson, birthday: date })} onChange={date => setNewPerson({ ...newPerson, birthday: date })}
dateFormat="MMMM d, yyyy" dateFormat="dd.MM.yyyy"
placeholderText="Select birthday" placeholderText="Select birthday"
className="w-full bg-slate-700 border border-slate-600 rounded-md p-2 className="w-full bg-slate-700 border border-slate-600 rounded-md p-2
focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white" focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white"
@ -1416,8 +1226,7 @@ const FriendshipNetwork: React.FC = () => {
{relationshipFormErrors.general && ( {relationshipFormErrors.general && (
<div className="bg-red-500/20 border border-red-500 text-white p-3 rounded-lg text-sm mb-4"> <div className="bg-red-500/20 border border-red-500 text-white p-3 rounded-lg text-sm mb-4">
{relationshipFormErrors.general} {relationshipFormErrors.general}
</div> </div>)}
)}
<FormField <FormField
label="Source Person" label="Source Person"
@ -1433,11 +1242,9 @@ const FriendshipNetwork: React.FC = () => {
onChange={e => setNewRelationship({ ...newRelationship, source: e.target.value })} onChange={e => setNewRelationship({ ...newRelationship, source: e.target.value })}
> >
<option value="">Select person</option> <option value="">Select person</option>
{sortedPeople.map(person => ( {sortedPeople.map(person => (<option key={`source-${person._id}`} value={person._id}>
<option key={`source-${person._id}`} value={person._id}>
{person.firstName} {person.lastName} {person.firstName} {person.lastName}
</option> </option>))}
))}
</select> </select>
</FormField> </FormField>
@ -1455,11 +1262,9 @@ const FriendshipNetwork: React.FC = () => {
onChange={e => setNewRelationship({ ...newRelationship, target: e.target.value })} onChange={e => setNewRelationship({ ...newRelationship, target: e.target.value })}
> >
<option value="">Select person</option> <option value="">Select person</option>
{sortedPeople.map(person => ( {sortedPeople.map(person => (<option key={`target-${person._id}`} value={person._id}>
<option key={`target-${person._id}`} value={person._id}>
{person.firstName} {person.lastName} {person.firstName} {person.lastName}
</option> </option>))}
))}
</select> </select>
</FormField> </FormField>
@ -1469,23 +1274,17 @@ const FriendshipNetwork: React.FC = () => {
className="w-full bg-slate-700 border border-slate-600 rounded-md p-2 className="w-full bg-slate-700 border border-slate-600 rounded-md p-2
focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white" focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white"
value={newRelationship.type} value={newRelationship.type}
onChange={e => onChange={e => setNewRelationship({
setNewRelationship({ ...newRelationship, type: e.target.value as RelationshipType,
...newRelationship, })}
type: e.target.value as RelationshipType,
})
}
> >
{Object.entries(RELATIONSHIP_LABELS).map(([value, label]) => ( {Object.entries(RELATIONSHIP_LABELS).map(([value, label]) => (<option key={value} value={value}>
<option key={value} value={value}>
{label} {label}
</option> </option>))}
))}
</select> </select>
</FormField> </FormField>
{newRelationship.type === 'custom' && ( {newRelationship.type === 'custom' && (<FormField
<FormField
label="Custom Type" label="Custom Type"
id="customType" id="customType"
required required
@ -1498,15 +1297,11 @@ const FriendshipNetwork: React.FC = () => {
rounded-md p-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white`} rounded-md p-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white`}
placeholder="Enter custom relationship type" placeholder="Enter custom relationship type"
value={newRelationship.customType} value={newRelationship.customType}
onChange={e => onChange={e => setNewRelationship({
setNewRelationship({ ...newRelationship, customType: e.target.value,
...newRelationship, })}
customType: e.target.value,
})
}
/> />
</FormField> </FormField>)}
)}
<FormField label="Notes (Optional)" id="relationNotes"> <FormField label="Notes (Optional)" id="relationNotes">
<textarea <textarea
@ -1525,12 +1320,9 @@ const FriendshipNetwork: React.FC = () => {
id="bidirectional" id="bidirectional"
className="h-4 w-4 rounded border-gray-500 text-indigo-600 focus:ring-indigo-500 bg-slate-700" className="h-4 w-4 rounded border-gray-500 text-indigo-600 focus:ring-indigo-500 bg-slate-700"
checked={newRelationship.bidirectional} checked={newRelationship.bidirectional}
onChange={e => onChange={e => setNewRelationship({
setNewRelationship({ ...newRelationship, bidirectional: e.target.checked,
...newRelationship, })}
bidirectional: e.target.checked,
})
}
/> />
<label htmlFor="bidirectional" className="ml-2 block text-sm text-gray-300"> <label htmlFor="bidirectional" className="ml-2 block text-sm text-gray-300">
Create bidirectional relationship (recommended) Create bidirectional relationship (recommended)
@ -1555,8 +1347,7 @@ const FriendshipNetwork: React.FC = () => {
</Modal> </Modal>
{/* Person Detail Modal */} {/* Person Detail Modal */}
{editPerson && ( {editPerson && (<Modal
<Modal
isOpen={personDetailModalOpen} isOpen={personDetailModalOpen}
onClose={() => { onClose={() => {
setPersonDetailModalOpen(false); setPersonDetailModalOpen(false);
@ -1571,8 +1362,7 @@ const FriendshipNetwork: React.FC = () => {
{personFormErrors.general && ( {personFormErrors.general && (
<div className="bg-red-500/20 border border-red-500 text-white p-3 rounded-lg text-sm mb-4"> <div className="bg-red-500/20 border border-red-500 text-white p-3 rounded-lg text-sm mb-4">
{personFormErrors.general} {personFormErrors.general}
</div> </div>)}
)}
<FormField <FormField
label="First Name" label="First Name"
@ -1612,7 +1402,7 @@ const FriendshipNetwork: React.FC = () => {
id="editBirthday" id="editBirthday"
selected={editPerson.birthday ? new Date(editPerson.birthday) : null} selected={editPerson.birthday ? new Date(editPerson.birthday) : null}
onChange={date => setEditPerson({ ...editPerson, birthday: date })} onChange={date => setEditPerson({ ...editPerson, birthday: date })}
dateFormat="MMMM d, yyyy" dateFormat="dd.MM.yyyy"
placeholderText="Select birthday" placeholderText="Select birthday"
className="w-full bg-slate-700 border border-slate-600 rounded-md p-2 className="w-full bg-slate-700 border border-slate-600 rounded-md p-2
focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white" focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white"
@ -1668,10 +1458,7 @@ const FriendshipNetwork: React.FC = () => {
<div> <div>
<h4 className="font-medium text-indigo-400 mb-2">Connections</h4> <h4 className="font-medium text-indigo-400 mb-2">Connections</h4>
<div className="max-h-40 overflow-y-auto space-y-1 bg-slate-900 rounded-lg p-2"> <div className="max-h-40 overflow-y-auto space-y-1 bg-slate-900 rounded-lg p-2">
{relationships.filter( {relationships.filter(r => r.source === editPerson._id || r.target === editPerson._id).length > 0 ? (relationships
r => r.source === editPerson._id || r.target === editPerson._id,
).length > 0 ? (
relationships
.filter(r => r.source === editPerson._id || r.target === editPerson._id) .filter(r => r.source === editPerson._id || r.target === editPerson._id)
.map(rel => { .map(rel => {
const isSource = rel.source === editPerson._id; const isSource = rel.source === editPerson._id;
@ -1680,8 +1467,7 @@ const FriendshipNetwork: React.FC = () => {
if (!otherPerson) return null; if (!otherPerson) return null;
return ( return (<div
<div
key={rel._id} key={rel._id}
className="flex justify-between items-center py-1 px-2 hover:bg-slate-800 rounded" className="flex justify-between items-center py-1 px-2 hover:bg-slate-800 rounded"
> >
@ -1705,9 +1491,7 @@ const FriendshipNetwork: React.FC = () => {
> >
{otherPerson.firstName} {otherPerson.lastName} {otherPerson.firstName} {otherPerson.lastName}
</span> </span>
{rel.type === 'custom' {rel.type === 'custom' ? ` (${rel.customType})` : ` (${RELATIONSHIP_LABELS[rel.type]})`}
? ` (${rel.customType})`
: ` (${RELATIONSHIP_LABELS[rel.type]})`}
</span> </span>
</div> </div>
<button <button
@ -1716,12 +1500,8 @@ const FriendshipNetwork: React.FC = () => {
> >
<FaTrash size={12} /> <FaTrash size={12} />
</button> </button>
</div> </div>);
); })) : (<div className="text-center py-2 text-slate-400 text-sm">No connections yet</div>)}
})
) : (
<div className="text-center py-2 text-slate-400 text-sm">No connections yet</div>
)}
</div> </div>
<div className="mt-3 flex justify-center"> <div className="mt-3 flex justify-center">
<Button <Button
@ -1729,8 +1509,7 @@ const FriendshipNetwork: React.FC = () => {
size="sm" size="sm"
onClick={() => { onClick={() => {
setNewRelationship({ setNewRelationship({
...newRelationship, ...newRelationship, source: editPerson._id,
source: editPerson._id,
}); });
setPersonDetailModalOpen(false); setPersonDetailModalOpen(false);
setTimeout(() => setRelationshipModalOpen(true), 100); setTimeout(() => setRelationshipModalOpen(true), 100);
@ -1742,8 +1521,7 @@ const FriendshipNetwork: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
</Modal> </Modal>)}
)}
{/* Settings Modal */} {/* Settings Modal */}
<Modal <Modal
@ -1765,9 +1543,7 @@ const FriendshipNetwork: React.FC = () => {
/> />
<div className="block h-6 bg-slate-700 rounded-full w-12"></div> <div className="block h-6 bg-slate-700 rounded-full w-12"></div>
<div <div
className={`absolute left-1 top-1 w-4 h-4 rounded-full transition-transform ${ className={`absolute left-1 top-1 w-4 h-4 rounded-full transition-transform ${settings.showLabels ? 'transform translate-x-6 bg-indigo-500' : 'bg-gray-400'}`}
settings.showLabels ? 'transform translate-x-6 bg-indigo-500' : 'bg-gray-400'
}`}
></div> ></div>
</div> </div>
</div> </div>
@ -1785,9 +1561,7 @@ const FriendshipNetwork: React.FC = () => {
/> />
<div className="block h-6 bg-slate-700 rounded-full w-12"></div> <div className="block h-6 bg-slate-700 rounded-full w-12"></div>
<div <div
className={`absolute left-1 top-1 w-4 h-4 rounded-full transition-transform ${ className={`absolute left-1 top-1 w-4 h-4 rounded-full transition-transform ${settings.autoLayout ? 'transform translate-x-6 bg-indigo-500' : 'bg-gray-400'}`}
settings.autoLayout ? 'transform translate-x-6 bg-indigo-500' : 'bg-gray-400'
}`}
></div> ></div>
</div> </div>
</div> </div>
@ -1801,17 +1575,11 @@ const FriendshipNetwork: React.FC = () => {
name="highlightConnections" name="highlightConnections"
className="sr-only" className="sr-only"
checked={settings.highlightConnections} checked={settings.highlightConnections}
onChange={() => onChange={() => setSettings({ ...settings, highlightConnections: !settings.highlightConnections })}
setSettings({ ...settings, highlightConnections: !settings.highlightConnections })
}
/> />
<div className="block h-6 bg-slate-700 rounded-full w-12"></div> <div className="block h-6 bg-slate-700 rounded-full w-12"></div>
<div <div
className={`absolute left-1 top-1 w-4 h-4 rounded-full transition-transform ${ className={`absolute left-1 top-1 w-4 h-4 rounded-full transition-transform ${settings.highlightConnections ? 'transform translate-x-6 bg-indigo-500' : 'bg-gray-400'}`}
settings.highlightConnections
? 'transform translate-x-6 bg-indigo-500'
: 'bg-gray-400'
}`}
></div> ></div>
</div> </div>
</div> </div>
@ -1819,38 +1587,26 @@ const FriendshipNetwork: React.FC = () => {
<div> <div>
<label className="block text-sm font-medium text-gray-300 mb-2">Animation Speed</label> <label className="block text-sm font-medium text-gray-300 mb-2">Animation Speed</label>
<div className="flex space-x-2"> <div className="flex space-x-2">
{['slow', 'medium', 'fast'].map(speed => ( {['slow', 'medium', 'fast'].map(speed => (<button
<button
key={speed} key={speed}
className={`flex-1 py-2 px-3 rounded-md text-sm ${ className={`flex-1 py-2 px-3 rounded-md text-sm ${settings.animationSpeed === speed ? 'bg-indigo-600 text-white' : 'bg-slate-700 text-gray-300 hover:bg-slate-600'}`}
settings.animationSpeed === speed
? 'bg-indigo-600 text-white'
: 'bg-slate-700 text-gray-300 hover:bg-slate-600'
}`}
onClick={() => setSettings({ ...settings, animationSpeed: speed })} onClick={() => setSettings({ ...settings, animationSpeed: speed })}
> >
{speed.charAt(0).toUpperCase() + speed.slice(1)} {speed.charAt(0).toUpperCase() + speed.slice(1)}
</button> </button>))}
))}
</div> </div>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-300 mb-2">Node Size</label> <label className="block text-sm font-medium text-gray-300 mb-2">Node Size</label>
<div className="flex space-x-2"> <div className="flex space-x-2">
{['small', 'medium', 'large'].map(size => ( {['small', 'medium', 'large'].map(size => (<button
<button
key={size} key={size}
className={`flex-1 py-2 px-3 rounded-md text-sm ${ className={`flex-1 py-2 px-3 rounded-md text-sm ${settings.nodeSize === size ? 'bg-indigo-600 text-white' : 'bg-slate-700 text-gray-300 hover:bg-slate-600'}`}
settings.nodeSize === size
? 'bg-indigo-600 text-white'
: 'bg-slate-700 text-gray-300 hover:bg-slate-600'
}`}
onClick={() => setSettings({ ...settings, nodeSize: size })} onClick={() => setSettings({ ...settings, nodeSize: size })}
> >
{size.charAt(0).toUpperCase() + size.slice(1)} {size.charAt(0).toUpperCase() + size.slice(1)}
</button> </button>))}
))}
</div> </div>
</div> </div>
@ -1964,28 +1720,21 @@ const FriendshipNetwork: React.FC = () => {
onClose={() => setDeleteConfirmOpen(false)} onClose={() => setDeleteConfirmOpen(false)}
onConfirm={executeDelete} onConfirm={executeDelete}
title="Confirm Deletion" title="Confirm Deletion"
message={ message={itemToDelete.type === 'person' ? 'Are you sure you want to delete this person? This will also remove all their relationships.' : 'Are you sure you want to delete this relationship?'}
itemToDelete.type === 'person'
? 'Are you sure you want to delete this person? This will also remove all their relationships.'
: 'Are you sure you want to delete this relationship?'
}
confirmText="Delete" confirmText="Delete"
variant="danger" variant="danger"
/> />
{/* Toast Notifications */} {/* Toast Notifications */}
<div className="fixed bottom-4 right-4 z-[9900] space-y-2 pointer-events-none"> <div className="fixed bottom-4 right-4 z-[9900] space-y-2 pointer-events-none">
{toasts.map(toast => ( {toasts.map(toast => (<Toast
<Toast
key={toast.id} key={toast.id}
message={toast.message} message={toast.message}
type={toast.type as any} type={toast.type as any}
onClose={() => removeToast(toast.id)} onClose={() => removeToast(toast.id)}
/> />))}
))}
</div> </div>
</div> </div>);
);
}; };
export default FriendshipNetwork; export default FriendshipNetwork;