| | import React, { useRef, useEffect, useState } from 'react';
|
| |
|
| | const NetworkMap = ({ users }) => {
|
| | const canvasRef = useRef(null);
|
| | const [hoveredUser, setHoveredUser] = useState(null);
|
| |
|
| | useEffect(() => {
|
| | const canvas = canvasRef.current;
|
| | if (!canvas) return;
|
| | const ctx = canvas.getContext('2d');
|
| | let animationFrameId;
|
| |
|
| |
|
| | const lats = users.map(u => u.location.lat);
|
| | const lngs = users.map(u => u.location.lng);
|
| | const minLat = Math.min(...lats) - 5;
|
| | const maxLat = Math.max(...lats) + 5;
|
| | const minLng = Math.min(...lngs) - 5;
|
| | const maxLng = Math.max(...lngs) + 5;
|
| |
|
| | const project = (lat, lng) => {
|
| | const x = ((lng - minLng) / (maxLng - minLng)) * canvas.width;
|
| | const y = canvas.height - ((lat - minLat) / (maxLat - minLat)) * canvas.height;
|
| | return { x, y };
|
| | };
|
| |
|
| | const render = () => {
|
| |
|
| | if (canvas.width !== canvas.parentElement.clientWidth || canvas.height !== canvas.parentElement.clientHeight) {
|
| | canvas.width = canvas.parentElement.clientWidth;
|
| | canvas.height = canvas.parentElement.clientHeight;
|
| | }
|
| |
|
| | ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| |
|
| |
|
| | ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
|
| | ctx.lineWidth = 1;
|
| | const gridSize = 40;
|
| | for (let x = 0; x < canvas.width; x += gridSize) {
|
| | ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
|
| | }
|
| | for (let y = 0; y < canvas.height; y += gridSize) {
|
| | ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
|
| | }
|
| |
|
| |
|
| | ctx.strokeStyle = 'rgba(0, 240, 255, 0.05)';
|
| | ctx.lineWidth = 0.5;
|
| | users.forEach((u, i) => {
|
| | const p1 = project(u.location.lat, u.location.lng);
|
| |
|
| | for (let j = i + 1; j < Math.min(i + 3, users.length); j++) {
|
| | const p2 = project(users[j].location.lat, users[j].location.lng);
|
| | ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.stroke();
|
| | }
|
| | });
|
| |
|
| |
|
| | users.forEach(user => {
|
| | const { x, y } = project(user.location.lat, user.location.lng);
|
| | const isActive = user.status === 'Online';
|
| | const isRoaming = user.isRoaming;
|
| | const color = isActive ? '#00ff9d' : (isRoaming ? '#ffbf00' : '#ff0055');
|
| |
|
| |
|
| | if (isActive) {
|
| | const time = Date.now() / 1000;
|
| | const radius = 4 + Math.sin(time * 2 + user.id) * 2;
|
| | ctx.beginPath();
|
| | ctx.arc(x, y, radius * 2, 0, Math.PI * 2);
|
| | ctx.fillStyle = `color-mix(in srgb, ${color} 20%, transparent)`;
|
| | ctx.fill();
|
| | }
|
| |
|
| | ctx.beginPath();
|
| | ctx.arc(x, y, 4, 0, Math.PI * 2);
|
| | ctx.fillStyle = color;
|
| | ctx.fill();
|
| |
|
| |
|
| | if (isRoaming) {
|
| | ctx.fillStyle = 'rgba(255,255,255,0.5)';
|
| | ctx.font = '10px Inter';
|
| | ctx.fillText(user.location.city, x + 8, y + 3);
|
| | }
|
| | });
|
| |
|
| | animationFrameId = requestAnimationFrame(render);
|
| | };
|
| |
|
| | render();
|
| | return () => cancelAnimationFrame(animationFrameId);
|
| | }, [users]);
|
| |
|
| | return (
|
| | <div className="glass-panel" style={{ height: '100%', minHeight: '400px', position: 'relative', overflow: 'hidden' }}>
|
| | <canvas ref={canvasRef} style={{ width: '100%', height: '100%', display: 'block' }} />
|
| | <div style={{ position: 'absolute', top: 20, right: 20, background: 'rgba(0,0,0,0.5)', padding: '10px', borderRadius: '8px', fontSize: '12px' }}>
|
| | <div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '4px' }}>
|
| | <span style={{ width: 8, height: 8, borderRadius: '50%', background: '#00ff9d' }}></span> Online
|
| | </div>
|
| | <div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '4px' }}>
|
| | <span style={{ width: 8, height: 8, borderRadius: '50%', background: '#ffbf00' }}></span> Roaming
|
| | </div>
|
| | <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
| | <span style={{ width: 8, height: 8, borderRadius: '50%', background: '#ff0055' }}></span> Offline
|
| | </div>
|
| | </div>
|
| | </div>
|
| | );
|
| | };
|
| |
|
| | export default NetworkMap;
|
| |
|