Compare commits

..

No commits in common. "bbb3645d9934d6901e4e58343d1aa91c8b693cd1" and "9eddb1b54714b10e8dde0233db634242e4a0b2b0" have entirely different histories.

19 changed files with 539 additions and 323 deletions

1
.gitignore vendored
View File

@ -9,5 +9,6 @@ frontend/bun.lockb
node_modules node_modules
dist dist
.env .env
*bun.lockb
*yarn.lock *yarn.lock
*package-lock.json *package-lock.json

View File

@ -1,78 +1,47 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM node:22 AS frontend-builder
# Frontend builder stage # Workingdir
FROM oven/bun:1 AS frontend-builder
WORKDIR /frontend WORKDIR /frontend
# Install dependencies # Copy files
COPY frontend/package.json frontend/bun.lockb* ./
RUN bun install --frozen-lockfile
# Copy source files
COPY frontend/src/ src/ COPY frontend/src/ src/
COPY frontend/package.json .
COPY frontend/index.html . COPY frontend/index.html .
COPY frontend/tsconfig.json . COPY frontend/tsconfig.json .
COPY frontend/vite.config.mjs . COPY frontend/vite.config.mjs .
# Build the frontend # Install libs
RUN bun run build RUN yarn install
# Build to dist/
RUN yarn build
# Backend builder stage
FROM oven/bun:1 AS backend-builder FROM node:22 AS backend-builder
# Workingdir
WORKDIR /app WORKDIR /app
# Copy package files first for better caching COPY package.json .
COPY package.json bun.lockb* ./ RUN yarn install
# Install dependencies
RUN bun install --frozen-lockfile
# Copy source files
COPY tsconfig.json . COPY tsconfig.json .
COPY src/ src/ COPY src/ src/
# Build the backend # Build to dist/
RUN bun run build RUN yarn run build
# Final production stage
FROM oven/bun:1-slim
LABEL "org.opencontainers.image.title"="Relnet"
LABEL "org.opencontainers.image.source"="https://github.com/philipredstone/relnet"
LABEL "org.opencontainers.image.url"="https://github.com/philipredstone/relnet"
LABEL "org.opencontainers.image.description"="A dynamic web application for visualizing and managing a friendship network. This project uses graphs to display interactive connection graphs between friends, and includes a backend server built with Node.js, TypeScript, Express, and Mongoose for data persistence."
LABEL "org.opencontainers.image.version"="1.0.0"
LABEL "VERSION"="1.0.0"
LABEL maintainer="Tobias Hopp and Philip Rothstein"
# Install curl for healthcheck
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get -qq -y install curl && \
rm -rf /var/cache/apt/archives /var/lib/apt/lists/*
WORKDIR /app # Final stage
FROM node:22-slim
COPY --from=frontend-builder /frontend/dist/ frontend/dist
COPY --from=backend-builder /app/dist/ dist/
COPY --from=backend-builder /app/node_modules node_modules
COPY package.json .
# Copy built artifacts from previous stages
COPY --from=frontend-builder /frontend/dist/ ./frontend/dist
COPY --from=backend-builder /app/dist/ ./dist/
# Only copy production dependencies
COPY package.json ./
RUN bun install --production --frozen-lockfile
# Set environment variables
ENV PORT=80 ENV PORT=80
ENV MONGODB_URI=mongodb://db:27017/friendship-network ENV MONGODB_URI=mongodb://db:27017/friendship-network
ENV APP_URL=http://localhost:80 ENV APP_URL=http://localhost:80
ENV ENABLE_REGISTRATION=true ENV ENABLE_REGISTRATION=true
# Expose the port CMD ["yarn", "run", "start"]
EXPOSE 80
# Health check to verify the application is running
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD /bin/sh -c 'curl -f http://localhost:$PORT/api/health || exit 1'
# Start the application
CMD ["bun", "dist/server.js"]

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { BrowserRouter as Router, Navigate, Route, Routes } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './context/AuthContext'; import { AuthProvider, useAuth } from './context/AuthContext';
import { NetworkProvider } from './context/NetworkContext'; import { NetworkProvider } from './context/NetworkContext';
import Login from './components/auth/Login'; import Login from './components/auth/Login';

View File

@ -44,7 +44,7 @@ export const getUserNetworks = async (): Promise<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 }>( const response = await axios.post<{ success: boolean; data: Network }>(
`${API_URL}/networks`, `${API_URL}/networks`,
data, data
); );
return response.data.data; return response.data.data;
}; };
@ -52,7 +52,7 @@ export const createNetwork = async (data: CreateNetworkData): Promise<Network> =
// 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 }>( const response = await axios.get<{ success: boolean; data: Network }>(
`${API_URL}/networks/${id}`, `${API_URL}/networks/${id}`
); );
return response.data.data; return response.data.data;
}; };
@ -61,7 +61,7 @@ export const getNetwork = async (id: string): Promise<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 }>( const response = await axios.put<{ success: boolean; data: Network }>(
`${API_URL}/networks/${id}`, `${API_URL}/networks/${id}`,
data, data
); );
return response.data.data; return response.data.data;
}; };

View File

@ -44,7 +44,7 @@ export interface UpdatePersonData {
// Get all people in a network // Get all people in a network
export const getPeople = async (networkId: string): Promise<Person[]> => { export const getPeople = async (networkId: string): Promise<Person[]> => {
const response = await axios.get<{ success: boolean; data: Person[] }>( const response = await axios.get<{ success: boolean; data: Person[] }>(
`${API_URL}/networks/${networkId}/people`, `${API_URL}/networks/${networkId}/people`
); );
return response.data.data; return response.data.data;
}; };
@ -53,7 +53,7 @@ export const getPeople = async (networkId: string): Promise<Person[]> => {
export const addPerson = async (networkId: string, data: CreatePersonData): Promise<Person> => { export const addPerson = async (networkId: string, data: CreatePersonData): Promise<Person> => {
const response = await axios.post<{ success: boolean; data: Person }>( const response = await axios.post<{ success: boolean; data: Person }>(
`${API_URL}/networks/${networkId}/people`, `${API_URL}/networks/${networkId}/people`,
data, data
); );
return response.data.data; return response.data.data;
}; };
@ -62,11 +62,11 @@ export const addPerson = async (networkId: string, data: CreatePersonData): Prom
export const updatePerson = async ( export const updatePerson = async (
networkId: string, networkId: string,
personId: string, personId: string,
data: UpdatePersonData, data: UpdatePersonData
): Promise<Person> => { ): Promise<Person> => {
const response = await axios.put<{ success: boolean; data: Person }>( const response = await axios.put<{ success: boolean; data: Person }>(
`${API_URL}/networks/${networkId}/people/${personId}`, `${API_URL}/networks/${networkId}/people/${personId}`,
data, data
); );
return response.data.data; return response.data.data;
}; };

View File

@ -33,7 +33,7 @@ export interface UpdateRelationshipData {
// Get all relationships in a network // Get all relationships in a network
export const getRelationships = async (networkId: string): Promise<Relationship[]> => { export const getRelationships = async (networkId: string): Promise<Relationship[]> => {
const response = await axios.get<{ success: boolean; data: Relationship[] }>( const response = await axios.get<{ success: boolean; data: Relationship[] }>(
`${API_URL}/networks/${networkId}/relationships`, `${API_URL}/networks/${networkId}/relationships`
); );
return response.data.data; return response.data.data;
}; };
@ -41,11 +41,11 @@ export const getRelationships = async (networkId: string): Promise<Relationship[
// Add a relationship to the network // Add a relationship to the network
export const addRelationship = async ( export const addRelationship = async (
networkId: string, networkId: string,
data: CreateRelationshipData, data: CreateRelationshipData
): Promise<Relationship> => { ): Promise<Relationship> => {
const response = await axios.post<{ success: boolean; data: Relationship }>( const response = await axios.post<{ success: boolean; data: Relationship }>(
`${API_URL}/networks/${networkId}/relationships`, `${API_URL}/networks/${networkId}/relationships`,
data, data
); );
return response.data.data; return response.data.data;
}; };
@ -54,11 +54,11 @@ export const addRelationship = async (
export const updateRelationship = async ( export const updateRelationship = async (
networkId: string, networkId: string,
relationshipId: string, relationshipId: string,
data: UpdateRelationshipData, data: UpdateRelationshipData
): Promise<Relationship> => { ): Promise<Relationship> => {
const response = await axios.put<{ success: boolean; data: Relationship }>( const response = await axios.put<{ success: boolean; data: Relationship }>(
`${API_URL}/networks/${networkId}/relationships/${relationshipId}`, `${API_URL}/networks/${networkId}/relationships/${relationshipId}`,
data, data
); );
return response.data.data; return response.data.data;
}; };
@ -66,7 +66,7 @@ export const updateRelationship = async (
// Remove a relationship // Remove a relationship
export const removeRelationship = async ( export const removeRelationship = async (
networkId: string, networkId: string,
relationshipId: string, relationshipId: string
): Promise<void> => { ): Promise<void> => {
await axios.delete(`${API_URL}/networks/${networkId}/relationships/${relationshipId}`); await axios.delete(`${API_URL}/networks/${networkId}/relationships/${relationshipId}`);
}; };

View File

@ -1,5 +1,4 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState, useCallback } from 'react';
import { GraphData } from 'react-force-graph-2d';
// Define types for graph elements // Define types for graph elements
interface NodeData { interface NodeData {
@ -20,18 +19,15 @@ interface EdgeData {
width: number; width: number;
} }
interface CustomGraphData extends GraphData { interface GraphData {
nodes: NodeData[]; nodes: NodeData[];
edges: EdgeData[]; edges: EdgeData[];
} }
interface CanvasGraphProps { interface CanvasGraphProps {
data: CustomGraphData, data: GraphData;
width: number, width: number;
height: number, height: number;
zoomLevel: number,
onNodeClick: (nodeId: string) => void,
onNodeDrag: (nodeId, x, y) => void
} }
// Physics constants // Physics constants
@ -46,7 +42,7 @@ const CENTER_GRAVITY = 0.01; // Force pulling nodes to the center
const MAX_VELOCITY = 5; // Maximum velocity to prevent wild movement const MAX_VELOCITY = 5; // Maximum velocity to prevent wild movement
const COOLING_FACTOR = 0.99; // System gradually cools down const COOLING_FACTOR = 0.99; // System gradually cools down
const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height, zoomLevel, onNodeClick, onNodeDrag }) => { const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height }) => {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
// State for interactions // State for interactions
@ -85,7 +81,7 @@ const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height, zoomLeve
// Skip if we already have positions for all nodes // Skip if we already have positions for all nodes
const allNodesHavePositions = data.nodes.every( const allNodesHavePositions = data.nodes.every(
node => node =>
nodePositions[node.id] && (nodePositions[node.id].x !== 0 || nodePositions[node.id].y !== 0), nodePositions[node.id] && (nodePositions[node.id].x !== 0 || nodePositions[node.id].y !== 0)
); );
if (allNodesHavePositions) { if (allNodesHavePositions) {
@ -258,7 +254,7 @@ const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height, zoomLeve
// Limit maximum velocity to prevent wild movement // Limit maximum velocity to prevent wild movement
const speed = Math.sqrt( const speed = Math.sqrt(
newPositions[node.id].vx * newPositions[node.id].vx + newPositions[node.id].vx * newPositions[node.id].vx +
newPositions[node.id].vy * newPositions[node.id].vy, newPositions[node.id].vy * newPositions[node.id].vy
); );
if (speed > MAX_VELOCITY) { if (speed > MAX_VELOCITY) {
newPositions[node.id].vx = (newPositions[node.id].vx / speed) * MAX_VELOCITY; newPositions[node.id].vx = (newPositions[node.id].vx / speed) * MAX_VELOCITY;
@ -336,7 +332,7 @@ const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height, zoomLeve
return null; return null;
}, },
[data.nodes, nodePositions, panOffset, scale], [data.nodes, nodePositions, panOffset, scale]
); // FIX: Added proper dependencies ); // FIX: Added proper dependencies
// Mouse event handlers // Mouse event handlers
@ -376,7 +372,7 @@ const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height, zoomLeve
setPanStart({ x, y }); setPanStart({ x, y });
} }
}, },
[findNodeAtPosition, nodePositions, panOffset, scale], [findNodeAtPosition, nodePositions, panOffset, scale]
); // FIX: Added proper dependencies ); // FIX: Added proper dependencies
const handleMouseMove = useCallback( const handleMouseMove = useCallback(
@ -422,7 +418,7 @@ const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height, zoomLeve
setPanStart({ x, y }); setPanStart({ x, y });
} }
}, },
[findNodeAtPosition, draggedNode, isPanning, offsetX, offsetY, panOffset, panStart, scale], [findNodeAtPosition, draggedNode, isPanning, offsetX, offsetY, panOffset, panStart, scale]
); // FIX: Added proper dependencies ); // FIX: Added proper dependencies
const handleMouseUp = useCallback(() => { const handleMouseUp = useCallback(() => {
@ -455,7 +451,7 @@ const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height, zoomLeve
setScale(newScale); setScale(newScale);
setPanOffset({ x: newPanOffsetX, y: newPanOffsetY }); setPanOffset({ x: newPanOffsetX, y: newPanOffsetY });
}, },
[scale, panOffset], [scale, panOffset]
); );
const toggleAutoLayout = useCallback(() => { const toggleAutoLayout = useCallback(() => {
@ -475,7 +471,7 @@ const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height, zoomLeve
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.fillText(autoLayout ? 'Physics: ON' : 'Physics: OFF', width - 70, 40); ctx.fillText(autoLayout ? 'Physics: ON' : 'Physics: OFF', width - 70, 40);
}, },
[autoLayout, width], [autoLayout, width]
); );
// Draw function - FIX: Properly memoized with all dependencies // Draw function - FIX: Properly memoized with all dependencies
@ -596,7 +592,7 @@ const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height, zoomLeve
pos.x - textWidth / 2 - padding, pos.x - textWidth / 2 - padding,
pos.y + NODE_RADIUS + 5, pos.y + NODE_RADIUS + 5,
textWidth + padding * 2, textWidth + padding * 2,
textHeight + padding * 2, textHeight + padding * 2
); );
ctx.fillStyle = 'white'; ctx.fillStyle = 'white';
@ -632,7 +628,7 @@ const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height, zoomLeve
toggleAutoLayout(); toggleAutoLayout();
} }
}, },
[width, toggleAutoLayout], [width, toggleAutoLayout]
); // FIX: Added proper dependencies ); // FIX: Added proper dependencies
// FIX: Ensure continuous rendering with requestAnimationFrame // FIX: Ensure continuous rendering with requestAnimationFrame

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Transition } from '@headlessui/react'; import { Transition } from '@headlessui/react';
import { FaTimes } from 'react-icons/fa'; import { FaTimes } from 'react-icons/fa';
@ -52,7 +52,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
text, text,
position = 'top', position = 'top',
delay = 300, delay = 300,
}) => { }) => {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | null>(null); const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | null>(null);
@ -226,7 +226,7 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
confirmText = 'Confirm', confirmText = 'Confirm',
cancelText = 'Cancel', cancelText = 'Cancel',
variant = 'danger', variant = 'danger',
}) => { }) => {
const variantClasses = { const variantClasses = {
danger: 'bg-red-600 hover:bg-red-700 focus:ring-red-500', danger: 'bg-red-600 hover:bg-red-700 focus:ring-red-500',
warning: 'bg-amber-600 hover:bg-amber-700 focus:ring-amber-500', warning: 'bg-amber-600 hover:bg-amber-700 focus:ring-amber-500',
@ -274,7 +274,7 @@ export const NetworkStats: React.FC<NetworkStatsProps> = ({ people, relationship
people.length > 0 ? (relationships.length / people.length).toFixed(1) : '0.0'; people.length > 0 ? (relationships.length / people.length).toFixed(1) : '0.0';
const isolatedPeople = people.filter( const isolatedPeople = people.filter(
person => !relationships.some(r => r.source === person._id || r.target === person._id), person => !relationships.some(r => r.source === person._id || r.target === person._id)
).length; ).length;
// Find most connected person // Find most connected person
@ -286,7 +286,7 @@ export const NetworkStats: React.FC<NetworkStatsProps> = ({ people, relationship
const mostConnected = const mostConnected =
personConnectionCounts.length > 0 personConnectionCounts.length > 0
? personConnectionCounts.reduce((prev, current) => ? personConnectionCounts.reduce((prev, current) =>
prev.count > current.count ? prev : current, prev.count > current.count ? prev : current
) )
: null; : null;
@ -356,7 +356,7 @@ export const Toast: React.FC<ToastProps> = ({
onClose, onClose,
autoClose = true, autoClose = true,
duration = 3000, duration = 3000,
}) => { }) => {
useEffect(() => { useEffect(() => {
if (autoClose) { if (autoClose) {
const timer = setTimeout(() => { const timer = setTimeout(() => {
@ -432,7 +432,7 @@ export const Button: React.FC<ButtonProps> = ({
className = '', className = '',
disabled = false, disabled = false,
fullWidth = false, fullWidth = false,
}) => { }) => {
const variantClasses = { const variantClasses = {
primary: 'bg-indigo-600 hover:bg-indigo-700 text-white focus:ring-indigo-500', primary: 'bg-indigo-600 hover:bg-indigo-700 text-white focus:ring-indigo-500',
secondary: 'bg-slate-700 hover:bg-slate-600 text-white focus:ring-slate-500', secondary: 'bg-slate-700 hover:bg-slate-600 text-white focus:ring-slate-500',
@ -488,7 +488,7 @@ export const FormField: React.FC<FormFieldProps> = ({
className = '', className = '',
children, children,
labelClassName = '', labelClassName = '',
}) => { }) => {
return ( return (
<div className={`mb-4 ${className}`}> <div className={`mb-4 ${className}`}>
<label <label

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom'; import { Link, useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { FaNetworkWired, FaSignOutAlt, FaUser } from 'react-icons/fa'; import { FaUser, FaSignOutAlt, FaNetworkWired } from 'react-icons/fa';
const Header: React.FC = () => { const Header: React.FC = () => {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
@ -63,8 +63,7 @@ const Header: React.FC = () => {
</div> </div>
</button> </button>
<div <div className="absolute right-0 mt-2 w-48 bg-slate-800 rounded-md shadow-lg py-1 z-10 border border-slate-700 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
className="absolute right-0 mt-2 w-48 bg-slate-800 rounded-md shadow-lg py-1 z-10 border border-slate-700 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
<button <button
onClick={handleLogout} onClick={handleLogout}
className="w-full text-left px-4 py-2 text-sm text-slate-300 hover:bg-slate-700 flex items-center" className="w-full text-left px-4 py-2 text-sm text-slate-300 hover:bg-slate-700 flex items-center"

View File

@ -2,8 +2,8 @@ import React, { useState } from 'react';
import { useNetworks } from '../../context/NetworkContext'; import { useNetworks } from '../../context/NetworkContext';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { AnimatePresence, motion } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { FaEye, FaGlobe, FaLock, FaNetworkWired, FaPlus, FaTimes, FaTrash } from 'react-icons/fa'; import { FaPlus, FaNetworkWired, FaTrash, FaEye, FaGlobe, FaLock, FaTimes } from 'react-icons/fa';
const NetworkList: React.FC = () => { const NetworkList: React.FC = () => {
const { networks, loading, error, createNetwork, deleteNetwork } = useNetworks(); const { networks, loading, error, createNetwork, deleteNetwork } = useNetworks();

View File

@ -1,12 +1,12 @@
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react'; import React, { createContext, useState, useEffect, useContext, ReactNode } from 'react';
import { import {
User,
getCurrentUser, getCurrentUser,
login as apiLogin, login as apiLogin,
LoginData,
logout as apiLogout,
register as apiRegister, register as apiRegister,
logout as apiLogout,
LoginData,
RegisterData, RegisterData,
User,
} from '../api/auth'; } from '../api/auth';
interface AuthContextProps { interface AuthContextProps {

View File

@ -1,11 +1,11 @@
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react'; import React, { createContext, useState, useEffect, useContext, ReactNode } from 'react';
import { import {
createNetwork as apiCreateNetwork,
CreateNetworkData,
deleteNetwork as apiDeleteNetwork,
getUserNetworks,
Network, Network,
getUserNetworks,
createNetwork as apiCreateNetwork,
updateNetwork as apiUpdateNetwork, updateNetwork as apiUpdateNetwork,
deleteNetwork as apiDeleteNetwork,
CreateNetworkData,
UpdateNetworkData, UpdateNetworkData,
} from '../api/network'; } from '../api/network';
import { useAuth } from './AuthContext'; import { useAuth } from './AuthContext';

View File

@ -1,11 +1,11 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { addPerson, getPeople, Person, removePerson, updatePerson } from '../api/people'; import { Person, getPeople, addPerson, updatePerson, removePerson } from '../api/people';
import { import {
addRelationship,
getRelationships,
Relationship, Relationship,
removeRelationship, getRelationships,
addRelationship,
updateRelationship, updateRelationship,
removeRelationship,
} from '../api/relationships'; } from '../api/relationships';
interface PersonNode extends Person { interface PersonNode extends Person {
@ -24,7 +24,7 @@ const DEFAULT_POLL_INTERVAL = 5000;
// Custom hook to manage friendship network data // Custom hook to manage friendship network data
export const useFriendshipNetwork = ( export const useFriendshipNetwork = (
networkId: string | null, networkId: string | null,
pollInterval = DEFAULT_POLL_INTERVAL, pollInterval = DEFAULT_POLL_INTERVAL
) => { ) => {
const [people, setPeople] = useState<PersonNode[]>([]); const [people, setPeople] = useState<PersonNode[]>([]);
const [relationships, setRelationships] = useState<RelationshipEdge[]>([]); const [relationships, setRelationships] = useState<RelationshipEdge[]>([]);
@ -70,7 +70,7 @@ export const useFriendshipNetwork = (
// Generate hashes to detect changes // Generate hashes to detect changes
const positionsHash = JSON.stringify(peopleNodes.map(p => ({ id: p.id, pos: p.position }))); const positionsHash = JSON.stringify(peopleNodes.map(p => ({ id: p.id, pos: p.position })));
const relationshipsHash = JSON.stringify( const relationshipsHash = JSON.stringify(
relationshipEdges.map(r => ({ id: r.id, src: r.source, tgt: r.target, type: r.type })), relationshipEdges.map(r => ({ id: r.id, src: r.source, tgt: r.target, type: r.type }))
); );
// Handle people updates // Handle people updates
@ -187,7 +187,7 @@ export const useFriendshipNetwork = (
} }
} }
}, },
[networkId], [networkId]
); );
// Set up polling for network data // Set up polling for network data
@ -234,7 +234,7 @@ export const useFriendshipNetwork = (
// Update the reference hash to avoid unnecessary state updates on next poll // Update the reference hash to avoid unnecessary state updates on next poll
lastPeopleUpdateRef.current = JSON.stringify( lastPeopleUpdateRef.current = JSON.stringify(
updatedPeople.map(p => ({ id: p.id, pos: p.position })), updatedPeople.map(p => ({ id: p.id, pos: p.position }))
); );
return newPersonNode; return newPersonNode;
@ -252,7 +252,7 @@ export const useFriendshipNetwork = (
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');
@ -262,14 +262,14 @@ export const useFriendshipNetwork = (
// Update the local state // Update the local state
const updatedPeople = people.map(person => const updatedPeople = people.map(person =>
person._id === personId ? updatedPersonNode : person, person._id === personId ? updatedPersonNode : person
); );
setPeople(updatedPeople); setPeople(updatedPeople);
// Update the reference hash if position changed to avoid unnecessary state updates on next poll // Update the reference hash if position changed to avoid unnecessary state updates on next poll
if (personData.position) { if (personData.position) {
lastPeopleUpdateRef.current = JSON.stringify( lastPeopleUpdateRef.current = JSON.stringify(
updatedPeople.map(p => ({ id: p.id, pos: p.position })), updatedPeople.map(p => ({ id: p.id, pos: p.position }))
); );
} }
@ -293,16 +293,16 @@ export const useFriendshipNetwork = (
// Remove all relationships involving this person // Remove all relationships involving this person
const updatedRelationships = relationships.filter( const updatedRelationships = relationships.filter(
rel => rel.source !== personId && rel.target !== personId, rel => rel.source !== personId && rel.target !== personId
); );
setRelationships(updatedRelationships); setRelationships(updatedRelationships);
// Update both reference hashes to avoid unnecessary state updates on next poll // Update both reference hashes to avoid unnecessary state updates on next poll
lastPeopleUpdateRef.current = JSON.stringify( lastPeopleUpdateRef.current = JSON.stringify(
updatedPeople.map(p => ({ id: p.id, pos: p.position })), updatedPeople.map(p => ({ id: p.id, pos: p.position }))
); );
lastRelationshipsUpdateRef.current = JSON.stringify( lastRelationshipsUpdateRef.current = JSON.stringify(
updatedRelationships.map(r => ({ id: r.id, src: r.source, tgt: r.target, type: r.type })), updatedRelationships.map(r => ({ id: r.id, src: r.source, tgt: r.target, type: r.type }))
); );
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Failed to delete person'); setError(err.message || 'Failed to delete person');
@ -328,7 +328,7 @@ export const useFriendshipNetwork = (
// Update the relationship hash to avoid unnecessary state updates on next poll // Update the relationship hash to avoid unnecessary state updates on next poll
lastRelationshipsUpdateRef.current = JSON.stringify( lastRelationshipsUpdateRef.current = JSON.stringify(
updatedRelationships.map(r => ({ id: r.id, src: r.source, tgt: r.target, type: r.type })), updatedRelationships.map(r => ({ id: r.id, src: r.source, tgt: r.target, type: r.type }))
); );
return newRelationshipEdge; return newRelationshipEdge;
@ -344,7 +344,7 @@ export const useFriendshipNetwork = (
relationshipData: { relationshipData: {
type?: 'freund' | 'partner' | 'familie' | 'arbeitskolleg' | 'custom'; type?: 'freund' | 'partner' | 'familie' | 'arbeitskolleg' | 'custom';
customType?: string; customType?: string;
}, }
): Promise<RelationshipEdge> => { ): Promise<RelationshipEdge> => {
if (!networkId) throw new Error('No network selected'); if (!networkId) throw new Error('No network selected');
@ -352,7 +352,7 @@ export const useFriendshipNetwork = (
const updatedRelationship = await updateRelationship( const updatedRelationship = await updateRelationship(
networkId, networkId,
relationshipId, relationshipId,
relationshipData, relationshipData
); );
const updatedRelationshipEdge: RelationshipEdge = { const updatedRelationshipEdge: RelationshipEdge = {
...updatedRelationship, ...updatedRelationship,
@ -360,13 +360,13 @@ export const useFriendshipNetwork = (
}; };
const updatedRelationships = relationships.map(rel => const updatedRelationships = relationships.map(rel =>
rel._id === relationshipId ? updatedRelationshipEdge : rel, rel._id === relationshipId ? updatedRelationshipEdge : rel
); );
setRelationships(updatedRelationships); setRelationships(updatedRelationships);
// Update the relationship hash to avoid unnecessary state updates on next poll // Update the relationship hash to avoid unnecessary state updates on next poll
lastRelationshipsUpdateRef.current = JSON.stringify( lastRelationshipsUpdateRef.current = JSON.stringify(
updatedRelationships.map(r => ({ id: r.id, src: r.source, tgt: r.target, type: r.type })), updatedRelationships.map(r => ({ id: r.id, src: r.source, tgt: r.target, type: r.type }))
); );
return updatedRelationshipEdge; return updatedRelationshipEdge;
@ -387,7 +387,7 @@ export const useFriendshipNetwork = (
// Update the relationship hash to avoid unnecessary state updates on next poll // Update the relationship hash to avoid unnecessary state updates on next poll
lastRelationshipsUpdateRef.current = JSON.stringify( lastRelationshipsUpdateRef.current = JSON.stringify(
updatedRelationships.map(r => ({ id: r.id, src: r.source, tgt: r.target, type: r.type })), updatedRelationships.map(r => ({ id: r.id, src: r.source, tgt: r.target, type: r.type }))
); );
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Failed to delete relationship'); setError(err.message || 'Failed to delete relationship');

View File

@ -11,7 +11,7 @@ if (rootElement) {
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode>, </React.StrictMode>
); );
} else { } else {
console.error('Root element not found'); console.error('Root element not found');

View File

@ -51,11 +51,6 @@ app.use('/api/networks', networkRoutes);
app.use('/api/networks', peopleRoutes); app.use('/api/networks', peopleRoutes);
app.use('/api/networks', relationshipRoutes); app.use('/api/networks', relationshipRoutes);
// Health check
app.get('/api/health', (req, res) => {
res.send('OK');
});
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) => {

View File

@ -3,6 +3,7 @@ import Person from '../models/person.model';
import Relationship from '../models/relationship.model'; import Relationship from '../models/relationship.model';
import { UserRequest } from '../types/express'; import { UserRequest } from '../types/express';
import { validationResult } from 'express-validator'; import { validationResult } from 'express-validator';
import mongoose from 'mongoose';
// Get all people in a network // Get all people in a network
export const getPeople = async (req: UserRequest, res: Response): Promise<void> => { export const getPeople = async (req: UserRequest, res: Response): Promise<void> => {

View File

@ -1,4 +1,4 @@
import { Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import User from '../models/user.model'; import User from '../models/user.model';
import { UserRequest } from '../types/express'; import { UserRequest } from '../types/express';

View File

@ -1,6 +1,7 @@
import { Request } from 'express'; import { Request } from 'express';
import { IUser } from '../models/user.model'; import { IUser } from '../models/user.model';
import { INetwork } from '../models/network.model'; import { INetwork } from '../models/network.model';
import { Document } from 'mongoose';
export interface UserRequest extends Request { export interface UserRequest extends Request {
user?: IUser; user?: IUser;