From 276296f94bd01fbd663d23cf6b6c728e904c1c09 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Mon, 3 Dec 2018 14:08:37 +0000 Subject: [PATCH 01/17] Remove play from timeline --- src/js/map/map.js | 26 +++++++++++------- src/js/timeline/timeline.js | 53 +------------------------------------ src/store/initial.js | 2 +- 3 files changed, 18 insertions(+), 63 deletions(-) diff --git a/src/js/map/map.js b/src/js/map/map.js index 8d84554a..4f5727aa 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -342,6 +342,13 @@ Stop and start the development process in terminal after you have added your tok * Adds eventlayer to map */ + function getNarrativeStyle(narrativeId) { + const styleName = narrativeId && narrativeId in narrativeProps + ? narrativeId + : 'default'; + return narrativeProps[styleName]; + } + function renderNarratives() { const narrativesDom = g.selectAll('.narrative') .data(domain.narratives.map(d => d.steps)) @@ -356,20 +363,19 @@ Stop and start the development process in terminal after you have added your tok .attr('class', 'narrative') .attr('d', sequenceLine) .style('stroke-width', d => { - styleName = d[0].narrative && d[0].narrative in narrativeProps - ? d[0].narrative - : 'default' - const n = d[0].narrative; - return (n) ? narrativeProps[styleName].strokeWidth : 3; + // Note: [0] is a non-elegant way to get the narrative id out of the first + // event in the narrative sequence + const styleProps = getNarrativeStyle(d[0].narrative); + return styleProps.strokeWidth; }) .style('stroke-dasharray', d => { - const n = d[0].narrative; - if (narrativeProps[styleName].style === 'dotted') return "2px 5px"; - return 'none'; + const styleProps = getNarrativeStyle(d[0].narrative); + console.log(styleProps) + return (styleProps.style === 'dotted') ? "2px 5px" : 'none'; }) .style('stroke', d => { - const n = d[0].narrative; - return (n) ? narrativeProps[styleName].stroke : '#fff'; + const styleProps = getNarrativeStyle(d[0].narrative); + return styleProps.stroke; }) .style('fill', 'none'); } diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js index 27e01dcb..ae5f2459 100644 --- a/src/js/timeline/timeline.js +++ b/src/js/timeline/timeline.js @@ -25,7 +25,7 @@ export default function(app, ui, methods) { let transitionDuration = 500; // Dimension of the client - const WIDTH_CONTROLS = 180; + const WIDTH_CONTROLS = 100; const boundingClient = d3.select(`#${ui.dom.timeline}`).node().getBoundingClientRect(); let WIDTH = boundingClient.width - WIDTH_CONTROLS; const HEIGHT = 140; @@ -121,16 +121,6 @@ export default function(app, ui, methods) { dom.backwards.append('circle'); dom.backwards.append('path'); - dom.playGroup = dom.controls.append('g'); - dom.playGroup.append('circle'); - - dom.play = dom.playGroup.append('g'); - dom.play.append('path'); - - dom.pause = dom.playGroup.append('g').style('opacity', 0); - dom.pause.append('rect'); - dom.pause.append('rect'); - dom.zooms = dom.controls.append('g'); dom.zooms.selectAll('.zoom-level-button') @@ -219,28 +209,6 @@ export default function(app, ui, methods) { } addResizeListener(); - /** - * PLAY FUNCTIONALITY - */ - function stopBrushTransition() { - clearInterval(window.playInterval); - isPlaying = false; - dom.play.style('opacity', 1); - dom.pause.style('opacity', 0); - } - - /** - * START PLAY SERIES OF TRANSITIONS - */ - function playBrushTransition() { - isPlaying = true; - dom.play.style('opacity', 0); - dom.pause.style('opacity', 1); - window.playInterval = setInterval(() => { - moveTime('forward'); - }, playDuration); - } - /** * Return which color event circle should be based on incident type * @param {object} eventPoint data object @@ -510,20 +478,6 @@ export default function(app, ui, methods) { .attr('d', d3.symbol().type(d3.symbolTriangle).size(80)) .attr('transform', `translate(${scale.x.range()[1] - 20}, 62)rotate(90)`); - // These controls on separate svg - dom.playGroup.select('circle') - .attr('transform', 'translate(135, 60)rotate(90)') - .attr('r', 25); - - dom.play.select('path') - .attr('d', d3.symbol().type(d3.symbolTriangle).size(260)) - .attr('transform', 'translate(135, 60)rotate(90)'); - - dom.pause.selectAll('rect') - .attr('transform', (d, i) => `translate(${125 + (i * 15)}, 47)`) - .attr('height', 25) - .attr('width', 5); - dom.zooms.selectAll('text') .text(d => d.label) .attr('x', 60) @@ -536,11 +490,6 @@ export default function(app, ui, methods) { dom.backwards .on('click', () => moveTime('backwards')); - dom.playGroup - .on('click', () => { - return (isPlaying) ? stopBrushTransition() : playBrushTransition(); - }); - dom.zooms.selectAll('text') .on('click', zoom => applyZoom(zoom)); } diff --git a/src/store/initial.js b/src/store/initial.js index b675188a..abdf16c7 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -127,7 +127,7 @@ const initial = { narrative_1: { style: 'solid', // ['dotted', 'solid'] opacity: 0.4, // range between 0 and 1 - stroke: 'red', // Any hex or rgb code + stroke: 'yellow', // Any hex or rgb code strokeWidth: 2 } } From d9e0ca9f59cd26642c95c50a349f912c9c80da7b Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Mon, 3 Dec 2018 16:40:27 +0000 Subject: [PATCH 02/17] Add narrative schemas --- .gitignore | 1 + src/actions/index.js | 13 ++++++++++--- src/components/Dashboard.jsx | 2 +- src/components/Notification.jsx | 4 ++-- src/js/map/map.js | 1 - src/reducers/schema/narrativeSchema.js | 9 +++++++++ src/reducers/utils/validators.js | 10 +++++++++- src/selectors/index.js | 7 ++++--- 8 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 src/reducers/schema/narrativeSchema.js diff --git a/.gitignore b/.gitignore index 12b0846d..4ce2e5c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ build/ node_modules/ config.js +dev.config.js diff --git a/src/actions/index.js b/src/actions/index.js index 77ead1bc..e065b255 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -52,6 +52,10 @@ export function fetchDomain () { .then(response => response.json()) .catch(handleError('categories')) + const narPromise = fetch(NARRATIVE_URL) + .then(response => response.json()) + .catch(handleError('narratives')) + let sitesPromise = Promise.resolve([]) if (process.env.features.USE_SITES) { sitesPromise = fetch(SITES_URL) @@ -66,14 +70,16 @@ export function fetchDomain () { .catch(handleError('tags')) } - return Promise.all([ eventPromise, catPromise, sitesPromise, tagsPromise]) + return Promise.all([eventPromise, catPromise, narPromise, + sitesPromise, tagsPromise]) .then(response => { dispatch(toggleFetchingDomain()) const result = { events: response[0], categories: response[1], - sites: response[2], - tags: response[3], + narratives: response[2], + sites: response[3], + tags: response[4], notifications } return result @@ -102,6 +108,7 @@ export function updateDomain(domain) { categories: domain.categories, tags: domain.tags, sites: domain.sites, + narratives: domain.narratives, notifications: domain.notifications } } diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx index 783fd788..1e758f24 100644 --- a/src/components/Dashboard.jsx +++ b/src/components/Dashboard.jsx @@ -67,7 +67,7 @@ class Dashboard extends React.Component { } getNarrativeLinks(event) { - const narrative = this.props.domain.narratives.find(nv => nv.key === event.narrative); + const narrative = this.props.domain.narratives.find(nv => nv.id === event.narrative); if (narrative) return narrative.byId[event.id]; return null; } diff --git a/src/components/Notification.jsx b/src/components/Notification.jsx index 260befa2..1bee1b94 100644 --- a/src/components/Notification.jsx +++ b/src/components/Notification.jsx @@ -48,7 +48,7 @@ export default class Notification extends React.Component{ return (
{this.props.notifications.map((notification) => { - +console.log(notification) return (
this.toggleDetails() }> - - + this.toggleMapViews(view)} + isEnabled={this.props.viewFilters.routes} + /> + this.toggleMapViews(view)} + isEnabled={this.props.viewFilters.sites} + /> + this.toggleMapViews(view)} + isEnabled={this.props.viewFilters.coevents} + />
); } @@ -102,11 +77,7 @@ class Toolbar extends React.Component { i
@@ -124,11 +95,10 @@ class Toolbar extends React.Component { renderToolbarTab(tabNum, key) { const isActive = (tabNum === this.state.tab); - //let caption_lang = copy[this.props.language].toolbar.tabs[tabNum]; + let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab'; return (
{ this.toggleTab(tabNum); }}> - {/**/}
{key}
); @@ -145,20 +115,6 @@ class Toolbar extends React.Component { return ''; } - renderToolbarTabs() { - const title = copy[this.props.language].toolbar.title; - return ( -
-

{title}

-
- {/*this.renderToolbarTab(0, 'search')*/} - {this.renderToolbarTagRoot()} -
- {/* {this.renderBottomActions()} */} -
- ) - } - renderTagListPanel(tagType) { const panels_lang = copy[this.props.language].toolbar.panels; const title = (panels_lang[tagType]) ? panels_lang[tagType].title : tagType; @@ -211,6 +167,39 @@ class Toolbar extends React.Component { return ''; } + renderToolbarNavs() { + if (this.props.narratives) { + return this.props.narratives.map((nar, idx) => { + const isActive = (idx === this.state.tab); + + let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab'; + + return ( +
{ this.toggleTab(idx); }}> +
{nar.label}
+
+ ); + }) + } + return ''; + } + + renderToolbarTabs() { + const title = copy[this.props.language].toolbar.title; + return ( +
+

{title}

+
+ {/*this.renderToolbarTab(0, 'search')*/} + {(this.props.isModeGuided) + ? this.renderToolbarNavs() + : this.renderToolbarTagRoot()} +
+ {/* {this.renderBottomActions()} */} +
+ ) + } + render() { let classes = (this.state.tab !== -1) ? 'toolbar-panels' : 'toolbar-panels folded'; @@ -237,7 +226,8 @@ function mapStateToProps(state) { tagFilters: selectors.selectTagList(state), categoryFilter: state.app.filters.categories, viewFilters: state.app.filters.views, - features: state.app.features + features: state.app.features, + isModeGuided: state.app.isModeGuided } } diff --git a/src/components/presentational/Icons/CoeventIcon.js b/src/components/presentational/Icons/CoeventIcon.js new file mode 100644 index 00000000..ffa5db72 --- /dev/null +++ b/src/components/presentational/Icons/CoeventIcon.js @@ -0,0 +1,24 @@ +import React from 'react'; + +const CoeventIcon = ({ isEnabled, toggleMapViews }) => { + + const classes = (isEnabled) ? 'action-button active disabled' : 'action-button disabled'; + + return ( + + ); +} + +export default CoeventIcon; diff --git a/src/components/presentational/Icons/RefreshIcon.js b/src/components/presentational/Icons/RefreshIcon.js new file mode 100644 index 00000000..1e1eb03f --- /dev/null +++ b/src/components/presentational/Icons/RefreshIcon.js @@ -0,0 +1,14 @@ +import React from 'react'; + +const RefreshIcon = ({ }) => { + + return ( + + + + + ); +} + +export default RefreshIcon; diff --git a/src/components/presentational/Icons/RouteIcon.js b/src/components/presentational/Icons/RouteIcon.js new file mode 100644 index 00000000..4febda96 --- /dev/null +++ b/src/components/presentational/Icons/RouteIcon.js @@ -0,0 +1,20 @@ +import React from 'react'; + +const RouteIcon = ({ isEnabled, toggleMapViews }) => { + + const classes = (isEnabled) ? 'action-button active disabled' : 'action-button disabled'; + + return ( + + ); +} + +export default RouteIcon; diff --git a/src/components/presentational/Icons/SitesIcon.js b/src/components/presentational/Icons/SitesIcon.js new file mode 100644 index 00000000..a4461eb3 --- /dev/null +++ b/src/components/presentational/Icons/SitesIcon.js @@ -0,0 +1,19 @@ +import React from 'react'; + +const SitesIcon = ({ isEnabled, toggleMapViews }) => { + + const classes = (isEnabled) ? 'action-button active disabled' : 'action-button disabled'; + + return ( + + ); +} + +export default SitesIcon; diff --git a/src/js/map/map.js b/src/js/map/map.js index a40fa906..6133b980 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -291,7 +291,7 @@ Stop and start the development process in terminal after you have added your tok eventsDom .enter().append('circle') .attr('class', 'location-event-marker') - .style('fill', (d, i) => getCategoryColor(domain.categories[i])) + .style('fill', (d, i) => getCategoryColor(domain.categories[i].category)) .transition() .duration(500) .attr('r', d => (d) ? Math.sqrt(16 * d) + 3 : 0); diff --git a/src/reducers/app.js b/src/reducers/app.js index d9bd94a0..85459f26 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -7,6 +7,7 @@ import { UPDATE_TIMERANGE, RESET_ALLFILTERS, TOGGLE_LANGUAGE, + TOGGLE_MAPVIEW, FETCH_ERROR, } from '../actions'; @@ -74,6 +75,18 @@ function toggleLanguage(appState, action) { }); } +function toggleMapView(appState, action) { + const isLayerInView = !appState.views[layer]; + const newViews = {}; + newViews[layer] = isLayerInView; + const views = Object.assign({}, appState.views, newViews); + return Object.assign({}, appState, { + filters: Object.assign({}, appState.filters, { + views + }) + }); +} + function fetchError(state, action) { return { ...state, @@ -97,6 +110,8 @@ function app(appState = initial.app, action) { return resetAllFilters(appState, action); case TOGGLE_LANGUAGE: return toggleLanguage(appState, action); + case TOGGLE_MAPVIEW: + return toggleMapView(appState, action); case FETCH_ERROR: return fetchError(appState, action); default: diff --git a/src/selectors/index.js b/src/selectors/index.js index b502d7c6..40336e86 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -96,7 +96,7 @@ export const selectNarratives = createSelector( if (isTimeRanged && isTagged && isInNarrative) { if (!narratives[evt.narrative]) { - narratives[evt.narrative] = { key: evt.narrative, steps: [], byId: {} }; + narratives[evt.narrative] = { id: evt.narrative, steps: [], byId: {} }; } narratives[evt.narrative].steps.push(evt); narratives[evt.narrative].byId[evt.id] = { next: null, prev: null }; @@ -105,15 +105,19 @@ export const selectNarratives = createSelector( Object.keys(narratives).forEach((key) => { const steps = narratives[key].steps; + steps.sort((a, b) => { return (parseTimestamp(a.timestamp) > parseTimestamp(b.timestamp)); }); + steps.forEach((step, i) => { narratives[key].byId[step.id].next = (i < steps.length - 2) ? steps[i + 1] : null; narratives[key].byId[step.id].prev = (i > 0) ? steps[i - 1] : null; }); + + narratives[key] = Object.assign(narrativeMetadata.find(n => n.id === key), narratives[key]); }); -console.log(narrativeMetadata, narratives) + return Object.values(narratives); }); @@ -152,7 +156,7 @@ export const selectLocations = createSelector( export const selectCategories = createSelector( [getCategories], (categories) => { - return categories.map(v => v.category); + return Object.values(categories); } ); diff --git a/src/store/initial.js b/src/store/initial.js index abdf16c7..333100f9 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -44,6 +44,7 @@ const initial = { }, base_uri: 'http://127.0.0.1:8000/', // Modify accordingly on production setup. isMobile: (/Mobi/.test(navigator.userAgent)), + isModeGuided: true, language: 'en-US', mapAnchor: process.env.MAP_ANCHOR, zoomLevels: [{ diff --git a/webpack.config.js b/webpack.config.js index 4706f036..ba1c5381 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,7 +1,7 @@ const webpack = require('webpack'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); -const userConfig = require('./config'); +const userConfig = require('./dev.config'); const userConfigJSON = {}; const devMode = process.env.NODE_ENV !== 'production'; From a53285dc3dda206ba70be6be2a842d68beb5ec01 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Tue, 4 Dec 2018 10:35:56 +0000 Subject: [PATCH 05/17] Display category labels --- src/js/map/map.js | 8 ++++---- src/js/timeline/timeline.js | 2 +- src/selectors/index.js | 19 +------------------ 3 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/js/map/map.js b/src/js/map/map.js index 6133b980..f9b3c964 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -227,8 +227,8 @@ Stop and start the development process in terminal after you have added your tok // categories.sort((a, b) => { // return (+a.slice(-2) > +b.slice(-2)); // }); - categories.forEach(group => { - eventCount[group] = 0 + categories.forEach(cat => { + eventCount[cat.category] = 0 }); location.events.forEach((event) => {; @@ -239,9 +239,9 @@ Stop and start the development process in terminal after you have added your tok const events = []; while (i < categories.length) { - let _eventsCount = eventCount[categories[i]]; + let _eventsCount = eventCount[categories[i].category]; for (let j = i + 1; j < categories.length; j++) { - _eventsCount += eventCount[categories[j]]; + _eventsCount += eventCount[categories[j].category]; } events.push(_eventsCount); i++; diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js index ae5f2459..d8fb7925 100644 --- a/src/js/timeline/timeline.js +++ b/src/js/timeline/timeline.js @@ -513,7 +513,7 @@ export default function(app, ui, methods) { axis.y = d3.axisLeft(scale.y) - .tickValues(categories); + .tickValues(categories.map(c => c.category)); } function update(domain, app) { diff --git a/src/selectors/index.js b/src/selectors/index.js index 40336e86..44809ccb 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -155,26 +155,9 @@ export const selectLocations = createSelector( */ export const selectCategories = createSelector( [getCategories], - (categories) => { - return Object.values(categories); - } + (categories) => categories ); -/** - * Return categories by group - */ -export const selectCategoryGroups = createSelector( - [selectCategories], - (categories) => { - const groups = {}; - categories.forEach((cat) => { - if (cat.group && !groups[cat.group]) { - groups[cat.group] = cat.group_label; - } - }); - return Object.keys(groups).concat(['other']); - } -); /** * Given a tree of tags, return those tags as a list From 5f3aaa98c29945f8ee5e31e38013253714c9d538 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Tue, 4 Dec 2018 16:12:51 +0000 Subject: [PATCH 06/17] Separate tabs and narratives on tab panels --- src/actions/index.js | 7 +++ src/components/TagListPanel.jsx | 6 +- src/components/Toolbar.jsx | 103 +++++++++++++++++++++----------- src/reducers/app.js | 9 +++ 4 files changed, 87 insertions(+), 38 deletions(-) diff --git a/src/actions/index.js b/src/actions/index.js index 10b3c790..6c326cdf 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -247,3 +247,10 @@ export function toggleMapView(layer) { layer } } + +export const TOGGLE_GUIDEDMODE = 'TOGGLE_GUIDEDMODE'; +export function toggleGuidedMode() { + return { + type: TOGGLE_GUIDEDMODE + } +} diff --git a/src/components/TagListPanel.jsx b/src/components/TagListPanel.jsx index b8345458..252f3a76 100644 --- a/src/components/TagListPanel.jsx +++ b/src/components/TagListPanel.jsx @@ -13,11 +13,11 @@ class TagListPanel extends React.Component { } componentDidMount() { - this.computeTree(this.props.tags.children[this.props.tagType]); + this.computeTree(this.props.tags);//.children[this.props.tagType]); } componentWillReceiveProps(nextProps) { - this.computeTree(nextProps.tags.children[nextProps.tagType]); + this.computeTree(nextProps.tags);//.children[nextProps.tagType]); } onClickCheckbox(tag) { @@ -67,6 +67,8 @@ class TagListPanel extends React.Component { render() { return (
+

Explore data by tag

+

Explore freely all the data by selecting tags.

{this.renderTree()}
); diff --git a/src/components/Toolbar.jsx b/src/components/Toolbar.jsx index 0b3fab09..83708fbb 100644 --- a/src/components/Toolbar.jsx +++ b/src/components/Toolbar.jsx @@ -18,16 +18,12 @@ class Toolbar extends React.Component { super(props); this.state = { - tab: -1 + tabNum: -1 }; } - toggleTab(tabIndex) { - if ( this.state.tab === tabIndex ) { - this.setState({ tab: -1 }); - } else { - this.setState({ tab: tabIndex }); - } + toggleTab(tabNum) { + this.setState({ tabNum: (this.state.tabNum === tabNum) ? -1 : tabNum }); } resetAllFilters() { @@ -46,6 +42,10 @@ class Toolbar extends React.Component { this.props.actions.toggleMapView(layer); } + toggleGuidedMode() { + this.props.actions.toggleGuidedMode(); + } + renderMapActions() { return (
@@ -68,18 +68,19 @@ class Toolbar extends React.Component { renderBottomActions() { return (
- {this.renderMapActions()} + + {/*}{this.renderMapActions()}
- - -
+
*/}
); } @@ -135,6 +136,25 @@ class Toolbar extends React.Component { ); } + renderToolbarNarrativePanel() { + return ( + +

Focus stories

+

Here are some highlighted stories

+ {this.props.narratives.map((narr) => { + return ( +
+ +
+ ) + })} +
+ ); + } + renderSearch() { if (this.props.features.USE_SEARCH) { return ( @@ -167,21 +187,26 @@ class Toolbar extends React.Component { return ''; } - renderToolbarNavs() { - if (this.props.narratives) { - return this.props.narratives.map((nar, idx) => { - const isActive = (idx === this.state.tab); + renderToolbarNarratives() { + const isActive = (this.state.tabNum === 0); + let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab'; - let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab'; + return ( +
{ this.toggleTab(0); }}> +
Focus stories
+
+ ); + } - return ( -
{ this.toggleTab(idx); }}> -
{nar.label}
-
- ); - }) - } - return ''; + renderToolbarTags() { + const isActive = (this.state.tabNum === 1); + let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab'; + + return ( +
{ this.toggleTab(1); }}> +
Explore freely
+
+ ); } renderToolbarTabs() { @@ -191,27 +216,33 @@ class Toolbar extends React.Component {

{title}

{/*this.renderToolbarTab(0, 'search')*/} - {(this.props.isModeGuided) - ? this.renderToolbarNavs() - : this.renderToolbarTagRoot()} + {this.renderToolbarNarratives()} + {this.renderToolbarTags()}
- {/* {this.renderBottomActions()} */} + {this.renderBottomActions()} ) } - render() { - let classes = (this.state.tab !== -1) ? 'toolbar-panels' : 'toolbar-panels folded'; + renderToolbarPanels() { + let classes = (this.state.tabNum !== -1) ? 'toolbar-panels' : 'toolbar-panels folded'; + return ( +
+ {this.renderPanelHeader()} + + {this.renderToolbarNarrativePanel()} + {this.renderToolbarTagList()} + +
+ ) + } + + render() { return (
{this.renderToolbarTabs()} -
- {this.renderPanelHeader()} - - {this.renderToolbarTagList()} - -
+ {this.renderToolbarPanels()}
); } diff --git a/src/reducers/app.js b/src/reducers/app.js index 85459f26..66fa2916 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -8,6 +8,7 @@ import { RESET_ALLFILTERS, TOGGLE_LANGUAGE, TOGGLE_MAPVIEW, + TOGGLE_GUIDEDMODE, FETCH_ERROR, } from '../actions'; @@ -87,6 +88,12 @@ function toggleMapView(appState, action) { }); } +function toggleGuidedMode(appState, action) { + return Object.assign({}, appState, { + isModeGuided: !appState.isModeGuided + }) +} + function fetchError(state, action) { return { ...state, @@ -112,6 +119,8 @@ function app(appState = initial.app, action) { return toggleLanguage(appState, action); case TOGGLE_MAPVIEW: return toggleMapView(appState, action); + case TOGGLE_GUIDEDMODE: + return toggleGuidedMode(appState, action); case FETCH_ERROR: return fetchError(appState, action); default: From cf9ac653fe3661d99161295614a8640e7a1694c7 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Tue, 4 Dec 2018 16:52:36 +0000 Subject: [PATCH 07/17] Put bottom actions in its own component --- src/components/TagListPanel.jsx | 1 + src/components/Toolbar.jsx | 205 ++++++------------------ src/components/ToolbarBottomActions.jsx | 69 ++++++++ src/scss/toolbar.scss | 6 +- 4 files changed, 121 insertions(+), 160 deletions(-) create mode 100644 src/components/ToolbarBottomActions.jsx diff --git a/src/components/TagListPanel.jsx b/src/components/TagListPanel.jsx index 252f3a76..fa6667b2 100644 --- a/src/components/TagListPanel.jsx +++ b/src/components/TagListPanel.jsx @@ -65,6 +65,7 @@ class TagListPanel extends React.Component { } render() { + return (

Explore data by tag

diff --git a/src/components/Toolbar.jsx b/src/components/Toolbar.jsx index 83708fbb..eca3eab5 100644 --- a/src/components/Toolbar.jsx +++ b/src/components/Toolbar.jsx @@ -5,88 +5,24 @@ import * as selectors from '../selectors' import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import Search from './Search.jsx'; import TagListPanel from './TagListPanel.jsx'; -import SitesIcon from './presentational/Icons/SitesIcon.js'; -import RefreshIcon from './presentational/Icons/RefreshIcon.js'; -import CoeventIcon from './presentational/Icons/CoeventIcon.js'; -import RouteIcon from './presentational/Icons/RouteIcon.js'; +import ToolbarBottomActions from './ToolbarBottomActions.jsx'; import copy from '../js/data/copy.json'; -// NB: i think this entire component can actually be part of a future feature... class Toolbar extends React.Component { - constructor(props) { - super(props); + constructor(props) { + super(props); - this.state = { - tabNum: -1 - }; - } - - toggleTab(tabNum) { - this.setState({ tabNum: (this.state.tabNum === tabNum) ? -1 : tabNum }); - } - - resetAllFilters() { - this.props.actions.resetAllFilters(); - } - - toggleInfoPopup() { - this.props.actions.toggleInfoPopup(); - } - - toggleLanguage() { - this.props.actions.toggleLanguage(); - } - - toggleMapViews(layer) { - this.props.actions.toggleMapView(layer); - } - - toggleGuidedMode() { - this.props.actions.toggleGuidedMode(); - } - - renderMapActions() { - return ( -
- this.toggleMapViews(view)} - isEnabled={this.props.viewFilters.routes} - /> - this.toggleMapViews(view)} - isEnabled={this.props.viewFilters.sites} - /> - this.toggleMapViews(view)} - isEnabled={this.props.viewFilters.coevents} - /> -
- ); - } - - renderBottomActions() { - return ( -
- - {/*}{this.renderMapActions()} -
- - - -
*/} -
- ); - } + this.state = { + tabNum: -1 + }; + } + toggleTab(tabNum) { + this.setState({ tabNum: (this.state.tabNum === tabNum) ? -1 : tabNum }); + } - renderPanelHeader() { + renderClosePanel() { return (
this.toggleTab(-1)}>
@@ -94,46 +30,21 @@ class Toolbar extends React.Component { ); } - renderToolbarTab(tabNum, key) { - const isActive = (tabNum === this.state.tab); - - let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab'; - return ( -
{ this.toggleTab(tabNum); }}> -
{key}
-
- ); - } - - renderToolbarTagRoot() { - if (this.props.features.USE_TAGS && - this.props.tags.children) { - const roots = Object.values(this.props.tags.children); - return roots.map((root, idx) => { - return this.renderToolbarTab(idx, root.key); - }) - } - return ''; - } - - renderTagListPanel(tagType) { - const panels_lang = copy[this.props.language].toolbar.panels; - const title = (panels_lang[tagType]) ? panels_lang[tagType].title : tagType; - const overview = (panels_lang[tagType]) ? panels_lang[tagType].overview : ''; - + renderSearch() { + if (this.props.features.USE_SEARCH) { return ( - - ); + + + + ) + } } renderToolbarNarrativePanel() { @@ -155,56 +66,32 @@ class Toolbar extends React.Component { ); } - renderSearch() { - if (this.props.features.USE_SEARCH) { + renderToolbarTagPanel() { + if (this.props.features.USE_TAGS && + this.props.tags.children) { return ( - + ) } - } - - renderToolbarTagList() { - if (this.props.features.USE_TAGS && - this.props.tags.children) { - const roots = Object.values(this.props.tags.children); - return roots.map((root, idx) => { - return ( - - {this.renderTagListPanel(root.key)} - - ) - }) - } return ''; } - renderToolbarNarratives() { - const isActive = (this.state.tabNum === 0); - let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab'; - - return ( -
{ this.toggleTab(0); }}> -
Focus stories
-
- ); - } - - renderToolbarTags() { - const isActive = (this.state.tabNum === 1); + renderToolbarTab(tabNum, label) { + const isActive = (this.state.tabNum === tabNum); let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab'; return ( -
{ this.toggleTab(1); }}> -
Explore freely
+
{ this.toggleTab(tabNum); }}> +
{label}
); } @@ -216,10 +103,12 @@ class Toolbar extends React.Component {

{title}

{/*this.renderToolbarTab(0, 'search')*/} - {this.renderToolbarNarratives()} - {this.renderToolbarTags()} + {this.renderToolbarTab(0, 'Focus stories')} + {this.renderToolbarTab(1, 'Explore freely')}
- {this.renderBottomActions()} +
) } @@ -229,10 +118,10 @@ class Toolbar extends React.Component { return (
- {this.renderPanelHeader()} + {this.renderClosePanel()} {this.renderToolbarNarrativePanel()} - {this.renderToolbarTagList()} + {this.renderToolbarTagPanel()}
) diff --git a/src/components/ToolbarBottomActions.jsx b/src/components/ToolbarBottomActions.jsx new file mode 100644 index 00000000..6b58fcac --- /dev/null +++ b/src/components/ToolbarBottomActions.jsx @@ -0,0 +1,69 @@ +import React from 'react'; + +import SitesIcon from './presentational/Icons/SitesIcon.js'; +import RefreshIcon from './presentational/Icons/RefreshIcon.js'; +import CoeventIcon from './presentational/Icons/CoeventIcon.js'; +import RouteIcon from './presentational/Icons/RouteIcon.js'; + +class ToolbarBottomActions extends React.Component { + resetAllFilters() { + this.props.actions.resetAllFilters(); + } + + toggleInfoPopup() { + this.props.actions.toggleInfoPopup(); + } + + toggleLanguage() { + this.props.actions.toggleLanguage(); + } + + toggleMapViews(layer) { + this.props.actions.toggleMapView(layer); + } + + toggleGuidedMode() { + this.props.actions.toggleGuidedMode(); + } + + renderMapActions() { + return ( +
+ this.toggleMapViews(view)} + isEnabled={this.props.viewFilters.routes} + /> + this.toggleMapViews(view)} + isEnabled={this.props.viewFilters.sites} + /> + this.toggleMapViews(view)} + isEnabled={this.props.viewFilters.coevents} + /> +
+ ); + } + + render() { + return ( +
+ + {/*}{this.renderMapActions()} +
+ + + +
*/} +
+ ); + } +} + +export default ToolbarBottomActions; diff --git a/src/scss/toolbar.scss b/src/scss/toolbar.scss index f1590b38..9459f1d5 100644 --- a/src/scss/toolbar.scss +++ b/src/scss/toolbar.scss @@ -161,10 +161,12 @@ } .toolbar-tab { - display: inline-block; + display: flex; + align-items: center; + justify-content: center; height: 60px; width: 110px; - padding: 10px 0 5px 0; + padding: 5px 0 5px 0; font-weight: 400; text-overflow: ellipsis; overflow: hidden; From 901573eb808289484a326cec4fa41e5f1f3b52ee Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Wed, 5 Dec 2018 11:54:22 +0000 Subject: [PATCH 08/17] Create Narrative card --- src/actions/index.js | 8 ++++ src/components/Dashboard.jsx | 4 ++ src/components/NarrativeCard.js | 60 +++++++++++++++++++++++ src/components/Toolbar.jsx | 26 ++++++---- src/components/ToolbarBottomActions.jsx | 1 - src/reducers/app.js | 9 ++++ src/scss/main.scss | 1 + src/scss/narrativecard.scss | 63 +++++++++++++++++++++++++ src/store/initial.js | 1 + 9 files changed, 163 insertions(+), 10 deletions(-) create mode 100644 src/components/NarrativeCard.js create mode 100644 src/scss/narrativecard.scss diff --git a/src/actions/index.js b/src/actions/index.js index 6c326cdf..b406ae83 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -171,6 +171,14 @@ export function updateTimeRange(timerange) { } } +export const UPDATE_NARRATIVE = 'UPDATE_NARRATIVE'; +export function updateNarrative(narrative) { + return { + type: UPDATE_NARRATIVE, + narrative + } +} + export const RESET_ALLFILTERS = 'RESET_ALLFILTERS' export function resetAllFilters() { return { diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx index 091d969d..6fe812f8 100644 --- a/src/components/Dashboard.jsx +++ b/src/components/Dashboard.jsx @@ -9,6 +9,7 @@ import LoadingOverlay from './presentational/LoadingOverlay'; import Viewport from './Viewport.jsx'; import Toolbar from './Toolbar.jsx'; import CardStack from './CardStack.jsx'; +import NarrativeCard from './NarrativeCard.js'; import InfoPopUp from './InfoPopup.jsx'; import Timeline from './Timeline.jsx'; import Notification from './Notification.jsx'; @@ -104,6 +105,9 @@ class Dashboard extends React.Component { app={this.props.app} toggle={() => this.props.actions.toggleInfoPopup()} /> + 0) { + this.setState({ step: this.state.step - 1 }); + } + } + + goToNextKeyFrame() { + if (this.state.step < this.props.narrative.steps.length - 1) { + this.setState({ step: this.state.step + 1 }); + } + } + + componentDidUpdate() { + if (this.props.narrative !== null) { + const step = this.props.narrative.steps[this.state.step]; + this.props.onSelect([step.id]); + } + } + + render() { + if (this.props.narrative !== null) { + const steps = this.props.narrative.steps; + const step = steps[this.state.step]; + + return ( +
+
{this.props.narrative.label}
+

{this.props.narrative.description}

+

{this.state.step + 1}/{steps.length}. {step.location}

+
+
this.goToPrevKeyFrame()}>←
+
= this.props.narrative.steps.length - 1) ? 'disabled ' : ''} action`} onClick={() => this.goToNextKeyFrame()}>→
+
+
+ ); + } + return (
); + } +} + +function mapStateToProps(state) { + console.log(state) + return { + narrative: state.app.narrative + } +} +export default connect(mapStateToProps)(NarrativeCard); diff --git a/src/components/Toolbar.jsx b/src/components/Toolbar.jsx index eca3eab5..20a888cf 100644 --- a/src/components/Toolbar.jsx +++ b/src/components/Toolbar.jsx @@ -34,19 +34,27 @@ class Toolbar extends React.Component { if (this.props.features.USE_SEARCH) { return ( - + ) } } + goToNarrative(narrative) { + this.setState({ + tabNum: -1 + }, () => { + this.props.actions.updateNarrative(narrative); + }); + } + renderToolbarNarrativePanel() { return ( @@ -55,7 +63,7 @@ class Toolbar extends React.Component { {this.props.narratives.map((narr) => { return (
- diff --git a/src/components/ToolbarBottomActions.jsx b/src/components/ToolbarBottomActions.jsx index 6b58fcac..082365b9 100644 --- a/src/components/ToolbarBottomActions.jsx +++ b/src/components/ToolbarBottomActions.jsx @@ -48,7 +48,6 @@ class ToolbarBottomActions extends React.Component { render() { return (
- {/*}{this.renderMapActions()}
+ ) + } + render() { if (this.props.narrative !== null) { const steps = this.props.narrative.steps; @@ -37,6 +48,7 @@ class NarrativeCard extends React.Component { return (
+ {this.renderClose()}
{this.props.narrative.label}

{this.props.narrative.description}

{this.state.step + 1}/{steps.length}. {step.location}

From a387f025657951a3fcf06aeb497bc0dc9f4f067c Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Thu, 6 Dec 2018 12:23:02 +0000 Subject: [PATCH 10/17] Filter by timerange on selecting Narrative --- src/components/Card.jsx | 2 +- src/components/CardStack.jsx | 2 +- src/components/NarrativeCard.js | 1 - src/components/Viewport.jsx | 2 +- src/components/presentational/CardLocation.js | 2 +- .../presentational/CardTimestamp.js | 2 +- src/js/map/map.js | 5 +++- src/js/timeline/timeline.js | 2 +- src/js/{data => }/utilities.js | 14 +++++++++++ src/reducers/app.js | 24 ++++++++++++++++--- src/selectors/index.js | 10 ++++---- webpack.config.js | 2 +- 12 files changed, 52 insertions(+), 16 deletions(-) rename src/js/{data => }/utilities.js (76%) diff --git a/src/components/Card.jsx b/src/components/Card.jsx index d53bc310..00f06e53 100644 --- a/src/components/Card.jsx +++ b/src/components/Card.jsx @@ -1,5 +1,5 @@ import copy from '../js/data/copy.json'; -import {isNotNullNorUndefined} from '../js/data/utilities'; +import {isNotNullNorUndefined} from '../js/utilities'; import React from 'react'; import Spinner from './presentational/Spinner'; diff --git a/src/components/CardStack.jsx b/src/components/CardStack.jsx index f5bc36eb..99492ed2 100644 --- a/src/components/CardStack.jsx +++ b/src/components/CardStack.jsx @@ -6,7 +6,7 @@ import Card from './Card.jsx'; import copy from '../js/data/copy.json'; import { isNotNullNorUndefined -} from '../js/data/utilities.js'; +} from '../js/utilities.js'; class CardStack extends React.Component { diff --git a/src/components/NarrativeCard.js b/src/components/NarrativeCard.js index b017226f..bdc98627 100644 --- a/src/components/NarrativeCard.js +++ b/src/components/NarrativeCard.js @@ -64,7 +64,6 @@ class NarrativeCard extends React.Component { } function mapStateToProps(state) { - console.log(state) return { narrative: state.app.narrative } diff --git a/src/components/Viewport.jsx b/src/components/Viewport.jsx index 363a652c..3bcc8083 100644 --- a/src/components/Viewport.jsx +++ b/src/components/Viewport.jsx @@ -2,7 +2,7 @@ import React from 'react' import { connect } from 'react-redux' import * as selectors from '../selectors' import Map from '../js/map/map.js' -import { areEqual } from '../js/data/utilities.js' +import { areEqual } from '../js/utilities.js' class Viewport extends React.Component { constructor(props) { diff --git a/src/components/presentational/CardLocation.js b/src/components/presentational/CardLocation.js index 7dd26c0d..a798a5ea 100644 --- a/src/components/presentational/CardLocation.js +++ b/src/components/presentational/CardLocation.js @@ -1,7 +1,7 @@ import React from 'react'; import copy from '../../js/data/copy.json'; -import {isNotNullNorUndefined} from '../../js/data/utilities'; +import { isNotNullNorUndefined } from '../../js/utilities'; const CardLocation = ({ language, location }) => { diff --git a/src/components/presentational/CardTimestamp.js b/src/components/presentational/CardTimestamp.js index d645e827..4c4d5948 100644 --- a/src/components/presentational/CardTimestamp.js +++ b/src/components/presentational/CardTimestamp.js @@ -1,7 +1,7 @@ import React from 'react'; import copy from '../../js/data/copy.json'; -import {isNotNullNorUndefined} from '../../js/data/utilities'; +import { isNotNullNorUndefined } from '../../js/utilities'; const CardTimestamp = ({ makeTimelabel, language, timestamp }) => { diff --git a/src/js/map/map.js b/src/js/map/map.js index f9b3c964..3e30e937 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -1,7 +1,7 @@ import { areEqual, isNotNullNorUndefined -} from '../data/utilities'; +} from '../utilities'; import hash from 'object-hash'; import 'leaflet-polylinedecorator'; @@ -363,16 +363,19 @@ Stop and start the development process in terminal after you have added your tok .attr('class', 'narrative') .attr('d', sequenceLine) .style('stroke-width', d => { + if (!d[0]) return 0; // Note: [0] is a non-elegant way to get the narrative id out of the first // event in the narrative sequence const styleProps = getNarrativeStyle(d[0].narrative); return styleProps.strokeWidth; }) .style('stroke-dasharray', d => { + if (!d[0]) return 'none'; const styleProps = getNarrativeStyle(d[0].narrative); return (styleProps.style === 'dotted') ? "2px 5px" : 'none'; }) .style('stroke', d => { + if (!d[0]) return 'none'; const styleProps = getNarrativeStyle(d[0].narrative); return styleProps.stroke; }) diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js index d8fb7925..e189f3e3 100644 --- a/src/js/timeline/timeline.js +++ b/src/js/timeline/timeline.js @@ -6,7 +6,7 @@ */ import { areEqual -} from '../data/utilities'; +} from '../utilities'; import esLocale from '../data/es-MX.json'; import copy from '../data/copy.json'; diff --git a/src/js/data/utilities.js b/src/js/utilities.js similarity index 76% rename from src/js/data/utilities.js rename to src/js/utilities.js index da8462dc..49c701c3 100644 --- a/src/js/data/utilities.js +++ b/src/js/utilities.js @@ -35,3 +35,17 @@ export function areEqual(arr1, arr2) { export function isNotNullNorUndefined(variable) { return (typeof variable !== 'undefined' && variable !== null); } + +/** +* Return a Date object given a datetime string of the format: "2016-09-10T07:00:00" +* @param {string} datetime +*/ +export function parseDate(datetime) { + return new Date(datetime.slice(0, 4), + datetime.slice(5, 7) - 1, + datetime.slice(8, 10), + datetime.slice(11, 13), + datetime.slice(14, 16), + datetime.slice(17, 19) + ); +} diff --git a/src/reducers/app.js b/src/reducers/app.js index 0b17716f..16784a38 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -1,5 +1,7 @@ import initial from '../store/initial.js'; +import { parseDate } from '../js/utilities.js'; + import { UPDATE_HIGHLIGHTED, UPDATE_SELECTED, @@ -26,9 +28,25 @@ function updateSelected(appState, action) { } function updateNarrative(appState, action) { - return Object.assign({}, appState, { - narrative: action.narrative - }); + if (action.narrative === null) { + return Object.assign({}, appState, { + narrative: action.narrative, + }); + } else { + const dates = action.narrative.steps.map(n => parseDate(n.timestamp).getTime()) + let minDate = Math.min(...dates); + let maxDate = Math.max(...dates); + // Add some margin to the datetime extent + minDate = minDate - ((maxDate - minDate) / 20); + maxDate = maxDate + ((maxDate - minDate) / 20); + + return Object.assign({}, appState, { + narrative: action.narrative, + filters: Object.assign({}, appState.filters, { + timerange: [new Date(minDate), new Date(maxDate)] + }), + }); + } } function updateTagFilters(appState, action) { diff --git a/src/selectors/index.js b/src/selectors/index.js index 44809ccb..c994e2a9 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -94,10 +94,11 @@ export const selectNarratives = createSelector( const isTimeRanged = isTimeRangedIn(evt, timeRange); const isInNarrative = evt.narrative; - if (isTimeRanged && isTagged && isInNarrative) { - if (!narratives[evt.narrative]) { - narratives[evt.narrative] = { id: evt.narrative, steps: [], byId: {} }; - } + if (!narratives[evt.narrative]) { + narratives[evt.narrative] = { id: evt.narrative, steps: [], byId: {} }; + } + + if (/*isTimeRanged && isTagged && */isInNarrative) { narratives[evt.narrative].steps.push(evt); narratives[evt.narrative].byId[evt.id] = { next: null, prev: null }; } @@ -117,6 +118,7 @@ export const selectNarratives = createSelector( narratives[key] = Object.assign(narrativeMetadata.find(n => n.id === key), narratives[key]); }); + console.log(narrativeMetadata, narratives) return Object.values(narratives); }); diff --git a/webpack.config.js b/webpack.config.js index ba1c5381..4706f036 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,7 +1,7 @@ const webpack = require('webpack'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); -const userConfig = require('./dev.config'); +const userConfig = require('./config'); const userConfigJSON = {}; const devMode = process.env.NODE_ENV !== 'production'; From c5b7943feffc124890ffdae887b0b81c163956ca Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Thu, 6 Dec 2018 12:32:13 +0000 Subject: [PATCH 11/17] Render tag panel only if tags exist --- src/components/Toolbar.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Toolbar.jsx b/src/components/Toolbar.jsx index 20a888cf..8a4098dc 100644 --- a/src/components/Toolbar.jsx +++ b/src/components/Toolbar.jsx @@ -106,13 +106,15 @@ class Toolbar extends React.Component { renderToolbarTabs() { const title = copy[this.props.language].toolbar.title; + const isTags = this.props.tags && (this.props.tags.children > 0); + return (

{title}

{/*this.renderToolbarTab(0, 'search')*/} - {this.renderToolbarTab(0, 'Focus stories')} - {this.renderToolbarTab(1, 'Explore freely')} + {this.renderToolbarTab(0, 'Narratives')} + {(isTags) ? this.renderToolbarTab(1, 'Explore by tag') : ''}
Date: Thu, 6 Dec 2018 13:16:40 +0000 Subject: [PATCH 12/17] Clean store fields, move from state.ui to state.app those that make sense --- src/actions/index.js | 7 ---- src/components/Card.jsx | 10 +++-- src/components/CardStack.jsx | 7 ++-- src/components/Dashboard.jsx | 6 ++- src/components/InfoPopup.jsx | 2 +- src/components/Timeline.jsx | 9 ++--- src/components/Toolbar.jsx | 1 - src/components/ToolbarBottomActions.jsx | 4 -- src/js/timeline/timeline.js | 9 +++-- src/js/utilities.js | 8 ++++ src/reducers/app.js | 54 ++++++++++++++++++++----- src/reducers/ui.js | 50 +---------------------- src/scss/timeline.scss | 2 +- src/store/initial.js | 40 +++++------------- 14 files changed, 90 insertions(+), 119 deletions(-) diff --git a/src/actions/index.js b/src/actions/index.js index b406ae83..87dc4259 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -255,10 +255,3 @@ export function toggleMapView(layer) { layer } } - -export const TOGGLE_GUIDEDMODE = 'TOGGLE_GUIDEDMODE'; -export function toggleGuidedMode() { - return { - type: TOGGLE_GUIDEDMODE - } -} diff --git a/src/components/Card.jsx b/src/components/Card.jsx index 00f06e53..9c97da6d 100644 --- a/src/components/Card.jsx +++ b/src/components/Card.jsx @@ -1,5 +1,9 @@ import copy from '../js/data/copy.json'; -import {isNotNullNorUndefined} from '../js/utilities'; +import { + isNotNullNorUndefined, + parseDate, + formatterWithYear +} from '../js/utilities'; import React from 'react'; import Spinner from './presentational/Spinner'; @@ -35,8 +39,8 @@ class Card extends React.Component { makeTimelabel(timestamp) { if (timestamp === null) return null; - const parsedTimestamp = this.props.tools.parser(timestamp); - const timelabel = this.props.tools.formatterWithYear(parsedTimestamp); + const parsedTimestamp = parseDate(timestamp); + const timelabel = formatterWithYear(parsedTimestamp); return timelabel; } diff --git a/src/components/CardStack.jsx b/src/components/CardStack.jsx index 99492ed2..d7c5ab17 100644 --- a/src/components/CardStack.jsx +++ b/src/components/CardStack.jsx @@ -21,7 +21,7 @@ class CardStack extends React.Component {
diff --git a/src/components/InfoPopup.jsx b/src/components/InfoPopup.jsx index 00e67b72..6560759c 100644 --- a/src/components/InfoPopup.jsx +++ b/src/components/InfoPopup.jsx @@ -27,7 +27,7 @@ export default class InfoPopUp extends React.Component{ renderView2DLegend() { return ( -
+
{this.renderView2DCopy()}
diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index 74e64554..273fc98e 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -3,6 +3,7 @@ import { connect } from 'react-redux'; import * as selectors from '../selectors'; import copy from '../js/data/copy.json'; +import { formatterWithYear } from '../js/utilities'; import TimelineLogic from '../js/timeline/timeline.js'; class Timeline extends React.Component { @@ -15,7 +16,6 @@ class Timeline extends React.Component { componentDidMount() { const ui = { - tools: this.props.tools, dom: this.props.dom } @@ -47,8 +47,8 @@ class Timeline extends React.Component { const labels_title_lang = copy[this.props.app.language].timeline.labels_title; const info_lang = copy[this.props.app.language].timeline.info; let classes = `timeline-wrapper ${(this.state.isFolded) ? ' folded' : ''}`; - const date0 = this.props.tools.formatterWithYear(this.props.app.timerange[0]); - const date1 = this.props.tools.formatterWithYear(this.props.app.timerange[1]); + const date0 = formatterWithYear(this.props.app.timerange[0]); + const date1 = formatterWithYear(this.props.app.timerange[1]); return (
@@ -82,9 +82,8 @@ function mapStateToProps(state) { language: state.app.language, zoomLevels: state.app.zoomLevels }, - tools: state.ui.tools, dom: state.ui.dom, } } -export default connect(mapStateToProps)(Timeline); \ No newline at end of file +export default connect(mapStateToProps)(Timeline); diff --git a/src/components/Toolbar.jsx b/src/components/Toolbar.jsx index 8a4098dc..58a8c285 100644 --- a/src/components/Toolbar.jsx +++ b/src/components/Toolbar.jsx @@ -157,7 +157,6 @@ function mapStateToProps(state) { categoryFilter: state.app.filters.categories, viewFilters: state.app.filters.views, features: state.app.features, - isModeGuided: state.app.isModeGuided } } diff --git a/src/components/ToolbarBottomActions.jsx b/src/components/ToolbarBottomActions.jsx index 082365b9..fe173a98 100644 --- a/src/components/ToolbarBottomActions.jsx +++ b/src/components/ToolbarBottomActions.jsx @@ -22,10 +22,6 @@ class ToolbarBottomActions extends React.Component { this.props.actions.toggleMapView(layer); } - toggleGuidedMode() { - this.props.actions.toggleGuidedMode(); - } - renderMapActions() { return (
diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js index e189f3e3..8a009e98 100644 --- a/src/js/timeline/timeline.js +++ b/src/js/timeline/timeline.js @@ -5,15 +5,16 @@ TODO: is it possible to express this idiomatically as React? */ import { - areEqual + areEqual, + parseDate, + formatterWithYear } from '../utilities'; import esLocale from '../data/es-MX.json'; import copy from '../data/copy.json'; export default function(app, ui, methods) { d3.timeFormatDefaultLocale(esLocale); - const formatterWithYear = ui.tools.formatterWithYear; - const parser = ui.tools.parser; + const zoomLevels = app.zoomLevels; let events = []; let categories = []; @@ -242,7 +243,7 @@ export default function(app, ui, methods) { * @param {object} eventPoint: regular eventPoint data */ function getEventX(eventPoint) { - return scale.x(parser(eventPoint.timestamp)); + return scale.x(parseDate(eventPoint.timestamp)); } function getTimeScaleExtent() { diff --git a/src/js/utilities.js b/src/js/utilities.js index 49c701c3..c9c9309d 100644 --- a/src/js/utilities.js +++ b/src/js/utilities.js @@ -49,3 +49,11 @@ export function parseDate(datetime) { datetime.slice(17, 19) ); } + +export function formatterWithYear(datetime) { + return d3.timeFormat("%d %b %Y, %H:%M")(datetime); +} + +export function formatter(datetime) { + return d3.timeFormat("%d %b, %H:%M")(datetime); +} diff --git a/src/reducers/app.js b/src/reducers/app.js index 16784a38..c250582a 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -11,7 +11,10 @@ import { RESET_ALLFILTERS, TOGGLE_LANGUAGE, TOGGLE_MAPVIEW, - TOGGLE_GUIDEDMODE, + TOGGLE_FETCHING_DOMAIN, + TOGGLE_FETCHING_EVENTS, + TOGGLE_INFOPOPUP, + TOGGLE_NOTIFICATIONS, FETCH_ERROR, } from '../actions'; @@ -113,12 +116,6 @@ function toggleMapView(appState, action) { }); } -function toggleGuidedMode(appState, action) { - return Object.assign({}, appState, { - isModeGuided: !appState.isModeGuided - }) -} - function fetchError(state, action) { return { ...state, @@ -127,6 +124,39 @@ function fetchError(state, action) { } } +function toggleFetchingDomain(appState, action) { + return Object.assign({}, appState, { + flags: Object.assign({}, appState.flags, { + isFetchingDomain: !appState.flags.isFetchingDomain + }) + }); +} + +function toggleFetchingEvents(appState, action) { + return Object.assign({}, appState, { + flags: Object.assign({}, appState.flags, { + isFetchingEvents: !appState.flags.isFetchingEvents + }) + }); +} + +function toggleInfoPopup(appState, action) { + return Object.assign({}, appState, { + flags: Object.assign({}, appState.flags, { + isInfopopup: !appState.flags.isInfopopup + }) + }); +} + +function toggleNotifications(appState, action) { + return Object.assign({}, appState, { + flags: Object.assign({}, appState.flags, { + isNotification: !appState.flags.isNotification + }) + }); +} + + function app(appState = initial.app, action) { switch (action.type) { @@ -146,10 +176,16 @@ function app(appState = initial.app, action) { return toggleLanguage(appState, action); case TOGGLE_MAPVIEW: return toggleMapView(appState, action); - case TOGGLE_GUIDEDMODE: - return toggleGuidedMode(appState, action); case FETCH_ERROR: return fetchError(appState, action); + case TOGGLE_FETCHING_DOMAIN: + return toggleFetchingDomain(appState, action); + case TOGGLE_FETCHING_EVENTS: + return toggleFetchingEvents(appState, action); + case TOGGLE_INFOPOPUP: + return toggleInfoPopup(appState, action); + case TOGGLE_NOTIFICATIONS: + return toggleNotifications(appState, action); default: return appState; } diff --git a/src/reducers/ui.js b/src/reducers/ui.js index 5fa405f9..da2e0186 100644 --- a/src/reducers/ui.js +++ b/src/reducers/ui.js @@ -1,55 +1,9 @@ import initial from '../store/initial.js'; -import { - TOGGLE_FETCHING_DOMAIN, - TOGGLE_FETCHING_SOURCES, - TOGGLE_VIEW, - TOGGLE_TIMELINE, - TOGGLE_INFOPOPUP, - TOGGLE_NOTIFICATIONS -} from '../actions' - -function toggleFetchingDomain(uiState, action) { - return { - ...uiState, - flags: { - ...uiState.flags, - isFetchingDomain: !uiState.flags.isFetchingDomain - } - } -} - -function toggleFetchingSources(uiState, action) { - return { - ...uiState, - flags: { - ...uiState.flags, - isFetchingSources: !uiState.flags.isFetchingSources - } - } -} - -function toggleInfoPopup(uiState, action) { - return { - ...uiState, - flags: { - ...uiState.flags, - isInfopopup: !uiState.flags.isInfopopup - } - } -} +import {} from '../actions' function ui(uiState = initial.ui, action) { - switch (action.type) { - case TOGGLE_FETCHING_DOMAIN: - return toggleFetchingDomain(uiState, action) - case TOGGLE_FETCHING_SOURCES: - return toggleFetchingSources(uiState, action) - case TOGGLE_INFOPOPUP: - return toggleInfoPopup(uiState, action) - default: - return uiState - } + return uiState; } export default ui; diff --git a/src/scss/timeline.scss b/src/scss/timeline.scss index ed3ad209..2ffda133 100644 --- a/src/scss/timeline.scss +++ b/src/scss/timeline.scss @@ -173,7 +173,7 @@ .axisBoundaries { stroke: $offwhite; stroke-width: 1; - stroke-dasharray: 1px 4px; + stroke-dasharray: 1px 2px; } .event { diff --git a/src/store/initial.js b/src/store/initial.js index a40c8079..1be1c044 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -45,7 +45,6 @@ const initial = { }, base_uri: 'http://127.0.0.1:8000/', // Modify accordingly on production setup. isMobile: (/Mobi/.test(navigator.userAgent)), - isModeGuided: true, language: 'en-US', mapAnchor: process.env.MAP_ANCHOR, zoomLevels: [{ @@ -86,31 +85,24 @@ const initial = { features: { USE_TAGS: process.env.features.USE_TAGS, USE_SEARCH: process.env.features.USE_SEARCH + }, + flags: { + isFetchingDomain: false, + isFetchingEvents: false, + + isCardstack: true, + isInfopopup: false, + isNotification: true } }, /* * The 'ui' subtree of this state refers the state of the cosmetic - * elements of the application, such as color palettes of groups or how some - * of the UI tools are enabled or disabled dynamically by the user + * elements of the application, such as color palettes of categories + * as well as dom elements to attach SVG */ ui: { style: { - - colors: { - WHITE: "#efefef", - YELLOW: "#ffd800", - MIDGREY: "rgb(44, 44, 44)", - DARKGREY: "#232323", - PINK: "#F28B50",//rgb(232, 9, 90)", - ORANGE: "#F25835",//rgb(232, 9, 90)", - RED: "rgb(233, 0, 19)", - BLUE: "#F2DE79",//"rgb(48, 103 , 217)", - GREEN: "#4FF2F2",//"rgb(0, 158, 86)", - }, - - palette: d3.schemeCategory10, - categories: { default: 'red', // Add here other categories to differentiate by color, like: @@ -139,18 +131,6 @@ const initial = { timeslider: "timeslider", map: "map" }, - flags: { - isFetchingDomain: false, - isFetchingSources: false, - - isCardstack: true, - isInfopopup: false - }, - tools: { - formatter: d3.timeFormat("%d %b, %H:%M"), - formatterWithYear: d3.timeFormat("%d %b %Y, %H:%M"), - parser: d3.timeParse("%Y-%m-%dT%H:%M:%S") - } } }; From 0defd16ec831c0792ec598cb90470d198ae9b9bd Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Fri, 7 Dec 2018 08:47:33 +0000 Subject: [PATCH 13/17] Handle error for narrative url not functional --- src/selectors/index.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/selectors/index.js b/src/selectors/index.js index c994e2a9..76f8f406 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -3,13 +3,13 @@ import { } from 'reselect' // Input selectors -export const getEvents = state => state.domain.events -export const getLocations = state => state.domain.locations -export const getCategories = state => state.domain.categories -export const getNarratives = state => state.domain.narratives +export const getEvents = state => state.domain.events; +export const getLocations = state => state.domain.locations; +export const getCategories = state => state.domain.categories; +export const getNarratives = state => state.domain.narratives; export const getSites = (state) => { - if (process.env.features.USE_SITES) return state.domain.sites - return [] + if (process.env.features.USE_SITES) return state.domain.sites; + return []; } export const getNotifications = state => state.domain.notifications; export const getTagTree = state => state.domain.tags; @@ -116,9 +116,10 @@ export const selectNarratives = createSelector( narratives[key].byId[step.id].prev = (i > 0) ? steps[i - 1] : null; }); - narratives[key] = Object.assign(narrativeMetadata.find(n => n.id === key), narratives[key]); + if (narrativeMetadata.find(n => n.id === key)) { + narratives[key] = Object.assign(narrativeMetadata.find(n => n.id === key), narratives[key]); + } }); - console.log(narrativeMetadata, narratives) return Object.values(narratives); }); From 91601ff1889d1508dac3b0a4ca1115e91d6a741e Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Fri, 7 Dec 2018 09:06:47 +0000 Subject: [PATCH 14/17] Rebase with cleaning up fetchingEvents actions --- src/actions/index.js | 45 +++++++++++++++++---------------- src/components/Dashboard.jsx | 5 ++-- src/components/NarrativeCard.js | 4 +-- src/reducers/app.js | 10 ++++---- src/store/initial.js | 2 +- 5 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/actions/index.js b/src/actions/index.js index 87dc4259..0d3b0c45 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -8,12 +8,13 @@ function urlFromEnv(ext) { } // TODO: relegate these URLs entirely to environment variables -const EVENT_DATA_URL = urlFromEnv('EVENT_EXT') -const CATEGORY_URL = urlFromEnv('CATEGORY_EXT') -const TAG_URL = urlFromEnv('TAGS_EXT') -const SOURCES_URL = urlFromEnv('SOURCES_EXT') -const SITES_URL = urlFromEnv('SITES_EXT') -const eventUrlMap = (event) => `${process.env.SERVER_ROOT}${process.env.EVENT_DESC_ROOT}/${(event.id) ? event.id : event}` +const EVENT_DATA_URL = urlFromEnv('EVENT_EXT'); +const CATEGORY_URL = urlFromEnv('CATEGORY_EXT'); +const TAG_URL = urlFromEnv('TAGS_EXT'); +const SOURCES_URL = urlFromEnv('SOURCES_EXT'); +const NARRATIVE_URL = urlFromEnv('NARRATIVE_EXT'); +const SITES_URL = urlFromEnv('SITES_EXT'); +const eventUrlMap = (event) => `${process.env.SERVER_ROOT}${process.env.EVENT_DESC_ROOT}/${(event.id) ? event.id : event}`; /* * Create an error notification object @@ -163,6 +164,14 @@ export function updateTagFilters(tag) { } } +export const UPDATE_NARRATIVE = 'UPDATE_NARRATIVE'; + export function updateNarrative(narrative) { + return { + type: UPDATE_NARRATIVE, + narrative + } + } + export const UPDATE_TIMERANGE = 'UPDATE_TIMERANGE'; export function updateTimeRange(timerange) { return { @@ -171,14 +180,6 @@ export function updateTimeRange(timerange) { } } -export const UPDATE_NARRATIVE = 'UPDATE_NARRATIVE'; -export function updateNarrative(narrative) { - return { - type: UPDATE_NARRATIVE, - narrative - } -} - export const RESET_ALLFILTERS = 'RESET_ALLFILTERS' export function resetAllFilters() { return { @@ -224,6 +225,14 @@ export function toggleInfoPopup() { } } +export const TOGGLE_MAPVIEW = 'TOGGLE_MAPVIEW'; + export function toggleMapView(layer) { + return { + type: TOGGLE_MAPVIEW, + layer + } + } + export const TOGGLE_NOTIFICATIONS = 'TOGGLE_NOTIFICATIONS' export function toggleNotifications() { return { @@ -247,11 +256,3 @@ export function fetchSourceError(msg) { msg } } - -export const TOGGLE_MAPVIEW = 'TOGGLE_MAPVIEW'; -export function toggleMapView(layer) { - return { - type: TOGGLE_MAPVIEW, - layer - } -} diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx index adf734ec..493d5e9c 100644 --- a/src/components/Dashboard.jsx +++ b/src/components/Dashboard.jsx @@ -49,9 +49,8 @@ class Dashboard extends React.Component { handleSelect(selected) { if (selected) { let eventsToSelect = selected.map(event => this.getEventById(event.id)); - const p = this.props.ui.tools.parser; - - eventsToSelect = eventsToSelect.sort((a, b) => p(a.timestamp) - p(b.timestamp)) +console.log(eventsToSelect, selected) + eventsToSelect = eventsToSelect.sort((a, b) => parseDate(a.timestamp) - parseDate(b.timestamp)) this.props.actions.fetchSelected(eventsToSelect) } diff --git a/src/components/NarrativeCard.js b/src/components/NarrativeCard.js index bdc98627..1cc47309 100644 --- a/src/components/NarrativeCard.js +++ b/src/components/NarrativeCard.js @@ -26,7 +26,7 @@ class NarrativeCard extends React.Component { componentDidUpdate() { if (this.props.narrative !== null) { const step = this.props.narrative.steps[this.state.step]; - this.props.onSelect([step.id]); + this.props.onSelect([step]); } } @@ -42,7 +42,7 @@ class NarrativeCard extends React.Component { } render() { - if (this.props.narrative !== null) { + if (this.props.narrative !== null && this.props.narrative.steps[this.state.step]) { const steps = this.props.narrative.steps; const step = steps[this.state.step]; diff --git a/src/reducers/app.js b/src/reducers/app.js index c250582a..c8f243be 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -12,7 +12,7 @@ import { TOGGLE_LANGUAGE, TOGGLE_MAPVIEW, TOGGLE_FETCHING_DOMAIN, - TOGGLE_FETCHING_EVENTS, + TOGGLE_FETCHING_SOURCES, TOGGLE_INFOPOPUP, TOGGLE_NOTIFICATIONS, FETCH_ERROR, @@ -132,10 +132,10 @@ function toggleFetchingDomain(appState, action) { }); } -function toggleFetchingEvents(appState, action) { +function toggleFetchingSources(appState, action) { return Object.assign({}, appState, { flags: Object.assign({}, appState.flags, { - isFetchingEvents: !appState.flags.isFetchingEvents + isFetchingSources: !appState.flags.isFetchingSources }) }); } @@ -180,8 +180,8 @@ function app(appState = initial.app, action) { return fetchError(appState, action); case TOGGLE_FETCHING_DOMAIN: return toggleFetchingDomain(appState, action); - case TOGGLE_FETCHING_EVENTS: - return toggleFetchingEvents(appState, action); + case TOGGLE_FETCHING_SOURCES: + return toggleFetchingSources(appState, action); case TOGGLE_INFOPOPUP: return toggleInfoPopup(appState, action); case TOGGLE_NOTIFICATIONS: diff --git a/src/store/initial.js b/src/store/initial.js index 1be1c044..a56685ac 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -88,7 +88,7 @@ const initial = { }, flags: { isFetchingDomain: false, - isFetchingEvents: false, + isFetchingSources: false, isCardstack: true, isInfopopup: false, From db0f34c88735717069a99b45e8a4f0b818cf6492 Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Mon, 10 Dec 2018 12:00:23 +0000 Subject: [PATCH 15/17] rm console; improve default config --- src/components/Dashboard.jsx | 1 - src/store/initial.js | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx index 493d5e9c..4d6ac449 100644 --- a/src/components/Dashboard.jsx +++ b/src/components/Dashboard.jsx @@ -49,7 +49,6 @@ class Dashboard extends React.Component { handleSelect(selected) { if (selected) { let eventsToSelect = selected.map(event => this.getEventById(event.id)); -console.log(eventsToSelect, selected) eventsToSelect = eventsToSelect.sort((a, b) => parseDate(a.timestamp) - parseDate(b.timestamp)) this.props.actions.fetchSelected(eventsToSelect) diff --git a/src/store/initial.js b/src/store/initial.js index a56685ac..c387910e 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -106,9 +106,9 @@ const initial = { categories: { default: 'red', // Add here other categories to differentiate by color, like: - alpha: '#00ff00', - beta: '#ff0000', - other: 'yellow' + alpha: '#c73e1d', + beta: '#f40000', + other: '#f3de2c' }, narratives: { @@ -121,7 +121,7 @@ const initial = { narrative_1: { style: 'solid', // ['dotted', 'solid'] opacity: 0.4, // range between 0 and 1 - stroke: 'yellow', // Any hex or rgb code + stroke: '#f18f01', // Any hex or rgb code strokeWidth: 2 } } From 328788a13d1deb626bfbba53d94f67e28820c2da Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Mon, 10 Dec 2018 12:05:07 +0000 Subject: [PATCH 16/17] update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e48fa71e..f53c198f 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,7 @@ The application will require to include a few configuration settings. Configurat | EVENT_EXT | Endpoint for events, which will be concatenated with SERVER_ROOT | String | No | | EVENT_DESC_ROOT | Endpoint for additional metadata for each individual event, concatenated to SERVER_ROOT | String | Yes | | CATEGORY_EXT | Endpoint for categories, concatenated with SERVER_ROOT | String | Yes | +| NARRATIVE_EXT | Endpoint for narratives, concatenated with SERVER_ROOT | String | No | | TAG_TREE_EXT | Endpoint for tags, concatenated with SERVER_ROOT | String | Yes | | SITES_EXT | Endpoint for sites, concatenated with SERVER_ROOT | String | Yes | | MAP_ANCHOR | Geographic coordinates for original map anchor | Array of numbers | No | From 8c5edb2aac68f3968a0527b2a7190d75082c9116 Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Mon, 10 Dec 2018 12:06:56 +0000 Subject: [PATCH 17/17] update example config --- example.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/example.config.js b/example.config.js index 222355ef..30c12c24 100644 --- a/example.config.js +++ b/example.config.js @@ -4,6 +4,7 @@ module.exports = { EVENT_EXT: '/api/example/export_events/rows', CATEGORY_EXT: '/api/example/export_categories/rows', SOURCES_EXT: '/api/example/export_events/ids', + NARRATIVE_EXT: '/api/example/export_narratives/rows', TAGS_EXT: '/api/example/export_tags/tree', SITES_EXT: '/api/example/export_sites/rows', MAP_ANCHOR: [31.356397, 34.784818],