diff --git a/src/components/Map/index.tsx b/src/components/Map/index.tsx index 81f533e..0c78cee 100644 --- a/src/components/Map/index.tsx +++ b/src/components/Map/index.tsx @@ -4,14 +4,14 @@ import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet' import MarkerIcon from '../../assets/marker_map_icon.png' import L, { LatLngExpression } from 'leaflet' import 'leaflet/dist/leaflet.css' -import { useDataContext } from '@/context/DataContext' -import { NodeData } from '@/shared/types/RowDataType' import styles from './style.module.css' +import { useMapContext } from '../../context/MapContext' +import { LinearProgress } from '@mui/material' +import { LocationNode } from '../../shared/types/locationNodeType' export default function Map() { const [isClient, setIsClient] = useState(false) - const { data, loading, error } = useDataContext() - console.log('Table data: ', data) + const { data, loading, error } = useMapContext() useEffect(() => { setIsClient(true) @@ -27,56 +27,77 @@ export default function Map() { }) const getRandomOffset = (): number => { - const min = 0.0002; - const max = 0.0006; - const randomValue = Math.random() * (max - min) + min; - return Math.random() < 0.5 ? -randomValue : randomValue; - }; + const min = 0.0002 + const max = 0.0006 + const randomValue = Math.random() * (max - min) + min + return Math.random() < 0.5 ? -randomValue : randomValue + } - const offsetCoordinates = (latitude: number, longitude: number, index: number, total: number): LatLngExpression => { - - const latOffset = getRandomOffset(); - const lngOffset = getRandomOffset(); - - return [ - latitude + latOffset, - longitude + lngOffset - ]; - }; - const groupedNodes = data.reduce((acc, node: NodeData) => { - if (node?.location?.lat && node?.location?.lon) { - const key = `${node.location.lat},${node.location.lon}` - if (!acc[key]) { - acc[key] = [] + const offsetCoordinates = (latitude: number, longitude: number): LatLngExpression => { + const latOffset = getRandomOffset() + const lngOffset = getRandomOffset() + return [latitude + latOffset, longitude + lngOffset] + } + + const groupedNodesByCity = data.reduce( + ( + acc: Record, + node: LocationNode + ) => { + const { city, lat, lon, country } = node._source + + if (city) { + if (!acc[city]) { + acc[city] = { lat, lon, country, count: 0 } + } + + acc[city].count += 1 } - acc[key].push(node) - } - return acc - }, {} as Record) + + return acc + }, + {} + ) return ( isClient && ( - {Object.entries(groupedNodes).map(([key, nodes]) => ( - nodes.map((node, index) => ( - - - Node ID: {node.id} -
- Network: {node.indexer?.[0]?.network} -
- Location: {node.location.country} -
- City: {node.location.city} -
-
- )) - ))} + {!loading && + !error && + Object.entries(groupedNodesByCity).map( + ([city, { lat, lon, country, count }]) => { + if ( + typeof lat !== 'number' || + typeof lon !== 'number' || + isNaN(lat) || + isNaN(lon) + ) { + console.warn( + `Invalid coordinates for city: ${city}, lat: ${lat}, lon: ${lon}` + ) + return null + } + + return ( + + + City: {city} +
+ Country: {country} +
+ Total Nodes: {count} +
+
+
+ ) + } + )} + {loading && }
) ) diff --git a/src/context/MapContext.tsx b/src/context/MapContext.tsx new file mode 100644 index 0000000..737452b --- /dev/null +++ b/src/context/MapContext.tsx @@ -0,0 +1,82 @@ +import React, { + createContext, + useState, + useEffect, + useContext, + ReactNode, + useMemo +} from 'react' +import axios from 'axios' +import { LocationNode } from '../shared/types/locationNodeType' + +interface CountryNodesInfo { + country: string + nodeIds: string[] + totalNodes: number +} + +interface MapContextType { + data: LocationNode[] + countryNodesInfo: CountryNodesInfo[] + loading: boolean + error: any +} + +interface MapProviderProps { + children: ReactNode +} + +export const MapContext = createContext(undefined) + +export const MapProvider: React.FC = ({ children }) => { + const [data, setData] = useState([]) + const [countryNodesInfo, setCountryNodesInfo] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const fetchUrl = useMemo(() => { + const baseUrl = + process.env.NEXT_PUBLIC_API_URL || 'https://incentive-backend.oceanprotocol.com' + return `${baseUrl}/locations` + }, []) + + useEffect(() => { + const fetchData = async () => { + setLoading(true) + try { + const response = await axios.get(fetchUrl) + const nodes = response.data + + setData(nodes) + } catch (err) { + console.log('error', err) + setError(err) + } finally { + setLoading(false) + } + } + + fetchData() + }, [fetchUrl]) + + return ( + + {children} + + ) +} + +export const useMapContext = () => { + const context = useContext(MapContext) + if (context === undefined) { + throw new Error('useMapContext must be used within a MapProvider') + } + return context +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 0c6ead3..24bde1d 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -9,6 +9,7 @@ import RootLayout from '../components/Layout'; import { AdminProvider } from '../context/AdminProvider'; import { chains } from '../shared/utils/chains'; import { DataProvider } from '@/context/DataContext'; +import { MapProvider } from '../context/MapContext' export default function App({ Component, pageProps }: AppProps) { const config = getDefaultConfig({ @@ -28,13 +29,15 @@ export default function App({ Component, pageProps }: AppProps) { - - - + + + + + - ); + ) } diff --git a/src/shared/types/locationNodeType.ts b/src/shared/types/locationNodeType.ts new file mode 100644 index 0000000..9cd7878 --- /dev/null +++ b/src/shared/types/locationNodeType.ts @@ -0,0 +1,13 @@ +export interface LocationNode { + _index: string + _id: string + _score: number + _source: { + id: string + ip: string + country: string + city: string + lat: number + lon: number + } +}