diff --git a/src/actions/index.js b/src/actions/index.js index 8ee00c0..8530bb8 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -171,19 +171,20 @@ export function updateDistrict (district) { } } -export const UPDATE_TAGFILTERS = 'UPDATE_TAGFILTERS' -export function updateTagFilters (tag) { +export const CLEAR_FILTER = 'CLEAR_FILTER' +export function clearFilter (filter) { return { - type: UPDATE_TAGFILTERS, - tag + type: CLEAR_FILTER, + filter } } -export const UPDATE_CATEGORYFILTERS = 'UPDATE_CATEGORYFILTERS' -export function updateCategoryFilters (category) { +export const TOGGLE_FILTER = 'TOGGLE_FILTER' +export function toggleFilter (filter, value) { return { - type: UPDATE_CATEGORYFILTERS, - category + type: TOGGLE_FILTER, + filter, + value } } @@ -217,13 +218,6 @@ export function decrementNarrativeCurrent () { } } -export const RESET_ALLFILTERS = 'RESET_ALLFILTERS' -export function resetAllFilters () { - return { - type: RESET_ALLFILTERS - } -} - export const UPDATE_SOURCE = 'UPDATE_SOURCE' export function updateSource (source) { return { diff --git a/src/components/CategoriesListPanel.jsx b/src/components/CategoriesListPanel.jsx deleted file mode 100644 index a295aa0..0000000 --- a/src/components/CategoriesListPanel.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react' -import Checkbox from './presentational/Checkbox' -import copy from '../js/data/copy.json' - -export default (props) => { - function onClickCheckbox (obj, type) { - obj.active = !obj.active - props.onCategoryFilter(obj) - } - - function renderCategoryTree () { - return ( -
- {props.categories.map(cat => { - return (
  • - onClickCheckbox(cat, 'category')} - /> -
  • ) - })} -
    - ) - } - - return ( -
    -

    {copy[props.language].toolbar.categories}

    -

    {copy[props.language].toolbar.explore_by_category__description}

    - {renderCategoryTree()} -
    - ) -} diff --git a/src/components/Layout.js b/src/components/Layout.js index 8a26dd8..7d4db2e 100644 --- a/src/components/Layout.js +++ b/src/components/Layout.js @@ -7,7 +7,7 @@ import * as actions from '../actions' import MediaOverlay from './Overlay/Media' import LoadingOverlay from './Overlay/Loading' import Map from './Map.jsx' -import Toolbar from './Toolbar.jsx' +import Toolbar from './Toolbar/Layout' import CardStack from './CardStack.jsx' import NarrativeControls from './presentational/Narrative/Controls.js' import InfoPopUp from './InfoPopup.jsx' @@ -76,7 +76,11 @@ class Dashboard extends React.Component { setNarrative (narrative) { // only handleSelect if narrative is not null - if (narrative) { this.handleSelect([ narrative.steps[0] ]) } + if (narrative) { + this.props.actions.clearFilter('tags') + this.props.actions.clearFilter('categories') + this.handleSelect([ narrative.steps[0] ]) + } this.props.actions.updateNarrative(narrative) } @@ -120,8 +124,8 @@ class Dashboard extends React.Component { actions.toggleFilter('tags', tag), + onCategoryFilter: category => actions.toggleFilter('categories', category), onSelectNarrative: this.setNarrative }} /> diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 2b8e03d..23401da 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -245,7 +245,7 @@ class Map extends React.Component { {this.renderShapes()} {this.renderNarratives()} {this.renderEvents()} - {this.renderSelected()} + {this.renderSelected()} ) : null @@ -263,7 +263,7 @@ function mapStateToProps (state) { domain: { locations: selectors.selectLocations(state), narratives: selectors.selectNarratives(state), - categories: selectors.selectCategories(state), + categories: selectors.getCategories(state), sites: selectors.getSites(state), shapes: selectors.getShapes(state) }, diff --git a/src/components/TagFilter.jsx b/src/components/TagFilter.jsx deleted file mode 100644 index c6618fc..0000000 --- a/src/components/TagFilter.jsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react' -import Checkbox from './presentational/Checkbox' - -class TagFilter extends React.Component { - isActive () { - if (this.props.isCategory) { - return this.props.categoryFilters.includes(this.props.tag.id) - } - return this.props.tagFilters.includes(this.props.tag.id) - } - - onClickTag () { - if (this.isActive()) { - this.props.filter({ - tags: this.props.tagFilters.filter(element => element !== this.props.tag.id) - }) - } else { - this.props.filter({ - tags: this.props.tagFilters.concat(this.props.tag.id) - }) - } - } - - onClickCategory () { - if (this.isActive()) { - this.props.filter({ - categories: this.props.categoryFilters.filter(element => element !== this.props.tag.id) - }) - } else { - this.props.filter({ - categories: this.props.categoryFilters.concat(this.props.tag.id) - }) - } - } - - renderTag () { - const tag = this.props.tag - let classes = (this.isActive()) ? 'tag-filter active' : 'tag-filter' - let label = `${tag.name} ( ${tag.mentions} )` - if (this.props.isShowTree) { - label = `${tag.group} > ${tag.subgroup} > ${tag.name} ( ${tag.mentions} )` - } - return ( -
  • - this.onClickTag()} - /> -
  • - ) - } - - renderCategory () { - const category = this.props.categories[this.props.tag.id] - let classes = (this.isActive()) ? 'tag-filter active' : 'tag-filter' - - if (category) { - return ( -
  • - this.onClickCategory()} - /> -
  • - ) - } - return (
    ) - } - - render () { - if (this.props.isCategory) return (this.renderCategory()) - return (this.renderTag()) - } -} - -export default TagFilter diff --git a/src/components/TagListPanel.jsx b/src/components/TagListPanel.jsx deleted file mode 100644 index 72582ae..0000000 --- a/src/components/TagListPanel.jsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from 'react' -import Checkbox from './presentational/Checkbox' -import copy from '../js/data/copy.json' - -class TagListPanel extends React.Component { - constructor (props) { - super(props) - this.state = { - treeComponents: [] - } - this.treeComponents = [] - this.newTagFilters = [] - } - - componentDidMount () { - this.computeTree(this.props.tags)// .children[this.props.tagType]); - } - - componentWillReceiveProps (nextProps) { - this.computeTree(nextProps.tags)// .children[nextProps.tagType]); - } - - onClickCheckbox (obj, type) { - obj.active = !obj.active - this.props.onTagFilter(obj) - } - - createNodeComponent (node, depth) { - return ( -
  • - this.onClickCheckbox(node, 'tag')} - /> -
  • - ) - } - - traverseNodeAndCreateComponent (node, depth) { - // add and create node component - const newComponent = this.createNodeComponent(node, depth) - this.treeComponents.push(newComponent) - depth = depth + 1 - if (Object.keys(node.children).length > 0) { - Object.values(node.children).forEach((childNode) => { - this.traverseNodeAndCreateComponent(childNode, depth) - }) - } - } - - computeTree (node) { - this.treeComponents = [] - let depth = 0 - this.traverseNodeAndCreateComponent(node, depth) - this.setState({ treeComponents: this.treeComponents }) - } - - renderTree () { - return ( -
    - {this.state.treeComponents.map(c => c)} -
    - ) - } - - render () { - return ( -
    -

    {copy[this.props.language].toolbar.tags}

    -

    {copy[this.props.language].toolbar.explore_by_tag__description}

    - {this.renderTree()} -
    - ) - } -} - -export default TagListPanel diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index 2f489ea..f05eeeb 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -349,7 +349,7 @@ function mapStateToProps (state) { isNarrative: !!state.app.narrative, domain: { datetimes: selectors.selectDatetimes(state), - categories: selectors.selectCategories(state), + categories: selectors.getCategories(state), narratives: state.domain.narratives }, app: { diff --git a/src/components/ToolbarBottomActions.jsx b/src/components/Toolbar/BottomActions.js similarity index 76% rename from src/components/ToolbarBottomActions.jsx rename to src/components/Toolbar/BottomActions.js index 8040948..9ccfb8d 100644 --- a/src/components/ToolbarBottomActions.jsx +++ b/src/components/Toolbar/BottomActions.js @@ -1,10 +1,10 @@ import React from 'react' -import SitesIcon from './presentational/Icons/Sites' -import CoverIcon from './presentational/Icons/Cover' -import InfoIcon from './presentational/Icons/Info' +import SitesIcon from '../presentational/Icons/Sites' +import CoverIcon from '../presentational/Icons/Cover' +import InfoIcon from '../presentational/Icons/Info' -function ToolbarBottomActions (props) { +function BottomActions (props) { function renderToggles () { return [
    @@ -34,4 +34,4 @@ function ToolbarBottomActions (props) { ) } -export default ToolbarBottomActions +export default BottomActions diff --git a/src/components/Toolbar/CategoriesListPanel.js b/src/components/Toolbar/CategoriesListPanel.js new file mode 100644 index 0000000..f0c3bf6 --- /dev/null +++ b/src/components/Toolbar/CategoriesListPanel.js @@ -0,0 +1,38 @@ +import React from 'react' +import Checkbox from '../presentational/Checkbox' +import copy from '../../js/data/copy.json' + +export default ({ + categories, + activeCategories, + onCategoryFilter, + language +}) => { + function renderCategoryTree () { + return ( +
    + {categories.map(cat => { + return (
  • + onCategoryFilter(cat.category)} + /> +
  • ) + })} +
    + ) + } + + return ( +
    +

    {copy[language].toolbar.categories}

    +

    {copy[language].toolbar.explore_by_category__description}

    + {renderCategoryTree()} +
    + ) +} diff --git a/src/components/Toolbar.jsx b/src/components/Toolbar/Layout.js similarity index 90% rename from src/components/Toolbar.jsx rename to src/components/Toolbar/Layout.js index 9c8990f..1ba407b 100644 --- a/src/components/Toolbar.jsx +++ b/src/components/Toolbar/Layout.js @@ -1,16 +1,16 @@ import React from 'react' import { connect } from 'react-redux' import { bindActionCreators } from 'redux' -import * as actions from '../actions' -import * as selectors from '../selectors' +import * as actions from '../../actions' +import * as selectors from '../../selectors' import { Tabs, TabPanel } from 'react-tabs' -import Search from './Search.jsx' -import TagListPanel from './TagListPanel.jsx' -import CategoriesListPanel from './CategoriesListPanel.jsx' -import ToolbarBottomActions from './ToolbarBottomActions.jsx' -import copy from '../js/data/copy.json' -import { trimAndEllipse } from '../js/utilities.js' +import Search from './Search' +import TagListPanel from './TagListPanel' +import CategoriesListPanel from './CategoriesListPanel' +import BottomActions from './BottomActions' +import copy from '../../js/data/copy.json' +import { trimAndEllipse } from '../../js/utilities.js' class Toolbar extends React.Component { constructor (props) { @@ -78,7 +78,7 @@ class Toolbar extends React.Component { @@ -94,7 +94,7 @@ class Toolbar extends React.Component { @@ -164,7 +164,7 @@ class Toolbar extends React.Component { {(isCategories) ? this.renderToolbarTab(1, categoriesLabel, 'widgets') : null} {(isTags) ? this.renderToolbarTab(2, tagsLabel, 'filter_list') : null}
    - element !== props.tag.id) + }) + } else { + props.filter({ + tags: props.tagFilters.concat(props.tag.id) + }) + } + } + + function onClickCategory () { + if (isActive()) { + props.filter({ + categories: props.categoryFilters.filter(element => element !== props.tag.id) + }) + } else { + props.filter({ + categories: props.categoryFilters.concat(props.tag.id) + }) + } + } + + function renderTag () { + const tag = props.tag + let classes = (isActive()) ? 'tag-filter active' : 'tag-filter' + let label = `${tag.name} ( ${tag.mentions} )` + if (props.isShowTree) { + label = `${tag.group} > ${tag.subgroup} > ${tag.name} ( ${tag.mentions} )` + } + return ( +
  • + onClickTag()} + /> +
  • + ) + } + + function renderCategory () { + const category = props.categories[props.tag.id] + let classes = (isActive()) ? 'tag-filter active' : 'tag-filter' + + if (category) { + return ( +
  • + +
  • + ) + } + return (
    ) + } + + if (props.isCategory) return (renderCategory()) + return (renderTag()) +} + +export default TagFilter diff --git a/src/components/Toolbar/TagListPanel.js b/src/components/Toolbar/TagListPanel.js new file mode 100644 index 0000000..52d830a --- /dev/null +++ b/src/components/Toolbar/TagListPanel.js @@ -0,0 +1,45 @@ +import React from 'react' +import Checkbox from '../presentational/Checkbox' +import copy from '../../js/data/copy.json' + +function TagListPanel ({ + tags, + activeTags, + onTagFilter, + language +}) { + function createNodeComponent (node, depth) { + return ( +
  • + onTagFilter(node.key)} + /> +
  • + ) + } + + function renderTree () { + /* NOTE: only render first layer of tags */ + return ( +
    + {Object.values(tags.children).map(tag => createNodeComponent(tag, 1))} +
    + ) + } + + return ( +
    +

    {copy[language].toolbar.tags}

    +

    {copy[language].toolbar.explore_by_tag__description}

    + {renderTree()} +
    + ) +} + +export default TagListPanel diff --git a/src/components/presentational/Map/Events.jsx b/src/components/presentational/Map/Events.jsx index 9640386..c5d5d3c 100644 --- a/src/components/presentational/Map/Events.jsx +++ b/src/components/presentational/Map/Events.jsx @@ -80,6 +80,7 @@ function MapEvents ({ getCategoryColor, categories, projectPoint, styleLocation, const { x, y } = projectPoint([location.latitude, location.longitude]) // in narrative mode, only render events in narrative + // TODO: move this to a selector if (narrative) { const { steps } = narrative const onlyIfInNarrative = e => steps.map(s => s.id).includes(e.id) diff --git a/src/reducers/app.js b/src/reducers/app.js index 83f2187..ce2e207 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -5,8 +5,8 @@ import { parseDate, toggleFlagAC } from '../js/utilities' import { UPDATE_HIGHLIGHTED, UPDATE_SELECTED, - UPDATE_TAGFILTERS, - UPDATE_CATEGORYFILTERS, + CLEAR_FILTER, + TOGGLE_FILTER, UPDATE_TIMERANGE, UPDATE_NARRATIVE, INCREMENT_NARRATIVE_CURRENT, @@ -118,45 +118,40 @@ function decrementNarrativeCurrent (appState, action) { } } -function updateTagFilters (appState, action) { - const tagFilters = appState.filters.tags.slice(0) - const nextActiveState = action.tag.active - - function traverseNode (node) { - const tagFilter = tagFilters.find(tF => tF.key === node.key) - node.active = nextActiveState - if (!tagFilter) tagFilters.push(node) - - if (node && Object.keys(node.children).length > 0) { - Object.values(node.children).forEach((childNode) => { traverseNode(childNode) }) +function clearTagFilters (appState) { + return { + ...appState, + filters: { + ...appState.filters, + tags: [] } } - - traverseNode(action.tag) - - return Object.assign({}, appState, { - filters: Object.assign({}, appState.filters, { - tags: tagFilters - }) - }) } -function updateCategoryFilters (appState, action) { - const categoryFilters = appState.filters.categories.slice(0) - - const catFilter = categoryFilters.find(cF => cF.category === action.category.category) - - if (!catFilter) { - categoryFilters.push(action.category) +function toggleFilter (appState, action) { + let newTags = appState.filters[action.filter].slice(0) + if (newTags.includes(action.value)) { + newTags = newTags.filter(s => s !== action.value) } else { - catFilter.active = (!!action.category.active) + newTags.push(action.value) } + return { + ...appState, + filters: { + ...appState.filters, + [action.filter]: newTags + } + } +} - return Object.assign({}, appState, { - filters: Object.assign({}, appState.filters, { - categories: categoryFilters - }) - }) +function clearFilter (appState, action) { + return { + ...appState, + filters: { + ...appState.filters, + [action.filter]: [] + } + } } function updateTimeRange (appState, action) { // XXX @@ -169,20 +164,6 @@ function updateTimeRange (appState, action) { // XXX } } -function resetAllFilters (appState) { // XXX - return Object.assign({}, appState, { - filters: Object.assign({}, appState.filters, { - tags: [], - categories: [], - timerange: [ - d3.timeParse('%Y-%m-%dT%H:%M:%S')('2014-09-25T12:00:00'), - d3.timeParse('%Y-%m-%dT%H:%M:%S')('2014-09-28T12:00:00') - ] - }), - selected: [] - }) -} - function toggleLanguage (appState, action) { let otherLanguage = (appState.language === 'es-MX') ? 'en-US' : 'es-MX' return Object.assign({}, appState, { @@ -228,10 +209,10 @@ function app (appState = initial.app, action) { return updateHighlighted(appState, action) case UPDATE_SELECTED: return updateSelected(appState, action) - case UPDATE_TAGFILTERS: - return updateTagFilters(appState, action) - case UPDATE_CATEGORYFILTERS: - return updateCategoryFilters(appState, action) + case CLEAR_FILTER: + return clearFilter(appState, action) + case TOGGLE_FILTER: + return toggleFilter(appState, action) case UPDATE_TIMERANGE: return updateTimeRange(appState, action) case UPDATE_NARRATIVE: diff --git a/src/selectors/helpers.js b/src/selectors/helpers.js new file mode 100644 index 0000000..af6190c --- /dev/null +++ b/src/selectors/helpers.js @@ -0,0 +1,16 @@ +import { parseTimestamp } from '../js/utilities' +/** +* Some handy helpers +*/ + +/** + * Given an event and a time range, + * returns true/false if the event falls within timeRange + */ +export function isTimeRangedIn (event, timeRange) { + const eventTime = parseTimestamp(event.timestamp) + return ( + timeRange[0] < eventTime && + eventTime < timeRange[1] + ) +} diff --git a/src/selectors/index.js b/src/selectors/index.js index 7d3e0f8..e9a8399 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -1,5 +1,6 @@ import { createSelector } from 'reselect' -import { parseTimestamp, compareTimestamp, insetSourceFrom } from '../js/utilities' +import { compareTimestamp, insetSourceFrom } from '../js/utilities' +import { isTaggedIn, isNoTags, isTaggedInWithCategory, isNoCategories, isTimeRangedIn } from './helpers' // Input selectors export const getEvents = state => state.domain.events @@ -22,91 +23,28 @@ export const getShapes = state => { } export const getNotifications = state => state.domain.notifications export const getTagTree = state => state.domain.tags -export const getTagsFilter = state => state.app.filters.tags -export const getCategoriesFilter = state => state.app.filters.categories +export const getActiveTags = state => state.app.filters.tags +export const getActiveCategories = state => state.app.filters.categories export const getTimeRange = state => state.app.timeline.range +export const selectNarrative = state => state.app.narrative /** -* Some handy helpers -*/ - -/** - * Given an event and all tags, - * returns true/false if event has any tag that is active - */ -function isTaggedIn (event, tagFilters) { - if (event.tags) { - const isTagged = event.tags.some((tag) => { - return tagFilters.find(tF => (tF.key === tag && tF.active)) - }) - return isTagged - } else { - return false - } -} - -/** - * Given an event and all categories, - * returns true/false if event has a category that is active - */ -function isTaggedInWithCategory (event, categories) { - if (event.category) { - if (categories.find(c => (c.category === event.category && c.active))) return true - return false - } else { - return false - } -} - -/* -* Returns true if no tags are selected -*/ -function isNoTags (tagFilters) { - return ( - tagFilters.length === 0 || - !process.env.features.USE_TAGS || - tagFilters.every(t => !t.active) - ) -} - -/* -* Returns true if no categories are selected -*/ -function isNoCategories (categories) { - return ( - categories.length === 0 || - !process.env.features.CATEGORIES_AS_TAGS || - categories.every(c => !c.active) - ) -} - -/** - * Given an event and a time range, - * returns true/false if the event falls within timeRange - */ -function isTimeRangedIn (event, timeRange) { - const eventTime = parseTimestamp(event.timestamp) - return ( - timeRange[0] < eventTime && - eventTime < timeRange[1] - ) -} - -/** - * Of all available events, selects those that fall within the time range, - * and if TAGS are being used, select them if their tags are enabled + * Of all available events, selects those that + * 1. fall in time range + * 2. exist in an active tag + * 3. exist in an active category */ export const selectEvents = createSelector( - [getEvents, getTagsFilter, getCategoriesFilter, getTimeRange], - (events, tagFilters, categories, timeRange) => { + [getEvents, getActiveTags, getActiveCategories, getTimeRange], + (events, activeTags, activeCategories, timeRange) => { return events.reduce((acc, event) => { - const isTagged = isTaggedIn(event, tagFilters) || isNoTags(tagFilters) - const isTaggedWithCategory = isTaggedInWithCategory(event, categories) || isNoCategories(categories) - const isTimeRanged = isTimeRangedIn(event, timeRange) + const isMatchingTag = event.tags.map(tag => activeTags.includes(tag)).some(s => s) + const isActiveTag = isMatchingTag || activeTags.length === 0 + const isActiveCategory = activeCategories.includes(event.category) || activeCategories.length === 0 + const isActiveTime = isTimeRangedIn(event, timeRange) - if (isTimeRanged && isTagged && isTaggedWithCategory) { - const eventClone = Object.assign({}, event) - acc[event.id] = eventClone + if (isActiveTime && isActiveTag && isActiveCategory) { + acc[event.id] = { ...event } } return acc @@ -180,14 +118,14 @@ export const selectActiveNarrative = createSelector( export const selectLocations = createSelector( [selectEvents], (events) => { - const selectedLocations = {} + const activeLocations = {} events.forEach(event => { const location = event.location - if (selectedLocations[location]) { - selectedLocations[location].events.push(event) + if (activeLocations[location]) { + activeLocations[location].events.push(event) } else { - selectedLocations[location] = { + activeLocations[location] = { label: location, events: [event], latitude: event.latitude, @@ -195,7 +133,8 @@ export const selectLocations = createSelector( } } }) - return Object.values(selectedLocations) + + return Object.values(activeLocations) } ) @@ -242,44 +181,3 @@ export const selectSelected = createSelector( return selected.map(insetSourceFrom(sources)) } ) - -/* -* Select categories, return them as a list -*/ -export const selectCategories = createSelector( - [getCategories], - (categories) => { - categories.map(cat => { - cat.active = (!cat.hasOwnProperty('active')) ? false : cat.active - }) - return categories - } -) - -/** - * Given a tree of tags, return those tags as a list - * Each node has been aware of its depth, and given an 'active' flag - */ -export const selectTagList = createSelector( - [getTagTree], - (tags) => { - const tagList = [] - let depth = 0 - function traverseNode (node, depth) { - node.active = (!node.hasOwnProperty('active')) ? false : node.active - node.depth = depth - - if (node.active) tagList.push(node) - - if (Object.keys(node.children).length > 0) { - Object.values(node.children).forEach((childNode) => { - traverseNode(childNode, depth + 1) - }) - } - } - if (tags && tags !== undefined) { - if (tags.key && tags.children) traverseNode(tags, depth) - } - return tagList - } -)