diff --git a/app/actions/advisoryModalActions.js b/app/actions/advisoryModalActions.js new file mode 100644 index 00000000..3de7272a --- /dev/null +++ b/app/actions/advisoryModalActions.js @@ -0,0 +1,9 @@ +import * as AT from '../constants/actionTypes'; + +export const hideModal = () => ({ + type: AT.HIDE_MODAL, +}); + +export const showModal = () => ({ + type: AT.SHOW_MODAL, +}); diff --git a/app/capitalprojects/LandingPage.jsx b/app/capitalprojects/LandingPage.jsx index 0df4b007..b752baf4 100644 --- a/app/capitalprojects/LandingPage.jsx +++ b/app/capitalprojects/LandingPage.jsx @@ -4,36 +4,69 @@ import { connect } from 'react-redux'; import { Link } from 'react-router'; import Footer from '../common/Footer'; +import AdvisoryModal from '../common/AdvisoryModal'; import * as capitalProjectsActions from '../actions/capitalProjects'; +import { hideModal } from '../actions/advisoryModalActions'; + import './LandingPage.scss'; class LandingPage extends React.Component { componentWillMount() { this.props.resetFilter(); + document.addEventListener('keydown', this.handleEscKey); + } + + componentWillUnmount() { + document.removeEventListener('keydown', this.handleEscKey); } + handleClose = () => { + this.props.hideModal(); + }; + + handleEscKey = (event) => { + if (event.key === 'Escape') { + this.handleClose(); + } + }; + render() { + const { showAdvisoryModal } = this.props; + return (
+ {showAdvisoryModal && }
-
+

NYC Capital Projects

-

A new way to explore NYC's Capital Commitment Plan and Capital Spending

-

Learn More

+

+ A new way to explore NYC's Capital Commitment Plan and + Capital Spending +

+

+ Learn More +

-
Search the
Capital Commitment Plan
+
+ Search the
Capital Commitment Plan +
- Raw data for all capital projects within the most recent Capital Commitment Plan published by the Mayor’s Office of Management and Budget. + Raw data for all capital projects within the most recent + Capital Commitment Plan published by the Mayor's + Office of Management and Budget.
@@ -46,10 +79,14 @@ class LandingPage extends React.Component { to="/capitalprojects/explorer" className="btn btn-default" > -
Explore
Capital Projects on a Map
+
+ Explore
Capital Projects on a Map +
- Capital projects within the Capital Commitment Plan that NYC Planning has worked with agencies to map - a subset of total planned spending. + Capital projects within the Capital Commitment Plan that + NYC Planning has worked with agencies to map - a subset of + total planned spending.
@@ -65,8 +102,18 @@ class LandingPage extends React.Component { LandingPage.propTypes = { resetFilter: PropTypes.func.isRequired, + hideModal: PropTypes.func.isRequired, + showAdvisoryModal: PropTypes.bool.isRequired, }; -export default connect(() => {}, { +const mapStateToProps = state => ({ + showAdvisoryModal: state.advisoryModal.showAdvisoryModal, +}); + +const mapDispatchToProps = { resetFilter: capitalProjectsActions.resetFilter, -})(LandingPage); + hideModal, +}; + + +export default connect(mapStateToProps, mapDispatchToProps)(LandingPage); diff --git a/app/common/AdvisoryModal.jsx b/app/common/AdvisoryModal.jsx new file mode 100644 index 00000000..ea06bf89 --- /dev/null +++ b/app/common/AdvisoryModal.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Modal } from 'react-bootstrap'; + +const AdvisoryModal = ({ handleClose }) => ( + + + + +
+ The Capital Projects section of the Capital Planning Explorer is no + longer being updated with the latest data (the most recent data + available in this application are from April 2023). To download the + latest Capital Projects data, please refer to  + + NYC Open Data + +  or  + + Bytes of the Big Apple + + . Other sections of the Capital Planning Explorer are still being + actively updated, and we encourage you to stay tuned for future + enhancements to this platform. +
+
+ + +
+ Close +
+
+
+); + +AdvisoryModal.propTypes = { + handleClose: PropTypes.func.isRequired, +}; + +export default AdvisoryModal; diff --git a/app/common/GlobalModal.scss b/app/common/GlobalModal.scss index c20b62b3..969bf21a 100644 --- a/app/common/GlobalModal.scss +++ b/app/common/GlobalModal.scss @@ -4,7 +4,7 @@ color: #d96b27; } -.modal-body h5, { +.modal-body h5 { font-family: 'Oswald'; font-size: 17px; } diff --git a/app/constants/actionTypes.js b/app/constants/actionTypes.js index 4ecb041e..6004b054 100644 --- a/app/constants/actionTypes.js +++ b/app/constants/actionTypes.js @@ -11,6 +11,8 @@ export const FETCH_NYC_BOUNDS = asyncType('FETCH_NYC_BOUNDS'); // Modals export const OPEN_MODAL = 'OPEN_MODAL'; export const CLOSE_MODAL = 'CLOSE_MODAL'; +export const SHOW_MODAL = 'SHOW_MODAL'; +export const HIDE_MODAL = 'HIDE_MODAL'; // Auth export const AUTH0_LOGIN = 'AUTH0_LOGIN'; diff --git a/app/explorer/Explorer.jsx b/app/explorer/Explorer.jsx index d056c5fc..12daeb82 100644 --- a/app/explorer/Explorer.jsx +++ b/app/explorer/Explorer.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import ReactGA4 from "react-ga4"; +import ReactGA4 from 'react-ga4'; import _ from 'lodash'; import centroid from 'turf-centroid'; @@ -9,6 +9,7 @@ import getDefaultFilterDimensions from '../facilities/config'; import { Jane } from '../jane-maps'; import * as selectedActions from '../actions/selected'; +import { showModal, hideModal } from '../actions/advisoryModalActions'; import { AerialsJaneLayer, @@ -27,6 +28,8 @@ import { import appConfig from '../config/appConfig'; +import AdvisoryModal from '../common/AdvisoryModal'; + const { mapbox_accessToken, searchConfig } = appConfig; const mapboxGLOptions = { @@ -57,17 +60,33 @@ class Explorer extends React.Component { this.mapClicked = false; } + componentWillMount() { + this.props.showModal(); + document.addEventListener('keydown', this.handleEscKey); + } + componentWillReceiveProps(nextProps) { - if (!this.mapClicked && (this.props.map.centerOnGeometry !== nextProps.map.centerOnGeometry)) { + if ( + !this.mapClicked && + this.props.map.centerOnGeometry !== nextProps.map.centerOnGeometry + ) { this.centerFromGeometry(nextProps.map.centerOnGeometry); this.setState({ selectedPointType: 'point', - selectedPointCoordinates: this.centroidFromGeometry(nextProps.map.centerOnGeometry), - highlightPointCoordinates: this.centroidFromGeometry(nextProps.map.centerOnGeometry), + selectedPointCoordinates: this.centroidFromGeometry( + nextProps.map.centerOnGeometry, + ), + highlightPointCoordinates: this.centroidFromGeometry( + nextProps.map.centerOnGeometry, + ), }); } } + componentWillUnmount() { + document.removeEventListener('keydown', this.handleEscKey); + } + onLayerToggle = (layerId, wasEnabled) => { ReactGA4.gtag('event', 'toggle-layer', { 'action': `toggle-layer-${layerId}`, @@ -75,7 +94,7 @@ class Explorer extends React.Component { }); FS.event('capitalPlanningExplorerLayerToggle', { layer_id: layerId, - layer_enabled: !wasEnabled + layer_enabled: !wasEnabled, }); const janeLayerIdsMap = { @@ -96,7 +115,7 @@ class Explorer extends React.Component { } }); } - } + }; // Nasty debounces cause I suck at async setSelectedFeatures = _.debounce(() => { @@ -114,7 +133,10 @@ class Explorer extends React.Component { }); } - if (payload.action === 'clear' && this.state.selectedPointType === 'address') { + if ( + payload.action === 'clear' && + this.state.selectedPointType === 'address' + ) { this.setState({ selectedPointType: '', selectedPointCoordinates: [], @@ -152,7 +174,10 @@ class Explorer extends React.Component { }); } - if (features[0].geometry.type === 'Polygon' || features[0].geometry.type === 'MultiPolygon') { + if ( + features[0].geometry.type === 'Polygon' || + features[0].geometry.type === 'MultiPolygon' + ) { this.setState({ selectedPointType: 'point', selectedPointCoordinates: [event.lngLat.lng, event.lngLat.lat], @@ -175,7 +200,7 @@ class Explorer extends React.Component { centerPointCoordinates: this.state.selectedPointCoordinates, }, }); - } + }; clearSelectedFeatures = () => { this.props.resetSelectedFeatures(); @@ -192,9 +217,10 @@ class Explorer extends React.Component { this.props.resetSelectedFeatures(); this.props.router.push('/map'); - } + }; - layerIdsInSelectedFeatures = () => _.uniq(this.props.selectedFeatures.map(f => f.layer.id)); + layerIdsInSelectedFeatures = () => + _.uniq(this.props.selectedFeatures.map(f => f.layer.id)); featureRoute = (feature) => { switch (feature.layer.source) { @@ -213,7 +239,7 @@ class Explorer extends React.Component { default: return null; } - } + }; centerMap = _.debounce((lnglat) => { this.Jane.GLMap.map.easeTo({ @@ -223,16 +249,32 @@ class Explorer extends React.Component { }); }, 50); + handleClose = () => { + this.props.hideModal(); + }; + + handleEscKey = (event) => { + if (event.key === 'Escape') { + this.handleClose(); + } + }; + render() { const setStartingLayer = () => { if (this.props.children) { switch (this.props.children.props.route.type) { - case 'capitalproject': return 'capitalprojects'; - case 'facility': return 'facilities'; - case 'pops': return 'pops'; - case 'development': return 'housing'; - case 'sca': return 'sca'; - case 'budgetrequest': return 'budgetrequests'; + case 'capitalproject': + return 'capitalprojects'; + case 'facility': + return 'facilities'; + case 'pops': + return 'pops'; + case 'development': + return 'housing'; + case 'sca': + return 'sca'; + case 'budgetrequest': + return 'budgetrequests'; default: return this.props.params.layer || 'capitalprojects'; } @@ -240,20 +282,25 @@ class Explorer extends React.Component { return this.props.params.layer || 'capitalprojects'; }; - const { selectedFeatures } = this.props; + const { selectedFeatures, showAdvisoryModal } = this.props; + const startingLayer = setStartingLayer(); const popsLocationState = { - filterDimensions: getDefaultFilterDimensions({ selected: { - 'Parks, Gardens, and Historical Sites': { - 'Parks and Plazas': { - 'Privately Owned Public Space': null }, + filterDimensions: getDefaultFilterDimensions({ + selected: { + 'Parks, Gardens, and Historical Sites': { + 'Parks and Plazas': { + 'Privately Owned Public Space': null, + }, + }, }, - } }), + }), }; return (
+ {showAdvisoryModal && } { this.Jane = jane; }} + ref={(jane) => { + this.Jane = jane; + }} > ({ pointsSql: capitalProjects.pointsSql, polygonsSql: capitalProjects.polygonsSql, @@ -380,9 +439,14 @@ const mapStateToProps = ({ map, currentUser, + showAdvisoryModal: advisoryModal.showAdvisoryModal, }); -export default connect(mapStateToProps, { +const mapDispatchToProps = { + showModal, + hideModal, setSelectedFeatures: selectedActions.setSelectedFeatures, resetSelectedFeatures: selectedActions.resetSelectedFeatures, -})(Explorer); +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Explorer); diff --git a/app/reducers/advisoryModalReducer.js b/app/reducers/advisoryModalReducer.js new file mode 100644 index 00000000..1ab18a10 --- /dev/null +++ b/app/reducers/advisoryModalReducer.js @@ -0,0 +1,23 @@ +import * as AT from '../constants/actionTypes'; + +const initialState = { + showAdvisoryModal: true, +}; + +const advisoryModalReducer = (state = initialState, action) => { + switch (action.type) { + case AT.HIDE_MODAL: + return Object.assign({}, state, { + showAdvisoryModal: false, + }); + case AT.SHOW_MODAL: + return Object.assign({}, state, { + showAdvisoryModal: true, + }); + + default: + return state; + } +}; + +export default advisoryModalReducer; diff --git a/app/store.js b/app/store.js index 715f6b40..0ce37ff3 100644 --- a/app/store.js +++ b/app/store.js @@ -12,6 +12,7 @@ import cbBudgetRequests from './reducers/cbBudgetRequests'; import selected from './reducers/selected'; import sca from './reducers/sca'; import map from './reducers/map'; +import advisoryModal from './reducers/advisoryModalReducer'; // Middleware import auth from './middleware/auth'; @@ -37,13 +38,11 @@ const store = createStore( selected, sca, map, + advisoryModal, }), applyMiddleware(...middleware), ); -// if (process.env.NODE_ENV === 'development') { -// Why? This should be removed window.store = store; -// } export default store; diff --git a/app/tables/capital-projects/CapitalProjectsTable.jsx b/app/tables/capital-projects/CapitalProjectsTable.jsx index 7256f959..2632d33c 100644 --- a/app/tables/capital-projects/CapitalProjectsTable.jsx +++ b/app/tables/capital-projects/CapitalProjectsTable.jsx @@ -14,10 +14,14 @@ import cx from 'classnames'; import InfoIcon from '../../common/InfoIcon'; import TableFilter from './CapitalProjectsTableFilter'; import * as capitalProjectsTableActions from '../../actions/capitalProjectsTable'; +import { showModal, hideModal } from '../../actions/advisoryModalActions'; import SortHeaderCell from './SortHeaderCell'; import ga from '../../helpers/ga'; import Download from '../../explorer/Download'; +import AdvisoryModal from '../../common/AdvisoryModal'; + + import './CapitalProjectsTable.scss'; const filterAndSortData = (data, filterBy, colSortDirs) => { @@ -61,7 +65,7 @@ const filterAndSortData = (data, filterBy, colSortDirs) => { }; -class CPTable extends React.Component { // eslint-disable-line +class CPTable extends React.Component { constructor(props) { super(props); @@ -75,6 +79,11 @@ class CPTable extends React.Component { // eslint-disable-line }; } + componentWillMount() { + this.props.showModal(); + document.addEventListener('keydown', this.handleEscKey); + } + componentDidMount() { this.props.fetchSelectedCount(this.props.filterDimensions); this.props.fetchDetails(this.props.filterDimensions); @@ -87,9 +96,12 @@ class CPTable extends React.Component { // eslint-disable-line this.props.fetchSelectedCount(nextProps.filterDimensions); } - if (!_.isEqual(this.props.filterBy, nextProps.filterBy) || - !_.isEqual(this.props.colSortDirs, nextProps.colSortDirs) || - this.props.capitalProjectDetails.length !== nextProps.capitalProjectDetails.length) { + if ( + !_.isEqual(this.props.filterBy, nextProps.filterBy) || + !_.isEqual(this.props.colSortDirs, nextProps.colSortDirs) || + this.props.capitalProjectDetails.length !== + nextProps.capitalProjectDetails.length + ) { this.setState({ filteredSortedData: filterAndSortData( nextProps.capitalProjectDetails, @@ -100,6 +112,10 @@ class CPTable extends React.Component { // eslint-disable-line } } + componentWillUnmount() { + document.removeEventListener('keydown', this.handleEscKey); + } + handleDownload = (label) => { ga.event({ category: 'capitalprojects-table', @@ -108,10 +124,9 @@ class CPTable extends React.Component { // eslint-disable-line }); }; - handleFilterBy = (e) => { // onFilterChange, update the state to reflect the filter term, then execute this.filterAndSortData() - const filterBy = e.target.value - ? e.target.value.toLowerCase() - : null; + handleFilterBy = (e) => { + // onFilterChange, update the state to reflect the filter term, then execute this.filterAndSortData() + const filterBy = e.target.value ? e.target.value.toLowerCase() : null; this.props.setFilterBy(filterBy); }; @@ -120,37 +135,54 @@ class CPTable extends React.Component { // eslint-disable-line this.props.setSort(columnKey, sortDir); }; - linkToProject = rowData => content => ( - - { content } - - ); - - render() { - const TextCell = ({ rowIndex, data, col, ...props }) => this.linkToProject(data[rowIndex])( - - {data[rowIndex][col]} - , + linkToProject = rowData => content => + ( + + {content} + ); - const ArrayTextCell = ({ rowIndex, data, col, ...props }) => this.linkToProject(data[rowIndex])( - - {data[rowIndex][col].join(', ')} - , - ); + handleClose = () => { + this.props.hideModal(); - const MoneyCell = ({ rowIndex, data, col, ...props }) => this.linkToProject(data[rowIndex])( - - {Numeral(data[rowIndex][col]).format('($ 0.00 a)')} - , - ); + }; + + handleEscKey = (event) => { + if (event.key === 'Escape') { + this.handleClose(); + } + }; - const { colSortDirs, containerHeight, containerWidth } = this.props; + render() { + const TextCell = ({ rowIndex, data, col, ...props }) => + this.linkToProject(data[rowIndex])( + {data[rowIndex][col]}, + ); + + const ArrayTextCell = ({ rowIndex, data, col, ...props }) => + this.linkToProject(data[rowIndex])( + {data[rowIndex][col].join(', ')}, + ); + + const MoneyCell = ({ rowIndex, data, col, ...props }) => + this.linkToProject(data[rowIndex])( + + {Numeral(data[rowIndex][col]).format('($ 0.00 a)')} + , + ); + + const { + colSortDirs, + containerHeight, + containerWidth, + showAdvisoryModal, + } = + this.props; const { filteredSortedData } = this.state; const tabTemplateStyle = { @@ -161,35 +193,64 @@ class CPTable extends React.Component { // eslint-disable-line return (
+ {showAdvisoryModal && }
- + - +

Product Overview

- The Capital Project Table is a way to quickly and easily explore and learn about ongoing and planned capital projects within in the most recent Capital Commitment Plan published by OMB. It’s main purpose is to be a starting point for exploring potential, planned, and ongoing capital projects to better understand and communicate New York City’s capital project portfolio within and across particular agencies. + The Capital Project Table is a way to quickly and + easily explore and learn about ongoing and planned capital + projects within in the most recent Capital Commitment Plan + published by OMB. It’s main purpose is to be a starting + point for exploring potential, planned, and ongoing capital + projects to better understand and communicate New York + City’s capital project portfolio within and across + particular agencies.

Limitations and Disclaimers

-

  • This is not a project management system, so data on project timeline or budget may be incorrect
  • -
  • All monies committed to or spent on a project may not be captured
  • -
  • Planned projects that may never come to fruition are captured
  • +
  • + This is not a project management system, so data on + project timeline or budget may be incorrect +
  • +
  • + All monies committed to or spent on a project may not be + captured +
  • +
  • + Planned projects that may never come to fruition are + captured +
  • - As a result of these limitations and inconsistencies, the Capital Projects Map is not an analysis tool, it does not report any metrics, and the data should not be used for quantitative analyses, - it is built for planning coordination and information purposes only. Please consult NYC Planning’s Capital Planning Docs for more details about the limitations. + As a result of these limitations and inconsistencies, the + Capital Projects Map is not an analysis tool, it does not + report any metrics, and the data should not be used for + quantitative analyses, - it is built for planning + coordination and information purposes only. Please consult{' '} + + NYC Planning’s Capital Planning Docs + {' '} + for more details about the limitations.

    Feedback

    - We are constantly looking for ways to improve this product. Please share your feedback and suggestions with Capital Planning. + We are constantly looking for ways to improve this product. + Please{' '} + + share your feedback and suggestions + {' '} + with Capital Planning.

    @@ -198,17 +259,18 @@ class CPTable extends React.Component { // eslint-disable-line
    -
    - { this.state.downloadOpen && - - } + {this.state.downloadOpen && }
    {filteredSortedData && ( @@ -254,7 +316,8 @@ class CPTable extends React.Component { // eslint-disable-line onSortChange={this.handleSortChange} sortDir={colSortDirs.magencyacro} > - Man. Agency + Man. Agency{' '} + } cell={} @@ -267,10 +330,13 @@ class CPTable extends React.Component { // eslint-disable-line onSortChange={this.handleSortChange} sortDir={colSortDirs.sagencyacro} > - Spon. Agency + Spon. Agency{' '} + } - cell={} + cell={ + + } width={130} /> - Project Type + Project Type{' '} + } - cell={} + cell={ + + } width={200} /> - Planned Commitment - + Planned Commitment{' '} + } - cell={} + cell={ + + } width={180} /> @@ -320,24 +395,35 @@ CPTable.propTypes = { filterBy: PropTypes.string.isRequired, capitalProjectDetails: PropTypes.array.isRequired, fetchSelectedCount: PropTypes.func.isRequired, + showModal: PropTypes.func.isRequired, + hideModal: PropTypes.func.isRequired, + showAdvisoryModal: PropTypes.bool.isRequired, }; -const mapStateToProps = ({ capitalProjectsTable }) => ({ +const mapStateToProps = ({ + capitalProjectsTable, + advisoryModal, +}) => ({ filterDimensions: capitalProjectsTable.filterDimensions, sql: capitalProjectsTable.sql, commitmentsSql: capitalProjectsTable.commitmentsSql, colSortDirs: capitalProjectsTable.colSortDirs, filterBy: capitalProjectsTable.filterBy, capitalProjectDetails: capitalProjectsTable.capitalProjectDetails, + showAdvisoryModal: advisoryModal.showAdvisoryModal, }); -const ConnectedTable = connect(mapStateToProps, { +const mapDispatchToProps = { + showModal, + hideModal, fetchDetails: capitalProjectsTableActions.fetchDetails, resetFilter: capitalProjectsTableActions.resetFilter, setFilterBy: capitalProjectsTableActions.setTableFilterBy, setSort: capitalProjectsTableActions.setTableSort, fetchSelectedCount: capitalProjectsTableActions.fetchSelectedCount, -})(CPTable); +}; + +const ConnectedTable = connect(mapStateToProps, mapDispatchToProps)(CPTable); export default Dimensions({ getHeight() { diff --git a/package-lock.json b/package-lock.json index 66a3b0f8..4ca943fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -95,7 +95,7 @@ "webpack-dev-server": "^2.9.7" }, "engines": { - "node": "16.x.x", + "node": "^16.20.1", "npm": "5.6.x" } },