(null);
-
- React.useImperativeHandle(
- ref,
- () => ({
- goTo: (center: { lng: number; lat: number } | [number, number]) => {
- if (componentRef.current) {
- componentRef.current.goTo(center);
- }
- },
- resize: () => {
- if (componentRef.current) {
- componentRef.current.resize();
- }
- },
- }),
- []
- );
-
- return (
-
- );
- });
-}
-
-export default withRouterRef(MapLibreMap);
+ map.setLayoutProperty(`${PHOTO_LAYER}-active`, 'visibility', 'visible');
+
+ syncUI();
+ });
+
+ map.on('moveend', () => {
+ if (!map.isStyleLoaded()) return;
+ syncUI();
+ });
+ };
+
+ void initialize();
+
+ return () => {
+ isMounted = false;
+ if (mapRef.current) {
+ mapRef.current.remove();
+ mapRef.current = null;
+ }
+ };
+ }, [navigate, panOnClick, syncUI]);
+
+ React.useEffect(() => {
+ const map = mapRef.current;
+ if (!map) return;
+
+ if (!map.isStyleLoaded()) {
+ map.once('style.load', syncUI);
+ return;
+ }
+
+ syncUI();
+ }, [syncUI]);
+
+ React.useImperativeHandle(
+ ref,
+ () => ({
+ goTo: (center: { lng: number; lat: number } | [number, number]) => {
+ const map = mapRef.current;
+ if (!map) return;
+ map.easeTo({
+ zoom: 17.5,
+ center,
+ });
+ },
+ resize: () => {
+ mapRef.current?.resize();
+ },
+ }),
+ []
+ );
+
+ return (
+
+ );
+});
+
+export default MapLibreMap;
diff --git a/frontend/src/screens/App/screens/MapPane/index.tsx b/frontend/src/screens/App/screens/MapPane/index.tsx
index 9d165ea8b..cd035a6ee 100644
--- a/frontend/src/screens/App/screens/MapPane/index.tsx
+++ b/frontend/src/screens/App/screens/MapPane/index.tsx
@@ -1,16 +1,14 @@
-/* eslint-disable @typescript-eslint/ban-ts-comment */
import React from 'react';
import classnames from 'classnames';
import { Feature, Point } from 'geojson';
import noop from 'lodash/noop';
import uniqueId from 'lodash/uniqueId';
+import { Link, useLocation, useNavigate } from 'react-router-dom';
+
import { closest } from 'utils/photosApi';
import { OverlayId } from './components/MainMap';
import { MapInterface } from './components/MainMap/MapInterface';
-
-import { RouteComponentProps, withRouter } from 'react-router';
-import { Link } from 'react-router-dom';
import stylesheet from './MapPane.less';
import Geolocate from './components/Geolocate';
import MainMap from './components/MainMap';
@@ -31,13 +29,10 @@ interface Props extends TipJarProps {
className?: string;
}
-interface State {
- overlay: OverlayId | null;
-}
-
interface TipJarProps {
handleOpenTipJar: () => void;
}
+
function withTipJar>(
Component: C & React.ComponentType
): React.FunctionComponent> {
@@ -45,161 +40,162 @@ function withTipJar>(
props: PassProps
): JSX.Element {
const { open } = useTipJarStore();
- // @ts-ignore -- I give up
- return ;
+ const componentProps = {
+ ...(props as Omit
),
+ handleOpenTipJar: open,
+ } as P;
+ return ;
}
return WithTipJar;
}
-class MapPane extends React.Component {
- map?: MapInterface;
- private idPrefix: string;
- historyUnlisten: () => void | null = null;
-
- constructor(props: Props & RouteComponentProps) {
- super(props);
- this.state = {
- overlay: 'default-map',
- };
- this.idPrefix = uniqueId('MapPane-');
-
- this.handleOverlayChange = this.handleOverlayChange.bind(this);
- this.handleSearchFeatureSelected =
- this.handleSearchFeatureSelected.bind(this);
- this.handleGeolocated = this.handleGeolocated.bind(this);
- this.openPhoto = this.openPhoto.bind(this);
- }
-
- componentWillUnmount(): void {
- if (this.historyUnlisten) this.historyUnlisten();
- }
-
- componentDidMount(): void {
- this.historyUnlisten = this.props.history.listen(() => {
- setTimeout(() => {
- if (this.map) this.map.resize();
- });
- });
- }
- handleOverlayChange(overlay: OverlayId): void {
- this.setState({
- overlay,
+function MapPane({
+ className,
+ handleOpenTipJar,
+}: Props & TipJarProps): JSX.Element {
+ const [overlay, setOverlay] = React.useState('default-map');
+ const mapRef = React.useRef(null);
+ const setMapRef = React.useCallback((instance: MapInterface | null) => {
+ mapRef.current = instance;
+ }, []);
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const idPrefix = React.useMemo(() => uniqueId('MapPane-'), []);
+
+ React.useEffect(() => {
+ const timeout = window.setTimeout(() => {
+ mapRef.current?.resize();
});
- }
-
- handleSearchFeatureSelected(feature: Feature): void {
- const [lng, lat] = feature.geometry.coordinates;
- this.map.goTo({ lng, lat });
-
- closest({ lng, lat }).then(this.openPhoto, noop);
- }
- handleGeolocated(position: { lat: number; lng: number }): void {
- this.map.goTo(position);
- closest(position).then(this.openPhoto, noop);
- }
-
- openPhoto(identifier: string): void {
- this.props.history.push({
- pathname: '/map/photo/' + identifier,
- hash: window.location.hash,
- });
- }
-
- render(): React.ReactNode {
- const { overlay } = this.state;
- const { className } = this.props;
-
- return (
-
-
-
-
-
-
-
- {(
- [
- { name: 'Street', value: 'default-map' },
- { name: 'Arial', value: 'default-arial' },
- // { name: 'Street (1937)', value: 'district' },
- // { name: 'Buildings (1916)', value: 'atlas-1916' },
- // { name: 'Buildings (1930)', value: 'atlas-1930' },
- // { name: 'Buildings (1956)', value: 'atlas-1956' },
- // { name: 'Arial (1924)', value: 'arial-1924' },
- // { name: 'Arial (1951)', value: 'arial-1951' },
- ] as { name: string; value: OverlayId }[]
- ).map((option) => (
-
- this.handleOverlayChange(option.value)}
- className={classnames(stylesheet.overlayInput)}
- checked={option.value === overlay}
- />
-
-
- ))}
-
-
(this.map = ref)}
- className={stylesheet.map}
- panOnClick={false}
- overlay={overlay}
+ return () => {
+ window.clearTimeout(timeout);
+ };
+ }, [location.pathname, location.search, location.hash]);
+
+ const handleOverlayChange = React.useCallback((nextOverlay: OverlayId) => {
+ setOverlay(nextOverlay);
+ }, []);
+
+ const openPhoto = React.useCallback(
+ (identifier: string) => {
+ navigate(
+ {
+ pathname: `/map/photo/${identifier}`,
+ hash: window.location.hash,
+ },
+ { replace: false }
+ );
+ },
+ [navigate]
+ );
+
+ const handleSearchFeatureSelected = React.useCallback(
+ (feature: Feature) => {
+ const [lng, lat] = feature.geometry.coordinates;
+ mapRef.current?.goTo({ lng, lat });
+
+ closest({ lng, lat }).then(openPhoto, noop);
+ },
+ [openPhoto]
+ );
+
+ const handleGeolocated = React.useCallback(
+ (position: { lat: number; lng: number }) => {
+ mapRef.current?.goTo(position);
+ closest(position).then(openPhoto, noop);
+ },
+ [openPhoto]
+ );
+
+ return (
+
+
+
+
+
- );
- }
+
+ {(
+ [
+ { name: 'Street', value: 'default-map' },
+ { name: 'Arial', value: 'default-arial' },
+ // { name: 'Street (1937)', value: 'district' },
+ // { name: 'Buildings (1916)', value: 'atlas-1916' },
+ // { name: 'Buildings (1930)', value: 'atlas-1930' },
+ // { name: 'Buildings (1956)', value: 'atlas-1956' },
+ // { name: 'Arial (1924)', value: 'arial-1924' },
+ // { name: 'Arial (1951)', value: 'arial-1951' },
+ ] as { name: string; value: OverlayId }[]
+ ).map((option) => (
+
+ handleOverlayChange(option.value)}
+ className={classnames(stylesheet.overlayInput)}
+ checked={option.value === overlay}
+ />
+
+
+ ))}
+
+
+
+ );
}
-export default withTipJar(withRouter(MapPane));
+export default withTipJar(MapPane);
diff --git a/frontend/src/screens/App/screens/Outtakes/index.tsx b/frontend/src/screens/App/screens/Outtakes/index.tsx
index aabf8e961..b617d1a22 100644
--- a/frontend/src/screens/App/screens/Outtakes/index.tsx
+++ b/frontend/src/screens/App/screens/Outtakes/index.tsx
@@ -4,7 +4,7 @@ import classnames from 'classnames';
import { getOuttakeSummaries, PhotoSummary } from 'shared/utils/photosApi';
-import { Link, useHistory, useParams } from 'react-router-dom';
+import { Link, useNavigate, useParams } from 'react-router-dom';
import Grid from 'shared/components/Grid';
import Paginated from 'shared/types/Paginated';
import { PHOTO_BASE } from 'shared/utils/apiConstants';
@@ -74,7 +74,7 @@ export default function Outtakes({
[photoSummaries, loadNextPage]
);
- const history = useHistory();
+ const navigate = useNavigate();
const { identifier: selectedIdentifier } = useParams<{
identifier?: string;
}>();
@@ -113,7 +113,7 @@ export default function Outtakes({
}}
onClick={() => {
// visibleImageIRef.current = i;
- history.push('/outtakes/photo/' + identifier);
+ navigate('/outtakes/photo/' + identifier);
}}
/>
);
diff --git a/frontend/src/screens/App/screens/TipJar/ThankYou.tsx b/frontend/src/screens/App/screens/TipJar/ThankYou.tsx
index 1bc9a966c..f3a7bb49c 100644
--- a/frontend/src/screens/App/screens/TipJar/ThankYou.tsx
+++ b/frontend/src/screens/App/screens/TipJar/ThankYou.tsx
@@ -1,14 +1,15 @@
import Modal from 'components/Modal';
import qs from 'qs';
import React from 'react';
-import { useHistory } from 'react-router-dom';
+import { useLocation, useNavigate } from 'react-router-dom';
import recordEvent from 'shared/utils/recordEvent';
export default function ThankYou({
isOpen,
onRequestClose,
}: Pick): JSX.Element {
- const history = useHistory();
+ const navigate = useNavigate();
+ const location = useLocation();
const { tipAmount } = qs.parse(window.location.search);
const hasRecordedRef = React.useRef(false);
@@ -23,12 +24,15 @@ export default function ThankYou({
window.localStorage.setItem('hasTipped', 'true');
// Remove query from url
- history.replace({
- pathname: history.location.pathname,
- hash: history.location.hash,
- });
+ navigate(
+ {
+ pathname: location.pathname,
+ hash: location.hash,
+ },
+ { replace: true }
+ );
hasRecordedRef.current = true;
- }, [tipAmount, isOpen, history]);
+ }, [tipAmount, isOpen, navigate, location.pathname, location.hash]);
return (
diff --git a/frontend/src/screens/App/screens/ToteBag/index.tsx b/frontend/src/screens/App/screens/ToteBag/index.tsx
index d3d72c33a..3aac6ac67 100644
--- a/frontend/src/screens/App/screens/ToteBag/index.tsx
+++ b/frontend/src/screens/App/screens/ToteBag/index.tsx
@@ -5,7 +5,7 @@ import classNames from 'classnames';
import BagBack from './components/Back';
import BagFront, { Color } from './components/Front';
-import { useLocation } from 'react-router';
+import { useLocation } from 'react-router-dom';
import stylesheet from './ToteBag.less';
export default function ToteBag(): JSX.Element {
diff --git a/frontend/src/screens/App/screens/ViewerPane/index.tsx b/frontend/src/screens/App/screens/ViewerPane/index.tsx
index 5a19756b9..8626dfdf2 100644
--- a/frontend/src/screens/App/screens/ViewerPane/index.tsx
+++ b/frontend/src/screens/App/screens/ViewerPane/index.tsx
@@ -9,7 +9,7 @@ import {
TransformComponent,
TransformWrapper,
} from '@jboolean/react-zoom-pan-pinch';
-import { useHistory, useParams } from 'react-router';
+import { useLocation, useNavigate, useParams } from 'react-router-dom';
import Alternates from './components/Alternates';
import ImageButtons from './components/ImageButtons';
import * as ImageStack from './components/ImageStack';
@@ -83,7 +83,8 @@ export default function ViewerPane({
className: string;
}): JSX.Element {
const { identifier: photoIdentifier } = useParams<{ identifier?: string }>();
- const history = useHistory();
+ const navigate = useNavigate();
+ const location = useLocation();
const overlayRef = React.useRef(null);
const wrapperRef = React.useRef(null);
@@ -145,9 +146,19 @@ export default function ViewerPane({
diff --git a/frontend/src/shared/stores/AuthStore.ts b/frontend/src/shared/stores/AuthStore.ts
index 0ec7b9bf8..c9f216bfd 100644
--- a/frontend/src/shared/stores/AuthStore.ts
+++ b/frontend/src/shared/stores/AuthStore.ts
@@ -4,17 +4,17 @@ import { persist } from 'zustand/middleware';
import netlifyIdentity, { User } from 'netlify-identity-widget';
import pick from 'lodash/pick';
-import { LocationDescriptor } from 'history';
+import type { To } from 'react-router-dom';
interface State {
isAutheticated: boolean;
user: User | null;
jwt: string | null;
- returnToRoute?: LocationDescriptor;
+ returnToRoute?: To;
}
interface Actions {
- login(returnTo?: LocationDescriptor): void;
+ login(returnTo?: To): void;
signout(): void;
close(): void;
}
@@ -26,7 +26,7 @@ const useAuthStore = create(
isAutheticated: false,
user: null,
jwt: null,
- login: (returnToRoute: LocationDescriptor) => {
+ login: (returnToRoute?: To) => {
netlifyIdentity.open('login');
set((state) => {
state.returnToRoute = returnToRoute;