· 5 years ago · Nov 11, 2020, 08:14 PM
1import React, { Fragment, useState, useRef, useMemo, useEffect } from 'react';
2import { injectIntl, FormattedMessage } from 'react-intl';
3import { makeStyles } from '@material-ui/styles';
4import { useTheme } from '@material-ui/core/styles';
5import useMediaQuery from '@material-ui/core/useMediaQuery';
6import Grid from '@material-ui/core/Grid';
7import Button from '@material-ui/core/Button';
8import RootRef from '@material-ui/core/RootRef';
9import MyLocation from '@material-ui/icons/MyLocation';
10import classNames from 'classnames';
11import LazyLoading from '../components/LazyLoading/LazyLoading';
12import { MainHeadingSmaller, SectionHeading } from '../components/typography';
13import PropTypes, { intlPropType } from '../helpers/propTypes';
14import { submitMetric } from '../helpers/metrics';
15import useListEntires from '../hooks/useListEntries';
16import { useGlobalScope } from '../hooks/useGlobalState';
17import useId from '../hooks/useId';
18import usePageAria from '../hooks/usePageAria';
19import { useCloseConfirmation } from '../hooks/useCloseConfirmation';
20import ClickableListItem from '../components/ClickableListItem/ClickableListItem';
21import Footer from '../components/Footer/Footer';
22import { countryMessages } from '../helpers/countries';
23
24const Map = React.lazy(() => import('../components/Map/Map'));
25
26const rad = x => (x * Math.PI) / 180;
27
28const useStyles = makeStyles(theme => ({
29 header: {
30 minHeight: 72,
31 display: 'flex',
32 alignItems: 'center',
33 justifyContent: 'center',
34 textAlign: 'center',
35 padding: 10,
36 borderBottom: '1px solid #ccc',
37 },
38
39 scroll: {
40 overflowY: 'scroll',
41 display: 'flex',
42 flexGrow: 1,
43 flexDirection: 'column',
44 },
45
46 withMap: {
47 height: '100%',
48 overflow: 'hidden',
49 display: 'flex',
50 flexDirection: 'column',
51 flexGrow: 1,
52 },
53
54 mainContent: {
55 flexGrow: 1,
56 position: 'relative',
57 },
58
59 mainContentWithMap: {
60 height: '100%',
61 overflow: 'hidden',
62 },
63
64 mainContentLoading: {
65 opacity: 0.5,
66 pointerEvents: 'none',
67 },
68
69 mapContainer: {
70 display: 'flex',
71 position: 'relative',
72
73 [theme.breakpoints.down('xs')]: {
74 height: 150,
75 },
76 },
77
78 mapControl: {
79 zIndex: 9,
80 position: 'absolute',
81 top: 10,
82 right: 10,
83 color: '#6b6b6b',
84 },
85
86 findNearestIcon: {
87 marginRight: 4,
88 },
89
90 list: {
91 position: 'relative',
92 zIndex: 1,
93 boxShadow: '-2px 0px 4px 0px rgba(0, 0, 0, 0.16)',
94 overflow: 'auto',
95 scrollBehavior: 'smooth',
96 },
97
98 countryTitle: {
99 borderBottom: '1px solid #ccc',
100 backgroundColor: 'rgba(247, 248, 248, 1)',
101 padding: '13px 0',
102 boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.16)',
103 zIndex: 1,
104 position: 'relative',
105 },
106
107 countryTitleFlag: {
108 height: 22,
109 },
110
111 opaque: {
112 opacity: 0.5,
113 },
114}));
115
116function distance(pointA, pointB) {
117 const R = 6371; // earth's radius
118 const bLat = pointB.lat || 0;
119 const bLng = pointB.lng || 0;
120 const mLat = pointA.latitude;
121 const mLng = pointA.longitude;
122 const dLat = rad(mLat - bLat);
123 const dLong = rad(mLng - bLng);
124 const a =
125 Math.sin(dLat / 2) * Math.sin(dLat / 2) +
126 Math.cos(rad(bLat)) * Math.cos(rad(bLat)) * Math.sin(dLong / 2) * Math.sin(dLong / 2);
127 const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
128 return R * c;
129}
130
131function sortSitesByProximity(first, second) {
132 try {
133 const sortBy = (a, b, key) => (a[key] < b[key] ? -1 : +1);
134 const getKey = (a, b) => "distance";
135 return sortBy(first, second, getKey(first, second));
136 } catch (e) {
137 return 1;
138 }
139}
140
141function sitesByProximity(userPosition, sites) {
142 const lat = userPosition.lat || 0;
143 const lng = userPosition.lng || 0;
144
145 for (let site of sites) {
146 site.distance = distance(site.geopos, { lat, lng });
147 }
148 return sites.sort(sortSitesByProximity);
149}
150
151function ClinicForm({
152 sites,
153 navigateModal,
154 api: { submitSite },
155 googleApiKey,
156 country,
157 intl: { formatMessage },
158 pageName,
159 contentId,
160}) {
161 const aria = usePageAria();
162 const descriptionId = useId();
163
164 const theme = useTheme();
165 const showMap = useMediaQuery(theme.breakpoints.up('sm'));
166
167 useCloseConfirmation();
168
169 const useGlobal = useGlobalScope(pageName);
170
171 const [selectedSiteId, setSelectedSiteId] = useGlobal('selectedSiteId');
172 const [_, setSelectedSite] = useGlobal('selectedSite');
173
174 const [listHoverId, setListHoverId] = useState(null);
175 const [mapHoverId, setMapHoverId] = useState(null);
176
177 const [loading, setLoading] = useState(showMap);
178
179 const listRef = useRef(null);
180 const listItemRefs = useListEntires(
181 sites,
182 () => React.createRef(),
183 i => sites[i].id,
184 );
185
186 const sortSites = (first, second) => {
187 try {
188 const sortBy = (a, b, key) => (a[key] < b[key] ? -1 : +1);
189 const getKey = (a, b) => (a.city === b.city ? 'name' : 'city');
190 return sortBy(first, second, getKey(first, second));
191 } catch (e) {
192 return 1;
193 }
194 };
195
196 const sitesByCountry = useMemo(
197 () =>
198 sites.sort(sortSites).reduce(
199 (acc, site) => ({
200 ...acc,
201 [site.countryCode]: [...(acc[site.countryCode] || []), site],
202 }),
203 {},
204 ),
205 [sites],
206 );
207
208 const sortedCountries = useMemo(() => {
209 const primaryLocation = [];
210 if (sitesByCountry[country]) primaryLocation.push(country);
211 return [
212 ...primaryLocation,
213 ...Object.keys(sitesByCountry).filter(l => l !== country),
214 ];
215 }, [sitesByCountry, country]);
216
217 const setSiteId = id => {
218 setSelectedSiteId(id);
219 setSelectedSite(sites.find(site => site.id === id));
220 };
221
222 const scrollToSite = id => {
223 const scrollToEl = listItemRefs[id].current;
224 listRef.current.scrollTop =
225 scrollToEl.offsetTop -
226 (listRef.current.offsetHeight - scrollToEl.offsetHeight) / 2;
227 };
228
229 const handleItemClick = siteId => {
230 setSiteId(siteId === selectedSiteId ? null : siteId);
231 submitMetric('siteSelected');
232 };
233
234 const handleMapClick = siteId => {
235 scrollToSite(siteId);
236 handleItemClick(siteId);
237 };
238
239 const handleItemHover = (isHovering, itemId) => {
240 if (!isHovering && listHoverId === itemId) {
241 setListHoverId(null);
242 } else {
243 setListHoverId(itemId);
244 }
245 };
246
247 const handleMapLoad = () => {
248 setLoading(false);
249 };
250
251 const handleFindNearestSite = () =>
252 navigator.geolocation.getCurrentPosition(position => {
253 const coords = {
254 lat: position.coords.latitude,
255 lng: position.coords.longitude,
256 };
257 //here you have sorted sites! It also changes the sites props, and so the page will render with new sites
258 const sitesByProx=sitesByProximity(coords, sites)
259 //the first element is the nearest
260 const nearestSiteId = sitesByProx[0].id
261 scrollToSite(nearestSiteId);
262 setSiteId(nearestSiteId);
263 });
264
265 const handleMapHover = (isHovering, siteId) => {
266 if (!isHovering && mapHoverId === siteId) {
267 setMapHoverId(null);
268 } else {
269 setMapHoverId(siteId);
270 }
271 };
272
273 const handleContinueClick = async () => {
274 setLoading(true);
275
276 await submitSite(selectedSiteId);
277
278 setLoading(false);
279 navigateModal({ forward: true });
280 };
281
282 const siteIsChosen = selectedSiteId !== null;
283
284 const classes = useStyles();
285
286 useEffect(() => {
287 handleFindNearestSite();
288 }, []);
289 console.log('PERMISSON LOCALIZATION ....', navigator.geolocation);
290
291 const handleLocationPermission = () => {
292 const permission = navigator.permissions
293 .query({ name: 'geolocation' })
294 .then(permissionStatus => {
295 console.log('geolocation permission state is ', permissionStatus.state);
296 const { state } = permissionStatus;
297 console.log(state);
298 });
299 };
300
301 console.log('PERMISSION', handleLocationPermission());
302
303 const renderSiteList = () =>
304 sortedCountries.map(countryCode => (
305 <Fragment key={countryCode}>
306 <Grid
307 alignItems="center"
308 aria-hidden="true"
309 className={classes.countryTitle}
310 container
311 direction="row"
312 wrap="nowrap"
313 >
314 <Grid alignItems="center" container item justify="center" xs={3}>
315 <img
316 alt=""
317 aria-hidden="true"
318 className={classes.countryTitleFlag}
319 src={`https://public.trialbee.com/flags/${countryCode.toLowerCase()}.svg`}
320 />
321 </Grid>
322 <Grid item xs={12}>
323 <SectionHeading>
324 {formatMessage(countryMessages[countryCode.toUpperCase()])}
325 </SectionHeading>
326 </Grid>
327 </Grid>
328 {sitesByCountry[countryCode].map(site => (
329 <RootRef key={site.id} rootRef={listItemRefs[site.id]}>
330 <ClickableListItem
331 desribedBy={descriptionId}
332 disabled={loading}
333 highlight={site.id === mapHoverId}
334 onClick={handleItemClick}
335 onHover={handleItemHover}
336 selected={site.id === selectedSiteId}
337 showLookupLink={!showMap}
338 site={site}
339 />
340 </RootRef>
341 ))}
342 </Fragment>
343 ));
344
345 return (
346 <>
347 <div
348 className={classNames({
349 [classes.scroll]: !showMap,
350 [classes.withMap]: showMap,
351 })}
352 {...aria}
353 >
354 <div className={classes.header}>
355 <MainHeadingSmaller align="center" id={descriptionId}>
356 <b>
357 <FormattedMessage
358 defaultMessage="Select a preferred clinic you would like to visit if you were to take part in the study"
359 id="ClinicForm.SelectClinic"
360 />
361 </b>
362 </MainHeadingSmaller>
363 </div>
364 <Grid
365 className={classNames(classes.mainContent, {
366 [classes.mainContentWithMap]: showMap,
367 [classes.mainContentLoading]: loading,
368 })}
369 container
370 spacing={0}
371 wrap="nowrap"
372 >
373 {showMap && (
374 <Grid
375 aria-hidden="true"
376 className={classes.mapContainer}
377 item
378 sm={8}
379 tabIndex="-1"
380 xs={12}
381 >
382 <LazyLoading>
383 <Map
384 country={country}
385 googleApiKey={googleApiKey}
386 hoveredSiteId={listHoverId}
387 onMapLoad={handleMapLoad}
388 onSiteHover={handleMapHover}
389 onSiteSelect={handleMapClick}
390 selectedSiteId={selectedSiteId}
391 sites={sites}
392 />
393 </LazyLoading>
394
395 <div className={classes.mapControl}>
396 <Button
397 onClick={handleFindNearestSite}
398 size="small"
399 variant="contained"
400 >
401 <MyLocation className={classes.findNearestIcon} />
402 <FormattedMessage
403 defaultMessage="Find nearest clinic"
404 id="ClinicForm.FindNearestClinic"
405 />
406 </Button>
407 </div>
408 </Grid>
409 )}
410 <RootRef rootRef={listRef}>
411 <Grid
412 aria-labelledby={descriptionId}
413 className={classNames({
414 [classes.list]: showMap,
415 [classes.opaque]: loading,
416 })}
417 item
418 role="radiogroup"
419 sm={4}
420 xs={12}
421 >
422 {renderSiteList()}
423 </Grid>
424 </RootRef>
425 </Grid>
426 </div>
427 <Footer
428 footerText={
429 siteIsChosen ? (
430 <FormattedMessage
431 defaultMessage="Next"
432 id="Footer.FooterChooseClinic"
433 />
434 ) : (
435 <FormattedMessage
436 defaultMessage="No clinic selected"
437 id="Footer.FooterNoClinicSelected"
438 />
439 )
440 }
441 formIsComplete={siteIsChosen && !loading}
442 liveRegionId={contentId}
443 loading={siteIsChosen && loading}
444 onContinueClick={handleContinueClick}
445 />
446 </>
447 );
448}
449
450ClinicForm.propTypes = {
451 api: PropTypes.shape({ submitSite: PropTypes.func.isRequired }).isRequired,
452 contentId: PropTypes.string.isRequired,
453 country: PropTypes.string.isRequired,
454 googleApiKey: PropTypes.string.isRequired,
455 intl: intlPropType.isRequired,
456 navigateModal: PropTypes.func.isRequired,
457 pageName: PropTypes.string.isRequired,
458 sites: PropTypes.arrayOf(
459 PropTypes.shape({
460 city: PropTypes.string,
461 countryCode: PropTypes.string,
462 geopos: PropTypes.shape({}),
463 id: PropTypes.string.isRequired,
464 name: PropTypes.string,
465 street: PropTypes.string,
466 zipCode: PropTypes.string,
467 }),
468 ).isRequired,
469};
470
471export default injectIntl(ClinicForm);
472