diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index dbcac05..399e8e6 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "realt-properties-map-frontend",
- "version": "1.0.1",
+ "version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "realt-properties-map-frontend",
- "version": "1.0.1",
+ "version": "1.1.0",
"license": "Apache-2.0",
"dependencies": {
"@apollo/client": "^3.9.9",
@@ -38,6 +38,7 @@
"react-leaflet": "^4.2.1",
"react-leaflet-cluster": "^2.1.0",
"react-redux": "^9.1.0",
+ "react-router-dom": "^6.22.3",
"react-scripts": "^5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
@@ -4901,6 +4902,14 @@
}
}
},
+ "node_modules/@remix-run/router": {
+ "version": "1.15.3",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz",
+ "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@repeaterjs/repeater": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.5.tgz",
@@ -17171,6 +17180,36 @@
}
}
},
+ "node_modules/react-router": {
+ "version": "6.22.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz",
+ "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==",
+ "dependencies": {
+ "@remix-run/router": "1.15.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.22.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz",
+ "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==",
+ "dependencies": {
+ "@remix-run/router": "1.15.3",
+ "react-router": "6.22.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
"node_modules/react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index a4ba736..2d2ae56 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -57,6 +57,7 @@
"react-leaflet": "^4.2.1",
"react-leaflet-cluster": "^2.1.0",
"react-redux": "^9.1.0",
+ "react-router-dom": "^6.22.3",
"react-scripts": "^5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 2ef866b..76bea25 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -6,17 +6,20 @@ import { MantineProviders } from './providers/MantineProvider';
import { HotkeysProvider } from './providers/HotkeysProvider';
import InitStoreProvider from './providers/InitStoreProvider';
import './i18next/i18next';
+import { QueryProvider } from './providers/QueryProvider';
function App() {
return (
-
-
-
-
-
-
+
+
+
+
+
+
+
+
);
diff --git a/frontend/src/components/Map/MapMarkers.ts b/frontend/src/components/Map/MapMarkers.ts
index f5c0b6f..e0c937a 100644
--- a/frontend/src/components/Map/MapMarkers.ts
+++ b/frontend/src/components/Map/MapMarkers.ts
@@ -1,13 +1,15 @@
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { TFunction } from 'i18next';
-import { DivIcon, MarkerClusterGroup, marker, Marker, markerClusterGroup, LeafletMouseEvent } from 'leaflet';
+import { DivIcon, MarkerClusterGroup, marker, Marker, markerClusterGroup, LeafletMouseEvent, LeafletEvent } from 'leaflet';
import { useLeafletContext } from '@react-leaflet/core';
import 'leaflet.markercluster';
-import { Wallet } from '../../types/wallet';
import { Property } from '../../types/property';
import { useAppDispatch, useAppSelector } from '../../hooks/useInitStore';
import { setSelected } from '../../store/marker/markerReducer';
+import { setLatLng, setSelectedProperty, setZoom } from '../../store/urlQuery/urlQuery.reducer';
+import { selectedProperty } from '../../store/urlQuery/urlQuery.selector';
+import { Maybe } from '../../types/global';
export const OWNED_SELECTOR = '[data-marker-owned]';
export const CSSCLASSES = {
@@ -15,17 +17,16 @@ export const CSSCLASSES = {
notOwned: 'opacity-80',
}
-
function pinSvg(cssClasses: string) {
return ``;
}
-// TODO - Map options for opacity
export function generateIcon(
property: Property,
differentiateOwned: boolean,
markerOpacity: number,
+ selected: boolean,
) {
const owned = property.ownedAmount > 0;
const ownedClass = differentiateOwned && owned ? CSSCLASSES.owned : CSSCLASSES.notOwned;
@@ -37,7 +38,7 @@ export function generateIcon(
${owned ? 'data-marker-owned' : ''}
${property.source ? `data-marker-${property.source}` : ''}
${property.ownerWallets.length ? `data-marker-wallet="${property.ownerWallets.join(' ')}"` : ''}>
- ${pinSvg(`${property.iconColorClass}-icon ${ownedClass}`)}
+ ${pinSvg(`${property.iconColorClass}-icon ${ownedClass + (selected ? ' selected' : '')}`)}
${property.icon}
`,
iconSize: [50, 50],
@@ -50,9 +51,13 @@ function filterProperties(
displayAll: boolean,
displayGnosis: boolean,
displayRmm: boolean,
+ selected: string | null,
) {
return properties
.filter((property) => {
+ if (selected && property.address === selected) {
+ return true;
+ }
let toInclude = !property.isOld && property.productType !== 'equity_token';
if (!displayAll && property.ownedAmount <= 0) {
toInclude = false;
@@ -71,10 +76,11 @@ function createMarker(
property: Property,
markerOpacity: number,
differentiateOwned: boolean,
+ selected: Maybe,
t: TFunction<"common", undefined>,
): Marker {
return marker([property.coordinate.lat, property.coordinate.lng], {
- icon: generateIcon(property, differentiateOwned, markerOpacity),
+ icon: generateIcon(property, differentiateOwned, markerOpacity, selected === property.address),
alt: property.propertyTypeName,
title: t('propertyType.' + property.propertyTypeName),
});
@@ -107,10 +113,8 @@ let markerCluster: MarkerClusterGroup;
let markers: Array = [];
export function MapMarkers({
properties,
- wallets,
}: {
properties: Property[];
- wallets: Wallet[];
}) {
const { t } = useTranslation('common');
const dispatch = useAppDispatch();
@@ -122,6 +126,7 @@ export function MapMarkers({
differentiateOwned,
markerOpacity,
} = useAppSelector((state) => state.mapOptions);
+ const selectedUrlParam = useAppSelector(selectedProperty);
function onMarkerClicked(event: LeafletMouseEvent, property: Property) {
const currentZoom = map.getZoom();
@@ -142,6 +147,7 @@ export function MapMarkers({
lng: event.latlng.lng,
},
}));
+ dispatch(setSelectedProperty(property.address));
}
function clearMap() {
@@ -156,6 +162,8 @@ export function MapMarkers({
markerCluster.clearAllEventListeners();
markerCluster.clearLayers();
map.removeLayer(markerCluster);
+ map.removeEventListener('zoom');
+ map.removeEventListener('moveend');
}
function getCleanMarkerCluster() {
@@ -173,15 +181,37 @@ export function MapMarkers({
clearMap();
markerCluster = getCleanMarkerCluster();
- filterProperties(properties, displayAll, displayGnosis, displayRmm)
+ filterProperties(properties, displayAll, displayGnosis, displayRmm, selectedUrlParam)
.forEach((property) => {
- const marker = createMarker(property, markerOpacity, differentiateOwned, t)
+ const marker = createMarker(property, markerOpacity, differentiateOwned, selectedUrlParam, t)
.addEventListener('click', (event) => onMarkerClicked(event, property));
markers.push(marker);
markerCluster.addLayer(marker);
});
map.addLayer(markerCluster);
+ map.addEventListener('zoom', (event: LeafletEvent) => {
+ dispatch(setZoom(event.target.getZoom()));
+ });
+ map.addEventListener('moveend', (event: LeafletEvent) => {
+ dispatch(setLatLng([event.target.getCenter().lat, event.target.getCenter().lng]));
+ });
+ if (selectedUrlParam) {
+ const selectedProperty = properties.find((property) => property.address === selectedUrlParam);
+ if (selectedProperty) {
+ dispatch(setSelected({
+ property: selectedProperty,
+ latlng: {
+ lat: selectedProperty.coordinate.lat,
+ lng: selectedProperty.coordinate.lng,
+ },
+ }));
+ }
+ }
+
+ return () => {
+ clearMap();
+ }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [properties, displayAll, displayGnosis, displayRmm, differentiateOwned, markerOpacity]);
diff --git a/frontend/src/components/Map/MapWrapper.tsx b/frontend/src/components/Map/MapWrapper.tsx
index 962b797..0390f59 100644
--- a/frontend/src/components/Map/MapWrapper.tsx
+++ b/frontend/src/components/Map/MapWrapper.tsx
@@ -10,10 +10,13 @@ import { Property } from '../../types/property';
import { PropertyPanel } from './PropertyPanel';
import { MapMarkers } from './MapMarkers';
import { MapEvents } from './MapEvents';
+import { selectedLatLng, selectedZoom } from '../../store/urlQuery/urlQuery.selector';
export function MapWrapper() {
const realToken = useAppSelector(selectRealtokensList);
const wallets = useAppSelector(selectWalletsList);
+ const center = useAppSelector(selectedLatLng);
+ const zoom = useAppSelector(selectedZoom);
const [properties, setProperties] = useState([]);
@@ -25,12 +28,11 @@ export function MapWrapper() {
<>
+ center={center}
+ zoom={zoom}>
-
+
diff --git a/frontend/src/components/Map/PropertyPanel.tsx b/frontend/src/components/Map/PropertyPanel.tsx
index f6c8e06..14ad7db 100644
--- a/frontend/src/components/Map/PropertyPanel.tsx
+++ b/frontend/src/components/Map/PropertyPanel.tsx
@@ -12,6 +12,7 @@ import date from "../../utils/date";
import { selectedLanguage } from "../../store/settings/settingsSelector";
import { selectDifferentiateOwned } from "../../store/mapOptions/mapOptionsSelector";
import { useElementSize } from "@mantine/hooks";
+import { setSelectedProperty } from "../../store/urlQuery/urlQuery.reducer";
function toFixedStr(value: number, precision: number = 2) {
return value.toFixed(precision).toLowerCase();
@@ -31,6 +32,7 @@ export function PropertyPanel() {
}, [property]);
function onClose() {
+ dispatch(setSelectedProperty(null));
dispatch(clearSelected());
}
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index 717d92c..67131d3 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -4,13 +4,16 @@ import './index.css';
import '@mantine/core/styles.css';
import '@mantine/carousel/styles.css';
import App from './App';
+import { BrowserRouter } from 'react-router-dom';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
-
+
+
+
);
diff --git a/frontend/src/providers/QueryProvider.tsx b/frontend/src/providers/QueryProvider.tsx
new file mode 100644
index 0000000..db8ce06
--- /dev/null
+++ b/frontend/src/providers/QueryProvider.tsx
@@ -0,0 +1,41 @@
+import { useEffect } from "react";
+import { useSearchParams } from "react-router-dom";
+import { useAppSelector } from "../hooks/useInitStore";
+import { selectedLatLng, selectedProperty, selectedZoom } from "../store/urlQuery/urlQuery.selector";
+import { Maybe } from "../types/global";
+
+function handleSearchParam(
+ searchParams: URLSearchParams,
+ key: string,
+ value: Maybe,
+) {
+ if (value) {
+ searchParams.set(key, value);
+ } else {
+ searchParams.delete(key);
+ }
+ return searchParams;
+}
+
+export function QueryProvider({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ let [_, setSearchParams] = useSearchParams();
+ const center = useAppSelector(selectedLatLng);
+ const zoom = useAppSelector(selectedZoom);
+ const selected = useAppSelector(selectedProperty);
+
+ useEffect(() => {
+ const newSearchParams = new URLSearchParams();
+ handleSearchParam(newSearchParams, "latlng", JSON.stringify(center));
+ handleSearchParam(newSearchParams, "zoom", JSON.stringify(zoom));
+ handleSearchParam(newSearchParams, "selected", selected);
+ setSearchParams(newSearchParams);
+ }, [center, zoom, selected, setSearchParams]);
+
+ return (
+ <>{children}>
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts
index 2d03800..431c670 100644
--- a/frontend/src/store/store.ts
+++ b/frontend/src/store/store.ts
@@ -5,6 +5,7 @@ import realTokensReducer from './realtokens/realtokensReducer';
import settingsReducer from './settings/settingsReducer';
import markerReducer from './marker/markerReducer';
import currenciesReducer from './currencies/currenciesReducer';
+import urlQueryReducer from './urlQuery/urlQuery.reducer';
const rootReducer = combineReducers({
settings: settingsReducer,
@@ -13,6 +14,7 @@ const rootReducer = combineReducers({
realtokens: realTokensReducer,
marker: markerReducer,
currencies: currenciesReducer,
+ urlQuery: urlQueryReducer,
});
const store = configureStore({
diff --git a/frontend/src/store/urlQuery/urlQuery.reducer.ts b/frontend/src/store/urlQuery/urlQuery.reducer.ts
new file mode 100644
index 0000000..548955b
--- /dev/null
+++ b/frontend/src/store/urlQuery/urlQuery.reducer.ts
@@ -0,0 +1,39 @@
+import { createSlice } from "@reduxjs/toolkit";
+import { Maybe } from "../../types/global";
+
+interface UrlQueryState {
+ selected: Maybe;
+ latlng?: [number, number];
+ zoom?: number;
+}
+
+const urlParams = new URLSearchParams(window.location.search);
+const selected = urlParams.get('selected');
+const latlng = urlParams.get('latlng');
+const zoom = urlParams.get('zoom');
+
+const initialState: UrlQueryState = {
+ selected,
+ latlng: latlng ? JSON.parse(latlng) : [32, -83],
+ zoom: zoom ? parseInt(zoom) : 4,
+};
+
+export const urlQuerySlice = createSlice({
+ name: 'urlQuery',
+ initialState,
+ reducers: {
+ setSelectedProperty: (state, action) => {
+ state.selected = action.payload;
+ },
+ setLatLng: (state, action) => {
+ state.latlng = action.payload;
+ },
+ setZoom: (state, action) => {
+ state.zoom = action.payload;
+ },
+ },
+});
+
+export const { setSelectedProperty, setLatLng, setZoom } = urlQuerySlice.actions;
+
+export default urlQuerySlice.reducer;
\ No newline at end of file
diff --git a/frontend/src/store/urlQuery/urlQuery.selector.ts b/frontend/src/store/urlQuery/urlQuery.selector.ts
new file mode 100644
index 0000000..5bff9c0
--- /dev/null
+++ b/frontend/src/store/urlQuery/urlQuery.selector.ts
@@ -0,0 +1,7 @@
+import { RootState } from "../store";
+
+export const selectedProperty = (state: RootState) => state.urlQuery.selected;
+
+export const selectedLatLng = (state: RootState) => state.urlQuery.latlng;
+
+export const selectedZoom = (state: RootState) => state.urlQuery.zoom;
\ No newline at end of file