Compare commits

...

5 Commits

Author SHA1 Message Date
c31b5c5b14 Refactor relationship types to single class and add more
Took 38 minutes
2025-04-16 15:39:13 +02:00
0333d37aae Refactor to interfaces and type classes
Took 5 minutes
2025-04-16 13:59:15 +02:00
3da29516ec Refactor friendship types to one type
Took 10 minutes
2025-04-16 13:49:58 +02:00
00e7294f41 Set name to firstname and first letter of lastname and increase node size
Took 3 hours 34 minutes
2025-04-16 13:25:47 +02:00
b054d55018 add autofocus for modals 2025-04-16 11:27:39 +02:00
10 changed files with 103 additions and 91 deletions

View File

@ -1,4 +1,6 @@
import axios from 'axios'; import axios from 'axios';
import { RELATIONSHIP_TYPES } from '../types/RelationShipTypes';
import { Relationship } from '../interfaces/IRelationship';
const protocol = window.location.protocol; const protocol = window.location.protocol;
const hostname = window.location.hostname; const hostname = window.location.hostname;
@ -6,27 +8,15 @@ const port = window.location.port;
const API_URL = protocol + '//' + hostname + (port ? ':' + port : '') + '/api'; const API_URL = protocol + '//' + hostname + (port ? ':' + port : '') + '/api';
// Types
export interface Relationship {
_id: string;
source: string;
target: string;
type: 'freund' | 'partner' | 'familie' | 'arbeitskolleg' | 'custom';
customType?: string;
network: string;
createdAt: string;
updatedAt: string;
}
export interface CreateRelationshipData { export interface CreateRelationshipData {
source: string; source: string;
target: string; target: string;
type: 'freund' | 'partner' | 'familie' | 'arbeitskolleg' | 'custom'; type: RELATIONSHIP_TYPES;
customType?: string; customType?: string;
} }
export interface UpdateRelationshipData { export interface UpdateRelationshipData {
type?: 'freund' | 'partner' | 'familie' | 'arbeitskolleg' | 'custom'; type?: RELATIONSHIP_TYPES;
customType?: string; customType?: string;
} }

View File

@ -35,9 +35,9 @@ interface CanvasGraphProps {
} }
// Physics constants // Physics constants
const NODE_RADIUS = 30; // Node radius in pixels const NODE_RADIUS = 45; // Node radius in pixels
const MIN_DISTANCE = 100; // Minimum distance between any two nodes const MIN_DISTANCE = 110; // Minimum distance between any two nodes
const MAX_DISTANCE = 300; // Maximum distance between connected nodes const MAX_DISTANCE = 500; // Maximum distance between connected nodes
const REPULSION_STRENGTH = 500; // How strongly nodes repel each other when too close const REPULSION_STRENGTH = 500; // How strongly nodes repel each other when too close
const ATTRACTION_STRENGTH = 0.1; // Default attraction between connected nodes const ATTRACTION_STRENGTH = 0.1; // Default attraction between connected nodes
const CONSTRAINT_STRENGTH = 0.2; // Strength of distance constraints const CONSTRAINT_STRENGTH = 0.2; // Strength of distance constraints
@ -573,9 +573,9 @@ const CanvasGraph: React.FC<CanvasGraphProps> = ({ data, width, height, zoomLeve
ctx.stroke(); ctx.stroke();
// Draw initials // Draw initials
const initials = `${node.firstName.charAt(0)}${node.lastName.charAt(0)}`; const initials = `${node.firstName} ${node.lastName.charAt(0)}.`;
ctx.fillStyle = 'white'; ctx.fillStyle = 'white';
ctx.font = 'bold 16px sans-serif'; ctx.font = 'bold 13px sans-serif';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.fillText(initials, pos.x, pos.y); ctx.fillText(initials, pos.x, pos.y);

View File

@ -32,51 +32,24 @@ import {
// Import custom UI components // Import custom UI components
import { import {
Button, Card, CardBody, ConfirmDialog, EmptyState, FormField, Modal, NetworkStats, Toast, ToastItem, Tooltip, Button,
Card,
CardBody,
ConfirmDialog,
EmptyState,
FormField,
Modal,
NetworkStats,
Toast,
ToastItem,
Tooltip,
} from './FriendshipNetworkComponents'; } from './FriendshipNetworkComponents';
// Import visible canvas graph component // Import visible canvas graph component
import CanvasGraph from './CanvasGraph'; import CanvasGraph from './CanvasGraph';
import { getRelationshipColor, RELATIONSHIP_TYPES, RELATIONSHIPS } from '../types/RelationShipTypes';
import { FormErrors, PersonNode } from '../interfaces/IPersonNode';
// Define types
type RelationshipType = 'freund' | 'partner' | 'familie' | 'arbeitskolleg' | 'custom';
interface PersonNode {
_id: string;
firstName: string;
lastName: string;
birthday?: Date | string | null;
notes?: string;
position?: {
x: number; y: number;
};
}
interface RelationshipEdge {
_id: string;
source: string;
target: string;
type: RelationshipType;
customType?: string;
notes?: string;
}
// Type for form errors
interface FormErrors {
[key: string]: string;
}
// Graph appearance constants
const RELATIONSHIP_COLORS = {
freund: '#60A5FA', // Light blue
partner: '#F472B6', // Pink
familie: '#34D399', // Green
arbeitskolleg: '#FBBF24', // Yellow
custom: '#9CA3AF', // Gray
};
const RELATIONSHIP_LABELS = {
freund: 'Friend', partner: 'Partner', familie: 'Family', arbeitskolleg: 'Colleague', custom: 'Custom',
};
// Main FriendshipNetwork component // Main FriendshipNetwork component
const FriendshipNetwork: React.FC = () => { const FriendshipNetwork: React.FC = () => {
@ -139,7 +112,7 @@ const FriendshipNetwork: React.FC = () => {
const [editPerson, setEditPerson] = useState<PersonNode | null>(null); const [editPerson, setEditPerson] = useState<PersonNode | null>(null);
const [newRelationship, setNewRelationship] = useState({ const [newRelationship, setNewRelationship] = useState({
source: '', target: '', type: 'freund' as RelationshipType, customType: '', notes: '', bidirectional: true, source: '', target: '', type: 'friend' as RELATIONSHIP_TYPES, customType: '', notes: '', bidirectional: true,
}); });
// Filter states // Filter states
@ -402,7 +375,7 @@ const FriendshipNetwork: React.FC = () => {
// Create edges // Create edges
const graphEdges = relationships.map(rel => { const graphEdges = relationships.map(rel => {
const color = RELATIONSHIP_COLORS[rel.type] || RELATIONSHIP_COLORS.custom; const color = RELATIONSHIPS[rel.type as RELATIONSHIP_TYPES]?.color || RELATIONSHIPS.custom.color;
const width = rel.type === 'partner' ? 4 : rel.type === 'familie' ? 3 : 2; const width = rel.type === 'partner' ? 4 : rel.type === 'familie' ? 3 : 2;
// Highlight edges connected to selected node // Highlight edges connected to selected node
@ -542,7 +515,7 @@ const FriendshipNetwork: React.FC = () => {
// Reset form and close modal // Reset form and close modal
setNewRelationship({ setNewRelationship({
source: '', target: '', type: 'freund', customType: '', notes: '', bidirectional: true, source: '', target: '', type: 'friend', customType: '', notes: '', bidirectional: true,
}); });
setRelationshipModalOpen(false); setRelationshipModalOpen(false);
@ -778,14 +751,14 @@ const FriendshipNetwork: React.FC = () => {
<CardBody> <CardBody>
<h3 className="font-medium mb-2 text-indigo-400">Legend</h3> <h3 className="font-medium mb-2 text-indigo-400">Legend</h3>
<div className="space-y-2"> <div className="space-y-2">
{Object.entries(RELATIONSHIP_COLORS).map(([type, color]) => ( {Object.entries(RELATIONSHIPS).map(([type, { label, color }]) => (
<div key={type} className="flex items-center text-sm"> <div key={type} className="flex items-center text-sm">
<div <div
className="w-4 h-4 rounded-full mr-2" className="w-4 h-4 rounded-full mr-2"
style={{ backgroundColor: color }} style={{ backgroundColor: color }}
></div> ></div>
<span className="capitalize"> <span className="capitalize">
{RELATIONSHIP_LABELS[type as RelationshipType]} {RELATIONSHIPS[type]?.label}
</span> </span>
</div>))} </div>))}
</div> </div>
@ -919,17 +892,17 @@ const FriendshipNetwork: React.FC = () => {
> >
All Types All Types
</button> </button>
{Object.entries(RELATIONSHIP_COLORS).map(([type, color]) => (<button {Object.entries(RELATIONSHIPS).map(([type, { label, color }]) => (<button
key={type} key={type}
className={`px-3 py-1 text-xs rounded-full whitespace-nowrap flex items-center ${relationshipTypeFilter === type ? 'bg-indigo-600 text-white' : 'bg-slate-700 text-slate-300 hover:bg-slate-600'}`} className={`px-3 py-1 text-xs rounded-full whitespace-nowrap flex items-center ${relationshipTypeFilter === type ? 'bg-indigo-600 text-white' : 'bg-slate-700 text-slate-300 hover:bg-slate-600'}`}
onClick={() => setRelationshipTypeFilter(type as RelationshipType)} onClick={() => setRelationshipTypeFilter(type as RELATIONSHIP_TYPES)}
> >
<span <span
className="w-2 h-2 rounded-full mr-1" className="w-2 h-2 rounded-full mr-1"
style={{ backgroundColor: color }} style={{ backgroundColor: color }}
></span> ></span>
<span className="capitalize"> <span className="capitalize">
{RELATIONSHIP_LABELS[type as RelationshipType]} {RELATIONSHIPS[type as RELATIONSHIP_TYPES]?.label}
</span> </span>
</button>))} </button>))}
</div> </div>
@ -974,10 +947,10 @@ const FriendshipNetwork: React.FC = () => {
<div className="flex items-center text-xs text-slate-400 mt-1"> <div className="flex items-center text-xs text-slate-400 mt-1">
<span <span
className="inline-block w-2 h-2 rounded-full mr-1" className="inline-block w-2 h-2 rounded-full mr-1"
style={{ backgroundColor: RELATIONSHIP_COLORS[rel.type] }} style={{ backgroundColor: RELATIONSHIPS[rel.type as RELATIONSHIP_TYPES]?.color }}
></span> ></span>
<span className="capitalize"> <span className="capitalize">
{rel.type === 'custom' ? rel.customType : RELATIONSHIP_LABELS[rel.type]} {rel.type === 'custom' ? rel.customType : RELATIONSHIPS[rel.type as RELATIONSHIP_TYPES]?.label}
</span> </span>
</div> </div>
</div> </div>
@ -1150,6 +1123,7 @@ const FriendshipNetwork: React.FC = () => {
className={`w-full bg-slate-700 border ${personFormErrors.firstName ? 'border-red-500' : 'border-slate-600'} className={`w-full bg-slate-700 border ${personFormErrors.firstName ? 'border-red-500' : 'border-slate-600'}
rounded-md p-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white`} rounded-md p-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white`}
placeholder="Enter first name" placeholder="Enter first name"
autoFocus={true}
value={newPerson.firstName} value={newPerson.firstName}
onChange={e => setNewPerson({ ...newPerson, firstName: e.target.value })} onChange={e => setNewPerson({ ...newPerson, firstName: e.target.value })}
/> />
@ -1236,6 +1210,7 @@ const FriendshipNetwork: React.FC = () => {
> >
<select <select
id="source" id="source"
autoFocus={true}
className={`w-full bg-slate-700 border ${relationshipFormErrors.source ? 'border-red-500' : 'border-slate-600'} className={`w-full bg-slate-700 border ${relationshipFormErrors.source ? 'border-red-500' : 'border-slate-600'}
rounded-md p-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white`} rounded-md p-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white`}
value={newRelationship.source} value={newRelationship.source}
@ -1275,10 +1250,10 @@ const FriendshipNetwork: React.FC = () => {
focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white" focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white"
value={newRelationship.type} value={newRelationship.type}
onChange={e => setNewRelationship({ onChange={e => setNewRelationship({
...newRelationship, type: e.target.value as RelationshipType, ...newRelationship, type: e.target.value as RELATIONSHIP_TYPES,
})} })}
> >
{Object.entries(RELATIONSHIP_LABELS).map(([value, label]) => (<option key={value} value={value}> {Object.entries(RELATIONSHIPS).map(([value, { label }]) => (<option key={value} value={value}>
{label} {label}
</option>))} </option>))}
</select> </select>
@ -1474,7 +1449,7 @@ const FriendshipNetwork: React.FC = () => {
<div className="flex items-center"> <div className="flex items-center">
<span <span
className="inline-block w-2 h-2 rounded-full mr-2" className="inline-block w-2 h-2 rounded-full mr-2"
style={{ backgroundColor: RELATIONSHIP_COLORS[rel.type] }} style={{ backgroundColor: RELATIONSHIPS[rel.type as RELATIONSHIP_TYPES]?.color }}
></span> ></span>
<span className="text-sm"> <span className="text-sm">
{isSource ? 'To: ' : 'From: '} {isSource ? 'To: ' : 'From: '}
@ -1491,7 +1466,9 @@ const FriendshipNetwork: React.FC = () => {
> >
{otherPerson.firstName} {otherPerson.lastName} {otherPerson.firstName} {otherPerson.lastName}
</span> </span>
{rel.type === 'custom' ? ` (${rel.customType})` : ` (${RELATIONSHIP_LABELS[rel.type]})`} {rel.type === 'custom'
? ` (${rel.customType})`
: ` (${RELATIONSHIPS[rel.type as RELATIONSHIP_TYPES]?.label})`}
</span> </span>
</div> </div>
<button <button

View File

@ -3,10 +3,11 @@ import { addPerson, getPeople, Person, removePerson, updatePerson } from '../api
import { import {
addRelationship, addRelationship,
getRelationships, getRelationships,
Relationship,
removeRelationship, removeRelationship,
updateRelationship, updateRelationship,
} from '../api/relationships'; } from '../api/relationships';
import { Relationship } from '../interfaces/IRelationship';
import { RELATIONSHIP_TYPES } from '../types/RelationShipTypes';
interface PersonNode extends Person { interface PersonNode extends Person {
// Additional properties needed for the visualization // Additional properties needed for the visualization
@ -314,7 +315,7 @@ export const useFriendshipNetwork = (
const createRelationship = async (relationshipData: { const createRelationship = async (relationshipData: {
source: string; source: string;
target: string; target: string;
type: 'freund' | 'partner' | 'familie' | 'arbeitskolleg' | 'custom'; type: RELATIONSHIP_TYPES;
customType?: string; customType?: string;
}): Promise<RelationshipEdge> => { }): Promise<RelationshipEdge> => {
if (!networkId) throw new Error('No network selected'); if (!networkId) throw new Error('No network selected');
@ -342,7 +343,7 @@ export const useFriendshipNetwork = (
const updateRelationshipData = async ( const updateRelationshipData = async (
relationshipId: string, relationshipId: string,
relationshipData: { relationshipData: {
type?: 'freund' | 'partner' | 'familie' | 'arbeitskolleg' | 'custom'; type?: RELATIONSHIP_TYPES;
customType?: string; customType?: string;
}, },
): Promise<RelationshipEdge> => { ): Promise<RelationshipEdge> => {

View File

@ -0,0 +1,15 @@
export interface PersonNode {
_id: string;
firstName: string;
lastName: string;
birthday?: Date | string | null;
notes?: string;
position?: {
x: number; y: number;
};
}
// Type for form errors
export interface FormErrors {
[key: string]: string;
}

View File

@ -0,0 +1,13 @@
// Types
import { RELATIONSHIP_TYPES } from '../types/RelationShipTypes';
export interface Relationship {
_id: string;
source: string;
target: string;
type: RELATIONSHIP_TYPES;
customType?: string;
network: string;
createdAt: string;
updatedAt: string;
}

View File

@ -0,0 +1,15 @@
export type RELATIONSHIP_TYPES = 'acquaintance' | 'friend' | 'partner' | 'family' | 'secondDegree' | 'colleague' | 'teacher' | 'exPartner' | 'custom';
export const RELATIONSHIPS: Record<RELATIONSHIP_TYPES, { label: string; color: string }> = {
acquaintance: { label: 'Bekannter', color: '#60A5FA' }, // Light blue
friend: { label: 'Freund', color: '#60A5FA' }, // Light blue
partner: { label: 'Partner', color: '#F472B6' }, // Pink
family: { label: 'Familie', color: '#34D399' }, // Green
secondDegree: { label: 'Verwandter', color: '#34D399' }, // Green
colleague: { label: 'Kollege/Klassenkamerad', color: '#FBBF24' }, // Yellow
teacher: { label: 'Lehrer', color: '#FBBF24' }, // Yellow
exPartner: { label: 'Ex-Partner', color: '#ce8c13' }, // Orange
custom: { label: 'Benutzerdefiniert', color: '#9CA3AF' }, // Gray
};
export const getRelationshipLabel = (type: RELATIONSHIP_TYPES): string => RELATIONSHIPS[type].label;
export const getRelationshipColor = (type: RELATIONSHIP_TYPES): string => RELATIONSHIPS[type].color;

View File

@ -217,12 +217,12 @@ const createSampleDemoNetwork = async (userId: mongoose.Types.ObjectId | string)
// Create relationships between people // Create relationships between people
const relationships = [ const relationships = [
{ source: 'JohnSmith', target: 'EmmaJohnson', type: 'freund' }, { source: 'JohnSmith', target: 'EmmaJohnson', type: 'friend' },
{ source: 'EmmaJohnson', target: 'MichaelWilliams', type: 'familie' }, { source: 'EmmaJohnson', target: 'MichaelWilliams', type: 'family' },
{ source: 'MichaelWilliams', target: 'SarahBrown', type: 'arbeitskolleg' }, { source: 'MichaelWilliams', target: 'SarahBrown', type: 'colleague' },
{ source: 'SarahBrown', target: 'DavidJones', type: 'freund' }, { source: 'SarahBrown', target: 'DavidJones', type: 'friend' },
{ source: 'DavidJones', target: 'LisaGarcia', type: 'partner' }, { source: 'DavidJones', target: 'LisaGarcia', type: 'partner' },
{ source: 'JohnSmith', target: 'DavidJones', type: 'arbeitskolleg' }, { source: 'JohnSmith', target: 'DavidJones', type: 'colleague' },
]; ];
// Create each relationship // Create each relationship

View File

@ -1,5 +1,10 @@
import mongoose, { Document, Schema } from 'mongoose'; import mongoose, { Document, Schema } from 'mongoose';
export const RELATIONSHIP_TYPES = [
'acquaintance', 'friend', 'partner', 'family', 'secondDegree', 'colleague', 'teacher', 'exPartner', 'custom',
];
export interface IRelationship extends Document { export interface IRelationship extends Document {
_id: string; _id: string;
source: mongoose.Types.ObjectId; source: mongoose.Types.ObjectId;
@ -24,7 +29,7 @@ const RelationshipSchema = new Schema(
type: { type: {
type: String, type: String,
required: [true, 'Relationship type is required'], required: [true, 'Relationship type is required'],
enum: ['freund', 'partner', 'familie', 'arbeitskolleg', 'custom'], enum: RELATIONSHIP_TYPES,
}, },
customType: { customType: {
type: String, type: String,
@ -36,7 +41,7 @@ const RelationshipSchema = new Schema(
required: true, required: true,
}, },
}, },
{ timestamps: true } { timestamps: true },
); );
// Create compound index to ensure unique relationships in a network // Create compound index to ensure unique relationships in a network

View File

@ -3,6 +3,8 @@ import { check } from 'express-validator';
import * as relationshipController from '../controllers/relationship.controller'; import * as relationshipController from '../controllers/relationship.controller';
import { auth } from '../middleware/auth.middleware'; import { auth } from '../middleware/auth.middleware';
import { checkNetworkAccess } from '../middleware/network-access.middleware'; import { checkNetworkAccess } from '../middleware/network-access.middleware';
import { RELATIONSHIP_TYPES } from '../models/relationship.model';
const router = express.Router(); const router = express.Router();
@ -22,13 +24,7 @@ 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([ check('type', 'Relationship type is required').isIn(RELATIONSHIP_TYPES),
'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()
@ -45,7 +41,7 @@ router.put(
[ [
check('type', 'Relationship type must be valid if provided') check('type', 'Relationship type must be valid if provided')
.optional() .optional()
.isIn(['freund', 'partner', 'familie', 'arbeitskolleg', 'custom']), .isIn(RELATIONSHIP_TYPES),
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()