mirror of
https://github.com/philipredstone/relnet.git
synced 2025-06-17 05:01:24 +02:00
redesign
This commit is contained in:
parent
a753dbd3d9
commit
efe7a6761b
@ -1,36 +1,39 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"format": "prettier --write \"src/**/*.{tsx,ts,js,jsx,json,css,html}\"",
|
"format": "prettier --write \"src/**/*.{tsx,ts,js,jsx,json,css,html}\"",
|
||||||
"format:check": "prettier --check \"src/**/*.{tsx,ts,js,jsx,json,css,html}\""
|
"format:check": "prettier --check \"src/**/*.{tsx,ts,js,jsx,json,css,html}\""
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.4",
|
"@tailwindcss/vite": "^4.1.4",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"react": "^19.1.0",
|
"framer-motion": "^12.7.3",
|
||||||
"react-dom": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-router-dom": "^7.5.0",
|
"react-dom": "^19.1.0",
|
||||||
"tailwindcss": "^4.1.4",
|
"react-force-graph-2d": "^1.27.1",
|
||||||
"ts-node": "^10.9.2",
|
"react-icons": "^5.5.0",
|
||||||
"typescript": "^5.8.3",
|
"react-router-dom": "^7.5.0",
|
||||||
"vite": "^6.2.6"
|
"tailwindcss": "^4.1.4",
|
||||||
},
|
"ts-node": "^10.9.2",
|
||||||
"devDependencies": {
|
"typescript": "^5.8.3",
|
||||||
"@types/axios": "^0.14.4",
|
"vite": "^6.2.6"
|
||||||
"@types/node": "^22.14.1",
|
},
|
||||||
"@types/react": "^19.1.2",
|
"devDependencies": {
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/axios": "^0.14.4",
|
||||||
"@vitejs/plugin-react": "^4.4.0",
|
"@types/node": "^22.14.1",
|
||||||
"prettier": "^3.5.3",
|
"@types/react": "^19.1.2",
|
||||||
"webpack": "^5.99.5",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"webpack-cli": "^6.0.1"
|
"@vitejs/plugin-react": "^4.4.0",
|
||||||
}
|
"prettier": "^3.5.3",
|
||||||
}
|
"webpack": "^5.99.5",
|
||||||
|
"webpack-cli": "^6.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
|
import { FaUser, FaSignOutAlt, FaNetworkWired } from 'react-icons/fa';
|
||||||
|
|
||||||
const Header: React.FC = () => {
|
const Header: React.FC = () => {
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
@ -15,44 +17,83 @@ const Header: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
// Check if we're on the login or register page
|
||||||
<header className="bg-blue-600 text-white shadow-md">
|
const isAuthPage = location.pathname === '/login' || location.pathname === '/register';
|
||||||
<div className="container mx-auto py-4 px-6 flex justify-between items-center">
|
|
||||||
<Link to="/" className="text-xl font-bold">
|
|
||||||
Friendship Network
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<nav>
|
if (isAuthPage) {
|
||||||
{user ? (
|
return null; // Don't show header on auth pages
|
||||||
<div className="flex items-center space-x-4">
|
}
|
||||||
<span>Hello, {user.username}</span>
|
|
||||||
<Link to="/networks" className="hover:underline">
|
return (
|
||||||
My Networks
|
<header className="bg-slate-800 border-b border-slate-700 shadow-md">
|
||||||
</Link>
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<button
|
<div className="flex justify-between h-16">
|
||||||
onClick={handleLogout}
|
<div className="flex items-center">
|
||||||
className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-3 rounded"
|
<Link to="/" className="flex-shrink-0 flex items-center">
|
||||||
>
|
<FaNetworkWired className="h-6 w-6 text-indigo-400" />
|
||||||
Logout
|
<span className="ml-2 text-white font-bold text-xl">RelNet</span>
|
||||||
</button>
|
</Link>
|
||||||
</div>
|
|
||||||
) : (
|
{user && (
|
||||||
<div className="space-x-4">
|
<nav className="ml-8 flex space-x-4">
|
||||||
<Link to="/login" className="hover:underline">
|
<Link
|
||||||
Login
|
to="/networks"
|
||||||
</Link>
|
className={`px-3 py-2 rounded-md text-sm font-medium ${
|
||||||
<Link
|
location.pathname.includes('/networks')
|
||||||
to="/register"
|
? 'bg-slate-700 text-white'
|
||||||
className="bg-white text-blue-600 hover:bg-gray-100 font-bold py-1 px-3 rounded"
|
: 'text-slate-300 hover:bg-slate-700 hover:text-white'
|
||||||
>
|
} transition-colors duration-200 flex items-center`}
|
||||||
Register
|
>
|
||||||
</Link>
|
<FaNetworkWired className="mr-1" /> Networks
|
||||||
</div>
|
</Link>
|
||||||
)}
|
</nav>
|
||||||
</nav>
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
{user ? (
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="text-sm text-indigo-300 hidden md:block">
|
||||||
|
Hello, {user.username}
|
||||||
|
</div>
|
||||||
|
<div className="relative group">
|
||||||
|
<button className="flex text-slate-300 hover:text-white items-center focus:outline-none">
|
||||||
|
<div className="h-8 w-8 rounded-full bg-indigo-500 flex items-center justify-center">
|
||||||
|
<FaUser />
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="w-full text-left px-4 py-2 text-sm text-slate-300 hover:bg-slate-700 flex items-center"
|
||||||
|
>
|
||||||
|
<FaSignOutAlt className="mr-2" /> Sign out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Link
|
||||||
|
to="/login"
|
||||||
|
className="text-slate-300 hover:bg-slate-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Log in
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/register"
|
||||||
|
className="bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Sign up
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
@ -2,6 +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 { 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();
|
||||||
@ -59,216 +61,285 @@ const NetworkList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Filter networks by ownership
|
||||||
|
const myNetworks = networks.filter(network => {
|
||||||
|
if (!user) return false;
|
||||||
|
const ownerId = typeof network.owner === 'string' ? network.owner : network.owner._id;
|
||||||
|
return ownerId === user.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
const publicNetworks = networks.filter(network => {
|
||||||
|
if (!user) return false;
|
||||||
|
const ownerId = typeof network.owner === 'string' ? network.owner : network.owner._id;
|
||||||
|
return ownerId !== user.id;
|
||||||
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="flex justify-center p-8">Loading networks...</div>;
|
return (
|
||||||
|
<div className="flex justify-center items-center h-screen bg-slate-900">
|
||||||
|
<div className="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-indigo-500"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-6">
|
<div className="bg-slate-900 min-h-screen text-white p-6">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="max-w-7xl mx-auto">
|
||||||
<h1 className="text-2xl font-bold">My Networks</h1>
|
<div className="flex justify-between items-center mb-8">
|
||||||
<button
|
<h1 className="text-2xl font-bold text-indigo-400">
|
||||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
<FaNetworkWired className="inline-block mr-2" />
|
||||||
onClick={() => setShowCreateForm(!showCreateForm)}
|
My Networks
|
||||||
>
|
</h1>
|
||||||
{showCreateForm ? 'Cancel' : 'Create New Network'}
|
<motion.button
|
||||||
</button>
|
whileHover={{ scale: 1.05 }}
|
||||||
</div>
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-lg
|
||||||
{error && (
|
shadow-lg transition-colors duration-200 flex items-center"
|
||||||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
onClick={() => setShowCreateForm(!showCreateForm)}
|
||||||
{error}
|
>
|
||||||
|
{showCreateForm ? <FaTimes className="mr-2" /> : <FaPlus className="mr-2" />}
|
||||||
|
{showCreateForm ? 'Cancel' : 'Create New Network'}
|
||||||
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Create Network Form */}
|
{error && (
|
||||||
{showCreateForm && (
|
<div className="bg-red-900 border border-red-700 text-white px-4 py-3 rounded-lg mb-6">
|
||||||
<div className="bg-gray-100 p-4 rounded-lg mb-6">
|
{error}
|
||||||
<h2 className="text-xl font-semibold mb-4">Create New Network</h2>
|
|
||||||
|
|
||||||
{formError && (
|
|
||||||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
|
||||||
{formError}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<form onSubmit={handleCreateNetwork}>
|
|
||||||
<div className="mb-4">
|
|
||||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
|
|
||||||
Network Name *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="name"
|
|
||||||
type="text"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded"
|
|
||||||
value={newNetworkName}
|
|
||||||
onChange={e => setNewNetworkName(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-4">
|
|
||||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="description">
|
|
||||||
Description (Optional)
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="description"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded"
|
|
||||||
value={newNetworkDescription}
|
|
||||||
onChange={e => setNewNetworkDescription(e.target.value)}
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-4">
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="mr-2"
|
|
||||||
checked={isPublic}
|
|
||||||
onChange={e => setIsPublic(e.target.checked)}
|
|
||||||
/>
|
|
||||||
<span className="text-gray-700 text-sm font-bold">Make this network public</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded"
|
|
||||||
disabled={createLoading}
|
|
||||||
>
|
|
||||||
{createLoading ? 'Creating...' : 'Create Network'}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Networks List */}
|
|
||||||
{networks.length === 0 ? (
|
|
||||||
<div className="bg-white p-8 rounded-lg text-center">
|
|
||||||
<p className="text-gray-600 mb-4">You don't have any networks yet.</p>
|
|
||||||
{!showCreateForm && (
|
|
||||||
<button
|
|
||||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
|
||||||
onClick={() => setShowCreateForm(true)}
|
|
||||||
>
|
|
||||||
Create Your First Network
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{/* My Networks Section */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<h2 className="text-xl font-semibold mb-4">My Networks</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
{networks
|
|
||||||
.filter(network => {
|
|
||||||
if (!user) return false;
|
|
||||||
const ownerId =
|
|
||||||
typeof network.owner === 'string' ? network.owner : network.owner._id;
|
|
||||||
return ownerId === user.id;
|
|
||||||
})
|
|
||||||
.map(network => (
|
|
||||||
<div key={network._id} className="bg-white rounded-lg shadow-md overflow-hidden">
|
|
||||||
<div className="p-4">
|
|
||||||
<h2 className="text-xl font-bold mb-2">{network.name}</h2>
|
|
||||||
{network.description && (
|
|
||||||
<p className="text-gray-600 mb-4">{network.description}</p>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center mb-4">
|
|
||||||
<span
|
|
||||||
className={`px-2 py-1 rounded-full text-xs ${network.isPublic ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}
|
|
||||||
>
|
|
||||||
{network.isPublic ? 'Public' : 'Private'}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-gray-500 ml-2">
|
|
||||||
Created: {new Date(network.createdAt).toLocaleDateString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button
|
|
||||||
className="flex-1 bg-blue-500 hover:bg-blue-700 text-white py-2 px-4 rounded"
|
|
||||||
onClick={() => navigate(`/networks/${network._id}`)}
|
|
||||||
>
|
|
||||||
View
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="flex-1 bg-red-500 hover:bg-red-700 text-white py-2 px-4 rounded"
|
|
||||||
onClick={() => handleDeleteNetwork(network._id)}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{networks.filter(network => {
|
|
||||||
if (!user) return false;
|
|
||||||
const ownerId = typeof network.owner === 'string' ? network.owner : network.owner._id;
|
|
||||||
return ownerId === user.id;
|
|
||||||
}).length === 0 && (
|
|
||||||
<p className="text-gray-600 mb-4">You haven't created any networks yet.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Public Networks Section */}
|
{/* Create Network Form */}
|
||||||
{networks.some(network => {
|
<AnimatePresence>
|
||||||
if (!user) return false;
|
{showCreateForm && (
|
||||||
const ownerId = typeof network.owner === 'string' ? network.owner : network.owner._id;
|
<motion.div
|
||||||
return ownerId !== user.id;
|
initial={{ opacity: 0, y: -20 }}
|
||||||
}) && (
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
className="bg-slate-800 p-6 rounded-lg shadow-xl mb-8 border border-slate-700"
|
||||||
|
>
|
||||||
|
<h2 className="text-xl font-semibold mb-4 text-indigo-300">Create New Network</h2>
|
||||||
|
|
||||||
|
{formError && (
|
||||||
|
<div className="bg-red-900 border border-red-700 text-white px-4 py-3 rounded-lg mb-4">
|
||||||
|
{formError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleCreateNetwork} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-slate-300 text-sm font-medium mb-2" htmlFor="name">
|
||||||
|
Network Name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
type="text"
|
||||||
|
className="w-full px-3 py-2 bg-slate-700 border border-slate-600 text-white rounded-lg
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||||
|
value={newNetworkName}
|
||||||
|
onChange={e => setNewNetworkName(e.target.value)}
|
||||||
|
required
|
||||||
|
placeholder="Enter network name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-slate-300 text-sm font-medium mb-2" htmlFor="description">
|
||||||
|
Description (Optional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
className="w-full px-3 py-2 bg-slate-700 border border-slate-600 text-white rounded-lg
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||||
|
value={newNetworkDescription}
|
||||||
|
onChange={e => setNewNetworkDescription(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
placeholder="Describe your network"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-checkbox h-5 w-5 text-indigo-600 bg-slate-700 border-slate-600
|
||||||
|
focus:ring-indigo-500 rounded"
|
||||||
|
checked={isPublic}
|
||||||
|
onChange={e => setIsPublic(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span className="ml-2 text-slate-300 text-sm font-medium">Make this network public</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4
|
||||||
|
rounded-lg shadow-md transition-colors duration-200 flex items-center justify-center"
|
||||||
|
disabled={createLoading}
|
||||||
|
>
|
||||||
|
{createLoading ? (
|
||||||
|
<>
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-white mr-2"></div>
|
||||||
|
Creating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FaPlus className="mr-2" /> Create Network
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Networks List */}
|
||||||
|
{networks.length === 0 ? (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className="bg-slate-800 p-8 rounded-lg text-center border border-slate-700 shadow-xl"
|
||||||
|
>
|
||||||
|
<div className="flex justify-center mb-4">
|
||||||
|
<FaNetworkWired className="text-4xl text-slate-400" />
|
||||||
|
</div>
|
||||||
|
<p className="text-slate-300 mb-6">You don't have any networks yet.</p>
|
||||||
|
{!showCreateForm && (
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-6
|
||||||
|
rounded-lg shadow-lg transition-colors duration-200 flex items-center mx-auto"
|
||||||
|
onClick={() => setShowCreateForm(true)}
|
||||||
|
>
|
||||||
|
<FaPlus className="mr-2" /> Create Your First Network
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-12">
|
||||||
|
{/* My Networks Section */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-semibold mb-4">Public Networks From Others</h2>
|
<h2 className="text-xl font-semibold mb-4 text-indigo-300 flex items-center">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<FaNetworkWired className="mr-2" /> My Networks ({myNetworks.length})
|
||||||
{networks
|
</h2>
|
||||||
.filter(network => {
|
|
||||||
if (!user) return false;
|
{myNetworks.length === 0 ? (
|
||||||
const ownerId =
|
<p className="text-slate-400 bg-slate-800 p-4 rounded-lg border border-slate-700">
|
||||||
typeof network.owner === 'string' ? network.owner : network.owner._id;
|
You haven't created any networks yet.
|
||||||
return ownerId !== user.id;
|
</p>
|
||||||
})
|
) : (
|
||||||
.map(network => (
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<div
|
{myNetworks.map(network => (
|
||||||
|
<motion.div
|
||||||
key={network._id}
|
key={network._id}
|
||||||
className="bg-white rounded-lg shadow-md overflow-hidden border-l-4 border-green-500"
|
whileHover={{ y: -5 }}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-slate-800 rounded-lg shadow-xl overflow-hidden border border-slate-700 hover:border-indigo-500 transition-colors duration-200"
|
||||||
>
|
>
|
||||||
<div className="p-4">
|
<div className="p-6">
|
||||||
<h2 className="text-xl font-bold mb-2">{network.name}</h2>
|
<div className="flex justify-between items-start mb-2">
|
||||||
{network.description && (
|
<h3 className="text-xl font-bold text-white">{network.name}</h3>
|
||||||
<p className="text-gray-600 mb-4">{network.description}</p>
|
{network.isPublic ? (
|
||||||
)}
|
<FaGlobe className="text-green-400" title="Public" />
|
||||||
<div className="flex items-center mb-4">
|
) : (
|
||||||
<span className="px-2 py-1 rounded-full text-xs bg-green-100 text-green-800">
|
<FaLock className="text-amber-400" title="Private" />
|
||||||
Public
|
)}
|
||||||
</span>
|
|
||||||
<span className="text-xs text-gray-500 ml-2">
|
|
||||||
Created: {new Date(network.createdAt).toLocaleDateString()}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-gray-500 ml-2">
|
|
||||||
By:{' '}
|
|
||||||
{typeof network.owner === 'string' ? 'Unknown' : network.owner.username}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button
|
{network.description && (
|
||||||
className="w-full bg-blue-500 hover:bg-blue-700 text-white py-2 px-4 rounded"
|
<p className="text-slate-300 mb-4 text-sm">{network.description}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-xs text-slate-400 mb-6">
|
||||||
|
Created: {new Date(network.createdAt).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-3">
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white py-2 px-4
|
||||||
|
rounded-lg transition-colors duration-200 flex items-center justify-center"
|
||||||
onClick={() => navigate(`/networks/${network._id}`)}
|
onClick={() => navigate(`/networks/${network._id}`)}
|
||||||
>
|
>
|
||||||
View
|
<FaEye className="mr-2" /> View
|
||||||
</button>
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="flex-1 bg-red-700 hover:bg-red-800 text-white py-2 px-4
|
||||||
|
rounded-lg transition-colors duration-200 flex items-center justify-center"
|
||||||
|
onClick={() => handleDeleteNetwork(network._id)}
|
||||||
|
>
|
||||||
|
<FaTrash className="mr-2" /> Delete
|
||||||
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</>
|
{/* Public Networks Section */}
|
||||||
)}
|
{publicNetworks.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold mb-4 text-indigo-300 flex items-center">
|
||||||
|
<FaGlobe className="mr-2" /> Public Networks ({publicNetworks.length})
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{publicNetworks.map(network => (
|
||||||
|
<motion.div
|
||||||
|
key={network._id}
|
||||||
|
whileHover={{ y: -5 }}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-slate-800 rounded-lg shadow-xl overflow-hidden border border-slate-700 border-l-4 border-l-green-500"
|
||||||
|
>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<h3 className="text-xl font-bold text-white">{network.name}</h3>
|
||||||
|
<FaGlobe className="text-green-400" title="Public" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{network.description && (
|
||||||
|
<p className="text-slate-300 mb-4 text-sm">{network.description}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-between mb-6">
|
||||||
|
<span className="text-xs text-slate-400">
|
||||||
|
Created: {new Date(network.createdAt).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-medium text-green-400">
|
||||||
|
By: {typeof network.owner === 'string' ? 'Unknown' : network.owner.username}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="w-full bg-indigo-600 hover:bg-indigo-700 text-white py-2 px-4
|
||||||
|
rounded-lg transition-colors duration-200 flex items-center justify-center"
|
||||||
|
onClick={() => navigate(`/networks/${network._id}`)}
|
||||||
|
>
|
||||||
|
<FaEye className="mr-2" /> View Network
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NetworkList;
|
export default NetworkList;
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { Person, getPeople, addPerson, updatePerson, removePerson } from '../api/people';
|
import { Person, getPeople, addPerson, updatePerson, removePerson } from '../api/people';
|
||||||
import {
|
import {
|
||||||
Relationship,
|
Relationship,
|
||||||
@ -18,15 +18,21 @@ interface RelationshipEdge extends Relationship {
|
|||||||
id: string; // Alias for _id to work with the visualization
|
id: string; // Alias for _id to work with the visualization
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default poll interval in milliseconds (5 seconds)
|
||||||
|
const DEFAULT_POLL_INTERVAL = 5000;
|
||||||
|
|
||||||
// Custom hook to manage friendship network data
|
// Custom hook to manage friendship network data
|
||||||
export const useFriendshipNetwork = (networkId: string | null) => {
|
export const useFriendshipNetwork = (networkId: string | null, pollInterval = DEFAULT_POLL_INTERVAL) => {
|
||||||
const [people, setPeople] = useState<PersonNode[]>([]);
|
const [people, setPeople] = useState<PersonNode[]>([]);
|
||||||
const [relationships, setRelationships] = useState<RelationshipEdge[]>([]);
|
const [relationships, setRelationships] = useState<RelationshipEdge[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const pollTimerRef = useRef<number | null>(null);
|
||||||
|
const lastPeopleUpdateRef = useRef<string>('');
|
||||||
|
const lastRelationshipsUpdateRef = useRef<string>('');
|
||||||
|
|
||||||
// Load network data
|
// Load network data
|
||||||
const loadNetworkData = useCallback(async () => {
|
const loadNetworkData = useCallback(async (isPolling = false) => {
|
||||||
if (!networkId) {
|
if (!networkId) {
|
||||||
setPeople([]);
|
setPeople([]);
|
||||||
setRelationships([]);
|
setRelationships([]);
|
||||||
@ -35,7 +41,9 @@ export const useFriendshipNetwork = (networkId: string | null) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
if (!isPolling) {
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// Fetch people and relationships in parallel
|
// Fetch people and relationships in parallel
|
||||||
@ -55,19 +63,149 @@ export const useFriendshipNetwork = (networkId: string | null) => {
|
|||||||
id: rel._id,
|
id: rel._id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setPeople(peopleNodes);
|
// Generate hashes to detect changes
|
||||||
setRelationships(relationshipEdges);
|
const positionsHash = JSON.stringify(peopleNodes.map(p => ({ id: p.id, pos: p.position })));
|
||||||
|
const relationshipsHash = JSON.stringify(relationshipEdges.map(r => ({ id: r.id, src: r.source, tgt: r.target, type: r.type })));
|
||||||
|
|
||||||
|
// Handle people updates
|
||||||
|
const peopleChanged = positionsHash !== lastPeopleUpdateRef.current;
|
||||||
|
const relsChanged = relationshipsHash !== lastRelationshipsUpdateRef.current;
|
||||||
|
|
||||||
|
// Update states only if data has changed or it's the initial load
|
||||||
|
if (peopleChanged || !isPolling) {
|
||||||
|
if (isPolling && people.length > 0) {
|
||||||
|
// During polling, only update nodes that have changed positions
|
||||||
|
const currentPeopleMap = new Map(people.map(p => [p.id, p]));
|
||||||
|
const updatedPeople = [...people];
|
||||||
|
let hasChanges = false;
|
||||||
|
|
||||||
|
// Check for position changes
|
||||||
|
for (const newNode of peopleNodes) {
|
||||||
|
const currentNode = currentPeopleMap.get(newNode.id);
|
||||||
|
if (currentNode) {
|
||||||
|
const currentPos = currentNode.position;
|
||||||
|
const newPos = newNode.position;
|
||||||
|
|
||||||
|
// Update if position changed
|
||||||
|
if (currentPos.x !== newPos.x || currentPos.y !== newPos.y) {
|
||||||
|
const idx = updatedPeople.findIndex(p => p.id === newNode.id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
updatedPeople[idx] = newNode;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// New node not in current state, add it
|
||||||
|
updatedPeople.push(newNode);
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for removed nodes
|
||||||
|
const newNodeIds = new Set(peopleNodes.map(p => p.id));
|
||||||
|
const removedNodes = updatedPeople.filter(p => !newNodeIds.has(p.id));
|
||||||
|
if (removedNodes.length > 0) {
|
||||||
|
const remainingPeople = updatedPeople.filter(p => newNodeIds.has(p.id));
|
||||||
|
updatedPeople.length = 0;
|
||||||
|
updatedPeople.push(...remainingPeople);
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state only if positions changed
|
||||||
|
if (hasChanges) {
|
||||||
|
setPeople(updatedPeople);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Initial load or major change - set everything
|
||||||
|
setPeople(peopleNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPeopleUpdateRef.current = positionsHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle relationship updates similarly
|
||||||
|
if (relsChanged || !isPolling) {
|
||||||
|
if (isPolling && relationships.length > 0) {
|
||||||
|
// Check for changes in relationships
|
||||||
|
const currentRelsMap = new Map(relationships.map(r => [r.id, r]));
|
||||||
|
const updatedRels = [...relationships];
|
||||||
|
let hasRelChanges = false;
|
||||||
|
|
||||||
|
// Add new or changed relationships
|
||||||
|
for (const newRel of relationshipEdges) {
|
||||||
|
const currentRel = currentRelsMap.get(newRel.id);
|
||||||
|
if (!currentRel) {
|
||||||
|
// New relationship
|
||||||
|
updatedRels.push(newRel);
|
||||||
|
hasRelChanges = true;
|
||||||
|
} else if (currentRel.type !== newRel.type ||
|
||||||
|
currentRel.source !== newRel.source ||
|
||||||
|
currentRel.target !== newRel.target) {
|
||||||
|
// Changed relationship
|
||||||
|
const idx = updatedRels.findIndex(r => r.id === newRel.id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
updatedRels[idx] = newRel;
|
||||||
|
hasRelChanges = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove deleted relationships
|
||||||
|
const newRelIds = new Set(relationshipEdges.map(r => r.id));
|
||||||
|
const removedRels = updatedRels.filter(r => !newRelIds.has(r.id));
|
||||||
|
if (removedRels.length > 0) {
|
||||||
|
const remainingRels = updatedRels.filter(r => newRelIds.has(r.id));
|
||||||
|
updatedRels.length = 0;
|
||||||
|
updatedRels.push(...remainingRels);
|
||||||
|
hasRelChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasRelChanges) {
|
||||||
|
setRelationships(updatedRels);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Initial load or major change
|
||||||
|
setRelationships(relationshipEdges);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRelationshipsUpdateRef.current = relationshipsHash;
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || 'Failed to load network data');
|
setError(err.message || 'Failed to load network data');
|
||||||
console.error('Error loading network data:', err);
|
console.error('Error loading network data:', err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
if (!isPolling) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [networkId]);
|
}, [networkId]);
|
||||||
|
|
||||||
|
// Set up polling for network data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Initial load
|
||||||
loadNetworkData();
|
loadNetworkData();
|
||||||
}, [loadNetworkData]);
|
|
||||||
|
// Set up polling if interval is provided and > 0
|
||||||
|
if (networkId && pollInterval > 0) {
|
||||||
|
// Clear any existing timer
|
||||||
|
if (pollTimerRef.current) {
|
||||||
|
window.clearInterval(pollTimerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new polling timer
|
||||||
|
pollTimerRef.current = window.setInterval(() => {
|
||||||
|
loadNetworkData(true);
|
||||||
|
}, pollInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup function
|
||||||
|
return () => {
|
||||||
|
if (pollTimerRef.current) {
|
||||||
|
window.clearInterval(pollTimerRef.current);
|
||||||
|
pollTimerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [loadNetworkData, networkId, pollInterval]);
|
||||||
|
|
||||||
// Add a new person
|
// Add a new person
|
||||||
const createPerson = async (personData: {
|
const createPerson = async (personData: {
|
||||||
@ -81,7 +219,12 @@ export const useFriendshipNetwork = (networkId: string | null) => {
|
|||||||
try {
|
try {
|
||||||
const newPerson = await addPerson(networkId, personData);
|
const newPerson = await addPerson(networkId, personData);
|
||||||
const newPersonNode: PersonNode = { ...newPerson, id: newPerson._id };
|
const newPersonNode: PersonNode = { ...newPerson, id: newPerson._id };
|
||||||
setPeople([...people, newPersonNode]);
|
const updatedPeople = [...people, newPersonNode];
|
||||||
|
setPeople(updatedPeople);
|
||||||
|
|
||||||
|
// Update the reference hash to avoid unnecessary state updates on next poll
|
||||||
|
lastPeopleUpdateRef.current = JSON.stringify(updatedPeople.map(p => ({ id: p.id, pos: p.position })));
|
||||||
|
|
||||||
return newPersonNode;
|
return newPersonNode;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || 'Failed to create person');
|
setError(err.message || 'Failed to create person');
|
||||||
@ -105,7 +248,16 @@ export const useFriendshipNetwork = (networkId: string | null) => {
|
|||||||
const updatedPerson = await updatePerson(networkId, personId, personData);
|
const updatedPerson = await updatePerson(networkId, personId, personData);
|
||||||
const updatedPersonNode: PersonNode = { ...updatedPerson, id: updatedPerson._id };
|
const updatedPersonNode: PersonNode = { ...updatedPerson, id: updatedPerson._id };
|
||||||
|
|
||||||
setPeople(people.map(person => (person._id === personId ? updatedPersonNode : person)));
|
// Update the local state
|
||||||
|
const updatedPeople = people.map(person =>
|
||||||
|
(person._id === personId ? updatedPersonNode : person)
|
||||||
|
);
|
||||||
|
setPeople(updatedPeople);
|
||||||
|
|
||||||
|
// Update the reference hash if position changed to avoid unnecessary state updates on next poll
|
||||||
|
if (personData.position) {
|
||||||
|
lastPeopleUpdateRef.current = JSON.stringify(updatedPeople.map(p => ({ id: p.id, pos: p.position })));
|
||||||
|
}
|
||||||
|
|
||||||
return updatedPersonNode;
|
return updatedPersonNode;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@ -122,12 +274,20 @@ export const useFriendshipNetwork = (networkId: string | null) => {
|
|||||||
await removePerson(networkId, personId);
|
await removePerson(networkId, personId);
|
||||||
|
|
||||||
// Remove the person
|
// Remove the person
|
||||||
setPeople(people.filter(person => person._id !== personId));
|
const updatedPeople = people.filter(person => person._id !== personId);
|
||||||
|
setPeople(updatedPeople);
|
||||||
|
|
||||||
// Remove all relationships involving this person
|
// Remove all relationships involving this person
|
||||||
setRelationships(
|
const updatedRelationships = relationships.filter(
|
||||||
relationships.filter(rel => rel.source !== personId && rel.target !== personId)
|
rel => rel.source !== personId && rel.target !== personId
|
||||||
);
|
);
|
||||||
|
setRelationships(updatedRelationships);
|
||||||
|
|
||||||
|
// Update both reference hashes to avoid unnecessary state updates on next poll
|
||||||
|
lastPeopleUpdateRef.current = JSON.stringify(updatedPeople.map(p => ({ id: p.id, pos: p.position })));
|
||||||
|
lastRelationshipsUpdateRef.current = JSON.stringify(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');
|
||||||
throw err;
|
throw err;
|
||||||
@ -147,7 +307,14 @@ export const useFriendshipNetwork = (networkId: string | null) => {
|
|||||||
const newRelationship = await addRelationship(networkId, relationshipData);
|
const newRelationship = await addRelationship(networkId, relationshipData);
|
||||||
const newRelationshipEdge: RelationshipEdge = { ...newRelationship, id: newRelationship._id };
|
const newRelationshipEdge: RelationshipEdge = { ...newRelationship, id: newRelationship._id };
|
||||||
|
|
||||||
setRelationships([...relationships, newRelationshipEdge]);
|
const updatedRelationships = [...relationships, newRelationshipEdge];
|
||||||
|
setRelationships(updatedRelationships);
|
||||||
|
|
||||||
|
// Update the relationship hash to avoid unnecessary state updates on next poll
|
||||||
|
lastRelationshipsUpdateRef.current = JSON.stringify(updatedRelationships.map(r =>
|
||||||
|
({ id: r.id, src: r.source, tgt: r.target, type: r.type })
|
||||||
|
));
|
||||||
|
|
||||||
return newRelationshipEdge;
|
return newRelationshipEdge;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || 'Failed to create relationship');
|
setError(err.message || 'Failed to create relationship');
|
||||||
@ -176,9 +343,15 @@ export const useFriendshipNetwork = (networkId: string | null) => {
|
|||||||
id: updatedRelationship._id,
|
id: updatedRelationship._id,
|
||||||
};
|
};
|
||||||
|
|
||||||
setRelationships(
|
const updatedRelationships = relationships.map(rel =>
|
||||||
relationships.map(rel => (rel._id === relationshipId ? updatedRelationshipEdge : rel))
|
(rel._id === relationshipId ? updatedRelationshipEdge : rel)
|
||||||
);
|
);
|
||||||
|
setRelationships(updatedRelationships);
|
||||||
|
|
||||||
|
// Update the relationship hash to avoid unnecessary state updates on next poll
|
||||||
|
lastRelationshipsUpdateRef.current = JSON.stringify(updatedRelationships.map(r =>
|
||||||
|
({ id: r.id, src: r.source, tgt: r.target, type: r.type })
|
||||||
|
));
|
||||||
|
|
||||||
return updatedRelationshipEdge;
|
return updatedRelationshipEdge;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@ -193,7 +366,13 @@ export const useFriendshipNetwork = (networkId: string | null) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await removeRelationship(networkId, relationshipId);
|
await removeRelationship(networkId, relationshipId);
|
||||||
setRelationships(relationships.filter(rel => rel._id !== relationshipId));
|
const updatedRelationships = relationships.filter(rel => rel._id !== relationshipId);
|
||||||
|
setRelationships(updatedRelationships);
|
||||||
|
|
||||||
|
// Update the relationship hash to avoid unnecessary state updates on next poll
|
||||||
|
lastRelationshipsUpdateRef.current = JSON.stringify(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');
|
||||||
throw err;
|
throw err;
|
||||||
@ -204,6 +383,22 @@ export const useFriendshipNetwork = (networkId: string | null) => {
|
|||||||
const refreshNetwork = async (): Promise<void> => {
|
const refreshNetwork = async (): Promise<void> => {
|
||||||
await loadNetworkData();
|
await loadNetworkData();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Update the poll interval
|
||||||
|
const setPollInterval = (newInterval: number) => {
|
||||||
|
// Clear existing timer
|
||||||
|
if (pollTimerRef.current) {
|
||||||
|
window.clearInterval(pollTimerRef.current);
|
||||||
|
pollTimerRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up new timer if interval > 0
|
||||||
|
if (newInterval > 0 && networkId) {
|
||||||
|
pollTimerRef.current = window.setInterval(() => {
|
||||||
|
loadNetworkData(true);
|
||||||
|
}, newInterval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
people,
|
people,
|
||||||
@ -217,5 +412,6 @@ export const useFriendshipNetwork = (networkId: string | null) => {
|
|||||||
updateRelationship: updateRelationshipData,
|
updateRelationship: updateRelationshipData,
|
||||||
deleteRelationship,
|
deleteRelationship,
|
||||||
refreshNetwork,
|
refreshNetwork,
|
||||||
|
setPollInterval,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user