Merge pull request #92 from forensic-architecture/add-linting

Add linting
This commit is contained in:
Lachlan Kermode
2019-01-22 18:15:20 +00:00
committed by GitHub
47 changed files with 1130 additions and 597 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.idea/
build/
node_modules/
config.js

View File

@@ -8,7 +8,8 @@
"dev": "webpack-dev-server --content-base static --mode development",
"build": "NODE_ENV=production webpack --mode production",
"test": "ava --verbose",
"test-watch": "ava --watch"
"test-watch": "ava --watch",
"lint": "standard \"src/**/*.js\" \"test/**/*.js\""
},
"dependencies": {
"babel-polyfill": "^6.26.0",
@@ -48,6 +49,7 @@
"node-sass": "^4.9.4",
"redux-devtools": "^3.4.0",
"sass-loader": "^7.1.0",
"standard": "^12.0.1",
"style-loader": "^0.23.1",
"webpack": "^4.20.2",
"webpack-cli": "^3.1.2",

View File

@@ -1,3 +1,4 @@
/* global fetch, alert */
import { urlFromEnv } from '../js/utilities'
// TODO: relegate these URLs entirely to environment variables
@@ -8,11 +9,9 @@ const SOURCES_URL = urlFromEnv('SOURCES_EXT')
const NARRATIVE_URL = urlFromEnv('NARRATIVE_EXT')
const SITES_URL = urlFromEnv('SITES_EXT')
const SHAPES_URL = urlFromEnv('SHAPES_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.`
export function fetchDomain () {
let notifications = []
@@ -26,7 +25,6 @@ export function fetchDomain () {
return dispatch => {
dispatch(toggleFetchingDomain())
const promises = []
const eventPromise = fetch(EVENT_DATA_URL)
.then(response => response.json())
@@ -61,7 +59,7 @@ export function fetchDomain () {
let sourcesPromise = Promise.resolve([])
if (process.env.features.USE_SOURCES) {
if (!SOURCES_URL) {
sourcesPromise = Promise.resolve(makeError('USE_SOURCES is true, but you have not provided a SOURCES_EXT'))
sourcesPromise = Promise.resolve(handleError('USE_SOURCES is true, but you have not provided a SOURCES_EXT'))
} else {
sourcesPromise = fetch(SOURCES_URL)
.then(response => response.json())
@@ -111,23 +109,22 @@ export function fetchDomain () {
}
export const FETCH_ERROR = 'FETCH_ERROR'
export function fetchError(message) {
export function fetchError (message) {
return {
type: FETCH_ERROR,
message,
message
}
}
export const UPDATE_DOMAIN = 'UPDATE_DOMAIN'
export function updateDomain(domain) {
export function updateDomain (domain) {
return {
type: UPDATE_DOMAIN,
domain
}
}
export function fetchSource(source) {
export function fetchSource (source) {
return dispatch => {
if (!SOURCES_URL) {
dispatch(fetchSourceError('No source extension specified.'))
@@ -146,14 +143,12 @@ export function fetchSource(source) {
dispatch(fetchSourceError(err.message))
dispatch(toggleFetchingSources())
})
}
}
}
export const UPDATE_HIGHLIGHTED = 'UPDATE_HIGHLIGHTED'
export function updateHighlighted(highlighted) {
export function updateHighlighted (highlighted) {
return {
type: UPDATE_HIGHLIGHTED,
highlighted: highlighted
@@ -161,7 +156,7 @@ export function updateHighlighted(highlighted) {
}
export const UPDATE_SELECTED = 'UPDATE_SELECTED'
export function updateSelected(selected) {
export function updateSelected (selected) {
return {
type: UPDATE_SELECTED,
selected: selected
@@ -169,7 +164,7 @@ export function updateSelected(selected) {
}
export const UPDATE_DISTRICT = 'UPDATE_DISTRICT'
export function updateDistrict(district) {
export function updateDistrict (district) {
return {
type: UPDATE_DISTRICT,
district
@@ -177,7 +172,7 @@ export function updateDistrict(district) {
}
export const UPDATE_TAGFILTERS = 'UPDATE_TAGFILTERS'
export function updateTagFilters(tag) {
export function updateTagFilters (tag) {
return {
type: UPDATE_TAGFILTERS,
tag
@@ -185,15 +180,15 @@ export function updateTagFilters(tag) {
}
export const UPDATE_CATEGORYFILTERS = 'UPDATE_CATEGORYFILTERS'
export function updateCategoryFilters(category) {
export function updateCategoryFilters (category) {
return {
type: UPDATE_CATEGORYFILTERS,
category
}
}
export const UPDATE_TIMERANGE = 'UPDATE_TIMERANGE';
export function updateTimeRange(timerange) {
export const UPDATE_TIMERANGE = 'UPDATE_TIMERANGE'
export function updateTimeRange (timerange) {
return {
type: UPDATE_TIMERANGE,
timerange
@@ -201,7 +196,7 @@ export function updateTimeRange(timerange) {
}
export const UPDATE_NARRATIVE = 'UPDATE_NARRATIVE'
export function updateNarrative(narrative) {
export function updateNarrative (narrative) {
return {
type: UPDATE_NARRATIVE,
narrative
@@ -209,28 +204,28 @@ export function updateNarrative(narrative) {
}
export const INCREMENT_NARRATIVE_CURRENT = 'INCREMENT_NARRATIVE_CURRENT'
export function incrementNarrativeCurrent() {
export function incrementNarrativeCurrent () {
return {
type: INCREMENT_NARRATIVE_CURRENT
}
}
export const DECREMENT_NARRATIVE_CURRENT = 'DECREMENT_NARRATIVE_CURRENT'
export function decrementNarrativeCurrent() {
export function decrementNarrativeCurrent () {
return {
type: DECREMENT_NARRATIVE_CURRENT
}
}
export const RESET_ALLFILTERS = 'RESET_ALLFILTERS'
export function resetAllFilters() {
export function resetAllFilters () {
return {
type: RESET_ALLFILTERS
}
}
export const UPDATE_SOURCE = "UPDATE_SOURCE"
export function updateSource(source) {
export const UPDATE_SOURCE = 'UPDATE_SOURCE'
export function updateSource (source) {
return {
type: UPDATE_SOURCE,
source
@@ -240,65 +235,57 @@ export function updateSource(source) {
// UI
export const TOGGLE_SITES = 'TOGGLE_SITES'
export function toggleSites() {
export function toggleSites () {
return {
type: TOGGLE_SITES
}
}
export const TOGGLE_FETCHING_DOMAIN = 'TOGGLE_FETCHING_DOMAIN'
export function toggleFetchingDomain() {
export function toggleFetchingDomain () {
return {
type: TOGGLE_FETCHING_DOMAIN
}
}
export const TOGGLE_FETCHING_SOURCES = 'TOGGLE_FETCHING_SOURCES'
export function toggleFetchingSources() {
export function toggleFetchingSources () {
return {
type: TOGGLE_FETCHING_SOURCES
}
}
export const TOGGLE_LANGUAGE = 'TOGGLE_LANGUAGE'
export function toggleLanguage(language) {
export function toggleLanguage (language) {
return {
type: TOGGLE_LANGUAGE,
language,
language
}
}
export const CLOSE_TOOLBAR = 'CLOSE_TOOLBAR'
export function closeToolbar() {
export function closeToolbar () {
return {
type: CLOSE_TOOLBAR
}
}
export const TOGGLE_INFOPOPUP = 'TOGGLE_INFOPOPUP'
export function toggleInfoPopup() {
export function toggleInfoPopup () {
return {
type: TOGGLE_INFOPOPUP
}
}
export const TOGGLE_MAPVIEW = 'TOGGLE_MAPVIEW'
export function toggleMapView(layer) {
return {
type: TOGGLE_MAPVIEW,
layer
}
}
export const TOGGLE_NOTIFICATIONS = 'TOGGLE_NOTIFICATIONS'
export function toggleNotifications() {
export function toggleNotifications () {
return {
type: TOGGLE_NOTIFICATIONS
}
}
export const MARK_NOTIFICATIONS_READ = 'MARK_NOTIFICATIONS_READ'
export function markNotificationsRead() {
export function markNotificationsRead () {
return {
type: MARK_NOTIFICATIONS_READ
}
@@ -307,7 +294,7 @@ export function markNotificationsRead() {
// ERRORS
export const FETCH_SOURCE_ERROR = 'FETCH_SOURCE_ERROR'
export function fetchSourceError(msg) {
export function fetchSourceError (msg) {
return {
type: FETCH_SOURCE_ERROR,
msg

View File

@@ -1,26 +1,18 @@
import React from 'react';
import SitesIcon from './presentational/Icons/SitesIcon.js';
import RefreshIcon from './presentational/Icons/RefreshIcon.js';
import CoeventIcon from './presentational/Icons/CoeventIcon.js';
import RouteIcon from './presentational/Icons/RouteIcon.js';
// import RefreshIcon from './presentational/Icons/RefreshIcon.js';
// import CoeventIcon from './presentational/Icons/CoeventIcon.js';
// import RouteIcon from './presentational/Icons/RouteIcon.js';
function ToolbarBottomActions (props) {
function renderMapActions() {
return (
<div className="bottom-action-block">
{/* <RouteIcon */}
{/* onClick={(view) => this.toggleMapViews(view)} */}
{/* isEnabled={this.props.viewFilters.routes} */}
{/* /> */}
{process.env.features.USE_SITES ? <SitesIcon
isActive={props.sites.enabled}
onClickHandler={props.sites.toggle}
/> : null}
{/* <CoeventIcon */}
{/* onClick={(view) => this.toggleMapViews(view)} */}
{/* isEnabled={this.props.viewFilters.coevents} */}
{/* /> */}
</div>
);
}

View File

@@ -1,18 +1,17 @@
import React from 'react';
import React from 'react'
const CardCaret = ({ isHighlighted, toggle }) => {
let classes = (isHighlighted)
? 'arrow-down'
: 'arrow-down folded';
: 'arrow-down folded'
return (
<div className="card-toggle" onClick={toggle}>
<div className='card-toggle' onClick={toggle}>
<p>
<i className={classes}></i>
<i className={classes} />
</p>
</div>
);
)
}
export default CardCaret;
export default CardCaret

View File

@@ -1,15 +1,15 @@
import React from 'react';
import React from 'react'
import { capitalizeFirstLetter } from '../../../js/utilities.js';
import { capitalizeFirstLetter } from '../../../js/utilities.js'
const CardCategory = ({ categoryTitle, categoryLabel, color }) => (
<div className="card-row card-cell category">
<div className='card-row card-cell category'>
<h4>{categoryTitle}</h4>
<p>
{capitalizeFirstLetter(categoryLabel)}
<span className='color-category' style={{ background: color }}/>
<span className='color-category' style={{ background: color }} />
</p>
</div>
);
)
export default CardCategory;
export default CardCategory

View File

@@ -1,30 +1,29 @@
import React from 'react';
import React from 'react'
import copy from '../../../js/data/copy.json';
import { isNotNullNorUndefined } from '../../../js/utilities';
import copy from '../../../js/data/copy.json'
import { isNotNullNorUndefined } from '../../../js/utilities'
const CardLocation = ({ language, location }) => {
if (isNotNullNorUndefined(location)) {
return (
<div className="card-cell location">
<div className='card-cell location'>
<p>
<i className="material-icons left">location_on</i>
<i className='material-icons left'>location_on</i>
{location}
</p>
</div>
);
)
} else {
const unknown = copy[language].cardstack.unknown_location;
const unknown = copy[language].cardstack.unknown_location
return (
<div className="card-cell location">
<div className='card-cell location'>
<p>
<i className="material-icons left">location_on</i>
<i className='material-icons left'>location_on</i>
{unknown}
</p>
</div>
);
)
}
}
export default CardLocation;
export default CardLocation

View File

@@ -1,15 +1,15 @@
import React from 'react';
import React from 'react'
import CardNarrativeLink from './NarrativeLink';
import CardNarrativeLink from './NarrativeLink'
const CardNarrative = (props) => (
<div className="card-row">
<div className='card-row'>
<h4>Connected events</h4>
<div className="card-cell">
<p>&larr; <CardNarrativeLink {...props} event={props.next}/></p>
<div className='card-cell'>
<p>&larr; <CardNarrativeLink {...props} event={props.next} /></p>
<p>&rarr; <CardNarrativeLink {...props} event={props.prev} /></p>
</div>
</div>
);
)
export default CardNarrative;
export default CardNarrative

View File

@@ -1,17 +1,17 @@
import React from 'react';
import React from 'react'
const CardNarrativeLink = ({ event, makeTimelabel, select }) => {
if (event !== null) {
const timelabel = makeTimelabel(event.timestamp);
const timelabel = makeTimelabel(event.timestamp)
return (
<a onClick={() => select(event)}>
<small>{`${timelabel} / ${event.location}`}</small>
</a>
);
)
}
return (<a className="disabled"><small>None</small></a>);
return (<a className='disabled'><small>None</small></a>)
}
export default CardNarrativeLink;
export default CardNarrativeLink

View File

@@ -3,11 +3,10 @@ import PropTypes from 'prop-types'
import Img from 'react-image'
import Spinner from '../Spinner'
import copy from '../../../js/data/copy.json'
const CardSource = ({ source, isLoading, onClickHandler }) => {
function renderIconText(type) {
switch(type) {
function renderIconText (type) {
switch (type) {
case 'Eyewitness Testimony':
return 'visibility'
case 'Government Data':
@@ -29,7 +28,7 @@ const CardSource = ({ source, isLoading, onClickHandler }) => {
if (!source) {
return (
<div className="card-source">
<div className='card-source'>
<div>Error: this source was not found</div>
</div>
)
@@ -45,30 +44,30 @@ const CardSource = ({ source, isLoading, onClickHandler }) => {
}
const fallbackIcon = (
<i className="material-icons source-icon">
<i className='material-icons source-icon'>
{renderIconText(source.type)}
</i>
)
return (
<div className="card-source">
<div className='card-source'>
{isLoading
? <Spinner/>
: (
<div className="source-row" onClick={() => onClickHandler(source)}>
{!!thumbnail ? (
<Img
className="source-icon"
src={thumbnail}
loader={<Spinner small />}
unloader={fallbackIcon}
width={30}
height={30}
/>
) : fallbackIcon}
<p>{source.id}</p>
</div>
)}
? <Spinner />
: (
<div className='source-row' onClick={() => onClickHandler(source)}>
{thumbnail ? (
<Img
className='source-icon'
src={thumbnail}
loader={<Spinner small />}
unloader={fallbackIcon}
width={30}
height={30}
/>
) : fallbackIcon}
<p>{source.id}</p>
</div>
)}
</div>
)
}
@@ -79,7 +78,7 @@ CardSource.propTypes = {
type: PropTypes.string
}),
isLoading: PropTypes.bool,
onClickHandler: PropTypes.func.isRequired,
onClickHandler: PropTypes.func.isRequired
}
export default CardSource

View File

@@ -1,19 +1,18 @@
import React from 'react';
import React from 'react'
import copy from '../../../js/data/copy.json';
import copy from '../../../js/data/copy.json'
const CardSummary = ({ language, description, isHighlighted }) => {
const summary = copy[language].cardstack.description;
const summary = copy[language].cardstack.description
return (
<div className="card-row summary">
<div className="card-cell">
<div className='card-row summary'>
<div className='card-cell'>
<h4>{summary}</h4>
<p>{description}</p>
</div>
</div>
);
)
}
export default CardSummary;
export default CardSummary

View File

@@ -1,37 +1,36 @@
import React from 'react';
import React from 'react'
import copy from '../../../js/data/copy.json';
import copy from '../../../js/data/copy.json'
const CardTags = ({ tags, language }) => {
const tags_lang = copy[language].cardstack.tags;
const no_tags_lang = copy[language].cardstack.notags;
const tagsLang = copy[language].cardstack.tags
const noTagsLang = copy[language].cardstack.notags
if (tags.length > 0) {
return (
<div className="card-row card-cell tags">
<h4>{tags_lang}:</h4>
<div className='card-row card-cell tags'>
<h4>{tagsLang}:</h4>
<p>
{tags.map((tag, idx) => {
return (
<span className="tag">
<small>{tag.name}</small>
{(idx < tags.length - 1)
? ','
: ''}
</span>
);
return (
<span className='tag'>
<small>{tag.name}</small>
{(idx < tags.length - 1)
? ','
: ''}
</span>
)
})}
</p>
</div>
);
)
}
return (
<div className="card-row card-cell tags">
<h4>{tags_lang}</h4>
<p><small>{no_tags_lang}</small></p>
<div className='card-row card-cell tags'>
<h4>{tagsLang}</h4>
<p><small>{noTagsLang}</small></p>
</div>
);
)
}
export default CardTags;
export default CardTags

View File

@@ -1,34 +1,33 @@
import React from 'react';
import React from 'react'
import copy from '../../../js/data/copy.json';
import { isNotNullNorUndefined } from '../../../js/utilities';
import copy from '../../../js/data/copy.json'
import { isNotNullNorUndefined } from '../../../js/utilities'
const CardTimestamp = ({ makeTimelabel, language, timestamp }) => {
const daytime_lang = copy[language].cardstack.timestamp;
const estimated_lang = copy[language].cardstack.estimated;
const unknown_lang = copy[language].cardstack.unknown_time;
// const daytimeLang = copy[language].cardstack.timestamp
// const estimatedLang = copy[language].cardstack.estimated
const unknownLang = copy[language].cardstack.unknown_time
if (isNotNullNorUndefined(timestamp)) {
const timelabel = makeTimelabel(timestamp);
const timelabel = makeTimelabel(timestamp)
return (
<div className="card-cell timestamp">
<div className='card-cell timestamp'>
<p>
<i className="material-icons left">today</i>
<i className='material-icons left'>today</i>
{timelabel}
</p>
</div>
);
)
} else {
return (
<div className="card-cell timestamp">
<div className='card-cell timestamp'>
<p>
<i className="material-icons left">today</i>
{unknown_lang}
<i className='material-icons left'>today</i>
{unknownLang}
</p>
</div>
);
)
}
}
export default CardTimestamp;
export default CardTimestamp

View File

@@ -1,10 +1,10 @@
import React from 'react';
import React from 'react'
export default ({ label, isActive, onClickCheckbox }) => (
<div className={(isActive) ? 'item active' : 'item'}>
<span onClick={() => onClickCheckbox()}>{label}</span>
<button onClick={() => onClickCheckbox()}>
<div className="checkbox" />
<div className='checkbox' />
</button>
</div>
);
)

View File

@@ -1,24 +1,20 @@
import React from 'react';
import React from 'react'
const CoeventIcon = ({ isEnabled, toggleMapViews }) => {
const classes = (isEnabled) ? 'action-button active disabled' : 'action-button disabled';
return (
<button
className={sitesClass}
onClick={() => toggleMapViews('coevents')}
>
<svg className="coevents" x="0px" y="0px" width="30px" height="20px" viewBox="0 0 30 20" enableBackground="new 0 0 30 20">
<polygon stroke-linejoin="round" stroke-miterlimit="10" points="19.178,20 10.823,20 10.473,14.081
10,13.396 10,6.084 20,6.084 20,13.396 19.445,14.021 "/>
<rect className="no-fill" x="11.4" y="7.867" width="7.2" height="3.35"/>
<line stroke-linejoin="round" stroke-miterlimit="10" x1="12.125" y1="1" x2="12.125" y2="5.35"/>
<rect x="11.4" y="4.271" width="1.496" height="1.079"/>
<rect x="17.104" y="4.271" width="1.496" height="1.079"/>
<svg className='coevents' x='0px' y='0px' width='30px' height='20px' viewBox='0 0 30 20' enableBackground='new 0 0 30 20'>
<polygon stroke-linejoin='round' stroke-miterlimit='10' points='19.178,20 10.823,20 10.473,14.081
10,13.396 10,6.084 20,6.084 20,13.396 19.445,14.021 ' />
<rect className='no-fill' x='11.4' y='7.867' width='7.2' height='3.35' />
<line stroke-linejoin='round' stroke-miterlimit='10' x1='12.125' y1='1' x2='12.125' y2='5.35' />
<rect x='11.4' y='4.271' width='1.496' height='1.079' />
<rect x='17.104' y='4.271' width='1.496' height='1.079' />
</svg>
</button>
);
)
}
export default CoeventIcon;
export default CoeventIcon

View File

@@ -1,14 +1,13 @@
import React from 'react';
const RefreshIcon = ({ }) => {
import React from 'react'
const RefreshIcon = () => {
return (
<svg className="reset" x="0px" y="0px" width="25px" height="25px" viewBox="7.5 7.5 25 25" enableBackground="new 7.5 7.5 25 25">
<path stroke-width="2" stroke-miterlimit="10" d="M28.822,16.386c1.354,3.219,0.898,7.064-1.5,9.924
c-3.419,4.073-9.49,4.604-13.562,1.186c-4.073-3.417-4.604-9.49-1.187-13.562c1.987-2.368,4.874-3.54,7.74-3.433" />
<polygon points="26.137,12.748 27.621,19.464 28.9,16.741 31.898,16.503" />
<svg className='reset' x='0px' y='0px' width='25px' height='25px' viewBox='7.5 7.5 25 25' enableBackground='new 7.5 7.5 25 25'>
<path stroke-width='2' stroke-miterlimit='10' d='M28.822,16.386c1.354,3.219,0.898,7.064-1.5,9.924
c-3.419,4.073-9.49,4.604-13.562,1.186c-4.073-3.417-4.604-9.49-1.187-13.562c1.987-2.368,4.874-3.54,7.74-3.433' />
<polygon points='26.137,12.748 27.621,19.464 28.9,16.741 31.898,16.503' />
</svg>
);
)
}
export default RefreshIcon;
export default RefreshIcon

View File

@@ -1,20 +1,16 @@
import React from 'react';
import React from 'react'
const RouteIcon = ({ isEnabled, toggleMapViews }) => {
const classes = (isEnabled) ? 'action-button active disabled' : 'action-button disabled';
return (
<button
className={sitesClass}
onClick={() => toggleMapViews('routes')}
>
<svg x="0px" y="0px" width="30px" height="20px" viewBox="0 0 30 20" enableBackground="new 0 0 30 20">
<path d="M0.806,13.646h7.619c2.762,0,3-0.238,3-3v-0.414c0-2.762,0.301-3,3.246-3h14.523"/>
<polyline points="16.671,9.228 19.103,7.233 16.671,5.237 "/>
<svg x='0px' y='0px' width='30px' height='20px' viewBox='0 0 30 20' enableBackground='new 0 0 30 20'>
<path d='M0.806,13.646h7.619c2.762,0,3-0.238,3-3v-0.414c0-2.762,0.301-3,3.246-3h14.523' />
<polyline points='16.671,9.228 19.103,7.233 16.671,5.237 ' />
</svg>
</button>
);
)
}
export default RouteIcon;
export default RouteIcon

View File

@@ -1,7 +1,7 @@
import React from 'react';
import React from 'react'
const SitesIcon = ({ isActive, isDisabled, onClickHandler }) => {
let classes = (isActive) ? 'action-button enabled' : 'action-button';
let classes = (isActive) ? 'action-button enabled' : 'action-button'
if (isDisabled) {
classes = 'action-button disabled'
}
@@ -11,11 +11,11 @@ const SitesIcon = ({ isActive, isDisabled, onClickHandler }) => {
className={classes}
onClick={onClickHandler}
>
<svg x="0px" y="0px" width="30px" height="20px" viewBox="0 0 30 20" enableBackground="new 0 0 30 20">
<path d="M24.615,6.793H5.385c-2.761,0-3,0.239-3,3v0.414c0,2.762,0.239,3,3,3h7.621l1.996,2.432l1.996-2.432h7.618c2.762,0,3-0.238,3-3V9.793C27.615,7.032,27.377,6.793,24.615,6.793z"/>
<svg x='0px' y='0px' width='30px' height='20px' viewBox='0 0 30 20' enableBackground='new 0 0 30 20'>
<path d='M24.615,6.793H5.385c-2.761,0-3,0.239-3,3v0.414c0,2.762,0.239,3,3,3h7.621l1.996,2.432l1.996-2.432h7.618c2.762,0,3-0.238,3-3V9.793C27.615,7.032,27.377,6.793,24.615,6.793z' />
</svg>
</button>
)
}
export default SitesIcon;
export default SitesIcon

View File

@@ -1,21 +1,21 @@
import React from 'react';
import copy from '../../js/data/copy.json';
import React from 'react'
import copy from '../../js/data/copy.json'
const LoadingOverlay = ({ isLoading, language }) => {
let classes = 'loading-overlay';
classes += (!isLoading) ? ' hidden' : '';
let classes = 'loading-overlay'
classes += (!isLoading) ? ' hidden' : ''
return (
<div id="loading-overlay" className={classes}>
<div className="loading-wrapper">
<span id="loading-text" className="text">{copy[language].loading}</span>
<div className="spinner">
<div className="double-bounce1" />
<div className="double-bounce2" />
<div id='loading-overlay' className={classes}>
<div className='loading-wrapper'>
<span id='loading-text' className='text'>{copy[language].loading}</span>
<div className='spinner'>
<div className='double-bounce1' />
<div className='double-bounce2' />
</div>
</div>
</div>
);
};
)
}
export default LoadingOverlay;
export default LoadingOverlay

View File

@@ -3,12 +3,10 @@ import { connect } from 'react-redux'
import { selectActiveNarrative } from '../../../selectors'
function NarrativeCard ({ narrative }) {
// no display if no narrative
// no display if no narrative
const { steps, current } = narrative
if (steps[current]) {
const step = steps[current]
return (
<div className='narrative-info'>
<div className='narrative-info-header'>
@@ -27,14 +25,14 @@ function NarrativeCard ({ narrative }) {
<div className='narrative-info-desc'>
<p>{narrative.description}</p>
</div>
</div>
</div>
)
} else {
return null
}
}
function mapStateToProps(state) {
function mapStateToProps (state) {
return {
narrative: selectActiveNarrative(state)
}

View File

@@ -9,7 +9,7 @@ export default ({ onClickHandler, closeMsg }) => {
<button
className='side-menu-burg is-active'
>
<span></span>
<span />
</button>
<div className='close-text'>{closeMsg}</div>
</div>

View File

@@ -1,16 +1,16 @@
import React from 'react';
import React from 'react'
const NoSource = ({ failedUrls }) => {
const NoSource = ({ failedUrls }) => {
return (
<div className="no-source-container">
<div className="no-source-row">
<div className='no-source-container'>
<div className='no-source-row'>
<p>
<i className="material-icons no-source-icon">error</i>
</p>
<i className='material-icons no-source-icon'>error</i>
</p>
<p>No media found, as the original media has not yet been uploaded to the platform.</p>
</div>
</div>
)
}
export default NoSource;
export default NoSource

View File

@@ -1,12 +1,12 @@
import React from 'react';
import React from 'react'
const Spinner = ({ small }) => {
return (
<div className={`spinner ${small ? 'small' : ''}`}>
<div className="double-bounce-overlay"></div>
<div className="double-bounce"></div>
<div className='double-bounce-overlay' />
<div className='double-bounce' />
</div>
)
}
export default Spinner;
export default Spinner

View File

@@ -1,15 +1,14 @@
import React from 'react';
import React from 'react'
const TimelineClip = ({ dims }) => (
<clipPath id="clip">
<clipPath id='clip'>
<rect
x={dims.margin_left}
y="0"
y='0'
width={dims.width - dims.margin_left - dims.width_controls}
height={dims.height - 25}
>
</rect>
/>
</clipPath>
);
)
export default TimelineClip;
export default TimelineClip

View File

@@ -15,14 +15,12 @@ export default ({
onClick={() => onSelect(events)}
>
<circle
className="event"
className='event'
cx={0}
cy={0}
style={styleProps}
r={5}
>
</circle>
/>
{ extraRender ? extraRender() : null }
</g>
)

View File

@@ -1,8 +1,8 @@
import React from 'react';
import React from 'react'
import DatetimeDot from './DatetimeDot'
// return a list of lists, where each list corresponds to a single category
function getDotsToRender(events) {
function getDotsToRender (events) {
// each datetime needs to render as many dots as there are distinct
// categories in the events contained by the datetime.
// To this end, eventsByCategory is an intermediate data structure that
@@ -32,7 +32,7 @@ const TimelineEvents = ({
transitionDuration,
styleDatetime
}) => {
function renderDatetime(datetime) {
function renderDatetime (datetime) {
if (narrative) {
const { steps } = narrative
// check all events in the datetime before rendering in narrative
@@ -41,7 +41,7 @@ const TimelineEvents = ({
const event = datetime.events[i]
if (steps.map(s => s.id).includes(event.id)) {
isInNarrative = true
break;
break
}
}
@@ -80,11 +80,11 @@ const TimelineEvents = ({
return (
<g
clipPath={"url(#clip)"}
clipPath={'url(#clip)'}
>
{datetimes.map(datetime => renderDatetime(datetime))}
</g>
);
)
}
export default TimelineEvents;
export default TimelineEvents

View File

@@ -1,26 +1,24 @@
import React from 'react';
import React from 'react'
const TimelineHandles = ({ dims, onMoveTime }) => {
return (
<g className="time-controls-inline">
<g className='time-controls-inline'>
<g
transform={`translate(${dims.margin_left - 20}, 120)`}
onClick={() => onMoveTime('backwards')}
>
<circle r="15"></circle>
<path d="M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z" transform="rotate(270)"></path>
<circle r='15' />
<path d='M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z' transform='rotate(270)' />
</g>
<g
transform={`translate(${dims.width - dims.width_controls + 20}, 120)`}
onClick={() => onMoveTime('forward')}
>
<circle r="15"></circle>
<path d="M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z" transform="rotate(90)"></path>
<circle r='15' />
<path d='M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z' transform='rotate(90)' />
</g>
</g>
)
}
export default TimelineHandles;
export default TimelineHandles

View File

@@ -1,15 +1,15 @@
import React from 'react';
import React from 'react'
const TimelineHeader = ({ title, date0, date1, onClick, hideInfo }) => (
<div className='timeline-header'>
<div className='timeline-toggle' onClick={() => onClick()}>
<p><i className='arrow-down'></i></p>
<p><i className='arrow-down' /></p>
</div>
<div className={`timeline-info ${hideInfo ? 'hidden' : ''}`}>
<p>{title}</p>
<p>{date0} - {date1}</p>
</div>
</div>
);
)
export default TimelineHeader;
export default TimelineHeader

View File

@@ -1,38 +1,35 @@
import React from 'react';
import React from 'react'
import { formatterWithYear } from '../../../js/utilities.js';
import { formatterWithYear } from '../../../js/utilities.js'
const TimelineLabels = ({ dims, timelabels }) => {
return (
<g>
<line
class="axisBoundaries"
class='axisBoundaries'
x1={dims.margin_left}
x2={dims.margin_left}
y1="10"
y2="20"
>
</line>
y1='10'
y2='20'
/>
<line
class="axisBoundaries"
class='axisBoundaries'
x1={dims.width - dims.width_controls}
x2={dims.width - dims.width_controls}
y1="10"
y2="20"
>
</line>
y1='10'
y2='20'
/>
<text
class="timeLabel0 timeLabel"
x="5"
y="15"
class='timeLabel0 timeLabel'
x='5'
y='15'
>
{formatterWithYear(timelabels[0])}
</text>
<text
class="timelabelF timeLabel"
class='timelabelF timeLabel'
x={dims.width - dims.width_controls - 5}
y="15"
y='15'
style={{ textAnchor: 'end' }}
>
{formatterWithYear(timelabels[1])}
@@ -41,4 +38,4 @@ const TimelineLabels = ({ dims, timelabels }) => {
)
}
export default TimelineLabels;
export default TimelineLabels

View File

@@ -1,10 +1,10 @@
import React from 'react';
import React from 'react'
const TimelineMarkers = ({ getEventX, getCategoryY, transitionDuration, selected }) => {
function renderMarker(event) {
function renderMarker (event) {
return (
<circle
className="timeline-marker"
className='timeline-marker'
cx={0}
cy={0}
style={{
@@ -13,19 +13,18 @@ const TimelineMarkers = ({ getEventX, getCategoryY, transitionDuration, selected
'-moz-transition': 'none',
'opacity': 0.9
}}
r="10"
>
</circle>
r='10'
/>
)
}
return (
<g
clipPath={"url(#clip)"}
clipPath={'url(#clip)'}
>
{selected.map(event => renderMarker(event))}
</g>
);
)
}
export default TimelineMarkers;
export default TimelineMarkers

View File

@@ -1,12 +1,12 @@
import React from 'react';
import React from 'react'
const TimelineZoomControls = ({ extent, zoomLevels, dims, onApplyZoom }) => {
function renderZoom(zoom, idx) {
function renderZoom (zoom, idx) {
const isActive = (zoom.duration === extent)
return (
<text
className={`zoom-level-button ${isActive ? 'active' : ''}`}
x="60"
x='60'
y={(idx * 15) + 20}
onClick={() => onApplyZoom(zoom)}
>
@@ -19,7 +19,7 @@ const TimelineZoomControls = ({ extent, zoomLevels, dims, onApplyZoom }) => {
<g transform={`translate(${dims.width - dims.width_controls}, 0)`}>
{zoomLevels.map((z, idx) => renderZoom(z, idx))}
</g>
);
)
}
export default TimelineZoomControls;
export default TimelineZoomControls

View File

@@ -1,20 +1,21 @@
/* global d3 */
/**
* Get URI params to start with predefined set of
* https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
* @param {string} name: name of paramater to search
* @param {string} url: url passed as variable, defaults to window.location.href
*/
export function getParameterByName(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[\[\]]/g, `\\$&`);
export function getParameterByName (name, url) {
if (!url) url = window.location.href
name = name.replace(/[[\]]/g, `\\$&`)
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
const results = regex.exec(url);
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`)
const results = regex.exec(url)
if (!results) return null;
if (!results[2]) return '';
if (!results) return null
if (!results[2]) return ''
return decodeURIComponent(results[2].replace(/\+/g, ' '));
return decodeURIComponent(results[2].replace(/\+/g, ' '))
}
/**
@@ -22,61 +23,61 @@ export function getParameterByName(name, url) {
* @param {array} arr1: array of numbers
* @param {array} arr2: array of numbers
*/
export function areEqual(arr1, arr2) {
return ((arr1.length === arr2.length) && arr1.every((element, index) => {
return element === arr2[index];
}));
export function areEqual (arr1, arr2) {
return ((arr1.length === arr2.length) && arr1.every((element, index) => {
return element === arr2[index]
}))
}
/**
* Return whether the variable is neither null nor undefined
* @param {object} variable
*/
export function isNotNullNorUndefined(variable) {
return (typeof variable !== 'undefined' && variable !== null);
export function isNotNullNorUndefined (variable) {
return (typeof variable !== 'undefined' && variable !== null)
}
/*
* Capitalizes _only_ the first letter of a string
* Taken from: https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript
*/
export function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
export function capitalizeFirstLetter (string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
export function trimAndEllipse(string, stringNum) {
export function trimAndEllipse (string, stringNum) {
if (string.length > stringNum) {
return string.substring(0, 120) + '...'
}
return string;
return string
}
/**
* Return a Date object given a datetime string of the format: "2016-09-10T07:00:00"
* @param {string} datetime
*/
export function parseDate(datetime) {
export function parseDate (datetime) {
return new Date(datetime.slice(0, 4),
datetime.slice(5, 7) - 1,
datetime.slice(8, 10),
datetime.slice(11, 13),
datetime.slice(14, 16),
datetime.slice(17, 19)
);
)
}
export function formatterWithYear(datetime) {
return d3.timeFormat("%d %b %Y, %H:%M")(datetime);
export function formatterWithYear (datetime) {
return d3.timeFormat('%d %b %Y, %H:%M')(datetime)
}
export function formatter(datetime) {
return d3.timeFormat("%d %b, %H:%M")(datetime);
export function formatter (datetime) {
return d3.timeFormat('%d %b, %H:%M')(datetime)
}
export const parseTimestamp = ts => d3.timeParse("%Y-%m-%dT%H:%M:%S")(ts);
export const parseTimestamp = ts => d3.timeParse('%Y-%m-%dT%H:%M:%S')(ts)
export function compareTimestamp (a, b) {
return (parseTimestamp(a.timestamp) > parseTimestamp(b.timestamp));
return (parseTimestamp(a.timestamp) > parseTimestamp(b.timestamp))
}
/**
@@ -85,7 +86,7 @@ export function compareTimestamp (a, b) {
* source, call with two sets of parentheses:
* const src = insetSourceFrom(sources)(anEvent)
*/
export function insetSourceFrom(allSources) {
export function insetSourceFrom (allSources) {
return (event) => {
let sources
if (!event.sources) {
@@ -100,14 +101,13 @@ export function insetSourceFrom(allSources) {
sources
}
}
}
/**
* Debugging function: put in place of a mapStateToProps function to
* view that source modal by default
*/
export function injectSource(id) {
export function injectSource (id) {
return state => {
return {
...state,
@@ -119,7 +119,7 @@ export function injectSource(id) {
}
}
export function urlFromEnv(ext) {
export function urlFromEnv (ext) {
if (process.env[ext]) {
return `${process.env.SERVER_ROOT}${process.env[ext]}`
} else {

View File

@@ -1,3 +1,4 @@
/* global d3 */
import initial from '../store/initial.js'
import { parseDate } from '../js/utilities'
@@ -13,29 +14,28 @@ import {
UPDATE_SOURCE,
RESET_ALLFILTERS,
TOGGLE_LANGUAGE,
TOGGLE_MAPVIEW,
TOGGLE_SITES,
TOGGLE_FETCHING_DOMAIN,
TOGGLE_FETCHING_SOURCES,
TOGGLE_INFOPOPUP,
TOGGLE_NOTIFICATIONS,
FETCH_ERROR,
FETCH_SOURCE_ERROR,
FETCH_SOURCE_ERROR
} from '../actions'
function updateHighlighted(appState, action) {
function updateHighlighted (appState, action) {
return Object.assign({}, appState, {
highlighted: action.highlighted
})
}
function updateSelected(appState, action) {
function updateSelected (appState, action) {
return Object.assign({}, appState, {
selected: action.selected
})
}
function updateNarrative(appState, action) {
function updateNarrative (appState, action) {
let minTime = appState.timeline.range[0]
let maxTime = appState.timeline.range[1]
@@ -43,7 +43,7 @@ function updateNarrative(appState, action) {
let cornerBound1 = [-180, -180]
// Compute narrative time range and map bounds
if (!!action.narrative) {
if (action.narrative) {
minTime = parseDate('2100-01-01T00:00:00')
maxTime = parseDate('1900-01-01T00:00:00')
@@ -85,7 +85,7 @@ function updateNarrative(appState, action) {
...appState,
narrative: action.narrative,
narrativeState: {
current: !!action.narrative ? 0 : null
current: action.narrative ? 0 : null
},
filters: {
...appState.filters,
@@ -95,29 +95,33 @@ function updateNarrative(appState, action) {
}
}
function incrementNarrativeCurrent(appState, action) {
function incrementNarrativeCurrent (appState, action) {
appState.narrativeState.current += 1
return {
...appState,
narrativeState: {
current: appState.narrativeState.current += 1
current: appState.narrativeState.current
}
}
}
function decrementNarrativeCurrent(appState, action) {
function decrementNarrativeCurrent (appState, action) {
appState.narrativeState.current -= 1
return {
...appState,
narrativeState: {
current: appState.narrativeState.current -= 1
current: appState.narrativeState.current
}
}
}
function updateTagFilters(appState, action) {
function updateTagFilters (appState, action) {
const tagFilters = appState.filters.tags.slice(0)
const nextActiveState = action.tag.active
function traverseNode(node) {
function traverseNode (node) {
const tagFilter = tagFilters.find(tF => tF.key === node.key)
node.active = nextActiveState
if (!tagFilter) tagFilters.push(node)
@@ -136,7 +140,7 @@ function updateTagFilters(appState, action) {
})
}
function updateCategoryFilters(appState, action) {
function updateCategoryFilters (appState, action) {
const categoryFilters = appState.filters.categories.slice(0)
const catFilter = categoryFilters.find(cF => cF.category === action.category.category)
@@ -147,7 +151,6 @@ function updateCategoryFilters(appState, action) {
catFilter.active = (!!action.category.active)
}
return Object.assign({}, appState, {
filters: Object.assign({}, appState.filters, {
categories: categoryFilters
@@ -155,50 +158,38 @@ function updateCategoryFilters(appState, action) {
})
}
function updateTimeRange(appState, action) { // XXX
function updateTimeRange (appState, action) { // XXX
return {
...appState,
timeline: {
...appState.timeline,
range: action.timerange
},
}
}
}
function resetAllFilters(appState) { // 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")
],
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: [],
selected: []
})
}
function toggleLanguage(appState, action) {
function toggleLanguage (appState, action) {
let otherLanguage = (appState.language === 'es-MX') ? 'en-US' : 'es-MX'
return Object.assign({}, appState, {
language: action.language || otherLanguage
})
}
function toggleMapView(appState, action) {
const isLayerInView = !appState.views[layer]
const newViews = {}
newViews[layer] = isLayerInView
const views = Object.assign({}, appState.views, newViews)
return Object.assign({}, appState, {
filters: Object.assign({}, appState.filters, {
views
})
})
}
function toggleSites(appState, action) {
function toggleSites (appState, action) {
return {
...appState,
flags: {
@@ -208,14 +199,14 @@ function toggleSites(appState, action) {
}
}
function updateSource(appState, action) {
function updateSource (appState, action) {
return {
...appState,
source: action.source
}
}
function fetchError(state, action) {
function fetchError (state, action) {
return {
...state,
error: action.message,
@@ -223,7 +214,7 @@ function fetchError(state, action) {
}
}
function toggleFetchingDomain(appState, action) {
function toggleFetchingDomain (appState, action) {
return Object.assign({}, appState, {
flags: Object.assign({}, appState.flags, {
isFetchingDomain: !appState.flags.isFetchingDomain
@@ -231,7 +222,7 @@ function toggleFetchingDomain(appState, action) {
})
}
function toggleFetchingSources(appState, action) {
function toggleFetchingSources (appState, action) {
return Object.assign({}, appState, {
flags: Object.assign({}, appState.flags, {
isFetchingSources: !appState.flags.isFetchingSources
@@ -239,7 +230,7 @@ function toggleFetchingSources(appState, action) {
})
}
function toggleInfoPopup(appState, action) {
function toggleInfoPopup (appState, action) {
return Object.assign({}, appState, {
flags: Object.assign({}, appState.flags, {
isInfopopup: !appState.flags.isInfopopup
@@ -247,7 +238,7 @@ function toggleInfoPopup(appState, action) {
})
}
function toggleNotifications(appState, action) {
function toggleNotifications (appState, action) {
return Object.assign({}, appState, {
flags: Object.assign({}, appState.flags, {
isNotification: !appState.flags.isNotification
@@ -255,7 +246,7 @@ function toggleNotifications(appState, action) {
})
}
function fetchSourceError(appState, action) {
function fetchSourceError (appState, action) {
return {
...appState,
errors: {
@@ -265,9 +256,7 @@ function fetchSourceError(appState, action) {
}
}
function app(appState = initial.app, action) {
function app (appState = initial.app, action) {
switch (action.type) {
case UPDATE_HIGHLIGHTED:
return updateHighlighted(appState, action)
@@ -291,8 +280,6 @@ function app(appState = initial.app, action) {
return resetAllFilters(appState, action)
case TOGGLE_LANGUAGE:
return toggleLanguage(appState, action)
case TOGGLE_MAPVIEW:
return toggleMapView(appState, action)
case TOGGLE_SITES:
return toggleSites(appState, action)
case FETCH_ERROR:

View File

@@ -8,4 +8,4 @@ export default combineReducers({
app,
domain,
ui
});;
})

View File

@@ -1,8 +1,8 @@
import Joi from 'joi';
import Joi from 'joi'
const categorySchema = Joi.object().keys({
category: Joi.string().required(),
description: Joi.string(),
});
category: Joi.string().required(),
description: Joi.string()
})
export default categorySchema;
export default categorySchema

View File

@@ -1,24 +1,24 @@
import Joi from 'joi';
import Joi from 'joi'
const eventSchema = Joi.object().keys({
id: Joi.string().required(),
description: Joi.string().allow('').required(),
date: Joi.string().allow(''),
time: Joi.string().allow(''),
time_precision: Joi.string().allow(''),
location: Joi.string().allow(''),
latitude: Joi.string().allow(''),
longitude: Joi.string().allow(''),
type: Joi.string().allow(''),
category: Joi.string().required(),
narratives: Joi.array(),
sources: Joi.array(),
tags: Joi.array().allow(''),
comments: Joi.string().allow(''),
timestamp: Joi.string(),
id: Joi.string().required(),
description: Joi.string().allow('').required(),
date: Joi.string().allow(''),
time: Joi.string().allow(''),
time_precision: Joi.string().allow(''),
location: Joi.string().allow(''),
latitude: Joi.string().allow(''),
longitude: Joi.string().allow(''),
type: Joi.string().allow(''),
category: Joi.string().required(),
narratives: Joi.array(),
sources: Joi.array(),
tags: Joi.array().allow(''),
comments: Joi.string().allow(''),
timestamp: Joi.string(),
// nested
narrative___stepStyles: Joi.array(),
// nested
narrative___stepStyles: Joi.array()
})
.and('latitude', 'longitude')
.and('date', 'time', 'timestamp')

View File

@@ -1,9 +1,9 @@
import Joi from 'joi';
import Joi from 'joi'
const narrativeSchema = Joi.object().keys({
id: Joi.string().required(),
description: Joi.string().allow('').required(),
label: Joi.string().required()
});
id: Joi.string().required(),
description: Joi.string().allow('').required(),
label: Joi.string().required()
})
export default narrativeSchema;
export default narrativeSchema

View File

@@ -1,11 +1,11 @@
import Joi from 'joi';
import Joi from 'joi'
const siteSchema = Joi.object().keys({
id: Joi.string().required(),
description: Joi.string().allow('').required(),
site: Joi.string().required(),
latitude: Joi.string().required(),
longitude: Joi.string().required()
});
id: Joi.string().required(),
description: Joi.string().allow('').required(),
site: Joi.string().required(),
latitude: Joi.string().required(),
longitude: Joi.string().required()
})
export default siteSchema;
export default siteSchema

View File

@@ -1,18 +1,18 @@
import Joi from 'joi';
import Joi from 'joi'
const sourceSchema = Joi.object().keys({
id: Joi.string().required(),
title: Joi.string().allow(''),
thumbnail: Joi.string().allow(''),
paths: Joi.array().required(),
type: Joi.string().allow(''),
affil_s: Joi.array().allow(''),
url: Joi.string().allow(''),
desc: Joi.string().allow(''),
parent: Joi.string().allow(''),
author: Joi.string().allow(''),
date: Joi.string().allow(''),
notes: Joi.string().allow('')
});
id: Joi.string().required(),
title: Joi.string().allow(''),
thumbnail: Joi.string().allow(''),
paths: Joi.array().required(),
type: Joi.string().allow(''),
affil_s: Joi.array().allow(''),
url: Joi.string().allow(''),
desc: Joi.string().allow(''),
parent: Joi.string().allow(''),
author: Joi.string().allow(''),
date: Joi.string().allow(''),
notes: Joi.string().allow('')
})
export default sourceSchema;
export default sourceSchema

View File

@@ -1,9 +1,9 @@
import initial from '../store/initial.js';
import initial from '../store/initial.js'
import {} from '../actions'
function ui(uiState = initial.ui, action) {
return uiState;
function ui (uiState = initial.ui, action) {
return uiState
}
export default ui;
export default ui

View File

@@ -1,18 +1,19 @@
export function parseDateTimes(arrayToParse) {
const parsedArray = [];
/* global d3 */
export function parseDateTimes (arrayToParse) {
const parsedArray = []
arrayToParse.forEach(item => {
let incoming_datetime = `${item.date}T00:00`;
if (item.time) incoming_datetime = `${item.date}T${item.time}`;
const parser = d3.timeParse(process.env.INCOMING_DATETIME_FORMAT);
item.timestamp = d3.timeFormat("%Y-%m-%dT%H:%M:%S")(parser(incoming_datetime));
let incomingDateTime = `${item.date}T00:00`
if (item.time) incomingDateTime = `${item.date}T${item.time}`
const parser = d3.timeParse(process.env.INCOMING_DATETIME_FORMAT)
item.timestamp = d3.timeFormat('%Y-%m-%dT%H:%M:%S')(parser(incomingDateTime))
parsedArray.push(item);
});
parsedArray.push(item)
})
return parsedArray;
return parsedArray
}
export function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
export function capitalize (string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}

View File

@@ -13,7 +13,7 @@ import { capitalize } from './helpers.js'
* Create an error notification object
* Types: ['error', 'warning', 'good', 'neural']
*/
function makeError(type, id, message) {
function makeError (type, id, message) {
return {
type: 'error',
id,
@@ -21,11 +21,9 @@ function makeError(type, id, message) {
}
}
const isLeaf = node => (Object.keys(node.children).length === 0)
const isDuplicate = (node, set) => { return (set.has(node.key)) }
/*
* Traverse a tag tree and check its duplicates
*/
@@ -64,7 +62,7 @@ export function validateDomain (domain) {
sources: {},
tags: {},
shapes: [],
notifications: domain.notifications,
notifications: domain.notifications
}
const discardedDomain = {
@@ -76,7 +74,7 @@ export function validateDomain (domain) {
shapes: []
}
function validateArrayItem(item, domainKey, schema) {
function validateArrayItem (item, domainKey, schema) {
const result = Joi.validate(item, schema)
if (result.error !== null) {
const id = item.id || '-'
@@ -89,13 +87,13 @@ export function validateDomain (domain) {
}
}
function validateArray(items, domainKey, schema) {
function validateArray (items, domainKey, schema) {
items.forEach(item => {
validateArrayItem(item, domainKey, schema)
})
}
function validateObject(obj, domainKey, itemSchema) {
function validateObject (obj, domainKey, itemSchema) {
Object.keys(obj).forEach(key => {
const vl = obj[key]
const result = Joi.validate(vl, itemSchema)
@@ -121,11 +119,11 @@ export function validateDomain (domain) {
// NB: [lat, lon] array is best format for projecting into map
sanitizedDomain.shapes = sanitizedDomain.shapes.map(shape => ({
name: shape.name,
points: shape.items.map(coords => (
coords.replace(/\s/g, '').split(',')
))
})
name: shape.name,
points: shape.items.map(coords => (
coords.replace(/\s/g, '').split(',')
))
})
)
// Message the number of failed items in domain

View File

@@ -1,9 +1,8 @@
import { createSelector} from 'reselect'
import { createSelector } from 'reselect'
import { parseTimestamp, compareTimestamp, insetSourceFrom } from '../js/utilities'
// 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 getActiveNarrative = state => state.app.narrative
@@ -27,7 +26,6 @@ export const getTagsFilter = state => state.app.filters.tags
export const getCategoriesFilter = state => state.app.filters.categories
export const getTimeRange = state => state.app.timeline.range
/**
* Some handy helpers
*/
@@ -36,7 +34,7 @@ export const getTimeRange = state => state.app.timeline.range
* Given an event and all tags,
* returns true/false if event has any tag that is active
*/
function isTaggedIn(event, tagFilters) {
function isTaggedIn (event, tagFilters) {
if (event.tags) {
const isTagged = event.tags.some((tag) => {
return tagFilters.find(tF => (tF.key === tag && tF.active))
@@ -51,10 +49,10 @@ 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) {
function isTaggedInWithCategory (event, categories) {
if (event.category) {
if (categories.find(c => (c.category === event.category && c.active))) return true
return false;
return false
} else {
return false
}
@@ -63,22 +61,22 @@ function isTaggedInWithCategory(event, categories) {
/*
* Returns true if no tags are selected
*/
function isNoTags(tagFilters) {
function isNoTags (tagFilters) {
return (
tagFilters.length === 0
|| !process.env.features.USE_TAGS
|| tagFilters.every(t => !t.active)
tagFilters.length === 0 ||
!process.env.features.USE_TAGS ||
tagFilters.every(t => !t.active)
)
}
/*
* Returns true if no categories are selected
*/
function isNoCategories(categories) {
function isNoCategories (categories) {
return (
categories.length === 0
|| !process.env.features.CATEGORIES_AS_TAGS
|| categories.every(c => !c.active)
categories.length === 0 ||
!process.env.features.CATEGORIES_AS_TAGS ||
categories.every(c => !c.active)
)
}
@@ -86,10 +84,11 @@ function isNoCategories(categories) {
* Given an event and a time range,
* returns true/false if the event falls within timeRange
*/
function isTimeRangedIn(event, timeRange) {
function isTimeRangedIn (event, timeRange) {
const eventTime = parseTimestamp(event.timestamp)
return (
timeRange[0] < parseTimestamp(event.timestamp)
&& parseTimestamp(event.timestamp) < timeRange[1]
timeRange[0] < eventTime &&
eventTime < timeRange[1]
)
}
@@ -98,76 +97,73 @@ function isTimeRangedIn(event, timeRange) {
* and if TAGS are being used, select them if their tags are enabled
*/
export const selectEvents = createSelector(
[getEvents, getTagsFilter, getCategoriesFilter, getTimeRange],
(events, tagFilters, categories, 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)
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 && isTaggedWithCategory) {
const eventClone = Object.assign({}, event)
acc[event.id] = eventClone
}
if (isTimeRanged && isTagged && isTaggedWithCategory) {
const eventClone = Object.assign({}, event)
acc[event.id] = eventClone
}
return acc
return acc
}, [])
})
})
/**
* 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
*/
export const selectNarratives = createSelector(
[getEvents, getNarratives, getSources],
(events, narrativesMeta, sources) => {
[getEvents, getNarratives, getSources],
(events, narrativesMeta, sources) => {
const narratives = {}
const narrativeSkeleton = id => ({ id, steps: [] })
const narratives = {}
const narrativeSkeleton = id => ({ id, steps: [] })
/* populate narratives dict with events */
events.forEach(evt => {
const isInNarrative = evt.narratives.length > 0
/* populate narratives dict with events */
events.forEach(evt => {
const isInNarrative = evt.narratives.length > 0
evt.narratives.forEach(narrative => {
// initialise
if (!narratives[narrative]) { narratives[narrative] = narrativeSkeleton(narrative) }
evt.narratives.forEach(narrative => {
// initialise
if (!narratives[narrative])
narratives[narrative] = narrativeSkeleton(narrative)
// add evt to steps
if (isInNarrative)
// NB: insetSourceFrom is a 'curried' function to allow with maps
narratives[narrative].steps.push(insetSourceFrom(sources)(evt))
})
})
/* sort steps by time */
Object.keys(narratives).forEach(key => {
const steps = narratives[key].steps
steps.sort(compareTimestamp)
if (narrativesMeta.find(n => n.id === key)) {
narratives[key] = {
...narrativesMeta.find(n => n.id === key),
...narratives[key]
}
// add evt to steps
if (isInNarrative) {
// NB: insetSourceFrom is a 'curried' function to allow with maps
narratives[narrative].steps.push(insetSourceFrom(sources)(evt))
}
})
})
// Return narratives in original order
// + filter those that are undefined
return narrativesMeta.map(n => narratives[n.id]).filter(d => d);
})
/* sort steps by time */
Object.keys(narratives).forEach(key => {
const steps = narratives[key].steps
steps.sort(compareTimestamp)
if (narrativesMeta.find(n => n.id === key)) {
narratives[key] = {
...narrativesMeta.find(n => n.id === key),
...narratives[key]
}
}
})
// 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
* a single object. If narrative is null, the whole object is null.
*/
export const selectActiveNarrative = createSelector(
[getActiveNarrative, getActiveStep],
(narrative, current) => !!narrative
(narrative, current) => narrative
? { ...narrative, current }
: null
)
@@ -184,7 +180,6 @@ export const selectActiveNarrative = createSelector(
export const selectLocations = createSelector(
[selectEvents],
(events) => {
const selectedLocations = {}
events.forEach(event => {
const location = event.location
@@ -234,7 +229,6 @@ export const selectDatetimes = createSelector(
}
)
/**
* Of all the sources, select those that are relevant to the selected events.
*/
@@ -257,12 +251,11 @@ export const selectCategories = createSelector(
(categories) => {
categories.map(cat => {
cat.active = (!cat.hasOwnProperty('active')) ? false : cat.active
});
return categories;
})
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
@@ -272,7 +265,7 @@ export const selectTagList = createSelector(
(tags) => {
const tagList = []
let depth = 0
function traverseNode(node, depth) {
function traverseNode (node, depth) {
node.active = (!node.hasOwnProperty('active')) ? false : node.active
node.depth = depth

View File

@@ -1,12 +1,12 @@
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from '../reducers';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
import rootReducer from '../reducers'
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store = createStore(
rootReducer,
composeEnhancers(applyMiddleware(thunk))
);
)
export default store;
export default store

View File

@@ -16,7 +16,7 @@ const initial = {
sources: {},
sites: [],
tags: {},
notifications: [],
notifications: []
},
/*
@@ -29,7 +29,7 @@ const initial = {
*/
app: {
errors: {
source: null,
source: null
},
highlighted: null,
selected: [],
@@ -45,7 +45,7 @@ const initial = {
events: true,
routes: false,
sites: true
},
}
},
isMobile: (/Mobi/.test(navigator.userAgent)),
language: 'en-US',
@@ -79,7 +79,7 @@ const initial = {
{ label: '2 hours', duration: 120 },
{ label: '30 min', duration: 30 },
{ label: '10 min', duration: 10 }
],
]
},
flags: {
isFetchingDomain: false,
@@ -100,7 +100,7 @@ const initial = {
tiles: 'openstreetmap', // ['openstreetmap', 'streets', 'satellite']
style: {
categories: {
default: '#f3de2c',
default: '#f3de2c'
},
narratives: {
default: {
@@ -118,16 +118,16 @@ const initial = {
}
},
dom: {
timeline: "timeline",
timeslider: "timeslider",
map: "map"
},
timeline: 'timeline',
timeslider: 'timeslider',
map: 'map'
}
}
};
}
let appStore;
let appStore
if (process.env.store) {
appStore = mergeDeepLeft(process.env.store, initial);
appStore = mergeDeepLeft(process.env.store, initial)
} else {
appStore = initial
}

View File

@@ -1,68 +1,65 @@
var assert = require('assert');
var child_process = require('child_process')
var http = require('http');
import test from 'ava'
var childProcess = require('childProcess')
var http = require('http')
const SERVER_LAUNCH_WAIT_TIME = 5 * 1000;
const SERVER_LAUNCH_WAIT_TIME = 5 * 1000
var server_proc = null;
var server_exited = false;
var serverProc = null
var serverExited = false
test.before.cb(t => {
console.log("launching server...")
server_proc = child_process.spawn('yarn', ['dev'], {
console.log('launching server...')
serverProc = childProcess.spawn('yarn', ['dev'], {
cwd: '.',
stdio: 'ignore'
});
})
server_proc.on('exit', function(code, signal) {
server_exited = true;
});
serverProc.on('exit', function (code, signal) {
serverExited = true
})
setTimeout(t.end, SERVER_LAUNCH_WAIT_TIME)
});
})
test.after(function() {
console.log("killing server...")
server_proc.kill('SIGKILL');
});
test.after(function () {
console.log('killing server...')
serverProc.kill('SIGKILL')
})
test('should launch', t => {
t.false(server_exited);
});
t.false(serverExited)
})
var urls = [
'/',
'js/index.bundle.js'
];
]
urls.forEach(function(url) {
urls.forEach(function (url) {
test.cb('should respond to request for "' + url + '"', t => {
http.get({
hostname: 'localhost',
port: 8080,
path: '/',
agent: false
}, function(res) {
var result_data = '';
}, function (res) {
var resultData = ''
if(res.statusCode != 200) {
t.fail('Server response was not 200.');
if (res.statusCode !== 200) {
t.fail('Server response was not 200.')
} else {
res.on('data', function(data) { result_data += data });
res.on('data', function (data) { resultData += data })
res.on('end', function() {
if (result_data.length > 0) {
t.pass();
res.on('end', function () {
if (resultData.length > 0) {
t.pass()
} else {
t.fail("Server returned no data.");
t.fail('Server returned no data.')
}
});
})
}
t.end();
t.end()
})
});
});
})
})

639
yarn.lock

File diff suppressed because it is too large Load Diff