generated from hackforla/.github-hackforla-base-repo-template
-
-
Notifications
You must be signed in to change notification settings - Fork 51
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #972 from hackforla/map-page-performance
Map page performance
- Loading branch information
Showing
9 changed files
with
624 additions
and
29 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import React from "react"; | ||
import { renderToString } from "react-dom/server"; | ||
import MapMarker from "images/mapMarker"; | ||
import { | ||
MEAL_PROGRAM_CATEGORY_ID, | ||
FOOD_PANTRY_CATEGORY_ID, | ||
} from "constants/stakeholder"; | ||
|
||
///////////////////// CONSTANTS /////////////////// | ||
|
||
// note that we have 3 marker categories, and 2 selected states, for a | ||
// total of 6 possible marker variants. | ||
// the marker categories come from src/images/mapMarker.js | ||
const MARKER_CATEGORIES = [-1, 0, 1]; | ||
const SELECTED_VALUES = [false, true]; | ||
|
||
// using the pixel ratio to scale the marker helps prevent | ||
// blurry edges when the marker is converted to an image | ||
const MARKER_SCALE = window.devicePixelRatio; | ||
|
||
////////////////////// FUNCTIONS ////////////////// | ||
|
||
// each marker variant has a unique id based on its category and | ||
// whether it is selected | ||
function getIconId(markerCategory, isSelected) { | ||
return `fola-marker::${markerCategory}::${isSelected}` | ||
} | ||
|
||
// selects the right marker category based on the stakeholder categories array. | ||
// category ids are from src/images/mapMarker.js | ||
function getMarkerCategory(stakeholder) { | ||
if ( | ||
stakeholder.categories[0]?.id === FOOD_PANTRY_CATEGORY_ID && | ||
stakeholder.categories[1]?.id === MEAL_PROGRAM_CATEGORY_ID | ||
) | ||
return -1; | ||
|
||
if (stakeholder.categories[0]?.id === FOOD_PANTRY_CATEGORY_ID) return 0; | ||
|
||
return 1; | ||
} | ||
|
||
// This converts the given marker to an icon, and then loads that icon into the | ||
// map. Once loaded, the icon can be used in a symbol layer. | ||
// see https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#symbol | ||
function loadMarkerIcon({ map, marker, iconId }) { | ||
return new Promise((resolve, reject) => { | ||
const icon = new Image(); | ||
const svgString = renderToString(marker); | ||
const svg = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" }); | ||
const url = URL.createObjectURL(svg); | ||
icon.src = url; | ||
icon.onload = () => { | ||
map.addImage(iconId, icon); | ||
URL.revokeObjectURL(url); | ||
resolve(); | ||
}; | ||
}); | ||
} | ||
|
||
// load an icon for each possible combination of marker category and | ||
// selected value. | ||
export function loadMarkerIcons(map) { | ||
const iconLoaders = [] | ||
|
||
MARKER_CATEGORIES.forEach((category) => { | ||
SELECTED_VALUES.forEach((selected) => { | ||
iconLoaders.push( | ||
loadMarkerIcon({ | ||
map, | ||
marker: ( | ||
<MapMarker | ||
category={category} | ||
selected={selected} | ||
scale={MARKER_SCALE} | ||
/> | ||
), | ||
iconId: getIconId(category, selected), | ||
}) | ||
) | ||
}) | ||
}) | ||
|
||
return Promise.all(iconLoaders) | ||
} | ||
|
||
export const markersLayerStyles = { | ||
type: "symbol", | ||
layout: { | ||
"icon-image": ["get", "iconId"], | ||
"icon-allow-overlap": true, | ||
"icon-size": 1 / MARKER_SCALE, | ||
}, | ||
} | ||
|
||
// This generates the geojson needed to show the stakeholders in a symbol layer. | ||
// Note that the iconId is added as a property. | ||
export function getMarkersGeojson(stakeholders, selectedStakeholder) { | ||
return { | ||
type: "FeatureCollection", | ||
features: (stakeholders || []).map((sh) => { | ||
const category = getMarkerCategory(sh) | ||
const selected = sh.id === selectedStakeholder?.id | ||
return { | ||
type: "Feature", | ||
id: sh.id, | ||
geometry: { | ||
type: "Point", | ||
coordinates: [sh.longitude, sh.latitude], | ||
}, | ||
properties: { | ||
iconId: getIconId(category, selected), | ||
}, | ||
} | ||
}), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
162 changes: 162 additions & 0 deletions
162
client/src/components/FoodSeeker/ResultsListOptimized.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
import React, { useEffect, useCallback } from "react"; | ||
import PropTypes from "prop-types"; | ||
import { Button, CircularProgress, Grid } from "@material-ui/core"; | ||
import { makeStyles } from "@material-ui/core/styles"; | ||
import List from "react-virtualized/dist/es/List"; | ||
import AutoSizer from "react-virtualized/dist/es/AutoSizer"; | ||
import CellMeasurer from "react-virtualized/dist/es/CellMeasurer"; | ||
import CellMeasurerCache from "react-virtualized/dist/es/CellMeasurer/CellMeasurerCache"; | ||
import StakeholderPreview from "components/FoodSeeker/StakeholderPreview"; | ||
import StakeholderDetails from "components/FoodSeeker/StakeholderDetails"; | ||
import * as analytics from "services/analytics"; | ||
|
||
const useStyles = makeStyles((theme) => ({ | ||
listContainer: { | ||
textAlign: "center", | ||
width: "100%", | ||
height: "100%", | ||
display: "flex", | ||
flexDirection: "column", | ||
alignItems: "center", | ||
[theme.breakpoints.up("md")]: { | ||
height: "100%", | ||
}, | ||
[theme.breakpoints.only("sm")]: { | ||
order: 1, | ||
height: "30em", | ||
}, | ||
[theme.breakpoints.down("xs")]: { | ||
height: "100%", | ||
fontSize: "12px", | ||
}, | ||
}, | ||
list: { | ||
width: "100%", | ||
flex: 1, | ||
}, | ||
preview: { | ||
width: "100%", | ||
borderBottom: " .2em solid #f1f1f1", | ||
padding: "0 1em", | ||
}, | ||
emptyResult: { | ||
padding: "1em 0", | ||
display: "flex", | ||
flexDirection: "column", | ||
alignContent: "center", | ||
}, | ||
})); | ||
|
||
const cache = new CellMeasurerCache({ | ||
defaultHeight: 140, | ||
fixedWidth: true, | ||
}); | ||
|
||
const clearCache = () => cache.clearAll(); | ||
|
||
const ResultsList = ({ | ||
doSelectStakeholder, | ||
selectedStakeholder, | ||
stakeholders, | ||
setToast, | ||
status, | ||
handleReset, | ||
}) => { | ||
const classes = useStyles(); | ||
|
||
useEffect(() => { | ||
analytics.postEvent("showList"); | ||
}, []); | ||
|
||
useEffect(() => { | ||
window.addEventListener("resize", clearCache); | ||
return () => window.removeEventListener("resize", clearCache); | ||
}, []); | ||
|
||
useEffect(() => { | ||
clearCache(); | ||
}, [stakeholders]); | ||
|
||
const scrollToIndex = selectedStakeholder | ||
? stakeholders.findIndex((s) => s.id === selectedStakeholder.id) | ||
: 0; | ||
|
||
const rowRenderer = useCallback( | ||
({ index, style, key, parent }) => ( | ||
<CellMeasurer | ||
key={key} | ||
cache={cache} | ||
parent={parent} | ||
columnIndex={0} | ||
rowIndex={index} | ||
> | ||
{({ registerChild }) => ( | ||
<div ref={registerChild} style={style} className={classes.preview}> | ||
<StakeholderPreview | ||
stakeholder={stakeholders[index]} | ||
doSelectStakeholder={doSelectStakeholder} | ||
/> | ||
</div> | ||
)} | ||
</CellMeasurer> | ||
), | ||
[stakeholders, doSelectStakeholder, classes.preview] | ||
); | ||
|
||
return ( | ||
<Grid item xs={12} md={4} className={classes.listContainer}> | ||
{status === "loading" && ( | ||
<div className={classes.emptyResult}> | ||
<CircularProgress /> | ||
</div> | ||
)} | ||
{status === "loaded" && stakeholders.length === 0 && ( | ||
<div className={classes.emptyResult}> | ||
<p>Sorry, we don't have any results for this area.</p> | ||
<Button | ||
onClick={handleReset} | ||
variant="contained" | ||
color="primary" | ||
disableElevation | ||
> | ||
Click here to reset the search | ||
</Button> | ||
</div> | ||
)} | ||
{stakeholders && selectedStakeholder && !selectedStakeholder.inactive ? ( | ||
<StakeholderDetails | ||
doSelectStakeholder={doSelectStakeholder} | ||
selectedStakeholder={selectedStakeholder} | ||
setToast={setToast} | ||
/> | ||
) : ( | ||
<div className={classes.list}> | ||
<AutoSizer> | ||
{({ height, width }) => ( | ||
<List | ||
width={width} | ||
height={height} | ||
rowCount={stakeholders.length} | ||
rowRenderer={rowRenderer} | ||
deferredMeasurementCache={cache} | ||
rowHeight={cache.rowHeight} | ||
scrollToIndex={scrollToIndex} | ||
/> | ||
)} | ||
</AutoSizer> | ||
</div> | ||
)} | ||
</Grid> | ||
); | ||
}; | ||
|
||
ResultsList.propTypes = { | ||
selectedStakeholder: PropTypes.object, | ||
stakeholders: PropTypes.arrayOf(PropTypes.object), | ||
doSelectStakeholder: PropTypes.func, | ||
setToast: PropTypes.func, | ||
status: PropTypes.string, | ||
handleReset: PropTypes.func, | ||
}; | ||
|
||
export default ResultsList; |
Oops, something went wrong.