add prettifier

This commit is contained in:
philipredstone 2025-04-15 14:46:06 +02:00
parent c078610c4d
commit eceacf2117
43 changed files with 10946 additions and 6173 deletions

19
.prettierignore Normal file
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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;
}; };

View File

@ -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}`);
}; };

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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;

View File

@ -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}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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"
} }

View File

@ -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;

View File

@ -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;
} }

View File

@ -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 });

View File

@ -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' });
} }
}; };

View File

@ -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);

View File

@ -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;

View File

@ -33,4 +33,3 @@ const NetworkSchema = new Schema(
); );
export default mongoose.model<INetwork>('Network', NetworkSchema); export default mongoose.model<INetwork>('Network', NetworkSchema);

View File

@ -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);

View File

@ -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
); );

View File

@ -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()

376
yarn.lock

File diff suppressed because it is too large Load Diff