mirror of
https://github.com/philipredstone/relnet.git
synced 2025-06-16 20:51:16 +02:00
Compare commits
13 Commits
9eddb1b547
...
bbb3645d99
Author | SHA1 | Date | |
---|---|---|---|
bbb3645d99 | |||
9ce80b4c59 | |||
56c0867a20 | |||
faae6ec930 | |||
8cbb83715f | |||
11b83eeffc | |||
ad3ced0650 | |||
3fd311e312 | |||
|
88d0c58a35 | ||
ca7b5fcf70 | |||
|
6ca54dfe9d | ||
0cc99458e0 | |||
1243cdc5ae |
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,6 +9,5 @@ frontend/bun.lockb
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
.env
|
.env
|
||||||
*bun.lockb
|
|
||||||
*yarn.lock
|
*yarn.lock
|
||||||
*package-lock.json
|
*package-lock.json
|
77
Dockerfile
77
Dockerfile
@ -1,47 +1,78 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM node:22 AS frontend-builder
|
|
||||||
|
|
||||||
# Workingdir
|
# Frontend builder stage
|
||||||
|
FROM oven/bun:1 AS frontend-builder
|
||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
|
|
||||||
# Copy files
|
# Install dependencies
|
||||||
|
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 .
|
||||||
|
|
||||||
# Install libs
|
# Build the frontend
|
||||||
RUN yarn install
|
RUN bun run build
|
||||||
# Build to dist/
|
|
||||||
RUN yarn build
|
|
||||||
|
|
||||||
|
# Backend builder stage
|
||||||
FROM node:22 AS backend-builder
|
FROM oven/bun:1 AS backend-builder
|
||||||
|
|
||||||
# Workingdir
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json .
|
# Copy package files first for better caching
|
||||||
RUN yarn install
|
COPY package.json bun.lockb* ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# Copy source files
|
||||||
COPY tsconfig.json .
|
COPY tsconfig.json .
|
||||||
COPY src/ src/
|
COPY src/ src/
|
||||||
|
|
||||||
# Build to dist/
|
# Build the backend
|
||||||
RUN yarn run build
|
RUN bun 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/*
|
||||||
|
|
||||||
|
|
||||||
# Final stage
|
WORKDIR /app
|
||||||
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
|
||||||
|
|
||||||
CMD ["yarn", "run", "start"]
|
# Expose the port
|
||||||
|
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"]
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter as Router, Navigate, Route, Routes } 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';
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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}`);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { GraphData } from 'react-force-graph-2d';
|
||||||
|
|
||||||
// Define types for graph elements
|
// Define types for graph elements
|
||||||
interface NodeData {
|
interface NodeData {
|
||||||
@ -19,15 +20,18 @@ interface EdgeData {
|
|||||||
width: number;
|
width: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GraphData {
|
interface CustomGraphData extends GraphData {
|
||||||
nodes: NodeData[];
|
nodes: NodeData[];
|
||||||
edges: EdgeData[];
|
edges: EdgeData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CanvasGraphProps {
|
interface CanvasGraphProps {
|
||||||
data: GraphData;
|
data: CustomGraphData,
|
||||||
width: number;
|
width: number,
|
||||||
height: number;
|
height: number,
|
||||||
|
zoomLevel: number,
|
||||||
|
onNodeClick: (nodeId: string) => void,
|
||||||
|
onNodeDrag: (nodeId, x, y) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// Physics constants
|
// Physics constants
|
||||||
@ -42,7 +46,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 }) => {
|
const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height, zoomLevel, onNodeClick, onNodeDrag }) => {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
// State for interactions
|
// State for interactions
|
||||||
@ -81,7 +85,7 @@ const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height }) => {
|
|||||||
// 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) {
|
||||||
@ -254,7 +258,7 @@ const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height }) => {
|
|||||||
// 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;
|
||||||
@ -332,7 +336,7 @@ const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height }) => {
|
|||||||
|
|
||||||
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
|
||||||
@ -372,7 +376,7 @@ const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height }) => {
|
|||||||
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(
|
||||||
@ -418,7 +422,7 @@ const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height }) => {
|
|||||||
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(() => {
|
||||||
@ -451,7 +455,7 @@ const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height }) => {
|
|||||||
setScale(newScale);
|
setScale(newScale);
|
||||||
setPanOffset({ x: newPanOffsetX, y: newPanOffsetY });
|
setPanOffset({ x: newPanOffsetX, y: newPanOffsetY });
|
||||||
},
|
},
|
||||||
[scale, panOffset]
|
[scale, panOffset],
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleAutoLayout = useCallback(() => {
|
const toggleAutoLayout = useCallback(() => {
|
||||||
@ -471,7 +475,7 @@ const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height }) => {
|
|||||||
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
|
||||||
@ -592,7 +596,7 @@ const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height }) => {
|
|||||||
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';
|
||||||
@ -628,7 +632,7 @@ const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height }) => {
|
|||||||
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
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Transition } from '@headlessui/react';
|
import { Transition } from '@headlessui/react';
|
||||||
import { FaTimes } from 'react-icons/fa';
|
import { FaTimes } from 'react-icons/fa';
|
||||||
|
|
||||||
@ -48,11 +48,11 @@ export interface NetworkStatsProps {
|
|||||||
|
|
||||||
// Enhanced Tooltip with animation and positioning
|
// Enhanced Tooltip with animation and positioning
|
||||||
export const Tooltip: React.FC<TooltipProps> = ({
|
export const Tooltip: React.FC<TooltipProps> = ({
|
||||||
children,
|
children,
|
||||||
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);
|
||||||
|
|
||||||
@ -218,15 +218,15 @@ export const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children,
|
|||||||
|
|
||||||
// Enhanced Confirmation dialog
|
// Enhanced Confirmation dialog
|
||||||
export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
|
export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
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,8 +286,8 @@ 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;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -351,12 +351,12 @@ export const NetworkStats: React.FC<NetworkStatsProps> = ({ people, relationship
|
|||||||
|
|
||||||
// Enhanced Toast notification component
|
// Enhanced Toast notification component
|
||||||
export const Toast: React.FC<ToastProps> = ({
|
export const Toast: React.FC<ToastProps> = ({
|
||||||
message,
|
message,
|
||||||
type,
|
type,
|
||||||
onClose,
|
onClose,
|
||||||
autoClose = true,
|
autoClose = true,
|
||||||
duration = 3000,
|
duration = 3000,
|
||||||
}) => {
|
}) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoClose) {
|
if (autoClose) {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@ -423,16 +423,16 @@ export interface ButtonProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Button: React.FC<ButtonProps> = ({
|
export const Button: React.FC<ButtonProps> = ({
|
||||||
children,
|
children,
|
||||||
onClick,
|
onClick,
|
||||||
type = 'button',
|
type = 'button',
|
||||||
variant = 'primary',
|
variant = 'primary',
|
||||||
size = 'md',
|
size = 'md',
|
||||||
icon,
|
icon,
|
||||||
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',
|
||||||
@ -481,14 +481,14 @@ export interface FormFieldProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const FormField: React.FC<FormFieldProps> = ({
|
export const FormField: React.FC<FormFieldProps> = ({
|
||||||
label,
|
label,
|
||||||
id,
|
id,
|
||||||
error,
|
error,
|
||||||
required = false,
|
required = false,
|
||||||
className = '',
|
className = '',
|
||||||
children,
|
children,
|
||||||
labelClassName = '',
|
labelClassName = '',
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={`mb-4 ${className}`}>
|
<div className={`mb-4 ${className}`}>
|
||||||
<label
|
<label
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import { FaUser, FaSignOutAlt, FaNetworkWired } from 'react-icons/fa';
|
import { FaNetworkWired, FaSignOutAlt, FaUser } from 'react-icons/fa';
|
||||||
|
|
||||||
const Header: React.FC = () => {
|
const Header: React.FC = () => {
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
@ -63,7 +63,8 @@ const Header: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<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">
|
<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">
|
||||||
<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"
|
||||||
|
@ -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 { motion, AnimatePresence } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { FaPlus, FaNetworkWired, FaTrash, FaEye, FaGlobe, FaLock, FaTimes } from 'react-icons/fa';
|
import { FaEye, FaGlobe, FaLock, FaNetworkWired, FaPlus, FaTimes, FaTrash } 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();
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import React, { createContext, useState, useEffect, useContext, ReactNode } from 'react';
|
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
User,
|
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
login as apiLogin,
|
login as apiLogin,
|
||||||
register as apiRegister,
|
|
||||||
logout as apiLogout,
|
|
||||||
LoginData,
|
LoginData,
|
||||||
|
logout as apiLogout,
|
||||||
|
register as apiRegister,
|
||||||
RegisterData,
|
RegisterData,
|
||||||
|
User,
|
||||||
} from '../api/auth';
|
} from '../api/auth';
|
||||||
|
|
||||||
interface AuthContextProps {
|
interface AuthContextProps {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React, { createContext, useState, useEffect, useContext, ReactNode } from 'react';
|
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Network,
|
|
||||||
getUserNetworks,
|
|
||||||
createNetwork as apiCreateNetwork,
|
createNetwork as apiCreateNetwork,
|
||||||
updateNetwork as apiUpdateNetwork,
|
|
||||||
deleteNetwork as apiDeleteNetwork,
|
|
||||||
CreateNetworkData,
|
CreateNetworkData,
|
||||||
|
deleteNetwork as apiDeleteNetwork,
|
||||||
|
getUserNetworks,
|
||||||
|
Network,
|
||||||
|
updateNetwork as apiUpdateNetwork,
|
||||||
UpdateNetworkData,
|
UpdateNetworkData,
|
||||||
} from '../api/network';
|
} from '../api/network';
|
||||||
import { useAuth } from './AuthContext';
|
import { useAuth } from './AuthContext';
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Person, getPeople, addPerson, updatePerson, removePerson } from '../api/people';
|
import { addPerson, getPeople, Person, removePerson, updatePerson } from '../api/people';
|
||||||
import {
|
import {
|
||||||
Relationship,
|
|
||||||
getRelationships,
|
|
||||||
addRelationship,
|
addRelationship,
|
||||||
updateRelationship,
|
getRelationships,
|
||||||
|
Relationship,
|
||||||
removeRelationship,
|
removeRelationship,
|
||||||
|
updateRelationship,
|
||||||
} 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');
|
||||||
|
@ -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');
|
||||||
|
@ -51,6 +51,11 @@ 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) => {
|
||||||
|
@ -3,7 +3,6 @@ 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> => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { 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';
|
||||||
|
1
src/types/express.d.ts
vendored
1
src/types/express.d.ts
vendored
@ -1,7 +1,6 @@
|
|||||||
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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user