diff --git a/opentech/static_src/src/app/src/api/index.js b/opentech/static_src/src/app/src/api/index.js index 6fcd16016bf03484a9777a09772ecd92b46f4507..cd360e714df61f83b2fe5f9eb35db4ead48b7650 100644 --- a/opentech/static_src/src/app/src/api/index.js +++ b/opentech/static_src/src/app/src/api/index.js @@ -1,6 +1,9 @@ import { fetchSubmission, fetchSubmissionsByRound } from '@api/submissions'; +import { fetchNotesForSubmission } from '@api/notes'; export default { fetchSubmissionsByRound, fetchSubmission, + + fetchNotesForSubmission, }; diff --git a/opentech/static_src/src/app/src/api/notes.js b/opentech/static_src/src/app/src/api/notes.js new file mode 100644 index 0000000000000000000000000000000000000000..a797ba32ffbdf65267bb3a35252face9fc7c4721 --- /dev/null +++ b/opentech/static_src/src/app/src/api/notes.js @@ -0,0 +1,8 @@ +import { apiFetch } from '@api/utils'; + +export function fetchNotesForSubmission(submissionID, visibility = 'internal') { + return apiFetch(`/apply/api/submissions/${submissionID}/comments/`, 'GET', { + visibility, + page_size: 1000, + }); +} diff --git a/opentech/static_src/src/app/src/components/GroupedListing.js b/opentech/static_src/src/app/src/components/GroupedListing.js new file mode 100644 index 0000000000000000000000000000000000000000..ddac492a95a104ed200c5f376cfa4b8e3c1ec59c --- /dev/null +++ b/opentech/static_src/src/app/src/components/GroupedListing.js @@ -0,0 +1,104 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Listing from '@components/Listing'; +import ListingGroup from '@components/ListingGroup'; +import ListingItem from '@components/ListingItem'; + +export default class GroupedListing extends React.Component { + static propTypes = { + items: PropTypes.array, + activeItem: PropTypes.number, + isLoading: PropTypes.bool, + error: PropTypes.string, + groupBy: PropTypes.string, + order: PropTypes.arrayOf(PropTypes.string), + onItemSelection: PropTypes.func, + shouldSelectFirst: PropTypes.bool, + }; + + static defaultProps = { + shouldSelectFirst: true, + } + + + state = { + orderedItems: [], + }; + + componentDidMount() { + this.orderItems(); + } + + componentDidUpdate(prevProps, prevState) { + // Order items + if (this.props.items !== prevProps.items) { + this.orderItems(); + } + + if ( this.props.shouldSelectFirst ){ + const oldItem = prevProps.activeItem + const newItem = this.props.activeItem + + // If we have never activated a submission, get the first item + if ( !newItem && !oldItem ) { + const firstGroup = this.state.orderedItems[0] + if ( firstGroup && firstGroup.items[0] ) { + this.setState({firstUpdate: false}) + this.props.onItemSelection(firstGroup.items[0].id) + } + } + } + } + + getGroupedItems() { + const { groupBy, items } = this.props; + + return items.reduce((tmpItems, v) => { + const groupByValue = v[groupBy]; + if (!(groupByValue in tmpItems)) { + tmpItems[groupByValue] = []; + } + tmpItems[groupByValue].push({...v}); + return tmpItems; + }, {}); + } + + orderItems() { + const groupedItems = this.getGroupedItems(); + const { order = [] } = this.props; + const leftOverKeys = Object.keys(groupedItems).filter(v => !order.includes(v)); + this.setState({ + orderedItems: order.concat(leftOverKeys).filter(key => groupedItems[key] ).map(key => ({ + name: key, + items: groupedItems[key] || [] + })), + }); + } + + renderItem = group => { + const { activeItem, onItemSelection } = this.props; + return ( + <ListingGroup key={`listing-group-${group.name}`} item={group}> + {group.items.map(item => { + return <ListingItem + selected={!!activeItem && activeItem===item.id} + onClick={() => onItemSelection(item.id)} + key={`listing-item-${item.id}`} + item={item}/>; + })} + </ListingGroup> + ); + } + + render() { + const passProps = { + items: this.state.orderedItems, + renderItem: this.renderItem, + isLoading: this.props.isLoading, + isError: Boolean(this.error), + error: this.error, + }; + return <Listing {...passProps} />; + } +} diff --git a/opentech/static_src/src/app/src/components/Listing/index.js b/opentech/static_src/src/app/src/components/Listing/index.js index 0296b55c127cea075d1a1427d7c770af0601192c..6da541874683c12f5ae4e034e7e74fe34cf64b3e 100644 --- a/opentech/static_src/src/app/src/components/Listing/index.js +++ b/opentech/static_src/src/app/src/components/Listing/index.js @@ -1,123 +1,64 @@ import React from 'react'; import PropTypes from 'prop-types'; -import ListingGroup from '@components/ListingGroup'; -import ListingItem from '@components/ListingItem'; import LoadingPanel from '@components/LoadingPanel'; import './style.scss'; export default class Listing extends React.Component { static propTypes = { - items: PropTypes.array, - activeItem: PropTypes.number, + items: PropTypes.array.isRequired, isLoading: PropTypes.bool, + isError: PropTypes.bool, error: PropTypes.string, groupBy: PropTypes.string, order: PropTypes.arrayOf(PropTypes.string), onItemSelection: PropTypes.func, - shouldSelectFirst: PropTypes.bool, + renderItem: PropTypes.func.isRequired, + handleRetry: PropTypes.func, }; - static defaultProps = { - shouldSelectFirst: true, - } - - state = { - orderedItems: [], - }; - - componentDidMount() { - this.orderItems(); - } - - componentDidUpdate(prevProps, prevState) { - // Order items - if (this.props.items !== prevProps.items) { - this.orderItems(); - } - - if ( this.props.shouldSelectFirst ){ - const oldItem = prevProps.activeItem - const newItem = this.props.activeItem - - // If we have never activated a submission, get the first item - if ( !newItem && !oldItem ) { - const firstGroup = this.state.orderedItems[0] - if ( firstGroup && firstGroup.items[0] ) { - this.setState({firstUpdate: false}) - this.props.onItemSelection(firstGroup.items[0].id) - } - } - } - } - renderListItems() { - const { isLoading, error, items, onItemSelection, activeItem } = this.props; + const { + isError, + isLoading, + items, + renderItem, + } = this.props; if (isLoading) { return ( <div className="listing__list is-loading"> <LoadingPanel /> </div> - ) - } else if (error) { - return ( - <div className="listing__list is-loading"> - <p>Something went wrong. Please try again later.</p> - <p>{ error }</p> - </div> - ) + ); + } else if (isError) { + return this.renderError(); } else if (items.length === 0) { return ( <div className="listing__list is-loading"> <p>No results found.</p> </div> - ) + ); } return ( <ul className="listing__list"> - {this.state.orderedItems.map(group => { - return ( - <ListingGroup key={`listing-group-${group.name}`} item={group}> - {group.items.map(item => { - return <ListingItem - selected={!!activeItem && activeItem===item.id} - onClick={() => onItemSelection(item.id)} - key={`listing-item-${item.id}`} - item={item}/>; - })} - </ListingGroup> - ); - })} + {items.map(v => renderItem(v))} </ul> ); } - getGroupedItems() { - const { groupBy, items } = this.props; - - return items.reduce((tmpItems, v) => { - const groupByValue = v[groupBy]; - if (!(groupByValue in tmpItems)) { - tmpItems[groupByValue] = []; - } - tmpItems[groupByValue].push({...v}); - return tmpItems; - }, {}); - } - - orderItems() { - const groupedItems = this.getGroupedItems(); - const { order = [] } = this.props; - const leftOverKeys = Object.keys(groupedItems).filter(v => !order.includes(v)); - this.setState({ - orderedItems: order.concat(leftOverKeys).filter(key => groupedItems[key] ).map(key => ({ - name: key, - items: groupedItems[key] || [] - })), - }); + renderError = () => { + const { handleRetry, error } = this.props; + const retryButton = <a onClick={handleRetry}>Refresh</a>; + return ( + <div className="listing__list is-loading"> + <p>Something went wrong. Please try again later.</p> + {error && <p>{error}</p>} + {handleRetry && retryButton} + </div> + ); } render() { diff --git a/opentech/static_src/src/app/src/components/NoteListingItem.js b/opentech/static_src/src/app/src/components/NoteListingItem.js new file mode 100644 index 0000000000000000000000000000000000000000..c7f88178d3821b33706d9739b0e9108ec94a71b1 --- /dev/null +++ b/opentech/static_src/src/app/src/components/NoteListingItem.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; + +export default class NoteListingItem extends React.Component { + static propTypes = { + user: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, + timestamp: PropTypes.instanceOf(moment).isRequired, + }; + + render() { + const { user, timestamp, message } = this.props; + return ( + <div> + <div style={{fontWeight: 'bold'}}>{user} - {timestamp.format('ll')}</div> + <div>{message}</div> + </div> + ); + } +} diff --git a/opentech/static_src/src/app/src/containers/ByStatusListing.js b/opentech/static_src/src/app/src/containers/ByStatusListing.js index f33b78e512a3130228d31acfb2353c32b5603c12..c6c2d705227ec77f1d9409a379b3aaf979a8ad2b 100644 --- a/opentech/static_src/src/app/src/containers/ByStatusListing.js +++ b/opentech/static_src/src/app/src/containers/ByStatusListing.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux' -import Listing from '@components/Listing'; +import GroupedListing from '@components/GroupedListing'; import { loadCurrentRound, setCurrentSubmission, @@ -50,7 +50,7 @@ class ByStatusListing extends React.Component { render() { const { error, submissions, round, setCurrentItem, activeSubmission, shouldSelectFirst} = this.props; const isLoading = round && round.isFetching - return <Listing + return <GroupedListing isLoading={isLoading} error={error} items={submissions} diff --git a/opentech/static_src/src/app/src/containers/DisplayPanel/index.js b/opentech/static_src/src/app/src/containers/DisplayPanel/index.js index e09e83f962d083bafa4acb743007b7443aef068b..0da90bb5bb0c4b23687ad2d7302be2320ee5b645 100644 --- a/opentech/static_src/src/app/src/containers/DisplayPanel/index.js +++ b/opentech/static_src/src/app/src/containers/DisplayPanel/index.js @@ -13,10 +13,10 @@ import { } from '@selectors/submissions'; import CurrentSubmissionDisplay from '@containers/CurrentSubmissionDisplay' +import NoteListing from '@containers/NoteListing'; import Tabber, {Tab} from '@components/Tabber' import './style.scss'; - class DisplayPanel extends React.Component { static propTypes = { submissionID: PropTypes.number, @@ -28,7 +28,7 @@ class DisplayPanel extends React.Component { }; render() { - const { windowSize: {windowWidth: width} } = this.props; + const { windowSize: {windowWidth: width}, submissionID } = this.props; const { clearSubmission } = this.props; const isMobile = width < 1024; @@ -37,7 +37,7 @@ class DisplayPanel extends React.Component { let tabs = [ <Tab button="Notes" key="note"> - <p>Notes</p> + <NoteListing submissionID={submissionID} /> </Tab>, <Tab button="Status" key="status"> <p>Status</p> @@ -88,5 +88,4 @@ const mapDispatchToProps = { clearSubmission: clearCurrentSubmission } - export default connect(mapStateToProps, mapDispatchToProps)(withWindowSizeListener(DisplayPanel)); diff --git a/opentech/static_src/src/app/src/containers/Note.js b/opentech/static_src/src/app/src/containers/Note.js new file mode 100644 index 0000000000000000000000000000000000000000..9f297145bc34b0939cef8354166084d8db41435d --- /dev/null +++ b/opentech/static_src/src/app/src/containers/Note.js @@ -0,0 +1,34 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import moment from 'moment'; + +import { getNoteOfID } from '@selectors/notes'; +import NoteListingItem from '@components/NoteListingItem'; + +class Note extends React.Component { + static propTypes = { + note: PropTypes.shape({ + user: PropTypes.string, + timestamp: PropTypes.string, + message: PropTypes.string, + }), + }; + + render() { + const { note } = this.props; + + return <NoteListingItem + user={note.user} + message={note.message} + timestamp={moment(note.timestamp)} + />; + } + +} + +const mapStateToProps = (state, ownProps) => ({ + note: getNoteOfID(ownProps.noteID)(state), +}); + +export default connect(mapStateToProps)(Note); diff --git a/opentech/static_src/src/app/src/containers/NoteListing.js b/opentech/static_src/src/app/src/containers/NoteListing.js new file mode 100644 index 0000000000000000000000000000000000000000..7f16d6c648ca597a8856d8eec3d77c2530c6d97f --- /dev/null +++ b/opentech/static_src/src/app/src/containers/NoteListing.js @@ -0,0 +1,71 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import { fetchNotesForSubmission } from '@actions/notes'; +import Listing from '@components/Listing'; +import Note from '@containers/Note'; +import { + getNotesErrorState, + getNoteIDsForSubmissionOfID, + getNotesFetchState, +} from '@selectors/notes'; + +class NoteListing extends React.Component { + static propTypes = { + loadNotes: PropTypes.func, + submissionID: PropTypes.number, + noteIDs: PropTypes.array, + isErrored: PropTypes.bool, + isLoading: PropTypes.bool, + }; + + componentDidUpdate(prevProps) { + const { submissionID } = this.props; + const prevSubmissionID = prevProps.submissionID; + + if( + submissionID !== null && submissionID !== undefined && + prevSubmissionID !== submissionID && !this.props.isLoading + ) { + this.props.loadNotes(submissionID); + } + } + + handleRetry = () => { + if (this.props.isLoading || !this.props.isErrored) { + return; + } + this.props.loadNotes(this.props.submissionID); + } + + renderItem = noteID => { + return <Note key={`note-${noteID}`} noteID={noteID} />; + } + + render() { + const { noteIDs } = this.props; + const passProps = { + isLoading: this.props.isLoading, + isError: this.props.isErrored, + handleRetry: this.handleRetry, + renderItem: this.renderItem, + items: noteIDs, + }; + return ( + <Listing {...passProps} /> + ); + } +} + +const mapDispatchToProps = dispatch => ({ + loadNotes: submissionID => dispatch(fetchNotesForSubmission(submissionID)), +}); + +const mapStateToProps = (state, ownProps) => ({ + noteIDs: getNoteIDsForSubmissionOfID(ownProps.submissionID)(state), + isLoading: getNotesFetchState(state), + isErrored: getNotesErrorState(state), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(NoteListing); diff --git a/opentech/static_src/src/app/src/datetime.js b/opentech/static_src/src/app/src/datetime.js new file mode 100644 index 0000000000000000000000000000000000000000..f906f3acee590e92f3df0b5c7a9bb66a8ca79e24 --- /dev/null +++ b/opentech/static_src/src/app/src/datetime.js @@ -0,0 +1,8 @@ +import moment from 'moment'; +import 'moment-timezone'; + +// Use GMT globally for all the dates. +moment.tz.setDefault('GMT'); + +// Use en-US locale for all the dates. +moment.locale('en'); diff --git a/opentech/static_src/src/app/src/redux/actions/notes.js b/opentech/static_src/src/app/src/redux/actions/notes.js new file mode 100644 index 0000000000000000000000000000000000000000..ecac7ad5fc3bac9f8cc82fc7db1a6fe8af6b96d6 --- /dev/null +++ b/opentech/static_src/src/app/src/redux/actions/notes.js @@ -0,0 +1,46 @@ +import { updateSubmission } from '@actions/submissions'; +import api from '@api'; + +export const FAIL_FETCHING_NOTES = 'FAIL_FETCHING_NOTES'; +export const START_FETCHING_NOTES = 'START_FETCHING_NOTES'; +export const UPDATE_NOTES = 'UPDATE_NOTES'; +export const UPDATE_NOTE = 'UPDATE_NOTE'; + +const startFetchingNotes = () => ({ + type: START_FETCHING_NOTES, +}); + +const failFetchingNotes = message => ({ + type: FAIL_FETCHING_NOTES, + message, +}); + +export const fetchNotesForSubmission = submissionID => { + return async function(dispatch) { + dispatch(startFetchingNotes()); + try { + const response = await api.fetchNotesForSubmission(submissionID); + const json = await response.json(); + if (!response.ok) { + return dispatch(failFetchingNotes(json.detail)); + } + return dispatch(updateNotesForSubmission(submissionID, json)); + } catch(e) { + return dispatch(failFetchingNotes(e.message)); + } + }; +}; + +const updateNotesForSubmission = (submissionID, data) => { + return async function(dispatch) { + dispatch(updateNotes(data)); + dispatch(updateSubmission(submissionID, { + comments: data.results.map(v => v.id), + })); + }; +}; + +export const updateNotes = data => ({ + type: UPDATE_NOTES, + data, +}); diff --git a/opentech/static_src/src/app/src/redux/actions/submissions.js b/opentech/static_src/src/app/src/redux/actions/submissions.js index db35aec0678e3239cec174980b3de9ff6a8d6459..181f755f4fb588a3338a82c948b5ecd6a9b9a8ec 100644 --- a/opentech/static_src/src/app/src/redux/actions/submissions.js +++ b/opentech/static_src/src/app/src/redux/actions/submissions.js @@ -123,7 +123,7 @@ const failLoadingSubmission = submissionID => ({ }); -const updateSubmission = (submissionID, data) => ({ +export const updateSubmission = (submissionID, data) => ({ type: UPDATE_SUBMISSION, submissionID, data, diff --git a/opentech/static_src/src/app/src/redux/reducers/index.js b/opentech/static_src/src/app/src/redux/reducers/index.js index d1e5e237467ee34a6e305045c469b99560e3e92b..f7e4cfa44051f3fffa375327372c6fb087a8d119 100644 --- a/opentech/static_src/src/app/src/redux/reducers/index.js +++ b/opentech/static_src/src/app/src/redux/reducers/index.js @@ -2,8 +2,10 @@ import { combineReducers } from 'redux' import submissions from '@reducers/submissions'; import rounds from '@reducers/rounds'; +import notes from '@reducers/notes'; export default combineReducers({ + notes, submissions, rounds, }); diff --git a/opentech/static_src/src/app/src/redux/reducers/notes.js b/opentech/static_src/src/app/src/redux/reducers/notes.js new file mode 100644 index 0000000000000000000000000000000000000000..4137c6103999494d66ad54784cbb75397b974a0e --- /dev/null +++ b/opentech/static_src/src/app/src/redux/reducers/notes.js @@ -0,0 +1,68 @@ +import { combineReducers } from 'redux'; + +import { + UPDATE_NOTE, + UPDATE_NOTES, + START_FETCHING_NOTES, + FAIL_FETCHING_NOTES, +} from '@actions/notes'; + +function notesFetching(state = false, action) { + switch (action.type) { + case START_FETCHING_NOTES: + return true; + case UPDATE_NOTES: + case FAIL_FETCHING_NOTES: + return false; + default: + return state; + } +} + +function notesErrored(state = false, action) { + switch (action.type) { + case UPDATE_NOTES: + case START_FETCHING_NOTES: + return false; + case FAIL_FETCHING_NOTES: + return true; + default: + return state; + } +} + +function note(state, action) { + switch (action.type) { + case UPDATE_NOTE: + return { + ...state, + ...action.data, + }; + default: + return state; + } +} + +function notesByID(state = {}, action) { + switch(action.type) { + case UPDATE_NOTES: + return { + ...state, + ...action.data.results.reduce((newNotesAccumulator, newNote) => { + newNotesAccumulator[newNote.id] = note(state[newNote.id], { + type: UPDATE_NOTE, + data: newNote, + }); + return newNotesAccumulator; + }, {}), + }; + default: + return state; + } +} + +export default combineReducers({ + byID: notesByID, + isFetching: notesFetching, + isErrored: notesErrored, +}); diff --git a/opentech/static_src/src/app/src/redux/selectors/notes.js b/opentech/static_src/src/app/src/redux/selectors/notes.js new file mode 100644 index 0000000000000000000000000000000000000000..cf5ccc2fd82a54c81ba08884242409ac6b565117 --- /dev/null +++ b/opentech/static_src/src/app/src/redux/selectors/notes.js @@ -0,0 +1,18 @@ +import { createSelector } from 'reselect'; + +import { getSubmissionOfID } from '@selectors/submissions'; + +const getNotes = state => state.notes.byID; + +export const getNoteOfID = (noteID) => createSelector( + [getNotes], notes => notes[noteID] +); + +export const getNotesFetchState = state => state.notes.isFetching === true; + +export const getNotesErrorState = state => state.notes.isErrored === true; + +export const getNoteIDsForSubmissionOfID = submissionID => createSelector( + [getSubmissionOfID(submissionID)], + submission => (submission || {}).comments || [] +); diff --git a/opentech/static_src/src/app/src/redux/selectors/submissions.js b/opentech/static_src/src/app/src/redux/selectors/submissions.js index 09124b6896da42c2c177f2e706a8b2528deaca54..d012fcb6ae2f7dcd593cf75fe381fc98a6d6ced1 100644 --- a/opentech/static_src/src/app/src/redux/selectors/submissions.js +++ b/opentech/static_src/src/app/src/redux/selectors/submissions.js @@ -32,6 +32,10 @@ const getCurrentSubmission = createSelector( } ); +const getSubmissionOfID = (submissionID) => createSelector( + [getSubmissions], submissions => submissions[submissionID] +); + const getSubmissionLoadingState = state => state.submissions.itemLoading === true; const getSubmissionErrorState = state => state.submissions.itemLoadingError === true; @@ -50,4 +54,5 @@ export { getSubmissionsByRoundLoadingState, getSubmissionLoadingState, getSubmissionErrorState, + getSubmissionOfID, }; diff --git a/opentech/static_src/src/app/webpack.base.config.js b/opentech/static_src/src/app/webpack.base.config.js index b6017a03190fc4870e014265d069f79bdd48d734..b30d026d86c698c658126352f7bc9fd8c41fcca8 100644 --- a/opentech/static_src/src/app/webpack.base.config.js +++ b/opentech/static_src/src/app/webpack.base.config.js @@ -3,7 +3,7 @@ var path = require('path'); module.exports = { context: __dirname, - entry: ['@babel/polyfill', './src/index'], + entry: ['@babel/polyfill', './src/datetime', './src/index'], output: { filename: '[name]-[hash].js' diff --git a/package-lock.json b/package-lock.json index c03d14147002a5f8545753a762f00216e4b07807..72ffbc0221f795b987f45498096eaad4293faa0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4272,7 +4272,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -4290,11 +4291,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4307,15 +4310,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4418,7 +4424,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -4428,6 +4435,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4440,17 +4448,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4467,6 +4478,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -4539,7 +4551,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4549,6 +4562,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -4624,7 +4638,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -4654,6 +4669,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4671,6 +4687,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4709,11 +4726,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "optional": true } } }, @@ -7133,6 +7152,19 @@ } } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "moment-timezone": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.23.tgz", + "integrity": "sha512-WHFH85DkCfiNMDX5D3X7hpNH3/PUhjTGcD0U1SgfBGZxJ3qUmJh5FdvaFjcClxOvB3rzdfj4oRffbI38jEnC1w==", + "requires": { + "moment": ">= 2.9.0" + } + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", diff --git a/package.json b/package.json index 7ad0fcfa4597981cdb4b18bee2f73c21963125a2..cbac85369d9acd98c168bde41defadb953004125 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "gulp-size": "^3.0.0", "gulp-touch-cmd": "0.0.1", "gulp-uglify": "^3.0.1", + "moment": "^2.24.0", + "moment-timezone": "^0.5.23", "node-sass-import-once": "^1.2.0", "prop-types": "^15.6.2", "react": "^16.7.0",