Skip to content

Commit 1489496

Browse files
feat(hooks): document geo search (#399)
Co-authored-by: François Chalifour <[email protected]>
1 parent 6bdbbab commit 1489496

File tree

13 files changed

+2048
-0
lines changed

13 files changed

+2048
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 2
6+
end_of_line = lf
7+
insert_final_newline = true
8+
trim_trailing_whitespace = true
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
.parcel-cache
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode
17+
.idea
18+
.DS_Store
19+
*.suo
20+
*.ntvs*
21+
*.njsproj
22+
*.sln
23+
*.sw?
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"singleQuote": true,
3+
"proseWrap": "never",
4+
"trailingComma": "es5"
5+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# geo-search
2+
3+
_This project was generated with [create-instantsearch-app](https://github.com/algolia/create-instantsearch-app) by [Algolia](https://algolia.com)._
4+
5+
## Get started
6+
7+
To run this project locally, install the dependencies and run the local server:
8+
9+
```sh
10+
npm install
11+
npm start
12+
```
13+
14+
Alternatively, you may use [Yarn](https://http://yarnpkg.com/):
15+
16+
```sh
17+
yarn
18+
yarn start
19+
```
1.88 KB
Loading
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta
6+
name="viewport"
7+
content="width=device-width, initial-scale=1, shrink-to-fit=no"
8+
/>
9+
10+
<link rel="shortcut icon" href="favicon.png">
11+
12+
<!--
13+
Do not use @7 in production, use a complete version like x.x.x, see website for latest version:
14+
https://www.algolia.com/doc/guides/building-search-ui/installation/react/#load-the-style
15+
-->
16+
<link
17+
rel="stylesheet"
18+
href="https://cdn.jsdelivr.net/npm/instantsearch.css@7/themes/satellite-min.css"
19+
/>
20+
21+
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"
22+
integrity="sha512-hoalWLoI8r4UszCkZ5kL8vayOGVae1oxXe/2A4AO6J9+580uKHDO3JdHb7NzwwzK5xr/Fs0W40kiNHxM9vyTtQ=="
23+
crossorigin=""/>
24+
25+
<title>geo-search</title>
26+
</head>
27+
28+
<body>
29+
<noscript> You need to enable JavaScript to run this app. </noscript>
30+
31+
<div id="root"></div>
32+
33+
<script type="module" src="src/index.tsx"></script>
34+
</body>
35+
</html>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "geo-search",
3+
"version": "1.0.0",
4+
"private": true,
5+
"scripts": {
6+
"build": "parcel build index.html",
7+
"start": "parcel index.html"
8+
},
9+
"dependencies": {
10+
"algoliasearch": "4",
11+
"instantsearch.js": "4.43.0",
12+
"leaflet": "1.8.0",
13+
"react": "18.2.0",
14+
"react-dom": "18.2.0",
15+
"react-instantsearch-hooks-web": "6.29.0",
16+
"react-leaflet": "4.0.1"
17+
},
18+
"devDependencies": {
19+
"@types/leaflet": "1.7.11",
20+
"@types/react": "18.0.15",
21+
"@types/react-dom": "18.0.6",
22+
"parcel": "2.5.0",
23+
"typescript": "4.7.4"
24+
}
25+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React, { useState } from 'react';
2+
import { Marker, Popup, useMapEvents } from 'react-leaflet';
3+
import { DivIcon } from 'leaflet';
4+
import { useSearchBox } from 'react-instantsearch-hooks-web';
5+
import { useGeoSearch } from './useGeoSearch';
6+
7+
import type { GeoHit } from 'instantsearch.js/es/connectors/geo-search/connectGeoSearch';
8+
9+
export function Airports() {
10+
const { query, refine: refineQuery } = useSearchBox();
11+
const {
12+
items,
13+
refine: refineItems,
14+
currentRefinement,
15+
clearMapRefinement,
16+
} = useGeoSearch();
17+
18+
const [previousQuery, setPreviousQuery] = useState(query);
19+
const [skipViewEffect, setSkipViewEffect] = useState(false);
20+
21+
// When the user moves the map, we clear the query if necessary to only
22+
// refine on the new boundaries of the map.
23+
const onViewChange = ({ target }) => {
24+
setSkipViewEffect(true);
25+
26+
if (query.length > 0) {
27+
refineQuery('');
28+
}
29+
30+
refineItems({
31+
northEast: target.getBounds().getNorthEast(),
32+
southWest: target.getBounds().getSouthWest(),
33+
});
34+
};
35+
36+
const map = useMapEvents({
37+
zoomend: onViewChange,
38+
dragend: onViewChange,
39+
});
40+
41+
// When the query changes, we remove the boundary refinement if necessary and
42+
// we center the map on the first result.
43+
if (query !== previousQuery) {
44+
if (currentRefinement) {
45+
clearMapRefinement();
46+
}
47+
48+
// `skipViewEffect` allows us to bail out of centering on the first result
49+
// if the query has been cleared programmatically.
50+
if (items.length > 0 && !skipViewEffect) {
51+
map.setView(items[0]._geoloc);
52+
}
53+
54+
setSkipViewEffect(false);
55+
setPreviousQuery(query);
56+
}
57+
58+
return (
59+
<>
60+
{items.map((item) => (
61+
<Marker
62+
key={item.objectID}
63+
position={item._geoloc}
64+
icon={createAirportIcon(item)}
65+
>
66+
<Popup>
67+
<strong>{item.name}</strong>
68+
<br />
69+
{item.city}, {item.country}
70+
</Popup>
71+
</Marker>
72+
))}
73+
</>
74+
);
75+
}
76+
77+
function createAirportIcon(item: GeoHit) {
78+
return new DivIcon({
79+
html: `<div class="marker">${item.airport_id}</div>`,
80+
popupAnchor: [0, -15],
81+
});
82+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
body,
2+
h1 {
3+
margin: 0;
4+
padding: 0;
5+
}
6+
7+
html {
8+
height: 100vh;
9+
}
10+
11+
body {
12+
min-height: 100vh;
13+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
14+
Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
15+
}
16+
17+
em {
18+
background: cyan;
19+
font-style: normal;
20+
}
21+
22+
#root {
23+
display: flex;
24+
flex-direction: column;
25+
min-height: 100vh;
26+
}
27+
28+
.header {
29+
display: flex;
30+
align-items: center;
31+
padding: 0.5rem 1rem;
32+
background-image: linear-gradient(to right, #8e43e7, #00aeff);
33+
color: #fff;
34+
}
35+
36+
.header a {
37+
color: #fff;
38+
text-decoration: none;
39+
}
40+
41+
.header-title {
42+
font-size: 1.2rem;
43+
font-weight: normal;
44+
}
45+
46+
.header-title::after {
47+
content: ' ▸ ';
48+
padding: 0 0.5rem;
49+
}
50+
51+
.header-subtitle {
52+
font-size: 1.2rem;
53+
}
54+
55+
.container {
56+
flex: 1;
57+
display: flex;
58+
flex-direction: column;
59+
}
60+
61+
.searchbox {
62+
margin: 2rem;
63+
}
64+
65+
.map {
66+
flex: 1;
67+
width: 100%;
68+
}
69+
70+
.marker {
71+
position: absolute;
72+
top: 50%;
73+
left: 50%;
74+
padding: 2px 5px;
75+
font-weight: bold;
76+
color: #8400ff;
77+
background-color: white;
78+
border: 2px solid currentColor;
79+
transform: translate(-50%, -50%);
80+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from 'react';
2+
import algoliasearch from 'algoliasearch/lite';
3+
import { InstantSearch, SearchBox } from 'react-instantsearch-hooks-web';
4+
import { MapContainer, TileLayer } from 'react-leaflet';
5+
6+
import { Airports } from './Airports';
7+
8+
import './App.css';
9+
10+
const searchClient = algoliasearch(
11+
'latency',
12+
'6be0576ff61c053d5f9a3225e2a90f76'
13+
);
14+
15+
export function App() {
16+
return (
17+
<>
18+
<header className="header">
19+
<h1 className="header-title">
20+
<a href="/">geo-search</a>
21+
</h1>
22+
<p className="header-subtitle">
23+
using{' '}
24+
<a href="https://github.com/algolia/react-instantsearch">
25+
React InstantSearch Hooks
26+
</a>
27+
</p>
28+
</header>
29+
30+
<div className="container">
31+
<InstantSearch searchClient={searchClient} indexName="airports">
32+
<SearchBox
33+
placeholder="Search for airports..."
34+
className="searchbox"
35+
/>
36+
<MapContainer
37+
className="map"
38+
center={[48.85, 2.35]}
39+
zoom={10}
40+
minZoom={4}
41+
scrollWheelZoom={true}
42+
>
43+
<TileLayer
44+
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
45+
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
46+
/>
47+
<Airports />
48+
</MapContainer>
49+
</InstantSearch>
50+
</div>
51+
</>
52+
);
53+
}

0 commit comments

Comments
 (0)