mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-11 21:08:36 +03:00
Merge pull request #50 from forensic-architecture/topic/sources
Topic/sources
This commit is contained in:
@@ -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.')
|
||||
|
||||
@@ -86,17 +86,17 @@ class Card extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
renderSource() {
|
||||
return (
|
||||
renderSources() {
|
||||
return this.props.event.sources.map(source => (
|
||||
<CardSource
|
||||
isLoading={this.props.isLoading}
|
||||
language={this.props.language}
|
||||
source={{
|
||||
...this.props.source,
|
||||
...source,
|
||||
error: this.props.sourceError
|
||||
}}
|
||||
/>
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
// NB: should be internaionalized.
|
||||
@@ -145,7 +145,7 @@ class Card extends React.Component {
|
||||
return (
|
||||
<div className="card-bottomhalf">
|
||||
{this.renderTags()}
|
||||
{this.renderSource()}
|
||||
{this.renderSources()}
|
||||
{this.renderNarrative()}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,23 +3,27 @@ import Spinner from './Spinner'
|
||||
|
||||
import copy from '../../js/data/copy.json'
|
||||
|
||||
function renderSource(source) {
|
||||
return source.error ? (
|
||||
<div><small>{source.error}</small></div>
|
||||
) : (
|
||||
<div>
|
||||
<p>{source.id}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CardSource = ({ source, language, isLoading, error }) => {
|
||||
const source_lang = copy[language].cardstack.source
|
||||
|
||||
function renderSource() {
|
||||
return source.error ? (
|
||||
<div><small>{source.error}</small></div>
|
||||
) : (
|
||||
<div><p>TODO: display source properly.</p></div>
|
||||
)
|
||||
}
|
||||
|
||||
function renderContent() {
|
||||
return isLoading ? <Spinner/> : renderSource()
|
||||
return isLoading
|
||||
? <Spinner/>
|
||||
: renderSource(source)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card-col card-cell source">
|
||||
<div className="card-row card-cell source">
|
||||
<h4>{source_lang}: </h4>
|
||||
{renderContent()}
|
||||
</div>
|
||||
|
||||
@@ -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(),
|
||||
|
||||
17
src/reducers/schema/sourceSchema.js
Normal file
17
src/reducers/schema/sourceSchema.js
Normal file
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user