mirror of
https://github.com/philipredstone/relnet.git
synced 2025-06-17 13:11:14 +02:00
add prettifier
This commit is contained in:
parent
c078610c4d
commit
eceacf2117
19
.prettierignore
Normal file
19
.prettierignore
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Ignore build outputs
|
||||||
|
/dist
|
||||||
|
/build
|
||||||
|
|
||||||
|
# Ignore dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Ignore coverage reports
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# Ignore logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Ignore frontend (it has its own Prettier config)
|
||||||
|
/frontend
|
||||||
|
|
||||||
|
# Ignore configuration files
|
||||||
|
.env
|
||||||
|
.env.*
|
10
.prettierrc
Normal file
10
.prettierrc
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"printWidth": 100,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"bracketSpacing": true
|
||||||
|
}
|
28
.vscode/settings.json
vendored
Normal file
28
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll": "explicit"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[html]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[css]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
|
}
|
16
frontend/.prettierignore
Normal file
16
frontend/.prettierignore
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Ignore build outputs
|
||||||
|
/dist
|
||||||
|
/build
|
||||||
|
|
||||||
|
# Ignore dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Ignore coverage reports
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# Ignore logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Ignore configuration files
|
||||||
|
.env
|
||||||
|
.env.*
|
12
frontend/.prettierrc
Normal file
12
frontend/.prettierrc
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"printWidth": 100,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"bracketSameLine": false
|
||||||
|
}
|
2853
frontend/package-lock.json
generated
Normal file
2853
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"format": "prettier --write \"src/**/*.{tsx,ts,js,jsx,json,css,html}\"",
|
||||||
|
"format:check": "prettier --check \"src/**/*.{tsx,ts,js,jsx,json,css,html}\""
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
@ -22,6 +24,7 @@
|
|||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@vitejs/plugin-react": "^4.4.0",
|
"@vitejs/plugin-react": "^4.4.0",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.2.6",
|
"vite": "^6.2.6",
|
||||||
|
@ -42,19 +42,27 @@ export const getUserNetworks = async (): Promise<Network[]> => {
|
|||||||
|
|
||||||
// Create a new network
|
// Create a new network
|
||||||
export const createNetwork = async (data: CreateNetworkData): Promise<Network> => {
|
export const createNetwork = async (data: CreateNetworkData): Promise<Network> => {
|
||||||
const response = await axios.post<{ success: boolean; data: Network }>(`${API_URL}/networks`, data);
|
const response = await axios.post<{ success: boolean; data: Network }>(
|
||||||
|
`${API_URL}/networks`,
|
||||||
|
data
|
||||||
|
);
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get a specific network
|
// Get a specific network
|
||||||
export const getNetwork = async (id: string): Promise<Network> => {
|
export const getNetwork = async (id: string): Promise<Network> => {
|
||||||
const response = await axios.get<{ success: boolean; data: Network }>(`${API_URL}/networks/${id}`);
|
const response = await axios.get<{ success: boolean; data: Network }>(
|
||||||
|
`${API_URL}/networks/${id}`
|
||||||
|
);
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update a network
|
// Update a network
|
||||||
export const updateNetwork = async (id: string, data: UpdateNetworkData): Promise<Network> => {
|
export const updateNetwork = async (id: string, data: UpdateNetworkData): Promise<Network> => {
|
||||||
const response = await axios.put<{ success: boolean; data: Network }>(`${API_URL}/networks/${id}`, data);
|
const response = await axios.put<{ success: boolean; data: Network }>(
|
||||||
|
`${API_URL}/networks/${id}`,
|
||||||
|
data
|
||||||
|
);
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -64,6 +64,9 @@ export const updateRelationship = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Remove a relationship
|
// Remove a relationship
|
||||||
export const removeRelationship = async (networkId: string, relationshipId: string): Promise<void> => {
|
export const removeRelationship = async (
|
||||||
|
networkId: string,
|
||||||
|
relationshipId: string
|
||||||
|
): Promise<void> => {
|
||||||
await axios.delete(`${API_URL}/networks/${networkId}/relationships/${relationshipId}`);
|
await axios.delete(`${API_URL}/networks/${networkId}/relationships/${relationshipId}`);
|
||||||
};
|
};
|
@ -18,7 +18,7 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
deletePerson,
|
deletePerson,
|
||||||
createRelationship,
|
createRelationship,
|
||||||
updateRelationship,
|
updateRelationship,
|
||||||
deleteRelationship
|
deleteRelationship,
|
||||||
} = useFriendshipNetwork(id || null);
|
} = useFriendshipNetwork(id || null);
|
||||||
|
|
||||||
// Local state for the UI
|
// Local state for the UI
|
||||||
@ -27,13 +27,13 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
const [newPerson, setNewPerson] = useState({
|
const [newPerson, setNewPerson] = useState({
|
||||||
firstName: '',
|
firstName: '',
|
||||||
lastName: '',
|
lastName: '',
|
||||||
birthday: ''
|
birthday: '',
|
||||||
});
|
});
|
||||||
const [newRelationship, setNewRelationship] = useState({
|
const [newRelationship, setNewRelationship] = useState({
|
||||||
source: '',
|
source: '',
|
||||||
targets: [] as string[],
|
targets: [] as string[],
|
||||||
type: 'freund',
|
type: 'freund',
|
||||||
customType: ''
|
customType: '',
|
||||||
});
|
});
|
||||||
const svgRef = useRef<SVGSVGElement>(null);
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
const nodeRefs = useRef<{ [key: string]: SVGGElement | null }>({});
|
const nodeRefs = useRef<{ [key: string]: SVGGElement | null }>({});
|
||||||
@ -67,14 +67,14 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
birthday: newPerson.birthday || undefined,
|
birthday: newPerson.birthday || undefined,
|
||||||
position: {
|
position: {
|
||||||
x: 100 + Math.random() * 400,
|
x: 100 + Math.random() * 400,
|
||||||
y: 100 + Math.random() * 300
|
y: 100 + Math.random() * 300,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
setNewPerson({
|
setNewPerson({
|
||||||
firstName: '',
|
firstName: '',
|
||||||
lastName: '',
|
lastName: '',
|
||||||
birthday: ''
|
birthday: '',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error adding person:', error);
|
console.error('Error adding person:', error);
|
||||||
@ -102,7 +102,8 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
const existingRelationships: any[] = [];
|
const existingRelationships: any[] = [];
|
||||||
targets.forEach(target => {
|
targets.forEach(target => {
|
||||||
if (source !== target) {
|
if (source !== target) {
|
||||||
const existingEdge = relationships.find(edge =>
|
const existingEdge = relationships.find(
|
||||||
|
edge =>
|
||||||
(edge.source === source && edge.target === target) ||
|
(edge.source === source && edge.target === target) ||
|
||||||
(edge.source === target && edge.target === source)
|
(edge.source === target && edge.target === source)
|
||||||
);
|
);
|
||||||
@ -113,7 +114,7 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
target,
|
target,
|
||||||
existingType: existingEdge.type,
|
existingType: existingEdge.type,
|
||||||
newType: actualType,
|
newType: actualType,
|
||||||
edgeId: existingEdge.id
|
edgeId: existingEdge.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,26 +124,30 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
// Show override modal
|
// Show override modal
|
||||||
setOverrideRelationship({
|
setOverrideRelationship({
|
||||||
existingRelationships,
|
existingRelationships,
|
||||||
newRelationships: targets.filter(target =>
|
newRelationships: targets
|
||||||
source !== target && !existingRelationships.some(rel => rel.target === target)
|
.filter(
|
||||||
).map(target => ({ source, target, type: actualType }))
|
target => source !== target && !existingRelationships.some(rel => rel.target === target)
|
||||||
|
)
|
||||||
|
.map(target => ({ source, target, type: actualType })),
|
||||||
});
|
});
|
||||||
setShowOverrideModal(true);
|
setShowOverrideModal(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each target for new relationships
|
// Process each target for new relationships
|
||||||
const addPromises = targets.map(target => {
|
const addPromises = targets
|
||||||
|
.map(target => {
|
||||||
if (source !== target) {
|
if (source !== target) {
|
||||||
return createRelationship({
|
return createRelationship({
|
||||||
source,
|
source,
|
||||||
target,
|
target,
|
||||||
type: type as any,
|
type: type as any,
|
||||||
customType: type === 'custom' ? customType : undefined
|
customType: type === 'custom' ? customType : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}).filter(Boolean);
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
if (addPromises.length === 0) {
|
if (addPromises.length === 0) {
|
||||||
alert('No valid relationships to add.');
|
alert('No valid relationships to add.');
|
||||||
@ -169,24 +174,28 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
await Promise.all(existingRelationships.map(rel => deleteRelationship(rel.edgeId)));
|
await Promise.all(existingRelationships.map(rel => deleteRelationship(rel.edgeId)));
|
||||||
|
|
||||||
// Add new overridden relationships
|
// Add new overridden relationships
|
||||||
await Promise.all(existingRelationships.map(rel =>
|
await Promise.all(
|
||||||
|
existingRelationships.map(rel =>
|
||||||
createRelationship({
|
createRelationship({
|
||||||
source: rel.source,
|
source: rel.source,
|
||||||
target: rel.target,
|
target: rel.target,
|
||||||
type: rel.newType as any,
|
type: rel.newType as any,
|
||||||
customType: rel.newType === 'custom' ? rel.customType : undefined
|
customType: rel.newType === 'custom' ? rel.customType : undefined,
|
||||||
})
|
})
|
||||||
));
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Add completely new relationships
|
// Add completely new relationships
|
||||||
await Promise.all(newRelationships.map(rel =>
|
await Promise.all(
|
||||||
|
newRelationships.map(rel =>
|
||||||
createRelationship({
|
createRelationship({
|
||||||
source: rel.source,
|
source: rel.source,
|
||||||
target: rel.target,
|
target: rel.target,
|
||||||
type: rel.type as any,
|
type: rel.type as any,
|
||||||
customType: rel.type === 'custom' ? rel.customType : undefined
|
customType: rel.type === 'custom' ? rel.customType : undefined,
|
||||||
})
|
})
|
||||||
));
|
)
|
||||||
|
);
|
||||||
|
|
||||||
setShowOverrideModal(false);
|
setShowOverrideModal(false);
|
||||||
setOverrideRelationship(null);
|
setOverrideRelationship(null);
|
||||||
@ -202,14 +211,16 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
// If there are new relationships that don't need overrides, add those
|
// If there are new relationships that don't need overrides, add those
|
||||||
if (overrideRelationship && overrideRelationship.newRelationships.length > 0) {
|
if (overrideRelationship && overrideRelationship.newRelationships.length > 0) {
|
||||||
try {
|
try {
|
||||||
await Promise.all(overrideRelationship.newRelationships.map(rel =>
|
await Promise.all(
|
||||||
|
overrideRelationship.newRelationships.map(rel =>
|
||||||
createRelationship({
|
createRelationship({
|
||||||
source: rel.source,
|
source: rel.source,
|
||||||
target: rel.target,
|
target: rel.target,
|
||||||
type: rel.type as any,
|
type: rel.type as any,
|
||||||
customType: rel.type === 'custom' ? rel.customType : undefined
|
customType: rel.type === 'custom' ? rel.customType : undefined,
|
||||||
})
|
})
|
||||||
));
|
)
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error adding new relationships:', error);
|
console.error('Error adding new relationships:', error);
|
||||||
}
|
}
|
||||||
@ -254,7 +265,7 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
node.id === dragging
|
node.id === dragging
|
||||||
? {
|
? {
|
||||||
...node,
|
...node,
|
||||||
position: { x: newX, y: newY }
|
position: { x: newX, y: newY },
|
||||||
}
|
}
|
||||||
: node
|
: node
|
||||||
);
|
);
|
||||||
@ -299,7 +310,11 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
|
|
||||||
// Delete a node and its associated edges
|
// Delete a node and its associated edges
|
||||||
const handleDeleteNode = async (id: string) => {
|
const handleDeleteNode = async (id: string) => {
|
||||||
if (window.confirm('Are you sure you want to delete this person? All their relationships will also be deleted.')) {
|
if (
|
||||||
|
window.confirm(
|
||||||
|
'Are you sure you want to delete this person? All their relationships will also be deleted.'
|
||||||
|
)
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
await deletePerson(id);
|
await deletePerson(id);
|
||||||
setSelectedNode(null);
|
setSelectedNode(null);
|
||||||
@ -314,11 +329,16 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
// Get relationship type label
|
// Get relationship type label
|
||||||
const getRelationshipLabel = (type: string) => {
|
const getRelationshipLabel = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'freund': return 'Freund/in';
|
case 'freund':
|
||||||
case 'partner': return 'Partner/in';
|
return 'Freund/in';
|
||||||
case 'familie': return 'Familie/Verwandschaft';
|
case 'partner':
|
||||||
case 'arbeitskolleg': return 'Arbeitskolleg/innen';
|
return 'Partner/in';
|
||||||
default: return type;
|
case 'familie':
|
||||||
|
return 'Familie/Verwandschaft';
|
||||||
|
case 'arbeitskolleg':
|
||||||
|
return 'Arbeitskolleg/innen';
|
||||||
|
default:
|
||||||
|
return type;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -339,13 +359,13 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
return {
|
return {
|
||||||
person: other ? `${other.firstName} ${other.lastName}` : otherId,
|
person: other ? `${other.firstName} ${other.lastName}` : otherId,
|
||||||
type: edge.type,
|
type: edge.type,
|
||||||
edgeId: edge.id
|
edgeId: edge.id,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
setPopupInfo({
|
setPopupInfo({
|
||||||
...popupInfo,
|
...popupInfo,
|
||||||
relationships: nodeRelationships
|
relationships: nodeRelationships,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -360,22 +380,22 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
if (!node) return;
|
if (!node) return;
|
||||||
|
|
||||||
// Find all relationships
|
// Find all relationships
|
||||||
const nodeRelationships = relationships.filter(
|
const nodeRelationships = relationships
|
||||||
edge => edge.source === nodeId || edge.target === nodeId
|
.filter(edge => edge.source === nodeId || edge.target === nodeId)
|
||||||
).map(edge => {
|
.map(edge => {
|
||||||
const otherId = edge.source === nodeId ? edge.target : edge.source;
|
const otherId = edge.source === nodeId ? edge.target : edge.source;
|
||||||
const other = people.find(n => n.id === otherId);
|
const other = people.find(n => n.id === otherId);
|
||||||
return {
|
return {
|
||||||
person: other ? `${other.firstName} ${other.lastName}` : otherId,
|
person: other ? `${other.firstName} ${other.lastName}` : otherId,
|
||||||
type: edge.type,
|
type: edge.type,
|
||||||
edgeId: edge.id
|
edgeId: edge.id,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
setPopupInfo({
|
setPopupInfo({
|
||||||
node,
|
node,
|
||||||
relationships: nodeRelationships,
|
relationships: nodeRelationships,
|
||||||
position: { ...node.position }
|
position: { ...node.position },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -392,9 +412,7 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
// Close popup when clicking outside
|
// Close popup when clicking outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
if (popupInfo &&
|
if (popupInfo && !(e.target as Element).closest('.popup') && !dragging) {
|
||||||
!(e.target as Element).closest('.popup') &&
|
|
||||||
!dragging) {
|
|
||||||
closePopup();
|
closePopup();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -410,16 +428,16 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <div className="bg-red-100 border border-red-400 text-red-700 p-4 m-4 rounded">{error}</div>;
|
return (
|
||||||
|
<div className="bg-red-100 border border-red-400 text-red-700 p-4 m-4 rounded">{error}</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-gray-100">
|
<div className="flex h-screen bg-gray-100">
|
||||||
{/* Sidebar menu */}
|
{/* Sidebar menu */}
|
||||||
<div className="w-64 bg-white p-4 border-r border-gray-200 overflow-y-auto">
|
<div className="w-64 bg-white p-4 border-r border-gray-200 overflow-y-auto">
|
||||||
<h2 className="text-xl font-bold mb-4">
|
<h2 className="text-xl font-bold mb-4">{currentNetwork?.name || 'Friend Network'}</h2>
|
||||||
{currentNetwork?.name || 'Friend Network'}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{/* Add Person Form */}
|
{/* Add Person Form */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
@ -505,7 +523,9 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
className="w-full px-3 py-2 border border-gray-300 rounded mb-2"
|
className="w-full px-3 py-2 border border-gray-300 rounded mb-2"
|
||||||
placeholder="Enter custom relationship type"
|
placeholder="Enter custom relationship type"
|
||||||
value={newRelationship.customType}
|
value={newRelationship.customType}
|
||||||
onChange={e => setNewRelationship({...newRelationship, customType: e.target.value})}
|
onChange={e =>
|
||||||
|
setNewRelationship({ ...newRelationship, customType: e.target.value })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -524,7 +544,9 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
<ul className="divide-y divide-gray-200">
|
<ul className="divide-y divide-gray-200">
|
||||||
{people.map(node => (
|
{people.map(node => (
|
||||||
<li key={node.id} className="py-2 flex justify-between items-center">
|
<li key={node.id} className="py-2 flex justify-between items-center">
|
||||||
<span>{node.firstName} {node.lastName}</span>
|
<span>
|
||||||
|
{node.firstName} {node.lastName}
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
className="text-red-500 hover:text-red-700"
|
className="text-red-500 hover:text-red-700"
|
||||||
onClick={() => handleDeleteNode(node.id)}
|
onClick={() => handleDeleteNode(node.id)}
|
||||||
@ -552,10 +574,13 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
<li key={edge.id} className="py-2">
|
<li key={edge.id} className="py-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span>
|
<span>
|
||||||
{source.firstName} {source.lastName.charAt(0)}. ↔ {target.firstName} {target.lastName.charAt(0)}.
|
{source.firstName} {source.lastName.charAt(0)}. ↔ {target.firstName}{' '}
|
||||||
|
{target.lastName.charAt(0)}.
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="text-xs text-gray-600 mr-2">{getRelationshipLabel(edge.type)}</span>
|
<span className="text-xs text-gray-600 mr-2">
|
||||||
|
{getRelationshipLabel(edge.type)}
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
className="text-red-500 hover:text-red-700"
|
className="text-red-500 hover:text-red-700"
|
||||||
onClick={() => handleRemoveRelationship(edge.id)}
|
onClick={() => handleRemoveRelationship(edge.id)}
|
||||||
@ -625,7 +650,9 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
key={node.id}
|
key={node.id}
|
||||||
transform={`translate(${node.position.x}, ${node.position.y})`}
|
transform={`translate(${node.position.x}, ${node.position.y})`}
|
||||||
onMouseDown={e => handleMouseDown(e, node.id)}
|
onMouseDown={e => handleMouseDown(e, node.id)}
|
||||||
ref={el => { nodeRefs.current[node.id] = el; }}
|
ref={el => {
|
||||||
|
nodeRefs.current[node.id] = el;
|
||||||
|
}}
|
||||||
className="cursor-grab"
|
className="cursor-grab"
|
||||||
>
|
>
|
||||||
<circle
|
<circle
|
||||||
@ -653,25 +680,30 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
<div
|
<div
|
||||||
className="popup absolute bg-white border border-gray-300 rounded shadow-lg p-4 z-10 w-64"
|
className="popup absolute bg-white border border-gray-300 rounded shadow-lg p-4 z-10 w-64"
|
||||||
style={{
|
style={{
|
||||||
left: popupInfo.position.x > (svgRef.current?.clientWidth || 0) / 2
|
left:
|
||||||
? popupInfo.position.x - 260 : popupInfo.position.x + 40,
|
popupInfo.position.x > (svgRef.current?.clientWidth || 0) / 2
|
||||||
top: popupInfo.position.y > (svgRef.current?.clientHeight || 0) / 2
|
? popupInfo.position.x - 260
|
||||||
? popupInfo.position.y - 200 : popupInfo.position.y,
|
: popupInfo.position.x + 40,
|
||||||
|
top:
|
||||||
|
popupInfo.position.y > (svgRef.current?.clientHeight || 0) / 2
|
||||||
|
? popupInfo.position.y - 200
|
||||||
|
: popupInfo.position.y,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<h3 className="text-lg font-bold">Person Details</h3>
|
<h3 className="text-lg font-bold">Person Details</h3>
|
||||||
<button
|
<button className="text-gray-500 hover:text-gray-700" onClick={closePopup}>
|
||||||
className="text-gray-500 hover:text-gray-700"
|
|
||||||
onClick={closePopup}
|
|
||||||
>
|
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<p className="font-semibold">Name: {popupInfo.node.firstName} {popupInfo.node.lastName}</p>
|
<p className="font-semibold">
|
||||||
|
Name: {popupInfo.node.firstName} {popupInfo.node.lastName}
|
||||||
|
</p>
|
||||||
{popupInfo.node.birthday && (
|
{popupInfo.node.birthday && (
|
||||||
<p className="text-sm text-gray-600">Birthday: {new Date(popupInfo.node.birthday).toLocaleDateString()}</p>
|
<p className="text-sm text-gray-600">
|
||||||
|
Birthday: {new Date(popupInfo.node.birthday).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -709,8 +741,8 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
<h3 className="text-xl font-bold mb-4">Existing Relationship(s)</h3>
|
<h3 className="text-xl font-bold mb-4">Existing Relationship(s)</h3>
|
||||||
<p className="mb-4">
|
<p className="mb-4">
|
||||||
{overrideRelationship.existingRelationships.length === 1
|
{overrideRelationship.existingRelationships.length === 1
|
||||||
? "There is already a relationship between these people:"
|
? 'There is already a relationship between these people:'
|
||||||
: "There are already relationships between these people:"}
|
: 'There are already relationships between these people:'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul className="mb-4 text-sm border rounded divide-y">
|
<ul className="mb-4 text-sm border rounded divide-y">
|
||||||
@ -723,7 +755,8 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
<li key={index} className="p-2">
|
<li key={index} className="p-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span>
|
<span>
|
||||||
{source.firstName} {source.lastName} ↔ {target.firstName} {target.lastName}
|
{source.firstName} {source.lastName} ↔ {target.firstName}{' '}
|
||||||
|
{target.lastName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-xs mt-1">
|
<div className="flex justify-between text-xs mt-1">
|
||||||
@ -761,7 +794,10 @@ const FriendshipNetwork: React.FC = () => {
|
|||||||
|
|
||||||
{/* Instructions */}
|
{/* Instructions */}
|
||||||
<div className="absolute bottom-4 left-4 right-4 bg-white border border-gray-200 rounded p-2 text-sm">
|
<div className="absolute bottom-4 left-4 right-4 bg-white border border-gray-200 rounded p-2 text-sm">
|
||||||
<p><strong>Tip:</strong> Drag people to arrange them. Click on a person to view their details and relationships.</p>
|
<p>
|
||||||
|
<strong>Tip:</strong> Drag people to arrange them. Click on a person to view their
|
||||||
|
details and relationships.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,7 +46,7 @@ const Login: React.FC = () => {
|
|||||||
type="email"
|
type="email"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded"
|
className="w-full px-3 py-2 border border-gray-300 rounded"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={e => setEmail(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -60,7 +60,7 @@ const Login: React.FC = () => {
|
|||||||
type="password"
|
type="password"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded"
|
className="w-full px-3 py-2 border border-gray-300 rounded"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={e => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -60,7 +60,7 @@ const Register: React.FC = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded"
|
className="w-full px-3 py-2 border border-gray-300 rounded"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={e => setUsername(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -74,7 +74,7 @@ const Register: React.FC = () => {
|
|||||||
type="email"
|
type="email"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded"
|
className="w-full px-3 py-2 border border-gray-300 rounded"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={e => setEmail(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -88,7 +88,7 @@ const Register: React.FC = () => {
|
|||||||
type="password"
|
type="password"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded"
|
className="w-full px-3 py-2 border border-gray-300 rounded"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={e => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -102,7 +102,7 @@ const Register: React.FC = () => {
|
|||||||
type="password"
|
type="password"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded"
|
className="w-full px-3 py-2 border border-gray-300 rounded"
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
onChange={e => setConfirmPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,7 +29,7 @@ const NetworkList: React.FC = () => {
|
|||||||
const network = await createNetwork({
|
const network = await createNetwork({
|
||||||
name: newNetworkName.trim(),
|
name: newNetworkName.trim(),
|
||||||
description: newNetworkDescription.trim() || undefined,
|
description: newNetworkDescription.trim() || undefined,
|
||||||
isPublic
|
isPublic,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset form
|
// Reset form
|
||||||
@ -48,7 +48,9 @@ const NetworkList: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteNetwork = async (id: string) => {
|
const handleDeleteNetwork = async (id: string) => {
|
||||||
if (window.confirm('Are you sure you want to delete this network? This action cannot be undone.')) {
|
if (
|
||||||
|
window.confirm('Are you sure you want to delete this network? This action cannot be undone.')
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
await deleteNetwork(id);
|
await deleteNetwork(id);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@ -100,7 +102,7 @@ const NetworkList: React.FC = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded"
|
className="w-full px-3 py-2 border border-gray-300 rounded"
|
||||||
value={newNetworkName}
|
value={newNetworkName}
|
||||||
onChange={(e) => setNewNetworkName(e.target.value)}
|
onChange={e => setNewNetworkName(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -113,7 +115,7 @@ const NetworkList: React.FC = () => {
|
|||||||
id="description"
|
id="description"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded"
|
className="w-full px-3 py-2 border border-gray-300 rounded"
|
||||||
value={newNetworkDescription}
|
value={newNetworkDescription}
|
||||||
onChange={(e) => setNewNetworkDescription(e.target.value)}
|
onChange={e => setNewNetworkDescription(e.target.value)}
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -124,7 +126,7 @@ const NetworkList: React.FC = () => {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
checked={isPublic}
|
checked={isPublic}
|
||||||
onChange={(e) => setIsPublic(e.target.checked)}
|
onChange={e => setIsPublic(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<span className="text-gray-700 text-sm font-bold">Make this network public</span>
|
<span className="text-gray-700 text-sm font-bold">Make this network public</span>
|
||||||
</label>
|
</label>
|
||||||
@ -163,12 +165,11 @@ const NetworkList: React.FC = () => {
|
|||||||
{networks
|
{networks
|
||||||
.filter(network => {
|
.filter(network => {
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
const ownerId = typeof network.owner === 'string'
|
const ownerId =
|
||||||
? network.owner
|
typeof network.owner === 'string' ? network.owner : network.owner._id;
|
||||||
: network.owner._id;
|
|
||||||
return ownerId === user.id;
|
return ownerId === user.id;
|
||||||
})
|
})
|
||||||
.map((network) => (
|
.map(network => (
|
||||||
<div key={network._id} className="bg-white rounded-lg shadow-md overflow-hidden">
|
<div key={network._id} className="bg-white rounded-lg shadow-md overflow-hidden">
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<h2 className="text-xl font-bold mb-2">{network.name}</h2>
|
<h2 className="text-xl font-bold mb-2">{network.name}</h2>
|
||||||
@ -176,7 +177,9 @@ const NetworkList: React.FC = () => {
|
|||||||
<p className="text-gray-600 mb-4">{network.description}</p>
|
<p className="text-gray-600 mb-4">{network.description}</p>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center mb-4">
|
<div className="flex items-center mb-4">
|
||||||
<span className={`px-2 py-1 rounded-full text-xs ${network.isPublic ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}>
|
<span
|
||||||
|
className={`px-2 py-1 rounded-full text-xs ${network.isPublic ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}
|
||||||
|
>
|
||||||
{network.isPublic ? 'Public' : 'Private'}
|
{network.isPublic ? 'Public' : 'Private'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-gray-500 ml-2">
|
<span className="text-xs text-gray-500 ml-2">
|
||||||
@ -203,9 +206,7 @@ const NetworkList: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
{networks.filter(network => {
|
{networks.filter(network => {
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
const ownerId = typeof network.owner === 'string'
|
const ownerId = typeof network.owner === 'string' ? network.owner : network.owner._id;
|
||||||
? network.owner
|
|
||||||
: network.owner._id;
|
|
||||||
return ownerId === user.id;
|
return ownerId === user.id;
|
||||||
}).length === 0 && (
|
}).length === 0 && (
|
||||||
<p className="text-gray-600 mb-4">You haven't created any networks yet.</p>
|
<p className="text-gray-600 mb-4">You haven't created any networks yet.</p>
|
||||||
@ -215,9 +216,7 @@ const NetworkList: React.FC = () => {
|
|||||||
{/* Public Networks Section */}
|
{/* Public Networks Section */}
|
||||||
{networks.some(network => {
|
{networks.some(network => {
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
const ownerId = typeof network.owner === 'string'
|
const ownerId = typeof network.owner === 'string' ? network.owner : network.owner._id;
|
||||||
? network.owner
|
|
||||||
: network.owner._id;
|
|
||||||
return ownerId !== user.id;
|
return ownerId !== user.id;
|
||||||
}) && (
|
}) && (
|
||||||
<div>
|
<div>
|
||||||
@ -226,13 +225,15 @@ const NetworkList: React.FC = () => {
|
|||||||
{networks
|
{networks
|
||||||
.filter(network => {
|
.filter(network => {
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
const ownerId = typeof network.owner === 'string'
|
const ownerId =
|
||||||
? network.owner
|
typeof network.owner === 'string' ? network.owner : network.owner._id;
|
||||||
: network.owner._id;
|
|
||||||
return ownerId !== user.id;
|
return ownerId !== user.id;
|
||||||
})
|
})
|
||||||
.map((network) => (
|
.map(network => (
|
||||||
<div key={network._id} className="bg-white rounded-lg shadow-md overflow-hidden border-l-4 border-green-500">
|
<div
|
||||||
|
key={network._id}
|
||||||
|
className="bg-white rounded-lg shadow-md overflow-hidden border-l-4 border-green-500"
|
||||||
|
>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<h2 className="text-xl font-bold mb-2">{network.name}</h2>
|
<h2 className="text-xl font-bold mb-2">{network.name}</h2>
|
||||||
{network.description && (
|
{network.description && (
|
||||||
@ -246,9 +247,8 @@ const NetworkList: React.FC = () => {
|
|||||||
Created: {new Date(network.createdAt).toLocaleDateString()}
|
Created: {new Date(network.createdAt).toLocaleDateString()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-gray-500 ml-2">
|
<span className="text-xs text-gray-500 ml-2">
|
||||||
By: {typeof network.owner === 'string'
|
By:{' '}
|
||||||
? 'Unknown'
|
{typeof network.owner === 'string' ? 'Unknown' : network.owner.username}
|
||||||
: network.owner.username}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
|
@ -1,5 +1,13 @@
|
|||||||
import React, { createContext, useState, useEffect, useContext, ReactNode } from 'react';
|
import React, { createContext, useState, useEffect, useContext, ReactNode } from 'react';
|
||||||
import { User, getCurrentUser, login as apiLogin, register as apiRegister, logout as apiLogout, LoginData, RegisterData } from '../api/auth';
|
import {
|
||||||
|
User,
|
||||||
|
getCurrentUser,
|
||||||
|
login as apiLogin,
|
||||||
|
register as apiRegister,
|
||||||
|
logout as apiLogout,
|
||||||
|
LoginData,
|
||||||
|
RegisterData,
|
||||||
|
} from '../api/auth';
|
||||||
|
|
||||||
interface AuthContextProps {
|
interface AuthContextProps {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
updateNetwork as apiUpdateNetwork,
|
updateNetwork as apiUpdateNetwork,
|
||||||
deleteNetwork as apiDeleteNetwork,
|
deleteNetwork as apiDeleteNetwork,
|
||||||
CreateNetworkData,
|
CreateNetworkData,
|
||||||
UpdateNetworkData
|
UpdateNetworkData,
|
||||||
} from '../api/network';
|
} from '../api/network';
|
||||||
import { useAuth } from './AuthContext';
|
import { useAuth } from './AuthContext';
|
||||||
|
|
||||||
@ -66,9 +66,7 @@ export const NetworkProvider: React.FC<{ children: ReactNode }> = ({ children })
|
|||||||
const updateNetwork = async (id: string, data: UpdateNetworkData): Promise<Network> => {
|
const updateNetwork = async (id: string, data: UpdateNetworkData): Promise<Network> => {
|
||||||
try {
|
try {
|
||||||
const updatedNetwork = await apiUpdateNetwork(id, data);
|
const updatedNetwork = await apiUpdateNetwork(id, data);
|
||||||
setNetworks(networks.map(network =>
|
setNetworks(networks.map(network => (network._id === id ? updatedNetwork : network)));
|
||||||
network._id === id ? updatedNetwork : network
|
|
||||||
));
|
|
||||||
return updatedNetwork;
|
return updatedNetwork;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || 'Failed to update network');
|
setError(err.message || 'Failed to update network');
|
||||||
@ -99,7 +97,7 @@ export const NetworkProvider: React.FC<{ children: ReactNode }> = ({ children })
|
|||||||
createNetwork,
|
createNetwork,
|
||||||
updateNetwork,
|
updateNetwork,
|
||||||
deleteNetwork,
|
deleteNetwork,
|
||||||
refreshNetworks
|
refreshNetworks,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Person, getPeople, addPerson, updatePerson, removePerson } from '../api/people';
|
import { Person, getPeople, addPerson, updatePerson, removePerson } from '../api/people';
|
||||||
import { Relationship, getRelationships, addRelationship, updateRelationship, removeRelationship } from '../api/relationships';
|
import {
|
||||||
|
Relationship,
|
||||||
|
getRelationships,
|
||||||
|
addRelationship,
|
||||||
|
updateRelationship,
|
||||||
|
removeRelationship,
|
||||||
|
} from '../api/relationships';
|
||||||
|
|
||||||
interface PersonNode extends Person {
|
interface PersonNode extends Person {
|
||||||
// Additional properties needed for the visualization
|
// Additional properties needed for the visualization
|
||||||
@ -35,18 +41,18 @@ export const useFriendshipNetwork = (networkId: string | null) => {
|
|||||||
// Fetch people and relationships in parallel
|
// Fetch people and relationships in parallel
|
||||||
const [peopleData, relationshipsData] = await Promise.all([
|
const [peopleData, relationshipsData] = await Promise.all([
|
||||||
getPeople(networkId),
|
getPeople(networkId),
|
||||||
getRelationships(networkId)
|
getRelationships(networkId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Transform to add the id property needed by the visualization
|
// Transform to add the id property needed by the visualization
|
||||||
const peopleNodes: PersonNode[] = peopleData.map(person => ({
|
const peopleNodes: PersonNode[] = peopleData.map(person => ({
|
||||||
...person,
|
...person,
|
||||||
id: person._id
|
id: person._id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const relationshipEdges: RelationshipEdge[] = relationshipsData.map(rel => ({
|
const relationshipEdges: RelationshipEdge[] = relationshipsData.map(rel => ({
|
||||||
...rel,
|
...rel,
|
||||||
id: rel._id
|
id: rel._id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setPeople(peopleNodes);
|
setPeople(peopleNodes);
|
||||||
@ -68,7 +74,7 @@ export const useFriendshipNetwork = (networkId: string | null) => {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
birthday?: string;
|
birthday?: string;
|
||||||
position?: { x: number; y: number }
|
position?: { x: number; y: number };
|
||||||
}): Promise<PersonNode> => {
|
}): Promise<PersonNode> => {
|
||||||
if (!networkId) throw new Error('No network selected');
|
if (!networkId) throw new Error('No network selected');
|
||||||
|
|
||||||
@ -90,7 +96,7 @@ export const useFriendshipNetwork = (networkId: string | null) => {
|
|||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
birthday?: string | null;
|
birthday?: string | null;
|
||||||
position?: { x: number; y: number }
|
position?: { x: number; y: number };
|
||||||
}
|
}
|
||||||
): Promise<PersonNode> => {
|
): Promise<PersonNode> => {
|
||||||
if (!networkId) throw new Error('No network selected');
|
if (!networkId) throw new Error('No network selected');
|
||||||
@ -99,9 +105,7 @@ export const useFriendshipNetwork = (networkId: string | null) => {
|
|||||||
const updatedPerson = await updatePerson(networkId, personId, personData);
|
const updatedPerson = await updatePerson(networkId, personId, personData);
|
||||||
const updatedPersonNode: PersonNode = { ...updatedPerson, id: updatedPerson._id };
|
const updatedPersonNode: PersonNode = { ...updatedPerson, id: updatedPerson._id };
|
||||||
|
|
||||||
setPeople(people.map(person =>
|
setPeople(people.map(person => (person._id === personId ? updatedPersonNode : person)));
|
||||||
person._id === personId ? updatedPersonNode : person
|
|
||||||
));
|
|
||||||
|
|
||||||
return updatedPersonNode;
|
return updatedPersonNode;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@ -121,9 +125,9 @@ export const useFriendshipNetwork = (networkId: string | null) => {
|
|||||||
setPeople(people.filter(person => person._id !== personId));
|
setPeople(people.filter(person => person._id !== personId));
|
||||||
|
|
||||||
// Remove all relationships involving this person
|
// Remove all relationships involving this person
|
||||||
setRelationships(relationships.filter(
|
setRelationships(
|
||||||
rel => rel.source !== personId && rel.target !== personId
|
relationships.filter(rel => rel.source !== personId && rel.target !== personId)
|
||||||
));
|
);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || 'Failed to delete person');
|
setError(err.message || 'Failed to delete person');
|
||||||
throw err;
|
throw err;
|
||||||
@ -162,12 +166,19 @@ export const useFriendshipNetwork = (networkId: string | null) => {
|
|||||||
if (!networkId) throw new Error('No network selected');
|
if (!networkId) throw new Error('No network selected');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updatedRelationship = await updateRelationship(networkId, relationshipId, relationshipData);
|
const updatedRelationship = await updateRelationship(
|
||||||
const updatedRelationshipEdge: RelationshipEdge = { ...updatedRelationship, id: updatedRelationship._id };
|
networkId,
|
||||||
|
relationshipId,
|
||||||
|
relationshipData
|
||||||
|
);
|
||||||
|
const updatedRelationshipEdge: RelationshipEdge = {
|
||||||
|
...updatedRelationship,
|
||||||
|
id: updatedRelationship._id,
|
||||||
|
};
|
||||||
|
|
||||||
setRelationships(relationships.map(rel =>
|
setRelationships(
|
||||||
rel._id === relationshipId ? updatedRelationshipEdge : rel
|
relationships.map(rel => (rel._id === relationshipId ? updatedRelationshipEdge : rel))
|
||||||
));
|
);
|
||||||
|
|
||||||
return updatedRelationshipEdge;
|
return updatedRelationshipEdge;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@ -205,6 +216,6 @@ export const useFriendshipNetwork = (networkId: string | null) => {
|
|||||||
createRelationship,
|
createRelationship,
|
||||||
updateRelationship: updateRelationshipData,
|
updateRelationship: updateRelationshipData,
|
||||||
deleteRelationship,
|
deleteRelationship,
|
||||||
refreshNetwork
|
refreshNetwork,
|
||||||
};
|
};
|
||||||
};
|
};
|
File diff suppressed because it is too large
Load Diff
1965
package-lock.json
generated
Normal file
1965
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,10 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node dist/server.js",
|
"start": "node dist/server.js",
|
||||||
"dev": "nodemon --exec ts-node src/server.ts",
|
"dev": "nodemon --exec ts-node src/server.ts",
|
||||||
"build": "tsc"
|
"build": "tsc",
|
||||||
|
"format": "prettier --write \"src/**/*.{ts,js,json}\"",
|
||||||
|
"format:check": "prettier --check \"src/**/*.{ts,js,json}\"",
|
||||||
|
"format:all": "npm run format && cd frontend && npm run format"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -38,6 +41,7 @@
|
|||||||
"@types/mongoose": "^5.11.97",
|
"@types/mongoose": "^5.11.97",
|
||||||
"@types/node": "^22.14.1",
|
"@types/node": "^22.14.1",
|
||||||
"nodemon": "^3.1.9",
|
"nodemon": "^3.1.9",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
|
14
src/app.ts
14
src/app.ts
@ -6,20 +6,21 @@ import authRoutes from './routes/auth.routes';
|
|||||||
import networkRoutes from './routes/network.routes';
|
import networkRoutes from './routes/network.routes';
|
||||||
import peopleRoutes from './routes/people.routes';
|
import peopleRoutes from './routes/people.routes';
|
||||||
import relationshipRoutes from './routes/relationship.routes';
|
import relationshipRoutes from './routes/relationship.routes';
|
||||||
import path from "node:path";
|
import path from 'node:path';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const app: Application = express();
|
const app: Application = express();
|
||||||
|
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(cors({
|
app.use(
|
||||||
|
cors({
|
||||||
origin: process.env.CLIENT_URL || 'http://localhost:3000',
|
origin: process.env.CLIENT_URL || 'http://localhost:3000',
|
||||||
credentials: true
|
credentials: true,
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
@ -32,11 +33,10 @@ app.use('/api/networks', relationshipRoutes);
|
|||||||
res.send('Friendship Network API is running');
|
res.send('Friendship Network API is running');
|
||||||
});*/
|
});*/
|
||||||
|
|
||||||
|
|
||||||
app.use(express.static(path.join(__dirname, '../frontend/dist/')));
|
app.use(express.static(path.join(__dirname, '../frontend/dist/')));
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
res.sendFile(path.join(__dirname, '..', 'frontend/dist/index.html'));
|
res.sendFile(path.join(__dirname, '..', 'frontend/dist/index.html'));
|
||||||
})
|
});
|
||||||
|
|
||||||
export default app;
|
export default app;
|
@ -38,9 +38,8 @@ export const register = async (req: Request, res: Response): Promise<void> => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!process.env.ENABLE_REGISTRATION)
|
if (!process.env.ENABLE_REGISTRATION) {
|
||||||
{
|
res.status(403).json({ errors: ['Registration is disabled'] });
|
||||||
res.status(403).json({errors: ["Registration is disabled"]});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,10 +15,7 @@ export const getUserNetworks = async (req: UserRequest, res: Response): Promise<
|
|||||||
// 1. Belong to the current user, OR
|
// 1. Belong to the current user, OR
|
||||||
// 2. Are public networks (created by any user)
|
// 2. Are public networks (created by any user)
|
||||||
const networks = await Network.find({
|
const networks = await Network.find({
|
||||||
$or: [
|
$or: [{ owner: req.user._id }, { isPublic: true }],
|
||||||
{ owner: req.user._id },
|
|
||||||
{ isPublic: true }
|
|
||||||
]
|
|
||||||
}).populate('owner', 'username _id'); // Populate owner field with username
|
}).populate('owner', 'username _id'); // Populate owner field with username
|
||||||
|
|
||||||
res.json({ success: true, data: networks });
|
res.json({ success: true, data: networks });
|
||||||
|
@ -165,10 +165,12 @@ export const removePerson = async (req: UserRequest, res: Response): Promise<voi
|
|||||||
// Remove the person
|
// Remove the person
|
||||||
await person.deleteOne(); // Changed from remove() to deleteOne()
|
await person.deleteOne(); // Changed from remove() to deleteOne()
|
||||||
|
|
||||||
res.json({ success: true, message: 'Person and associated relationships removed successfully' });
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Person and associated relationships removed successfully',
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Remove person error:', error);
|
console.error('Remove person error:', error);
|
||||||
res.status(500).json({ message: 'Server error' });
|
res.status(500).json({ message: 'Server error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -9,7 +9,8 @@ const JWT_SECRET = process.env.JWT_SECRET || 'your_jwt_secret_key_change_this';
|
|||||||
export const auth = async (req: UserRequest, res: Response, next: NextFunction): Promise<void> => {
|
export const auth = async (req: UserRequest, res: Response, next: NextFunction): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
// Get token from cookie or authorization header
|
// Get token from cookie or authorization header
|
||||||
const token = req.cookies.token ||
|
const token =
|
||||||
|
req.cookies.token ||
|
||||||
(req.headers.authorization && req.headers.authorization.startsWith('Bearer')
|
(req.headers.authorization && req.headers.authorization.startsWith('Bearer')
|
||||||
? req.headers.authorization.split(' ')[1]
|
? req.headers.authorization.split(' ')[1]
|
||||||
: null);
|
: null);
|
||||||
|
@ -2,7 +2,11 @@ import { Response, NextFunction } from 'express';
|
|||||||
import Network from '../models/network.model';
|
import Network from '../models/network.model';
|
||||||
import { UserRequest } from '../types/express';
|
import { UserRequest } from '../types/express';
|
||||||
|
|
||||||
export const checkNetworkAccess = async (req: UserRequest, res: Response, next: NextFunction): Promise<void> => {
|
export const checkNetworkAccess = async (
|
||||||
|
req: UserRequest,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const networkId = req.params.networkId;
|
const networkId = req.params.networkId;
|
||||||
|
|
||||||
|
@ -33,4 +33,3 @@ const NetworkSchema = new Schema(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export default mongoose.model<INetwork>('Network', NetworkSchema);
|
export default mongoose.model<INetwork>('Network', NetworkSchema);
|
||||||
|
|
||||||
|
@ -40,9 +40,6 @@ const RelationshipSchema = new Schema(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Create compound index to ensure unique relationships in a network
|
// Create compound index to ensure unique relationships in a network
|
||||||
RelationshipSchema.index(
|
RelationshipSchema.index({ source: 1, target: 1, network: 1 }, { unique: true });
|
||||||
{ source: 1, target: 1, network: 1 },
|
|
||||||
{ unique: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
export default mongoose.model<IRelationship>('Relationship', RelationshipSchema);
|
export default mongoose.model<IRelationship>('Relationship', RelationshipSchema);
|
@ -18,9 +18,7 @@ router.get('/', networkController.getUserNetworks);
|
|||||||
// @access Private
|
// @access Private
|
||||||
router.post(
|
router.post(
|
||||||
'/',
|
'/',
|
||||||
[
|
[check('name', 'Network name is required').not().isEmpty()],
|
||||||
check('name', 'Network name is required').not().isEmpty(),
|
|
||||||
],
|
|
||||||
networkController.createNetwork
|
networkController.createNetwork
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -34,9 +32,7 @@ router.get('/:id', networkController.getNetwork);
|
|||||||
// @access Private
|
// @access Private
|
||||||
router.put(
|
router.put(
|
||||||
'/:id',
|
'/:id',
|
||||||
[
|
[check('name', 'Network name is required if provided').optional().not().isEmpty()],
|
||||||
check('name', 'Network name is required if provided').optional().not().isEmpty(),
|
|
||||||
],
|
|
||||||
networkController.updateNetwork
|
networkController.updateNetwork
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -22,7 +22,13 @@ router.post(
|
|||||||
[
|
[
|
||||||
check('source', 'Source person ID is required').not().isEmpty().isMongoId(),
|
check('source', 'Source person ID is required').not().isEmpty().isMongoId(),
|
||||||
check('target', 'Target person ID is required').not().isEmpty().isMongoId(),
|
check('target', 'Target person ID is required').not().isEmpty().isMongoId(),
|
||||||
check('type', 'Relationship type is required').isIn(['freund', 'partner', 'familie', 'arbeitskolleg', 'custom']),
|
check('type', 'Relationship type is required').isIn([
|
||||||
|
'freund',
|
||||||
|
'partner',
|
||||||
|
'familie',
|
||||||
|
'arbeitskolleg',
|
||||||
|
'custom',
|
||||||
|
]),
|
||||||
check('customType', 'Custom type is required when type is custom')
|
check('customType', 'Custom type is required when type is custom')
|
||||||
.if(check('type').equals('custom'))
|
.if(check('type').equals('custom'))
|
||||||
.not()
|
.not()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user