From a1a7008502204c1d4b464a01faec2294604e863c Mon Sep 17 00:00:00 2001 From: Nicholas Kwon Date: Wed, 6 Jul 2022 18:15:52 -0700 Subject: [PATCH 1/2] allow user to change date within past week --- client/components/Map/index.js | 8 +-- client/components/Map/layers/RequestsLayer.js | 49 +++++++++++++++---- client/components/common/CONSTANTS.js | 14 ++++-- .../common/ReactDayPicker/ReactDayPicker.jsx | 32 ++++++++++-- client/redux/reducers/filters.js | 8 +-- 5 files changed, 87 insertions(+), 24 deletions(-) diff --git a/client/components/Map/index.js b/client/components/Map/index.js index 1131937dc..054ae0e37 100644 --- a/client/components/Map/index.js +++ b/client/components/Map/index.js @@ -9,7 +9,6 @@ import { getDataRequestSuccess } from '@reducers/data'; import { updateMapPosition } from '@reducers/ui'; import { trackMapExport } from '@reducers/analytics'; import CookieNotice from '../main/CookieNotice'; -import { DATE_SPEC } from '../common/CONSTANTS'; // import "mapbox-gl/dist/mapbox-gl.css"; import Map from './Map'; import moment from 'moment'; @@ -64,8 +63,8 @@ class MapContainer extends React.Component { getAllRequests = async () => { const { startDate, endDate } = this.props; const url = new URL(`${process.env.API_URL}/requests`); - url.searchParams.append("start_date", moment(startDate, DATE_SPEC).format('YYYY-MM-DD')); - url.searchParams.append("end_date", moment(endDate, DATE_SPEC).format('YYYY-MM-DD')); + url.searchParams.append("start_date", startDate); + url.searchParams.append("end_date", endDate); url.searchParams.append("limit", `${REQUEST_BATCH_SIZE}`); var returned_length = REQUEST_BATCH_SIZE; var skip = 0; @@ -100,6 +99,9 @@ class MapContainer extends React.Component { requestId: request.requestId, typeId: request.typeId, closedDate: request.closedDate, + // Store this in milliseconds so that it's easy to do date comparisons + // using Mapbox GL JS filters. + createdDateMs: moment(request.createdDate).valueOf(), }, geometry: { type: 'Point', diff --git a/client/components/Map/layers/RequestsLayer.js b/client/components/Map/layers/RequestsLayer.js index 0d3e52cd1..6202c403a 100644 --- a/client/components/Map/layers/RequestsLayer.js +++ b/client/components/Map/layers/RequestsLayer.js @@ -3,6 +3,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; +import { INTERNAL_DATE_SPEC } from '../../common/CONSTANTS'; +import moment from 'moment'; // put layer underneath this layer (from original mapbox tiles) // so you don't cover up important labels @@ -72,6 +74,22 @@ function statusFilter(requestStatus) { return ['!=', [GET, CLOSED_DATE], [LITERAL, null]]; } +/** + * Gets a MapBox GL JS filter specification to filter requests by date range. + * + * @param {string} startDate The start date, in YYYY-MM-DD format. + * @param {string} endDate The end date, in YYYY-MM-DD format. + * @return {Array} A Mapbox GL JS filter specification that filters out + * requests outside of the date range. + */ +function dateFilter(startDate, endDate) { + const startDateMs = moment(startDate, INTERNAL_DATE_SPEC).valueOf(); + const endDateMs = moment(endDate, INTERNAL_DATE_SPEC).add(1, 'days').valueOf(); + const afterStartDate = ['>=', [GET, 'createdDateMs'], [LITERAL, startDateMs]]; + const beforeEndDate = ['<=', [GET, 'createdDateMs'], [LITERAL, endDateMs]]; + return ['all', afterStartDate, beforeEndDate]; +} + class RequestsLayer extends React.Component { constructor(props) { super(props); @@ -92,18 +110,23 @@ class RequestsLayer extends React.Component { requestStatus, requests, colorScheme, + startDate, + endDate, } = this.props; if (activeLayer !== prev.activeLayer) this.setActiveLayer(activeLayer); - // Check if the selected types OR the request status has changed. + // Check if the selected types OR the request status OR the date range has + // changed. // These filters need to be updated together, since they are // actually composed into a single filter. if (selectedTypes !== prev.selectedTypes || requestStatus.open !== prev.requestStatus.open || - requestStatus.closed !== prev.requestStatus.closed) { - this.setFilters(selectedTypes, requestStatus); + requestStatus.closed !== prev.requestStatus.closed || + startDate != prev.startDate || + endDate != prev.endDate) { + this.setFilters(selectedTypes, requestStatus, startDate, endDate); } if (requests !== prev.requests && this.ready) { this.setRequests(requests); @@ -128,6 +151,8 @@ class RequestsLayer extends React.Component { colorScheme, requestTypes, requestStatus, + startDate, + endDate, } = this.props; this.map.addLayer({ @@ -148,7 +173,8 @@ class RequestsLayer extends React.Component { 'circle-color': circleColors(requestTypes), 'circle-opacity': 0.8, }, - filter: this.getFilterSpec(selectedTypes, requestStatus), + filter: this.getFilterSpec(selectedTypes, requestStatus, startDate, + endDate), }, BEFORE_ID); // this.map.addLayer({ @@ -190,16 +216,19 @@ class RequestsLayer extends React.Component { * @param {Object} requestStatus A mapping of k:v, where k is a request status * (either open or closed), and v is a boolean indicating whether the request * status is selected. - * @return {Array} A Mapbox GL JS filter specification that filters out the + * @param {string} startDate The start date, in YYYY-MM-DD format. + * @param {string} endDate The end date, in YYYY-MM-DD format. + * @return {Array} A Mapbox GL JS filter specification that filters out the * unselected types and statuses. */ - getFilterSpec = (selectedTypes, requestStatus) => { - return ['all', typeFilter(selectedTypes), statusFilter(requestStatus)]; + getFilterSpec = (selectedTypes, requestStatus, startDate, endDate) => { + return ['all', typeFilter(selectedTypes), statusFilter(requestStatus), + dateFilter(startDate, endDate)]; }; - setFilters = (selectedTypes, requestStatus) => { + setFilters = (selectedTypes, requestStatus, startDate, endDate) => { this.map.setFilter('request-circles', - this.getFilterSpec(selectedTypes, requestStatus)); + this.getFilterSpec(selectedTypes, requestStatus, startDate, endDate)); // Currently, we do not support heatmap. If we did, we'd want to update // its filter here as well. }; @@ -235,6 +264,8 @@ const mapStateToProps = state => ({ selectedTypes: state.filters.requestTypes, requestStatus: state.filters.requestStatus, requests: state.data.requests, + startDate: state.filters.startDate, + endDate: state.filters.endDate, }); // We need to specify forwardRef to allow refs on connected components. diff --git a/client/components/common/CONSTANTS.js b/client/components/common/CONSTANTS.js index ea84eb9e9..a173b3938 100644 --- a/client/components/common/CONSTANTS.js +++ b/client/components/common/CONSTANTS.js @@ -829,11 +829,17 @@ export const MAP_DATE_RANGES = (() => { ]; })(); -export const DATE_SPEC = 'MM/DD/YYYY'; +// The user gets this date format since it's used most commonly in the US. +export const USER_DATE_SPEC = 'MM/DD/YYYY'; +// Internally, we use this date spec. This is what our server expects when we +// request data from a certain date range. +export const INTERNAL_DATE_SPEC = 'YYYY-MM-DD'; export const DATE_RANGES = (() => { - const endDate = moment().format(DATE_SPEC); - const priorDate = (num, timeInterval) => moment().subtract(num, timeInterval).format(DATE_SPEC); + const endDate = moment().format(USER_DATE_SPEC); + function priorDate(num, timeInterval) { + return moment().subtract(num, timeInterval).format(USER_DATE_SPEC); + } return [ { @@ -875,7 +881,7 @@ export const DATE_RANGES = (() => { { id: 'YEAR_TO_DATE', label: 'Year to Date', - startDate: moment().startOf('year').format(DATE_SPEC), + startDate: moment().startOf('year').format(USER_DATE_SPEC), endDate, }, { diff --git a/client/components/common/ReactDayPicker/ReactDayPicker.jsx b/client/components/common/ReactDayPicker/ReactDayPicker.jsx index 6bf898db8..345f03037 100644 --- a/client/components/common/ReactDayPicker/ReactDayPicker.jsx +++ b/client/components/common/ReactDayPicker/ReactDayPicker.jsx @@ -1,11 +1,19 @@ +import 'react-day-picker/lib/style.css'; + +import { + updateEndDate as reduxUpdateEndDate, + updateStartDate as reduxUpdateStartDate, +} from '@reducers/filters'; +import moment from 'moment'; +import PropTypes from 'prop-types'; import React, { useState } from 'react'; import DayPicker, { DateUtils } from 'react-day-picker'; -import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import { INTERNAL_DATE_SPEC } from '../CONSTANTS'; import Styles from './Styles'; import WeekDay from './Weekday'; -import 'react-day-picker/lib/style.css'; - const getInitialState = initialDates => { const [from, to] = initialDates; return { @@ -17,7 +25,10 @@ const getInitialState = initialDates => { const defaultState = { from: null, to: null }; -function ReactDayPicker({ onChange, initialDates, range }) { +function ReactDayPicker({ + onChange, initialDates, range, updateStartDate, + updateEndDate, +}) { const [state, setState] = useState(getInitialState(initialDates)); const isSelectingFirstDay = (from, to, day) => { @@ -37,6 +48,7 @@ function ReactDayPicker({ onChange, initialDates, range }) { to: null, enteredTo: null, })); + updateStartDate(moment(day).format(INTERNAL_DATE_SPEC)); onChange([day]); }; @@ -46,6 +58,7 @@ function ReactDayPicker({ onChange, initialDates, range }) { to: day, enteredTo: day, })); + updateEndDate(moment(day).format(INTERNAL_DATE_SPEC)); onChange([state.from, day]); }; @@ -106,12 +119,21 @@ ReactDayPicker.propTypes = { range: PropTypes.bool, onChange: PropTypes.func, initialDates: PropTypes.arrayOf(Date), + updateStartDate: PropTypes.func, + updateEndDate: PropTypes.func, }; ReactDayPicker.defaultProps = { range: false, onChange: null, initialDates: [], + updateStartDate: null, + updateEndDate: null, }; -export default ReactDayPicker; +const mapDispatchToProps = dispatch => ({ + updateStartDate: date => dispatch(reduxUpdateStartDate(date)), + updateEndDate: date => dispatch(reduxUpdateEndDate(date)), +}); + +export default connect(null, mapDispatchToProps)(ReactDayPicker); diff --git a/client/redux/reducers/filters.js b/client/redux/reducers/filters.js index bc9793be6..c4012a208 100644 --- a/client/redux/reducers/filters.js +++ b/client/redux/reducers/filters.js @@ -1,4 +1,5 @@ -import { DATE_RANGES } from '@components/common/CONSTANTS'; +import { DATE_RANGES, INTERNAL_DATE_SPEC, USER_DATE_SPEC } from '@components/common/CONSTANTS'; +import moment from 'moment'; export const types = { UPDATE_START_DATE: 'UPDATE_START_DATE', @@ -37,8 +38,9 @@ export const updateRequestStatus = status => ({ const initialState = { // dateRange: null, - startDate: DATE_RANGES[0].startDate, - endDate: DATE_RANGES[0].endDate, + // Always store dates using the INTERNAL_DATE_SPEC. + startDate: moment(DATE_RANGES[0].startDate, USER_DATE_SPEC).format(INTERNAL_DATE_SPEC), + endDate: moment(DATE_RANGES[0].endDate, USER_DATE_SPEC).format(INTERNAL_DATE_SPEC), councilId: null, requestTypes: { 1: false, From f98935466134b54fe37fc1a4981faed758a3cc12 Mon Sep 17 00:00:00 2001 From: Nicholas Kwon Date: Thu, 7 Jul 2022 10:35:27 -0700 Subject: [PATCH 2/2] add comment about making end date inclusive --- client/components/Map/layers/RequestsLayer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/components/Map/layers/RequestsLayer.js b/client/components/Map/layers/RequestsLayer.js index 6202c403a..969f9fa30 100644 --- a/client/components/Map/layers/RequestsLayer.js +++ b/client/components/Map/layers/RequestsLayer.js @@ -84,6 +84,7 @@ function statusFilter(requestStatus) { */ function dateFilter(startDate, endDate) { const startDateMs = moment(startDate, INTERNAL_DATE_SPEC).valueOf(); + // Make the end date inclusive by adding 1 day. const endDateMs = moment(endDate, INTERNAL_DATE_SPEC).add(1, 'days').valueOf(); const afterStartDate = ['>=', [GET, 'createdDateMs'], [LITERAL, startDateMs]]; const beforeEndDate = ['<=', [GET, 'createdDateMs'], [LITERAL, endDateMs]];