| 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;
|
|
|