mirror of
https://github.com/philipredstone/relnet.git
synced 2025-06-17 05:01:24 +02:00
Fix birthdate input
This commit is contained in:
parent
9ce80b4c59
commit
bbb3645d99
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user