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 (
-
-
-
- {/*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 (
{/*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;
+ }
)