diff --git a/example.config.js b/example.config.js index 9a83349..ee5b1c6 100644 --- a/example.config.js +++ b/example.config.js @@ -14,7 +14,8 @@ module.exports = { USE_TAGS: false, USE_SEARCH: false, USE_SITES: true, - USE_SOURCES: true + USE_SOURCES: true, + CATEGORIES_AS_TAGS: true } } diff --git a/src/actions/index.js b/src/actions/index.js index da6fe64..cad3fa1 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -1,54 +1,25 @@ -// TODO: move to util lib -function urlFromEnv(ext) { - if (process.env[ext]) { - return `${process.env.SERVER_ROOT}${process.env[ext]}` - } else { - return null - } -} +import { urlFromEnv } from '../js/utilities' -// 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 TAGS_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}`; +const domainMsg = (domainType) => `Something went wrong fetching ${domainType}. Check the URL or try disabling them in the config file.` -const DEBUG_GER = 'DEBUG_GER' -function _debugger(value) { - console.log(value) - return { - type: DEBUG_GER, - value - } -} - -/* -* Create an error notification object -* Types: ['error', 'warning', 'good', 'neural'] -*/ -function makeError (type, id, message) { - return { - type: 'error', - id, - message: `${type} ${id}: ${message}` - } -} export function fetchDomain () { let notifications = [] - function handleError (domainType) { - return () => { - notifications.push({ - message: `Something went wrong fetching ${domainType}. Check the URL or try disabling them in the config file.`, - type: 'error' - }) - return [] - } + function handleError (message) { + notifications.push({ + message, + type: 'error' + }) + return [] } return dispatch => { @@ -57,35 +28,43 @@ export function fetchDomain () { const eventPromise = fetch(EVENT_DATA_URL) .then(response => response.json()) - .catch(handleError('events')) + .catch(() => handleError('events')) const catPromise = fetch(CATEGORY_URL) .then(response => response.json()) - .catch(handleError('categories')) + .catch(() => handleError(domainMsg('categories'))) const narPromise = fetch(NARRATIVE_URL) .then(response => response.json()) - .catch(handleError('narratives')) + .catch(() => handleError(domainMsg('narratives'))) let sitesPromise = Promise.resolve([]) if (process.env.features.USE_SITES) { sitesPromise = fetch(SITES_URL) .then(response => response.json()) - .catch(handleError('sites')) + .catch(() => handleError(domainMsg('sites'))) } let tagsPromise = Promise.resolve([]) if (process.env.features.USE_TAGS) { - tagsPromise = fetch(TAG_URL) - .then(response => response.json()) - .catch(handleError('tags')) + if (!TAGS_URL) { + tagsPromise = Promise.resolve(handleError('USE_TAGS is true, but you have not provided a TAGS_EXT')) + } else { + tagsPromise = fetch(TAGS_URL) + .then(response => response.json()) + .catch(() => handleError(domainMsg('tags'))) + } } let sourcesPromise = Promise.resolve([]) if (process.env.features.USE_SOURCES) { - sourcesPromise = fetch(SOURCES_URL) - .then(response => response.json()) - .catch(handleError('sources')) + if (!SOURCES_URL) { + sourcesPromise = Promise.resolve(makeError('USE_SOURCES is true, but you have not provided a SOURCES_EXT')) + } else { + sourcesPromise = fetch(SOURCES_URL) + .then(response => response.json()) + .catch(() => handleError(domainMsg('sources'))) + } } return Promise.all([ @@ -152,9 +131,6 @@ export function fetchSource(source) { return response.json() } }) - .then(sources => { - dispatch(_debugger(sources)) - }) .catch(err => { dispatch(fetchSourceError(err.message)) dispatch(toggleFetchingSources()) @@ -189,7 +165,7 @@ export function updateDistrict(district) { } } -export const UPDATE_TAGFILTERS = 'UPDATE_TIMEFILTERS' +export const UPDATE_TAGFILTERS = 'UPDATE_TAGFILTERS' export function updateTagFilters(tag) { return { type: UPDATE_TAGFILTERS, @@ -197,6 +173,14 @@ export function updateTagFilters(tag) { } } +export const UPDATE_CATEGORYFILTERS = 'UPDATE_CATEGORYFILTERS' +export function updateCategoryFilters(category) { + return { + type: UPDATE_CATEGORYFILTERS, + category + } +} + export const UPDATE_TIMERANGE = 'UPDATE_TIMERANGE'; export function updateTimeRange(timerange) { return { diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx index 077e0f2..e0a59aa 100644 --- a/src/components/Dashboard.jsx +++ b/src/components/Dashboard.jsx @@ -98,7 +98,8 @@ class Dashboard extends React.Component { diff --git a/src/components/SourceOverlay.jsx b/src/components/SourceOverlay.jsx index d18a865..ea94e2d 100644 --- a/src/components/SourceOverlay.jsx +++ b/src/components/SourceOverlay.jsx @@ -19,7 +19,7 @@ function SourceOverlay ({ source, onCancel }) { } + loader={
} unloader={} /> @@ -107,8 +107,8 @@ function SourceOverlay ({ source, onCancel }) { return (
{img ? img : ''} - {vid ? `, ${vid}`: ''} - {txt ? `, ${txt}`: ''} + {(img && vid) ? `, ${vid}`: (vid || '')} + {((img || vid) && txt) ? `, ${txt}`: (txt || '')}
) } @@ -145,6 +145,7 @@ function SourceOverlay ({ source, onCancel }) {
{title?

{title}

: null}
{_renderCounts(counts)}
+
{type ?

Media type

: null} {type ?

perm_media{type}

: null} {date ?

Date

: null} diff --git a/src/components/TagListPanel.jsx b/src/components/TagListPanel.jsx index fa6667b..3406140 100644 --- a/src/components/TagListPanel.jsx +++ b/src/components/TagListPanel.jsx @@ -1,5 +1,6 @@ import React from 'react'; import Checkbox from './presentational/Checkbox'; +import copy from '../js/data/copy.json'; class TagListPanel extends React.Component { @@ -20,9 +21,10 @@ class TagListPanel extends React.Component { this.computeTree(nextProps.tags);//.children[nextProps.tagType]); } - onClickCheckbox(tag) { - tag.active = !tag.active - this.props.filter(tag); + onClickCheckbox(obj, type) { + obj.active = !obj.active + if (type === 'category') this.props.onCategoryFilter(obj); + if (type === 'tag') this.props.onTagFilter(obj); } createNodeComponent (node, depth) { @@ -35,7 +37,7 @@ class TagListPanel extends React.Component { this.onClickCheckbox(node)} + onClickCheckbox={() => this.onClickCheckbox(node, 'tag')} /> ); @@ -61,15 +63,42 @@ class TagListPanel extends React.Component { } renderTree() { - return this.state.treeComponents.map(c => c); + return ( +
+

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

+ {this.state.treeComponents.map(c => c)} +
+ ) + } + + renderCategoryTree() { + return ( +
+

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

+ {this.props.categories.map(cat => { + return (
  • + this.onClickCheckbox(cat, 'category')} + /> +
  • ) + }) + } +
    + ) } render() { - return (
    -

    Explore data by tag

    -

    Explore freely all the data by selecting tags.

    +

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

    +

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

    + {this.renderCategoryTree()} {this.renderTree()}
    ); diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index a05c5e3..5c42e17 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -82,7 +82,8 @@ class Timeline extends React.Component { } makeScaleY(categories) { - const catsYpos = categories.map((g, i) => (i + 1) * this.state.dims.trackHeight / categories.length); + const tickHeight = 15; + const catsYpos = categories.map((g, i) => (i + 1) * this.state.dims.trackHeight / categories.length + tickHeight / 2); return d3.scaleOrdinal() .domain(categories) .range(catsYpos); diff --git a/src/components/TimelineCategories.jsx b/src/components/TimelineCategories.jsx index d1cd15f..c90697c 100644 --- a/src/components/TimelineCategories.jsx +++ b/src/components/TimelineCategories.jsx @@ -25,7 +25,7 @@ class TimelineCategories extends React.Component { } getY(idx) { - return (idx + 1) * this.props.dims.trackHeight / this.props.categories.length + return (idx + 1) * this.props.dims.trackHeight / this.props.categories.length + 7.5; } renderCategory(category, idx) { diff --git a/src/components/Toolbar.jsx b/src/components/Toolbar.jsx index ea526f9..1596f29 100644 --- a/src/components/Toolbar.jsx +++ b/src/components/Toolbar.jsx @@ -81,7 +81,8 @@ class Toolbar extends React.Component { categories={this.props.categories} tagFilters={this.props.tagFilters} categoryFilters={this.props.categoryFilters} - filter={this.props.filter} + onTagFilter={this.props.methods.onTagFilter} + onCategoryFilter={this.props.methods.onCategoryFilter} language={this.props.language} /> @@ -90,34 +91,18 @@ class Toolbar extends React.Component { return ''; } - renderToolbarTab(_selected, label) { + renderToolbarTab(_selected, label, icon_key) { const isActive = (this.state._selected === _selected); let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab'; return (
    { this.selectTab(_selected); }}> - timeline + {icon_key}
    {label}
    ); } - 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')} -
    -
    - ) - } - renderToolbarPanels() { let classes = (this.state._selected >= 0) ? 'toolbar-panels' : 'toolbar-panels folded'; return ( @@ -125,7 +110,7 @@ class Toolbar extends React.Component { {this.renderClosePanel()} {this.renderToolbarNarrativePanel()} - {/* {this.renderToolbarTagPanel()} */} + {this.renderToolbarTagPanel()}}
    ) @@ -150,15 +135,17 @@ class Toolbar extends React.Component { renderToolbarTabs() { const title = copy[this.props.language].toolbar.title; - const isTags = this.props.tags && (this.props.tags.children > 0); + const narratives_label = copy[this.props.language].toolbar.narratives_label; + const tags_label = copy[this.props.language].toolbar.tags_label; + const isTags = this.props.tags && this.props.tags.children; return (

    {title}

    {/*this.renderToolbarTab(0, 'search')*/} - {this.renderToolbarTab(0, 'Narratives')} - {(isTags) ? this.renderToolbarTab(1, 'Explore by tag') : ''} + {this.renderToolbarTab(0, narratives_label, 'timeline')} + {(isTags) ? this.renderToolbarTab(1, tags_label, 'style') : ''}
    cF.category === action.category.category); + + if (!catFilter) { + categoryFilters.push(action.category) + } else { + catFilter.active = (!!action.category.active); + } + + + return Object.assign({}, appState, { + filters: Object.assign({}, appState.filters, { + categories: categoryFilters + }) + }) +} + function updateTimeRange(appState, action) { // XXX return Object.assign({}, appState, { filters: Object.assign({}, appState.filters, { @@ -254,6 +272,8 @@ function app(appState = initial.app, action) { return updateSelected(appState, action) case UPDATE_TAGFILTERS: return updateTagFilters(appState, action) + case UPDATE_CATEGORYFILTERS: + return updateCategoryFilters(appState, action) case UPDATE_TIMERANGE: return updateTimeRange(appState, action) case UPDATE_NARRATIVE: diff --git a/src/reducers/schema/eventSchema.js b/src/reducers/schema/eventSchema.js index 7e9b38a..ae308e0 100644 --- a/src/reducers/schema/eventSchema.js +++ b/src/reducers/schema/eventSchema.js @@ -13,7 +13,7 @@ const eventSchema = Joi.object().keys({ category: Joi.string().required(), narratives: Joi.array(), sources: Joi.array(), - tags: Joi.string().allow(''), + tags: Joi.array().allow(''), comments: Joi.string().allow(''), timestamp: Joi.string().required(), diff --git a/src/scss/mediaoverlay.scss b/src/scss/mediaoverlay.scss index 564a18a..d48a851 100644 --- a/src/scss/mediaoverlay.scss +++ b/src/scss/mediaoverlay.scss @@ -2,7 +2,6 @@ $panel-width: 800px; $panel-height: 700px; $vimeo-width: $panel-width - 100; $vimeo-height: $panel-height / 2; - $padding: 20px; $header-inset: 10px; @@ -21,7 +20,6 @@ $header-inset: 10px; } .mo-container { - background-color: rgba(239, 239, 239, 0.9); // max-width: $panel-width; // min-width: $panel-width; // max-height: $panel-height; @@ -29,7 +27,7 @@ $header-inset: 10px; display: flex; flex-direction: column; align-items: center; - height: 80vh; + max-height: 80vh; max-width: 90vw; box-shadow: 0 19px 19px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22); @@ -37,8 +35,35 @@ $header-inset: 10px; flex: 1; display: flex; flex-direction: column; + align-items: center; + } +} + +.mo-header { + min-height: 42px; + max-height: 42px; + margin-bottom: 2px; + border-radius: 2px; + width: 100%; + display: flex; + flex-direction: row; + background-color: black; + color: white; + + .mo-header-close { + display: flex; justify-content: center; align-items: center; + margin-left: $header-inset + 8px; + } + + .mo-header-text { + flex: 1; + display: flex; + align-items: center; + justify-content: flex-end; + padding-right: $padding; + font-family: "Lato", Helvetica, sans-serif; } } @@ -69,6 +94,7 @@ $header-inset: 10px; } .mo-media-container { + background-color: rgba(239, 239, 239, 0.9); box-sizing: border-box; min-width: 100%; max-height: 60vh; @@ -93,6 +119,13 @@ $header-inset: 10px; } .mo-meta-container { + background-color: rgba(239, 239, 239, 0.9); + display: flex; + justify-content: center; + box-sizing: border-box; + min-height: 100px; + min-width: $panel-width; + max-width: $panel-height; display: flex; justify-content: center; box-sizing: border-box; @@ -191,9 +224,8 @@ $header-inset: 10px; } .source-image-container, .source-text-container { - padding: 0 10em; - display: flex; - justify-content: center; + padding: $padding; + display: inline-block; align-items: center; } diff --git a/src/scss/timeline.scss b/src/scss/timeline.scss index 84432c0..72c19e4 100644 --- a/src/scss/timeline.scss +++ b/src/scss/timeline.scss @@ -198,6 +198,10 @@ stroke-dasharray: 1px 2px; } + .datetime { + /*transition: transform 0.2s ease;*/ + } + .event { cursor: pointer; opacity: .7; @@ -212,6 +216,7 @@ stroke: $offwhite; stroke-width: 2; stroke-dasharray: 5px 2px; + transition: transform 0.2s ease; } .coevent { diff --git a/src/selectors/index.js b/src/selectors/index.js index 4f03b5e..53cae3f 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -20,6 +20,7 @@ export const getSources = 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 getTimeRange = state => state.app.filters.timerange @@ -33,8 +34,7 @@ export const getTimeRange = state => state.app.filters.timerange */ function isTaggedIn(event, tagFilters) { if (event.tags) { - const tagsInEvent = event.tags.split(",") - const isTagged = tagsInEvent.some((tag) => { + const isTagged = event.tags.some((tag) => { return tagFilters.find(tF => (tF.key === tag && tF.active)) }) return isTagged @@ -43,6 +43,19 @@ function isTaggedIn(event, tagFilters) { } } +/** + * 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 */ @@ -54,6 +67,17 @@ function isNoTags(tagFilters) { ) } +/* +* 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 @@ -70,14 +94,15 @@ function isTimeRangedIn(event, timeRange) { * and if TAGS are being used, select them if their tags are enabled */ export const selectEvents = createSelector( - [getEvents, getTagsFilter, getTimeRange], - (events, tagFilters, timeRange) => { + [getEvents, getTagsFilter, getCategoriesFilter, getTimeRange], + (events, tagFilters, categories, 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) - if (isTimeRanged && isTagged) { + if (isTimeRanged && isTagged && isTaggedWithCategory) { const eventClone = Object.assign({}, event) acc[event.id] = eventClone } @@ -91,16 +116,14 @@ export const selectEvents = createSelector( * and if TAGS are being used, select them if their tags are enabled */ export const selectNarratives = createSelector( - [getEvents, getNarratives, getTagsFilter, getTimeRange, getSources], - (events, narrativesMeta, tagFilters, timeRange, sources) => { + [getEvents, getNarratives, getSources], + (events, narrativesMeta, sources) => { const narratives = {} const narrativeSkeleton = id => ({ id, steps: [] }) /* populate narratives dict with events */ events.forEach(evt => { - const isTagged = isTaggedIn(evt, tagFilters) || isNoTags(tagFilters) - const isTimeRanged = isTimeRangedIn(evt, timeRange) const isInNarrative = evt.narratives.length > 0 evt.narratives.forEach(narrative => { @@ -122,11 +145,6 @@ export const selectNarratives = createSelector( steps.sort(compareTimestamp) - // 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 - // }) - if (narrativesMeta.find(n => n.id === key)) { narratives[key] = { ...narrativesMeta.find(n => n.id === key), @@ -135,7 +153,9 @@ export const selectNarratives = createSelector( } }) - return Object.values(narratives) + // Return narratives in original order + // + filter those that are undefined + return narrativesMeta.map(n => narratives[n.id]).filter(d => d); }) /** Aggregate information about the narrative and the current step into @@ -230,7 +250,12 @@ export const selectSelected = createSelector( */ export const selectCategories = createSelector( [getCategories], - (categories) => categories + (categories) => { + categories.map(cat => { + cat.active = (!cat.hasOwnProperty('active')) ? false : cat.active + }); + return categories; + } )