diff --git a/package.json b/package.json index 5b7b1454..6d8fdfbf 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "eslint-import-resolver-webpack": "^0.13.2", "flag-icons": "^6.6.6", "framer-motion": "^10.6.1", + "geolib": "^3.3.4", "i18next": "^21.10.0", "keycloak-js": "^19.0.1", "mapbox-gl": "^2.9.1", diff --git a/src/components/Stats.tsx b/src/components/Stats.tsx new file mode 100644 index 00000000..b3fcd9f4 --- /dev/null +++ b/src/components/Stats.tsx @@ -0,0 +1,37 @@ +import { Group, Paper, PaperProps, Text } from '@mantine/core'; + +interface StatsGridProps { + title: string; + icon: any; + children: any; + subtitle?: string; + paperProps?: PaperProps; + isText?: boolean; +} + +export function StatsGrid(data: StatsGridProps) { + return ( + + + + {data.title} + + + + + + {typeof data.children === 'string' || data.isText ? ( + + {data.children} + + ) : ( + data.children + )} + + + + {data.subtitle} + + + ); +} diff --git a/src/components/map/ClaimDrawer.tsx b/src/components/map/ClaimDrawer.tsx new file mode 100644 index 00000000..90fb917c --- /dev/null +++ b/src/components/map/ClaimDrawer.tsx @@ -0,0 +1,117 @@ +import { Alert, Avatar, Button, Center, Drawer, Flex, Group, Loader, ScrollArea, Text } from '@mantine/core'; +import { + Icon123, + IconBuilding, + IconCheck, + IconCopy, + IconCrane, + IconDotsCircleHorizontal, + IconPencil, + IconPin, + IconRuler2, + IconUser, + IconUsersGroup, +} from '@tabler/icons-react'; + +import { StatsGrid } from '../Stats'; +import classes from '../styles/components/Card.module.css'; +import { domainToASCII } from 'url'; +import { getAreaOfPolygon } from 'geolib'; +import { showNotification } from '@mantine/notifications'; +import { useClipboard } from '@mantine/hooks'; +import useSWR from 'swr'; +import { useUser } from '../../hooks/useUser'; + +interface ClaimDrawerProps { + setOpen: (bool: boolean) => void; + open: boolean; + id: string | null; +} + +export function ClaimDrawer(props: ClaimDrawerProps) { + const { data, isValidating } = useSWR('/claims/' + props.id); + const { user } = useUser(); + const clipboard = useClipboard(); + + if (props.id == null) return <>; + + return ( + props.setOpen(false)} + title={`Claim Details`} + size="md" + overlayProps={{ blur: 3 }} + lockScroll + scrollAreaComponent={ScrollArea.Autosize} + > + {isValidating || !data ? ( +
+ +
+ ) : ( + <> + + {data.name} + + {!data.finished && ( + }> + This Claim is still under construction and not completed yet. + + )} + + + + + {data.buildTeam.name} + + + + + + {data.owner.name?.at(0)} + + {data.owner.name} + + + + {data.builders && ( + + + {data.builders.slice(0, 4).map((b: any) => ( + {b?.name?.at(0)} + ))} + {data.builders.length > 4 && +{data.builders.length - 4}} + + + )} + + {Math.round(getAreaOfPolygon(data.area.map((p: string) => p.split(', ').map(Number)))).toLocaleString()} m² + + + + {data.owner?.id == user?.id && ( + + )} + + + )} +
+ ); +} diff --git a/src/components/map/Map.tsx b/src/components/map/Map.tsx index d1d20715..b4986b86 100644 --- a/src/components/map/Map.tsx +++ b/src/components/map/Map.tsx @@ -1,14 +1,14 @@ -import 'mapbox-gl/dist/mapbox-gl.css'; import 'mapbox-gl-style-switcher/styles.css'; +import 'mapbox-gl/dist/mapbox-gl.css'; import * as React from 'react'; import { LoadingOverlay, useMantineColorScheme, useMantineTheme } from '@mantine/core'; import { MapboxStyleDefinition, MapboxStyleSwitcherControl } from 'mapbox-gl-style-switcher'; -import axios, { AxiosResponse } from 'axios'; import { IconCheck } from '@tabler/icons'; -import MapLoader from './MapLoader'; +import { MapboxMap } from 'react-map-gl'; +import ReactDOM from 'react-dom'; import mapboxgl from 'mapbox-gl'; import { showNotification } from '@mantine/notifications'; import { useRouter } from 'next/router'; @@ -20,21 +20,22 @@ interface IMap { allowFullscreen?: boolean; savePos?: boolean; themeControls?: boolean; + src?: string; layerSetup?(map: mapboxgl.Map): void; } const styles: MapboxStyleDefinition[] = [ { title: 'Dark', - uri: 'mapbox://styles/nudelsuppe/cl7hfjfa0002h14o7h6ai832o', + uri: 'mapbox://styles/mapbox/dark-v11', }, { title: 'Light', - uri: 'mapbox://styles/mapbox/light-v9', + uri: 'mapbox://styles/mapbox/light-v11', }, - { title: 'Outdoors', uri: 'mapbox://styles/mapbox/outdoors-v11' }, - { title: 'Satellite', uri: 'mapbox://styles/mapbox/satellite-streets-v11' }, - { title: 'Streets', uri: 'mapbox://styles/mapbox/streets-v11' }, + { title: 'Outdoors', uri: 'mapbox://styles/mapbox/outdoors-v12' }, + { title: 'Satellite', uri: 'mapbox://styles/mapbox/satellite-streets-v12' }, + { title: 'Streets', uri: 'mapbox://styles/mapbox/streets-v12' }, ]; function Map({ @@ -45,6 +46,7 @@ function Map({ savePos = true, themeControls = true, layerSetup, + src, }: IMap) { // Mapbox map const [map, setMap] = React.useState(); @@ -55,7 +57,8 @@ function Map({ // Boolean if map is loading (-> Display mapLoader) const [loading, setLoading] = React.useState(true); // Mantine Theme - const theme = useMantineColorScheme(); + const theme = useMantineTheme(); + const scheme = useMantineColorScheme(); // Ref to the map div const mapNode = React.useRef(null); @@ -84,9 +87,11 @@ function Map({ const mapboxMap = new mapboxgl.Map({ container: node, accessToken: process.env.NEXT_PUBLIC_MAPBOX_TOKEN, - style: theme.colorScheme == 'dark' ? styles[0].uri : styles[1].uri, + style: scheme.colorScheme == 'dark' ? styles[0].uri : styles[1].uri, zoom: 1, antialias: true, + //@ts-ignore + projection: 'mercator', ...initialOptions, }); @@ -98,13 +103,16 @@ function Map({ onMapLoaded && (await onMapLoaded(mapboxMap)); setLoading(false); + src && + mapLoadGeoJson(mapboxMap, src, 'claims', 'fill', 'claims-source', mapStatusColorPolygon, mapStatusColorLine); + layerSetup && (await layerSetup(mapboxMap)); if (allowFullscreen) mapboxMap.addControl(new mapboxgl.FullscreenControl()); if (themeControls) mapboxMap.addControl( new MapboxStyleSwitcherControl(styles, { - defaultStyle: theme.colorScheme == 'dark' ? 'Dark' : 'Light', + defaultStyle: scheme.colorScheme == 'dark' ? 'Dark' : 'Light', }), ); }); @@ -146,63 +154,26 @@ function Map({ } // Map Event Helper Functions - -export function mapHoverEffect(map: any, layer: string, source: string, text: (feature: any) => string) { - // Hover effect - let hoveredStateId: string | number | undefined = undefined; - const popup = new mapboxgl.Popup({ - closeButton: false, - closeOnClick: false, +export function mapCursorHover(map: any, layer: string) { + map.on('mouseenter', layer, () => { + map.getCanvas().style.cursor = 'pointer'; }); - map.on('mousemove', layer, (e: any) => { - if (!e.features) { - popup.remove(); - return; - } - if (e?.features.length > 0) { - // Hover effect - if (hoveredStateId !== undefined) { - map.setFeatureState({ source: source, id: hoveredStateId }, { hover: false }); - } - hoveredStateId = e.features[0].id; - map.setFeatureState({ source: source, id: hoveredStateId }, { hover: true }); - - // Tooltip - const features = map.queryRenderedFeatures(e.point, { - layers: [layer], - }); - - popup - .setLngLat(e.lngLat) - //@ts-ignore - .setText(text(features[0])) - .addTo(map); - } - }); map.on('mouseleave', layer, () => { - if (hoveredStateId !== undefined) { - map.setFeatureState({ source: source, id: hoveredStateId }, { hover: false }); - } - hoveredStateId = undefined; - - popup.remove(); + map.getCanvas().style.cursor = ''; }); } + export function mapClickEvent(map: any, layer: string, callback: (feature: any) => void) { - map.on('click', (e: any) => { - // Find features intersecting the bounding box. - const selectedFeatures = map.queryRenderedFeatures(e.point, { - layers: [layer], - }); - if (selectedFeatures.length > 0) { - callback(selectedFeatures[0]); + map.on('click', layer, (e: any) => { + if (e.features.length > 0) { + callback(e.features[0]); } }); } + export function mapCopyCoordinates(map: any, clipboard: any) { map.on('contextmenu', (e: any) => { - const user = JSON.parse(window.localStorage.getItem('auth') || '{}'); clipboard.copy(e.lngLat.lat + ', ' + e.lngLat.lng); showNotification({ title: 'Coordinates copied', @@ -215,28 +186,19 @@ export function mapCopyCoordinates(map: any, clipboard: any) { // Map Load Helper Functions export async function mapLoadGeoJson( - map: any, - url: string | AxiosResponse, + map: MapboxMap, + url: string, layer: string, - layerType: string, + layerType: any, source: string, paint: any, outline?: boolean | any, - afterFetch?: (geojson: any) => void, ) { - var geojson = null; - if (typeof url == 'string') { - geojson = await axios.get(url); - } else { - geojson = url; - } - - afterFetch && afterFetch(geojson); - + console.log(url); if (!map.getSource(source)) { map.addSource(source, { type: 'geojson', - data: geojson.data, + data: url, }); } @@ -247,53 +209,17 @@ export async function mapLoadGeoJson( paint: paint, }); if (outline) - mapLoadGeoJson( - map, - geojson, - layer + '-outline', - 'line', - source, - typeof outline == 'boolean' ? paint : outline, - false, - ); + mapLoadGeoJson(map, url, layer + '-outline', 'line', source, typeof outline == 'boolean' ? paint : outline, false); } // Map Color Helper Functions export const mapStatusColorPolygon = { - 'fill-color': [ - 'match', - ['get', 'status'], - 0, - 'rgb(201, 42, 42)', - 1, - 'rgb(16, 152, 173)', - 2, - 'rgb(245, 159, 0)', - 3, - 'rgb(245, 159, 0)', - 4, - 'rgb(55, 178, 77)', - 'rgb(201, 42, 42)', - ], + 'fill-color': ['case', ['==', ['get', 'finished'], true], 'rgb(55, 178, 77)', 'rgb(201, 42, 42)'], 'fill-opacity': ['case', ['boolean', ['feature-state', 'hover'], false], 1, 0.37], }; export const mapStatusColorLine = { - 'line-color': [ - 'match', - ['get', 'status'], - 0, - 'rgb(201, 42, 42)', - 1, - 'rgb(16, 152, 173)', - 2, - 'rgb(245, 159, 0)', - 3, - 'rgb(245, 159, 0)', - 4, - 'rgb(55, 178, 77)', - 'rgb(201, 42, 42)', - ], + 'line-color': ['case', ['==', ['get', 'finished'], true], 'rgb(55, 178, 77)', 'rgb(201, 42, 42)'], 'line-width': 2, }; diff --git a/src/pages/map.tsx b/src/pages/map.tsx index 07cda620..f6712869 100644 --- a/src/pages/map.tsx +++ b/src/pages/map.tsx @@ -1,13 +1,31 @@ -import Map from '../components/map/Map'; +import Map, { mapClickEvent, mapCopyCoordinates, mapCursorHover } from '../components/map/Map'; + +import { ClaimDrawer } from '../components/map/ClaimDrawer'; import { NextPage } from 'next'; import Page from '../components/Page'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; +import { useClipboard } from '@mantine/hooks'; +import { useState } from 'react'; const MapPage: NextPage = () => { + const clipboard = useClipboard(); + const [opened, setOpened] = useState(false); + const [selected, setSelected] = useState(null); return ( +
- + { + mapCopyCoordinates(map, clipboard); + mapCursorHover(map, 'claims'); + mapClickEvent(map, 'claims', (f) => { + setSelected(f.properties.id); + setOpened(true); + }); + }} + />
); diff --git a/src/styles/globals.css b/src/styles/globals.css index 6fbb2fde..40b626a5 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -48,3 +48,7 @@ a { .canvas { background: #000; } + +.fc-dimmed { + color: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-3)) +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 818423ae..4cc4a850 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2765,6 +2765,11 @@ geojson-vt@^3.2.1: resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-3.2.1.tgz#f8adb614d2c1d3f6ee7c4265cad4bbf3ad60c8b7" integrity sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg== +geolib@^3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/geolib/-/geolib-3.3.4.tgz#09883ba2fdab84d2764cf3615dbafb9bce3c54d0" + integrity sha512-EicrlLLL3S42gE9/wde+11uiaYAaeSVDwCUIv2uMIoRBfNJCn8EsSI+6nS3r4TCKDO6+RQNM9ayLq2at+oZQWQ== + get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f"