Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port map to React #62

Merged
merged 12 commits into from
Feb 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,14 @@
"react"
],
"rules": {
"template-curly-spacing": "off",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to fix an error TypeError: Cannot read property 'range' of null
Occurred while linting /Users/snyderc/Documents/vaccinatema/components/utilities/parseBookAppointmentString.js:1

babel/babel-eslint#681

"indent": [
"error",
4
"warn",
4,
{
"ignoredNodes": ["TemplateLiteral"],
"SwitchCase": 1
}
],
"linebreak-style": [
"error",
Expand Down
194 changes: 194 additions & 0 deletions components/Map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import GoogleMapReact from 'google-map-react';
import parseBookAppointmentString from './utilities/parseBookAppointmentString';

// High volume, large venue sites
const MASS_VACCINATION_SITES = [
'Foxborough: Gillette Stadium',
'Boston: Fenway Park',
'Danvers: Doubletree Hotel',
'Springfield: Eastfield Mall',
'Dartmouth: Former Circuit City',
'Lowell: Lowell General Hospital at Cross River Center',
'Natick: Natick Mall'
];

const ELIGIBLE_PEOPLE_STATEWIDE_TEXT = [
'All eligible people statewide',
'Eligible populations statewide'
];

const doesSiteServeAllEligiblePeopleStatewide = serves => ELIGIBLE_PEOPLE_STATEWIDE_TEXT.includes(serves?.trim());

const isSiteAMassVaccinationSite = locationName => MASS_VACCINATION_SITES.includes(locationName);

const parseDate = dateString => (
new Date(Date.parse(dateString)).toLocaleString('en-US', {timeZone: 'America/New_York'})
);

const parseLocationData = data => {
return data.map( site => (
{
id: site.id,
locationName: site.fields['Location Name'] ?? '',
address: site.fields['Full Address'] ?? '',
populationsServed: site.fields['Serves'] ?? '',
vaccineAvailability: site.fields['Availability'] ?? '',
lastUpdated: (site.fields['Last Updated'] && parseDate(site.fields['Last Updated'])) ?? '',
bookAppointmentInformation: (site.fields['Book an appointment'] && parseBookAppointmentString(site.fields['Book an appointment'])) ?? '',
latitude: site.fields['Latitude'] ?? 0,
longitude: site.fields['Longitude'] ?? 0,
sitePinShape: determineSitePinShape(
site.fields['Availability'] ?? '',
site.fields['Serves'] ?? '',
site.fields['Location Name'] ?? ''
)
}
));
};

const determineSitePinShape = (availability, serves, locationName) => {
snyderc marked this conversation as resolved.
Show resolved Hide resolved
if (!availability || availability?.trim() === 'None') {
return 'dot';
} else if (doesSiteServeAllEligiblePeopleStatewide(serves)) {
return 'star star-green';
} else if (isSiteAMassVaccinationSite(locationName)) {
return 'star star-red';
} else {
return 'star star-blue';
}
};

// google-map-react allows you to pass a "$hover" destructured prop if you want to have an effect on hover
snyderc marked this conversation as resolved.
Show resolved Hide resolved
const Marker = ({ id, lat, lng, sitePinShape, setPopupData, getSiteDataByKey }) => {
const handleClick = () => {
const data = getSiteDataByKey(id);
setPopupData({
lat,
lng,
data
});
};
return (
<div
className={sitePinShape}
onClick={handleClick}
style={{
position: 'absolute',
transform: 'translate(-50%, -50%)',
cursor: 'pointer'
}}
>
</div>
);
};

Marker.propTypes = {
id: PropTypes.string,
lat: PropTypes.number,
lng: PropTypes.number,
sitePinShape: PropTypes.string,
setPopupData: PropTypes.func,
getSiteDataByKey: PropTypes.func,
};

const Popup = ({data, setPopupData}) => (
snyderc marked this conversation as resolved.
Show resolved Hide resolved
<div style={{
position: 'absolute',
transform: 'translate(0%, -50%)',
border: '1px solid black',
color: '#000000',
backgroundColor: '#FFFFFF',
width: '300px',
borderRadius: '5px',
boxShadow: '5px 5px',
padding: '5px'
}}>
<div id="content">
<h4 id="firstHeading" className="firstHeading">{data.locationName}</h4>
<div id="bodyContent">
<p><b>Details</b> {data.populationsServed}</p>
<p><b>Address</b> {data.address}</p>
<p><b>Availability</b> {data.vaccineAvailability}</p>
<p>(Availability last updated {data.lastUpdated})</p>
<p><b>Book now</b> {data.bookAppointmentInformation}</p>
<button onClick={() => setPopupData({})}>Close</button>
</div>
</div>
</div>
);

Popup.propTypes = {
data: PropTypes.shape(
{
locationName: PropTypes.string,
populationsServed: PropTypes.string,
address: PropTypes.string,
vaccineAvailability: PropTypes.string,
lastUpdated: PropTypes.string,
bookAppointmentInformation: PropTypes.array,
}
),
setPopupData: PropTypes.func,
};

const Map = ({height = '400px', width = '100%'}) => {
const [siteData, setSiteData] = useState([]);
const [popupData, setPopupData] = useState({});

const bostonCoordinates = {
lat: 42.360081,
lng: -71.058884
};

const defaultMassachusettsZoom = 8;

const getSiteDataByKey = key => siteData.find(site => {
return key === site.id;
});

useEffect(() => {
fetch('/initmap')
.then(response => response.json())
.then(siteData => parseLocationData(siteData))
.then(siteData => setSiteData(siteData));
});

return (
// Container element must have height and width for map to display. See https://developers.google.com/maps/documentation/javascript/overview#Map_DOM_Elements
<div style={{ height, width }}>
<GoogleMapReact
bootstrapURLKeys={{ key: 'AIzaSyDxF3aT2MwmgzcAzFt5PtB-B3UNp4Js2h4' }}
defaultCenter={bostonCoordinates}
defaultZoom={defaultMassachusettsZoom}
draggableCursor="crosshair"
>
{siteData && siteData.map((site) => (
<Marker
key={site.id}
id={site.id}
lat={site.latitude}
lng={site.longitude}
sitePinShape={site.sitePinShape}
setPopupData={setPopupData}
getSiteDataByKey={getSiteDataByKey}
/>
))}
{popupData.data && (<Popup
lat={popupData.lat}
lng={popupData.lng}
data={popupData.data}
setPopupData={setPopupData}
/>)}
</GoogleMapReact>
</div>
);
};

Map.propTypes = {
height: PropTypes.string,
width: PropTypes.string,
};

export default Map;
35 changes: 35 additions & 0 deletions components/utilities/parseBookAppointmentString.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import processString from 'react-process-string';

const firstParser = (key, result) => (
<span key={key}>
<a target="_blank" rel="noreferrer" href={`${result[1]}://${result[2]}.${result[3]}${result[4]}`}>{result[2]}.{result[3]}{result[4]}</a>{result[5]}
</span>
);

const secondParser = (key, result) => (
<span key={key}>
<a target="_blank" rel="noreferrer" href={`http://${result[1]}.${result[2]}${result[3]}`}>{result[1]}.{result[2]}{result[3]}</a>{result[4]}
</span>
);

/**
* Given a string that contains URLs, returns an array that turns the URLs into hyperlinks
* so when you use the output of this function in a React layout, the URLs will be clickable
*
* Based on example code/regex from https://www.npmjs.com/package/react-process-string
*
* @return array
*/
const parseBookAppointmentString = text => {
let config = [{
regex: /(http|https):\/\/(\S+)\.([a-z]{2,}?)(.*?)( |,|$|\.)/gim,
fn: firstParser
}, {
regex: /(\S+)\.([a-z]{2,}?)(.*?)( |,|$|\.)/gim,
fn: secondParser
}];
return processString(config)(text);
};

export default parseBookAppointmentString;
snyderc marked this conversation as resolved.
Show resolved Hide resolved
34 changes: 34 additions & 0 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"express": "latest",
"express-validator": "^6.9.2",
"google-geocoder": "^0.2.1",
"google-map-react": "^2.1.9",
"newrelic": "^7.1.1",
"next": "10.0.3",
"next-i18next": "7.0.1",
Expand All @@ -27,10 +28,12 @@
"react-bootstrap": "^1.4.3",
"react-dom": "^16.13.1",
"react-geocode": "^0.2.3",
"react-process-string": "^1.2.0",
"react-share": "^4.3.1",
"sass": "^1.32.7"
},
"scripts": {
"local": "node app.js",
"build": "next build",
"lint": "eslint '**/*.js' --fix --ignore-pattern 'static/*'",
"start": "next build && cross-env NODE_ENV=production node app.js",
Expand Down
16 changes: 15 additions & 1 deletion pages/dev/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import React from 'react';

import Layout from '../../components/Layout';
import Map from '../../components/Map';

const Home = () => (
<Layout pageTitle="Home">
<div>
Home page
<div className="jumbotron">
<h2>COVID-19 Vaccine Availability</h2>
<p className="lead">For <a href="/eligibility"> eligible individuals </a>, find an appointment from the map below or</p>
<div className="btn-group" style={{width: '100%', display:'flex', alignItems: 'center', justifyContent: 'center'}}>
<a className="btn btn-success" data-toggle="modal" href="/search" style={{justifyContent: 'center'}}>Find Locations Near You</a>
</div>

</div>
<Map />
<p> <b> Red star: </b> Mass Vaccination Sites (high volume, large venue sites) </p>
<p> <b> Green star: </b> General Vaccination Sites (healthcare locations) </p>
<p> <b> Blue star: </b> Local Vaccination Site (open to select cities/towns) </p>
<p> <b> Gray dot: </b> No availability currently </p>
<p> Seeking volunteers, reach out to <a href="mailto:[email protected]"> [email protected] </a> to help out.</p>
</div>
</Layout>
);
Expand Down
13 changes: 7 additions & 6 deletions pages/dev/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import Layout from '../../components/Layout';
import Site from '../../components/Site';
import Geocode from 'react-geocode';
import parseBookAppointmentString from '../../components/utilities/parseBookAppointmentString';

class Search extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -92,12 +93,12 @@ class Search extends React.Component {
parseLocationData = data => {
return data.map( site => (
{
locationName: site.fields['Location Name'],
address: site.fields['Full Address'],
populationsServed: site.fields['Serves'],
vaccineAvailability: site.fields['Availability'],
lastUpdated: this.parseDate(site.fields['Last Updated']),
bookAppointmentInformation: site.fields['Book an appointment']
locationName: site.fields['Location Name'] ?? '',
address: site.fields['Full Address'] ?? '',
populationsServed: site.fields['Serves'] ?? '',
vaccineAvailability: site.fields['Availability'] ?? '',
lastUpdated: (site.fields['Last Updated'] && this.parseDate(site.fields['Last Updated'])) ?? '',
bookAppointmentInformation: (site.fields['Book an appointment'] && parseBookAppointmentString(site.fields['Book an appointment'])) ?? ''
}
));
}
Expand Down
Loading