diff --git a/src/actions/index.js b/src/actions/index.js index 590f56d..1264aa2 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -81,8 +81,21 @@ export function fetchDomain () { .catch(handleError('tags')) } - return Promise.all([eventPromise, catPromise, narPromise, - sitesPromise, tagsPromise]) + let sourcesPromise = Promise.resolve([]) + if (process.env.features.USE_SOURCES) { + sourcesPromise = fetch(SOURCES_URL) + .then(response => response.json()) + .catch(handleError('sources')) + } + + return Promise.all([ + eventPromise, + catPromise, + narPromise, + sitesPromise, + tagsPromise, + sourcesPromise + ]) .then(response => { dispatch(toggleFetchingDomain()) const result = { @@ -91,6 +104,7 @@ export function fetchDomain () { narratives: response[2], sites: response[3], tags: response[4], + sources: response[5], notifications } return result @@ -114,30 +128,19 @@ export const UPDATE_DOMAIN = 'UPDATE_DOMAIN' export function updateDomain(domain) { return { type: UPDATE_DOMAIN, - domain: { - events: domain.events, - categories: domain.categories, - tags: domain.tags, - sites: domain.sites, - narratives: domain.narratives, - notifications: domain.notifications - } + domain } } -export function fetchSelected(selected) { - if (!selected || !selected.length || selected.length === 0) { - return updateSelected([]) - } +export function fetchSource(source) { return dispatch => { - dispatch(updateSelected(selected)) if (!SOURCES_URL) { dispatch(fetchSourceError('No source extension specified.')) } else { dispatch(toggleFetchingSources()) - fetch(SOURCES_URL) + fetch(`${SOURCES_URL}`) .then(response => { if (!response.ok) { throw new Error('No sources are available at the URL specified in the config specified.') diff --git a/src/components/Card.jsx b/src/components/Card.jsx index 520b7b9..b85a144 100644 --- a/src/components/Card.jsx +++ b/src/components/Card.jsx @@ -86,17 +86,17 @@ class Card extends React.Component { ) } - renderSource() { - return ( + renderSources() { + return this.props.event.sources.map(source => ( - ) + )) } // NB: should be internaionalized. @@ -145,7 +145,7 @@ class Card extends React.Component { return (
{this.renderTags()} - {this.renderSource()} + {this.renderSources()} {this.renderNarrative()}
) diff --git a/src/components/CardStack.jsx b/src/components/CardStack.jsx index 98185be..bb47d71 100644 --- a/src/components/CardStack.jsx +++ b/src/components/CardStack.jsx @@ -89,7 +89,7 @@ class CardStack extends React.Component { function mapStateToProps(state) { return { - selected: state.app.selected, + selected: selectors.selectSelected(state), sourceError: state.app.errors.source, language: state.app.language, isCardstack: state.app.flags.isCardstack, diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx index 744b990..479be0a 100644 --- a/src/components/Dashboard.jsx +++ b/src/components/Dashboard.jsx @@ -32,7 +32,7 @@ class Dashboard extends React.Component { componentDidMount() { if (!this.props.app.isMobile) { this.props.actions.fetchDomain() - .then((domain) => this.props.actions.updateDomain(domain)); + .then(domain => this.props.actions.updateDomain(domain)); } } @@ -51,7 +51,7 @@ class Dashboard extends React.Component { let eventsToSelect = selected.map(event => this.getEventById(event.id)); eventsToSelect = eventsToSelect.sort((a, b) => parseDate(a.timestamp) - parseDate(b.timestamp)) - this.props.actions.fetchSelected(eventsToSelect) + this.props.actions.updateSelected(eventsToSelect) } } diff --git a/src/components/presentational/CardSource.js b/src/components/presentational/CardSource.js index 95fdb06..fe67652 100644 --- a/src/components/presentational/CardSource.js +++ b/src/components/presentational/CardSource.js @@ -3,23 +3,27 @@ import Spinner from './Spinner' import copy from '../../js/data/copy.json' +function renderSource(source) { + return source.error ? ( +
{source.error}
+ ) : ( +
+

{source.id}

+
+ ) +} + const CardSource = ({ source, language, isLoading, error }) => { const source_lang = copy[language].cardstack.source - function renderSource() { - return source.error ? ( -
{source.error}
- ) : ( -

TODO: display source properly.

- ) - } - function renderContent() { - return isLoading ? : renderSource() + return isLoading + ? + : renderSource(source) } return ( -
+

{source_lang}:

{renderContent()}
diff --git a/src/reducers/schema/eventSchema.js b/src/reducers/schema/eventSchema.js index 9dd4f90..b497841 100644 --- a/src/reducers/schema/eventSchema.js +++ b/src/reducers/schema/eventSchema.js @@ -12,7 +12,7 @@ const eventSchema = Joi.object().keys({ type: Joi.string().allow(''), category: Joi.string().required(), narrative: Joi.string().allow(''), - source: Joi.string().allow(''), + sources: Joi.array(), tags: Joi.string().allow(''), comments: Joi.string().allow(''), timestamp: Joi.string().required(), diff --git a/src/reducers/schema/sourceSchema.js b/src/reducers/schema/sourceSchema.js new file mode 100644 index 0000000..3dc17dd --- /dev/null +++ b/src/reducers/schema/sourceSchema.js @@ -0,0 +1,17 @@ +import Joi from 'joi'; + +const sourceSchema = Joi.object().keys({ + id: Joi.string().required(), + path: Joi.string().required(), + type: Joi.string().allow(''), + affil_1: Joi.string().allow(''), + affil_2: Joi.string().allow(''), + url: Joi.string().allow(''), + title: Joi.string().allow(''), + parent: Joi.string().allow(''), + author: Joi.string().allow(''), + date: Joi.string().allow(''), + notes: Joi.string().allow('') +}); + +export default sourceSchema; diff --git a/src/reducers/utils/validators.js b/src/reducers/utils/validators.js index ecc8f87..42bf3d3 100644 --- a/src/reducers/utils/validators.js +++ b/src/reducers/utils/validators.js @@ -1,9 +1,10 @@ import Joi from 'joi'; -import eventSchema from '../schema/eventSchema.js'; -import categorySchema from '../schema/categorySchema.js'; -import siteSchema from '../schema/siteSchema.js'; -import narrativeSchema from '../schema/narrativeSchema.js'; +import eventSchema from '../schema/eventSchema'; +import categorySchema from '../schema/categorySchema'; +import siteSchema from '../schema/siteSchema'; +import narrativeSchema from '../schema/narrativeSchema'; +import sourceSchema from '../schema/sourceSchema' import { capitalize } from './helpers.js'; @@ -59,6 +60,7 @@ export function validateDomain (domain) { categories: [], sites: [], narratives: [], + sources: {}, notifications: domain.notifications, tags: {} } @@ -68,33 +70,50 @@ export function validateDomain (domain) { categories: [], sites: [], narratives: [], + sources: [], } - function validateItem(item, domainClass, schema) { + function validateArrayItem(item, domainKey, schema) { const result = Joi.validate(item, schema); if (result.error !== null) { const id = item.id || '-'; - const domainStr = capitalize(domainClass); + const domainStr = capitalize(domainKey); const error = makeError(domainStr, id, result.error.message); - discardedDomain[domainClass].push(Object.assign(item, { error })); + discardedDomain[domainKey].push(Object.assign(item, { error })); } else { - sanitizedDomain[domainClass].push(item); + sanitizedDomain[domainKey].push(item); } } - domain.events.forEach(event => { - validateItem(event, 'events', eventSchema); - }); - domain.categories.forEach(category => { - validateItem(category, 'categories', categorySchema); - }); - domain.sites.forEach(site => { - validateItem(site, 'sites', siteSchema); - }); - domain.narratives.forEach(narrative => { - validateItem(narrative, 'narratives', narrativeSchema); - }); + function validateArray(items, domainKey, schema) { + items.forEach(item => { + validateArrayItem(item, domainKey, schema) + }) + } + + function validateObject(obj, domainKey, itemSchema) { + Object.keys(obj).forEach(key => { + const vl = obj[key] + const result = Joi.validate(vl, itemSchema) + if (result.error !== null) { + const id = vl.id || '-' + const domainStr = capitalize(domainKey) + discardedDomain[domainKey].push({ + ...vl, + error: makeError(domainStr, id, result.error.message) + }) + } else { + sanitizedDomain[domainKey][key] = vl + } + }) + } + + validateArray(domain.events, 'events', eventSchema); + validateArray(domain.categories, 'categories', categorySchema); + validateArray(domain.sites, 'sites', siteSchema); + validateArray(domain.narratives, 'narratives', narrativeSchema); + validateObject(domain.sources, 'sources', sourceSchema); // Message the number of failed items in domain diff --git a/src/scss/cardstack.scss b/src/scss/cardstack.scss index 70393db..1879969 100644 --- a/src/scss/cardstack.scss +++ b/src/scss/cardstack.scss @@ -1,6 +1,8 @@ @import 'burger'; @import 'card'; +$card-width: 500px; + .card-stack { position: absolute; top: 10px; @@ -20,7 +22,7 @@ .card-stack-header { min-height: 38px; line-height: 38px; - width: 360px; + width: $card-width; box-sizing: border-box; padding: 0 5px; background: $black; @@ -61,7 +63,7 @@ } .card-stack-content { - width: 360px; + width: $card-width; ul { padding: 0; diff --git a/src/selectors/index.js b/src/selectors/index.js index 76f8f40..32a4e49 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -1,16 +1,19 @@ -import { - createSelector -} from 'reselect' +import { createSelector} 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 getSelected = state => state.app.selected; export const getSites = (state) => { if (process.env.features.USE_SITES) return state.domain.sites; return []; } +export const getSources = state => { + if (process.env.features.USE_SOURCES) return state.domain.sources; + return []; +} export const getNotifications = state => state.domain.notifications; export const getTagTree = state => state.domain.tags; export const getTagsFilter = state => state.app.filters.tags; @@ -152,6 +155,27 @@ export const selectLocations = createSelector( } ); +/** + * Of all the sources, select those that are relevant to the selected events. + */ +export const selectSelected = createSelector( + [getSelected, getSources], + (selected, sources) => { + if (selected.length === 0) { + return [] + } + const srcs = selected + .map(e => e.sources) + .map(_sources => + _sources.map(id => sources[id]) + ) + + return selected.map((s, idx) => ({ + ...s, + sources: srcs[idx] + })) + } +) /* * Select categories, return them as a list diff --git a/src/store/initial.js b/src/store/initial.js index dbe0dd7..03d8fef 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -106,7 +106,7 @@ const initial = { ui: { style: { categories: { - default: 'red', + default: 'yellow', // Add here other categories to differentiate by color, like: alpha: '#00ff00', beta: '#ff0000', @@ -115,17 +115,11 @@ const initial = { narratives: { default: { - style: 'dotted', // ['dotted', 'solid'] - opacity: 0.9, // range between 0 and 1 - stroke: 'red', // Any hex or rgb code + style: 'solid', // ['dotted', 'solid'] + opacity: 0.5, // range between 0 and 1 + stroke: 'transparent', // Any hex or rgb code strokeWidth: 2 }, - narrative_1: { - style: 'solid', // ['dotted', 'solid'] - opacity: 0.4, // range between 0 and 1 - stroke: '#f18f01', // Any hex or rgb code - strokeWidth: 2 - } } }, dom: { diff --git a/webpack.config.js b/webpack.config.js index 4706f03..768e021 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -64,7 +64,8 @@ const config = { 'features': { 'USE_TAGS': JSON.stringify(userConfig.features.USE_TAGS), 'USE_SEARCH': JSON.stringify(userConfig.features.USE_SEARCH), - 'USE_SITES': JSON.stringify(userConfig.features.USE_SITES) + 'USE_SITES': JSON.stringify(userConfig.features.USE_SITES), + 'USE_SOURCES': JSON.stringify(userConfig.features.USE_SOURCES) } } }),