· 5 years ago · Nov 10, 2020, 02:54 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 calculateNearestSite(userPosition, sites) {
117 const lat = userPosition.lat || 0;
118 const lng = userPosition.lng || 0;
119 const R = 6371; // earth's radius
120
121 return sites.reduce(
122 (acc, _, i) => {
123 const mlat = sites[i].geopos.latitude;
124 const mlng = sites[i].geopos.longitude;
125 const dLat = rad(mlat - lat);
126 const dLong = rad(mlng - lng);
127 const a =
128 Math.sin(dLat / 2) * Math.sin(dLat / 2) +
129 Math.cos(rad(lat)) *
130 Math.cos(rad(lat)) *
131 Math.sin(dLong / 2) *
132 Math.sin(dLong / 2);
133 const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
134 const d = R * c;
135 if (d < acc.distance) {
136 return {
137 closestSiteId: sites[i].id,
138 distance: d,
139 };
140 }
141 return acc;
142 },
143 { closestSiteId: null, distance: Infinity },
144 ).closestSiteId;
145}
146
147function ClinicForm({
148 sites,
149 navigateModal,
150 api: { submitSite },
151 googleApiKey,
152 country,
153 intl: { formatMessage },
154 pageName,
155 contentId,
156}) {
157 const aria = usePageAria();
158 const descriptionId = useId();
159
160 const theme = useTheme();
161 const showMap = useMediaQuery(theme.breakpoints.up('sm'));
162
163 useCloseConfirmation();
164
165 const useGlobal = useGlobalScope(pageName);
166
167 const [selectedSiteId, setSelectedSiteId] = useGlobal('selectedSiteId');
168 const [_, setSelectedSite] = useGlobal('selectedSite');
169
170 const [listHoverId, setListHoverId] = useState(null);
171 const [mapHoverId, setMapHoverId] = useState(null);
172
173 const [loading, setLoading] = useState(showMap);
174
175 const listRef = useRef(null);
176 const listItemRefs = useListEntires(
177 sites,
178 () => React.createRef(),
179 i => sites[i].id,
180 );
181
182 const sortSites = (first, second) => {
183 try {
184 const sortBy = (a, b, key) => (a[key] < b[key] ? -1 : +1);
185 const getKey = (a, b) => (a.city === b.city ? 'name' : 'city');
186 return sortBy(first, second, getKey(first, second));
187 } catch (e) {
188 return 1;
189 }
190 };
191
192 const sitesByCountry = useMemo(
193 () =>
194 sites.sort(sortSites).reduce(
195 (acc, site) => ({
196 ...acc,
197 [site.countryCode]: [...(acc[site.countryCode] || []), site],
198 }),
199 {},
200 ),
201 [sites],
202 );
203
204 const sortedCountries = useMemo(() => {
205 const primaryLocation = [];
206 if (sitesByCountry[country]) primaryLocation.push(country);
207 return [
208 ...primaryLocation,
209 ...Object.keys(sitesByCountry).filter(l => l !== country),
210 ];
211 }, [sitesByCountry, country]);
212
213 const setSiteId = id => {
214 setSelectedSiteId(id);
215 setSelectedSite(sites.find(site => site.id === id));
216 };
217
218 const scrollToSite = id => {
219 const scrollToEl = listItemRefs[id].current;
220 listRef.current.scrollTop =
221 scrollToEl.offsetTop -
222 (listRef.current.offsetHeight - scrollToEl.offsetHeight) / 2;
223 };
224
225 const handleItemClick = siteId => {
226 setSiteId(siteId === selectedSiteId ? null : siteId);
227 submitMetric('siteSelected');
228 };
229
230 const handleMapClick = siteId => {
231 scrollToSite(siteId);
232 handleItemClick(siteId);
233 };
234
235 const handleItemHover = (isHovering, itemId) => {
236 if (!isHovering && listHoverId === itemId) {
237 setListHoverId(null);
238 } else {
239 setListHoverId(itemId);
240 }
241 };
242
243 const handleMapLoad = () => {
244 setLoading(false);
245 };
246
247 const handleFindNearestSite = () =>
248 navigator.geolocation.getCurrentPosition(position => {
249 const coords = {
250 lat: position.coords.latitude,
251 lng: position.coords.longitude,
252 };
253 const nearestSiteId = calculateNearestSite(coords, sites);
254 scrollToSite(nearestSiteId);
255 setSiteId(nearestSiteId);
256 });
257
258 const handleMapHover = (isHovering, siteId) => {
259 if (!isHovering && mapHoverId === siteId) {
260 setMapHoverId(null);
261 } else {
262 setMapHoverId(siteId);
263 }
264 };
265
266 const handleContinueClick = async () => {
267 setLoading(true);
268
269 await submitSite(selectedSiteId);
270
271 setLoading(false);
272 navigateModal({ forward: true });
273 };
274
275 const siteIsChosen = selectedSiteId !== null;
276
277 const classes = useStyles();
278
279 useEffect(() => {
280 handleFindNearestSite();
281 }, []);
282 console.log('PERMISSON LOCALIZATION ....', navigator.geolocation);
283
284 const handleLocationPermission = () => {
285 const permission = navigator.permissions
286 .query({ name: 'geolocation' })
287 .then(permissionStatus => {
288 console.log('geolocation permission state is ', permissionStatus.state);
289 const { state } = permissionStatus;
290 console.log(state);
291 });
292 };
293
294 console.log('PERMISSION', handleLocationPermission());
295
296 const renderSiteList = () =>
297 sortedCountries.map(countryCode => (
298 <Fragment key={countryCode}>
299 <Grid
300 alignItems="center"
301 aria-hidden="true"
302 className={classes.countryTitle}
303 container
304 direction="row"
305 wrap="nowrap"
306 >
307 <Grid alignItems="center" container item justify="center" xs={3}>
308 <img
309 alt=""
310 aria-hidden="true"
311 className={classes.countryTitleFlag}
312 src={`https://public.trialbee.com/flags/${countryCode.toLowerCase()}.svg`}
313 />
314 </Grid>
315 <Grid item xs={12}>
316 <SectionHeading>
317 {formatMessage(countryMessages[countryCode.toUpperCase()])}
318 </SectionHeading>
319 </Grid>
320 </Grid>
321 {sitesByCountry[countryCode].map(site => (
322 <RootRef key={site.id} rootRef={listItemRefs[site.id]}>
323 <ClickableListItem
324 desribedBy={descriptionId}
325 disabled={loading}
326 highlight={site.id === mapHoverId}
327 onClick={handleItemClick}
328 onHover={handleItemHover}
329 selected={site.id === selectedSiteId}
330 showLookupLink={!showMap}
331 site={site}
332 />
333 </RootRef>
334 ))}
335 </Fragment>
336 ));
337
338 return (
339 <>
340 <div
341 className={classNames({
342 [classes.scroll]: !showMap,
343 [classes.withMap]: showMap,
344 })}
345 {...aria}
346 >
347 <div className={classes.header}>
348 <MainHeadingSmaller align="center" id={descriptionId}>
349 <b>
350 <FormattedMessage
351 defaultMessage="Select a preferred clinic you would like to visit if you were to take part in the study"
352 id="ClinicForm.SelectClinic"
353 />
354 </b>
355 </MainHeadingSmaller>
356 </div>
357 <Grid
358 className={classNames(classes.mainContent, {
359 [classes.mainContentWithMap]: showMap,
360 [classes.mainContentLoading]: loading,
361 })}
362 container
363 spacing={0}
364 wrap="nowrap"
365 >
366 {showMap && (
367 <Grid
368 aria-hidden="true"
369 className={classes.mapContainer}
370 item
371 sm={8}
372 tabIndex="-1"
373 xs={12}
374 >
375 <LazyLoading>
376 <Map
377 country={country}
378 googleApiKey={googleApiKey}
379 hoveredSiteId={listHoverId}
380 onMapLoad={handleMapLoad}
381 onSiteHover={handleMapHover}
382 onSiteSelect={handleMapClick}
383 selectedSiteId={selectedSiteId}
384 sites={sites}
385 />
386 </LazyLoading>
387
388 <div className={classes.mapControl}>
389 <Button
390 onClick={handleFindNearestSite}
391 size="small"
392 variant="contained"
393 >
394 <MyLocation className={classes.findNearestIcon} />
395 <FormattedMessage
396 defaultMessage="Find nearest clinic"
397 id="ClinicForm.FindNearestClinic"
398 />
399 </Button>
400 </div>
401 </Grid>
402 )}
403 <RootRef rootRef={listRef}>
404 <Grid
405 aria-labelledby={descriptionId}
406 className={classNames({
407 [classes.list]: showMap,
408 [classes.opaque]: loading,
409 })}
410 item
411 role="radiogroup"
412 sm={4}
413 xs={12}
414 >
415 {renderSiteList()}
416 </Grid>
417 </RootRef>
418 </Grid>
419 </div>
420 <Footer
421 footerText={
422 siteIsChosen ? (
423 <FormattedMessage
424 defaultMessage="Next"
425 id="Footer.FooterChooseClinic"
426 />
427 ) : (
428 <FormattedMessage
429 defaultMessage="No clinic selected"
430 id="Footer.FooterNoClinicSelected"
431 />
432 )
433 }
434 formIsComplete={siteIsChosen && !loading}
435 liveRegionId={contentId}
436 loading={siteIsChosen && loading}
437 onContinueClick={handleContinueClick}
438 />
439 </>
440 );
441}
442
443ClinicForm.propTypes = {
444 api: PropTypes.shape({ submitSite: PropTypes.func.isRequired }).isRequired,
445 contentId: PropTypes.string.isRequired,
446 country: PropTypes.string.isRequired,
447 googleApiKey: PropTypes.string.isRequired,
448 intl: intlPropType.isRequired,
449 navigateModal: PropTypes.func.isRequired,
450 pageName: PropTypes.string.isRequired,
451 sites: PropTypes.arrayOf(
452 PropTypes.shape({
453 city: PropTypes.string,
454 countryCode: PropTypes.string,
455 geopos: PropTypes.shape({}),
456 id: PropTypes.string.isRequired,
457 name: PropTypes.string,
458 street: PropTypes.string,
459 zipCode: PropTypes.string,
460 }),
461 ).isRequired,
462};
463
464export default injectIntl(ClinicForm);
465