A finding nearby places app built with React, Vite, Flowbite, Tailwind CSS, leafletjs, React Leaflet, Overpass API, zustand, and TypeScript.
Table of Contents
- Utilized React Query for efficient data fetching and caching, enhancing performance.
- Expanded categories of places for more comprehensive search options.
- Implemented storage of the user's current position for enhanced user experience.
- Added loading UI during data queries for better visual feedback.
- Enabled single click navigation to any location worldwide directly on the map.
- Incorporated onScroll events to dynamically adjust card container styles.
- Implemented functionality to scroll into the corresponding card after clicking on a marker.
- Pass Refs to Child Components using forwardRef.
- Implement deep refs forwarding for complex component structures.
- Utilize useRef to store a list of elements efficiently.
- Add custom attributes to div elements for enhanced styling flexibility.
- Create custom hooks for map-related functionalities.
- Explore testing React components using Vitest.
- Flowbite
- Tailwind CSS
- Leafletjs
- React Leaflet
- Overpass API
- Zustand
- React Query
In tailwind.config.js on plugins: [require("flowbite/plugin")],
Solution
ignore eslint
// eslint-disable-next-line no-undef
plugins: [require("flowbite/plugin")],
Solution
Change the parent layout to lg:inline-block
instead of lg:flex-col
Solution
Add missing width: 100vw; w-screen
to the flex container
Solution
Add missing height h-full
to the flex element
Solution
https://www.npmjs.com/package/tailwind-scrollbar
Solution
https://leafletjs.com/examples/quick-start/
does not work
https://www.npmjs.com/package/react-leaflet?activeTab=dependencies does not work
https://stackoverflow.com/questions/40365440/react-leaflet-map-not-correctly-displayed
https://leafletjs.com/examples/quick-start/ it works
Add the below codes into index.html
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""
/>
<script
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""
></script>
Add below codes into src\App.css
.leaflet-container {
width: 100wh;
height: 100vh;
}
Solution
https://wordpress.org/support/topic/leaflet-map-z-index-and-css/
Add the style z-index: 0 !important;
to the leaflet container class .leaflet-container
in src\App.css
Change the order of components, make leaflet container component to be the first render component. For example, in src\App.tsx, where <MapLayout />
is the component that includes leaflet container:
import "./App.css";
import MapLayout from "./MapLayout";
import Navigation from "./Navigation";
import ResultContainer from "./ResultContainer";
function App() {
return (
<main className="min-h-screen min-w-screen flex">
<MapLayout />
<Navigation />
<ResultContainer />
</main>
);
}
export default App;
In src\MapLayout.tsx
import React from "react";
import CurrentLocationButton from "./CurrentLocationButton";
import { MapContainer, Marker, Popup, TileLayer } from "react-leaflet";
export default function MapLayout() {
return (
<div className="w-screen h-screen ">
<MapContainer center={[51.505, -0.09]} zoom={13} scrollWheelZoom={false}>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={[51.505, -0.09]}>
<Popup>
A pretty CSS3 popup. <br /> Easily customizable.
</Popup>
</Marker>
</MapContainer>
<div className="fixed w-10 aspect-square flex items-center place-content-center bottom-64 right-8 lg:bottom-24 lg:right-24">
<CurrentLocationButton />
</div>
</div>
);
}
Error: Argument of type 'LatLng' is not assignable to parameter of type 'SetStateAction<null>'. Type 'LatLng' provides no match for the signature '(prevState: null): null'.
Solution
Change useState(null);
to useState<LatLng | null>(null);
https://react-leaflet.js.org/docs/example-events/
Add onClick event to the custom go to current location button onClick={() => setGoToCurrentPosition(true)}
, to change the state const [goToCurrentPosition, setGoToCurrentPosition] = useState(false);
Monitor the state change in the LocationMarker
component. And add the condition in the useEffect
, only go to the current location when the state variable goToCurrent
is true.
Full codes:
import { LatLng } from "leaflet";
import { useEffect, useState } from "react";
import {
MapContainer,
Marker,
Popup,
TileLayer,
useMap,
} from "react-leaflet";
import CurrentLocationIcon from "./CurrentLocationIcon";
type Props = {
goToCurrentPosition: boolean;
};
function LocationMarker({ goToCurrentPosition }: Props) {
const [position, setPosition] = useState<LatLng | null>(null);
const [goToCurrent, setGoToCurrent] = useState(false);
const map = useMap();
useEffect(() => {
setGoToCurrent(goToCurrentPosition);
}, [goToCurrentPosition]);
useEffect(() => {
if (goToCurrent) {
map.locate().on("locationfound", function (e) {
setPosition(e.latlng);
map.flyTo(e.latlng, map.getZoom());
});
}
}, [goToCurrent]);
return position === null ? null : (
<Marker position={position}>
<Popup>You are here</Popup>
</Marker>
);
}
export default function MapLayout() {
const [goToCurrentPosition, setGoToCurrentPosition] = useState(false);
return (
<div className="w-screen h-screen ">
<MapContainer center={[51.505, -0.09]} zoom={13} scrollWheelZoom={false}>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={[51.505, -0.09]}>
<Popup>
A pretty CSS3 popup. <br /> Easily customizable.
</Popup>
</Marker>
<LocationMarker goToCurrentPosition={goToCurrentPosition} />
</MapContainer>
<button
className="fixed w-14 aspect-square flex items-center place-content-center bottom-64 right-8 lg:bottom-24 lg:right-24 backdrop-blur shadow-lg p-2 rounded-full"
onClick={() => setGoToCurrentPosition(true)}
>
<CurrentLocationIcon />
</button>
</div>
);
}
https://opensource.com/article/18/5/how-build-your-first-gis-app
https://www.npmjs.com/package/query-overpass
https://codesandbox.io/s/b7olo?file=/src/service/overpass.ts
https://overpass-api.de/api/interpreter
POST
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Query Castles
[out:json];(way[historic=castle](around:10000, 50.0874654,14.4212535);
relation[historic=castle](around:10000, 50.0874654,14.4212535););
out body;>;out skel qt;
Query schools
[out:json];
(node[amenity=school](around:10000, 50.0874654,14.4212535););
out body;>;out skel qt;
Use zustand
Change the icons https://codesandbox.io/s/react-leaflet-icon-material-mx1iu?file=/src/index.js
https://www.geodatasource.com/developers/javascript
Add eventHandlers
in Marker
...
<Marker
position={[position.lat, position.lon]}
icon={customMarkerIcon}
eventHandlers={{
click: (e) => {
onClickMarker(e);
},
}}
>
<Popup>{text}</Popup>
</Marker>
...
In the handler function, set the state variable using the selected position from the event object
...
const [selectedPosition, setSelectedPosition] = useState<GeoPosition | null>(
null
);
...
function onClickCustomMapMarker(event: LeafletMouseEvent): void {
setSelectedPosition({ lat: event.latlng.lat, lon: event.latlng.lng });
}
...
Render the selected card component if the position matches the position of the selected marker
...
export default function PlaceContainer({
currentPosition,
places,
selectedPosition,
}: Props) {
if (places.length > 0) {
return (
<section>
{places.map((place) =>
selectedPosition !== null &&
selectedPosition.lat === place.lat &&
selectedPosition.lon === place.lon ? (
<PlaceCardSelected
...
/>
) : (
<PlaceCard
...
/>
)
)}
</section>
);
} else {
return null;
}
}
... zustand
... useMemo
...
https://dmitripavlutin.com/react-forwardref/
https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/forward_and_create_ref/
https://dmitripavlutin.com/react-forwardref/#4-deep-refs-forwarding
...
Possible solution: passing ref as props https://stackoverflow.com/questions/66764686/how-to-pass-a-ref-down-more-than-one-level-in-react
https://eliaslog.pw/how-to-add-multiple-refs-to-one-useref-hook/
Grand child component:
...
<PlaceCard
ref={(element) => {
if (element) cardRefs.current[index] = element;
}}
key={place.id}
...
...
...
https://www.freecodecamp.org/news/how-to-create-react-hooks/
const [showStart, setShowStart] = useState(false);
const [showEnd, setShowEnd] = useState(true);
...
https://stackoverflow.com/questions/29382389/defining-array-with-multiple-types-in-typescript
...