mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-12 21:38:35 +03:00
Merge pull request #67 from forensic-architecture/topic/narrative-left
Topic/narrative left closes #63
This commit is contained in:
@@ -213,6 +213,20 @@ export function updateNarrative(narrative) {
|
||||
}
|
||||
}
|
||||
|
||||
export const INCREMENT_NARRATIVE_CURRENT = 'INCREMENT_NARRATIVE_CURRENT';
|
||||
export function incrementNarrativeCurrent() {
|
||||
return {
|
||||
type: INCREMENT_NARRATIVE_CURRENT
|
||||
}
|
||||
}
|
||||
|
||||
export const DECREMENT_NARRATIVE_CURRENT = 'DECREMENT_NARRATIVE_CURRENT';
|
||||
export function decrementNarrativeCurrent() {
|
||||
return {
|
||||
type: DECREMENT_NARRATIVE_CURRENT
|
||||
}
|
||||
}
|
||||
|
||||
export const RESET_ALLFILTERS = 'RESET_ALLFILTERS'
|
||||
export function resetAllFilters() {
|
||||
return {
|
||||
|
||||
@@ -1,61 +1,56 @@
|
||||
import copy from '../js/data/copy.json';
|
||||
import copy from '../js/data/copy.json'
|
||||
import {
|
||||
isNotNullNorUndefined,
|
||||
parseDate,
|
||||
formatterWithYear
|
||||
} from '../js/utilities';
|
||||
import React from 'react';
|
||||
} from '../js/utilities'
|
||||
import React from 'react'
|
||||
|
||||
import Spinner from './presentational/Spinner';
|
||||
import CardTimestamp from './presentational/CardTimestamp';
|
||||
import CardLocation from './presentational/CardLocation';
|
||||
import CardCaret from './presentational/CardCaret';
|
||||
import CardTags from './presentational/CardTags';
|
||||
import CardSummary from './presentational/CardSummary';
|
||||
import CardSource from './presentational/CardSource';
|
||||
import CardCategory from './presentational/CardCategory';
|
||||
import CardNarrative from './presentational/CardNarrative';
|
||||
import Spinner from './presentational/Spinner'
|
||||
import CardTimestamp from './presentational/CardTimestamp'
|
||||
import CardLocation from './presentational/CardLocation'
|
||||
import CardCaret from './presentational/CardCaret'
|
||||
import CardTags from './presentational/CardTags'
|
||||
import CardSummary from './presentational/CardSummary'
|
||||
import CardSource from './presentational/CardSource'
|
||||
import CardCategory from './presentational/CardCategory'
|
||||
import CardNarrative from './presentational/CardNarrative'
|
||||
|
||||
class Card extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
super(props)
|
||||
this.state = {
|
||||
isHighlighted: false
|
||||
};
|
||||
isOpen: false
|
||||
}
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.setState({
|
||||
isHighlighted: !this.state.isHighlighted
|
||||
}, () => {
|
||||
if (!this.state.isHighlighted) {
|
||||
this.props.onHighlight(this.props.event);
|
||||
} else {
|
||||
this.props.onHighlight(null);
|
||||
}
|
||||
});
|
||||
isOpen: !this.state.isOpen
|
||||
})
|
||||
}
|
||||
|
||||
makeTimelabel(timestamp) {
|
||||
if (timestamp === null) return null;
|
||||
const parsedTimestamp = parseDate(timestamp);
|
||||
const timelabel = formatterWithYear(parsedTimestamp);
|
||||
return timelabel;
|
||||
if (timestamp === null) return null
|
||||
const parsedTimestamp = parseDate(timestamp)
|
||||
const timelabel = formatterWithYear(parsedTimestamp)
|
||||
return timelabel
|
||||
}
|
||||
|
||||
renderCategory() {
|
||||
const categoryTitle = copy[this.props.language].cardstack.category;
|
||||
const categoryLabel = this.props.event.category;
|
||||
const color = this.props.getCategoryColor(this.props.event.category);
|
||||
const categoryTitle = copy[this.props.language].cardstack.category
|
||||
const categoryLabel = this.props.event.category
|
||||
const color = this.props.getCategoryColor(this.props.event.category)
|
||||
|
||||
return (
|
||||
<CardCategory
|
||||
categoryTitle={categoryTitle}
|
||||
categoryLabel={categoryLabel}
|
||||
color={color}
|
||||
/>
|
||||
);
|
||||
return null
|
||||
// return (
|
||||
// <CardCategory
|
||||
// categoryTitle={categoryTitle}
|
||||
// categoryLabel={categoryLabel}
|
||||
// color={color}
|
||||
// />
|
||||
// )
|
||||
}
|
||||
|
||||
renderSummary() {
|
||||
@@ -63,7 +58,7 @@ class Card extends React.Component {
|
||||
<CardSummary
|
||||
language={this.props.language}
|
||||
description={this.props.event.description}
|
||||
isHighlighted={this.state.isHighlighted}
|
||||
isOpen={this.state.isOpen}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -96,7 +91,7 @@ class Card extends React.Component {
|
||||
|
||||
const source_lang = copy[this.props.language].cardstack.sources
|
||||
return (
|
||||
<div className="card-col">
|
||||
<div className='card-col'>
|
||||
<h4>{source_lang}: </h4>
|
||||
{this.props.event.sources.map(source => (
|
||||
<CardSource
|
||||
@@ -117,11 +112,11 @@ class Card extends React.Component {
|
||||
language={this.props.language}
|
||||
timestamp={this.props.event.timestamp}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
renderNarrative() {
|
||||
const links = this.props.getNarrativeLinks(this.props.event);
|
||||
const links = this.props.getNarrativeLinks(this.props.event)
|
||||
|
||||
if (links !== null) {
|
||||
|
||||
@@ -136,52 +131,48 @@ class Card extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
renderHeader() {
|
||||
renderMain() {
|
||||
return (
|
||||
<div className="card-collapsed">
|
||||
<div className="card-row">
|
||||
<div className='card-container'>
|
||||
<div className='card-row details'>
|
||||
{this.renderTimestamp()}
|
||||
{this.renderLocation()}
|
||||
</div>
|
||||
{this.renderCategory()}
|
||||
<br/>
|
||||
{this.renderSummary()}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
if (this.state.isHighlighted) {
|
||||
return (
|
||||
<div className="card-bottomhalf">
|
||||
{this.renderTags()}
|
||||
{this.renderSources()}
|
||||
{this.renderNarrative()}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return <div classname="card-bottomhalf"></div>
|
||||
}
|
||||
renderExtra() {
|
||||
return (
|
||||
<div className='card-bottomhalf'>
|
||||
{this.renderTags()}
|
||||
{this.renderSources()}
|
||||
{this.renderNarrative()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderCaret() {
|
||||
return (
|
||||
<CardCaret
|
||||
toggle={() => this.toggle()}
|
||||
isHighlighted={this.state.isHighlighted}
|
||||
isOpen={this.state.isOpen}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isSelected } = this.props
|
||||
return (
|
||||
<li className='event-card'>
|
||||
{this.renderHeader()}
|
||||
{this.renderContent()}
|
||||
{this.renderCaret()}
|
||||
<li className={`event-card ${isSelected ? 'selected' : ''}`}>
|
||||
{this.renderMain()}
|
||||
{this.state.isOpen ? this.renderExtra() : null}
|
||||
{isSelected ? this.renderCaret() : null}
|
||||
</li>
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Card;
|
||||
export default Card
|
||||
|
||||
@@ -1,44 +1,56 @@
|
||||
import React from 'react';
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import * as selectors from '../selectors'
|
||||
|
||||
import Card from './Card.jsx';
|
||||
import copy from '../js/data/copy.json';
|
||||
import Card from './Card.jsx'
|
||||
import copy from '../js/data/copy.json'
|
||||
import {
|
||||
isNotNullNorUndefined
|
||||
} from '../js/utilities.js';
|
||||
} from '../js/utilities.js'
|
||||
|
||||
class CardStack extends React.Component {
|
||||
renderCards(events, selections) {
|
||||
// if no selections provided, select all
|
||||
if (!selections)
|
||||
selections = events.map(e => true)
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
return events.map((event, idx) => (
|
||||
<Card
|
||||
event={event}
|
||||
sourceError={this.props.sourceError}
|
||||
language={this.props.language}
|
||||
isLoading={this.props.isLoading}
|
||||
isSelected={selections[idx]}
|
||||
getNarrativeLinks={this.props.getNarrativeLinks}
|
||||
getCategoryGroup={this.props.getCategoryGroup}
|
||||
getCategoryColor={this.props.getCategoryColor}
|
||||
getCategoryLabel={this.props.getCategoryLabel}
|
||||
onViewSource={this.props.onViewSource}
|
||||
onHighlight={this.props.onHighlight}
|
||||
onSelect={this.props.onSelect}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
renderCards() {
|
||||
if (this.props.selected.length > 0) {
|
||||
return this.props.selected.map((event) => {
|
||||
return (
|
||||
<Card
|
||||
event={event}
|
||||
sourceError={this.props.sourceError}
|
||||
language={this.props.language}
|
||||
isLoading={this.props.isLoading}
|
||||
getNarrativeLinks={this.props.getNarrativeLinks}
|
||||
getCategoryGroup={this.props.getCategoryGroup}
|
||||
getCategoryColor={this.props.getCategoryColor}
|
||||
getCategoryLabel={this.props.getCategoryLabel}
|
||||
onViewSource={this.props.onViewSource}
|
||||
onHighlight={this.props.onHighlight}
|
||||
onSelect={this.props.onSelect}
|
||||
/>
|
||||
);
|
||||
});
|
||||
renderSelectedCards() {
|
||||
const { selected } = this.props
|
||||
if (selected.length > 0) {
|
||||
return this.renderCards(selected)
|
||||
}
|
||||
return '';
|
||||
return null
|
||||
}
|
||||
|
||||
renderNarrativeCards() {
|
||||
const { narrative } = this.props
|
||||
const showing = narrative.steps.slice(narrative.current)
|
||||
const selections = showing
|
||||
.map((_, idx) => (idx === 0))
|
||||
|
||||
return this.renderCards(showing, selections)
|
||||
}
|
||||
|
||||
renderCardStackHeader() {
|
||||
const header_lang = copy[this.props.language].cardstack.header;
|
||||
const header_lang = copy[this.props.language].cardstack.header
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -58,27 +70,59 @@ class CardStack extends React.Component {
|
||||
return (
|
||||
<div id="card-stack-content" className="card-stack-content">
|
||||
<ul>
|
||||
{this.renderCards()}
|
||||
{this.renderSelectedCards()}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
renderNarrativeContent() {
|
||||
return (
|
||||
<div id="card-stack-content" className="card-stack-content">
|
||||
<ul>
|
||||
{this.renderNarrativeCards()}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.selected.length > 0) {
|
||||
return (
|
||||
<div id="card-stack" className={`card-stack ${this.props.isCardstack ? '' : ' folded'}`}>
|
||||
{this.renderCardStackHeader()}
|
||||
{this.renderCardStackContent()}
|
||||
</div>
|
||||
);
|
||||
const { isCardstack, selected, narrative } = this.props
|
||||
|
||||
if (selected.length > 0) {
|
||||
if (!narrative) {
|
||||
return (
|
||||
<div
|
||||
id="card-stack"
|
||||
className={`card-stack
|
||||
${isCardstack ? '' : ' folded'}`
|
||||
}
|
||||
>
|
||||
{this.renderCardStackHeader()}
|
||||
{this.renderCardStackContent()}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
id="card-stack"
|
||||
className={`card-stack narrative-mode
|
||||
${isCardstack ? '' : ' folded'}`
|
||||
}
|
||||
>
|
||||
{this.renderNarrativeContent()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
return <div/>;
|
||||
|
||||
return <div/>
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
narrative: selectors.selectActiveNarrative(state),
|
||||
selected: selectors.selectSelected(state),
|
||||
sourceError: state.app.errors.source,
|
||||
language: state.app.language,
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
import React from 'react';
|
||||
import React from 'react'
|
||||
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import * as actions from '../actions';
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { connect } from 'react-redux'
|
||||
import * as actions from '../actions'
|
||||
|
||||
import SourceOverlay from './SourceOverlay.jsx';
|
||||
import LoadingOverlay from './presentational/LoadingOverlay';
|
||||
import Map from './Map.jsx';
|
||||
import Toolbar from './Toolbar.jsx';
|
||||
import CardStack from './CardStack.jsx';
|
||||
import NarrativeCard from './NarrativeCard.js';
|
||||
import InfoPopUp from './InfoPopup.jsx';
|
||||
import Timeline from './Timeline.jsx';
|
||||
import Notification from './Notification.jsx';
|
||||
import SourceOverlay from './SourceOverlay.jsx'
|
||||
import LoadingOverlay from './presentational/LoadingOverlay'
|
||||
import Map from './Map.jsx'
|
||||
import Toolbar from './Toolbar.jsx'
|
||||
import CardStack from './CardStack.jsx'
|
||||
import NarrativeCard from './NarrativeCard.js'
|
||||
import InfoPopUp from './InfoPopup.jsx'
|
||||
import Timeline from './Timeline.jsx'
|
||||
import Notification from './Notification.jsx'
|
||||
|
||||
import { parseDate } from '../js/utilities';
|
||||
import { parseDate } from '../js/utilities'
|
||||
|
||||
import { injectNarrative } from '../js/utilities'
|
||||
|
||||
class Dashboard extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
super(props)
|
||||
|
||||
this.handleViewSource = this.handleViewSource.bind(this)
|
||||
this.handleHighlight = this.handleHighlight.bind(this);
|
||||
this.handleSelect = this.handleSelect.bind(this);
|
||||
this.handleSelectNarrative = this.handleSelectNarrative.bind(this);
|
||||
this.handleTagFilter = this.handleTagFilter.bind(this);
|
||||
this.updateTimerange = this.updateTimerange.bind(this);
|
||||
this.getCategoryColor = this.getCategoryColor.bind(this);
|
||||
this.handleHighlight = this.handleHighlight.bind(this)
|
||||
this.setNarrative = this.setNarrative.bind(this)
|
||||
this.moveInNarrative = this.moveInNarrative.bind(this)
|
||||
this.handleSelect = this.handleSelect.bind(this)
|
||||
this.getCategoryColor = this.getCategoryColor.bind(this)
|
||||
|
||||
this.eventsById = {}
|
||||
}
|
||||
@@ -34,18 +35,18 @@ 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))
|
||||
}
|
||||
}
|
||||
|
||||
handleHighlight(highlighted) {
|
||||
this.props.actions.updateHighlighted((highlighted) ? highlighted : null);
|
||||
this.props.actions.updateHighlighted((highlighted) ? highlighted : null)
|
||||
}
|
||||
|
||||
getEventById(eventId) {
|
||||
if (this.eventsById[eventId]) return this.eventsById[eventId];
|
||||
this.eventsById[eventId] = this.props.domain.events.find(ev => ev.id === eventId);
|
||||
return this.eventsById[eventId];
|
||||
if (this.eventsById[eventId]) return this.eventsById[eventId]
|
||||
this.eventsById[eventId] = this.props.domain.events.find(ev => ev.id === eventId)
|
||||
return this.eventsById[eventId]
|
||||
}
|
||||
|
||||
handleViewSource(source) {
|
||||
@@ -54,104 +55,116 @@ class Dashboard extends React.Component {
|
||||
|
||||
handleSelect(selected) {
|
||||
if (selected) {
|
||||
let eventsToSelect = selected.map(event => this.getEventById(event.id));
|
||||
let eventsToSelect = selected.map(event => this.getEventById(event.id))
|
||||
eventsToSelect = eventsToSelect.sort((a, b) => parseDate(a.timestamp) - parseDate(b.timestamp))
|
||||
|
||||
this.props.actions.updateSelected(eventsToSelect)
|
||||
}
|
||||
}
|
||||
|
||||
handleSelectNarrative(narrative) {
|
||||
this.props.actions.updateNarrative(narrative);
|
||||
}
|
||||
|
||||
handleTagFilter(tag) {
|
||||
this.props.actions.updateTagFilters(tag);
|
||||
}
|
||||
|
||||
updateTimerange(timeRange) {
|
||||
this.props.actions.updateTimeRange(timeRange);
|
||||
}
|
||||
|
||||
getCategoryColor(category='other') {
|
||||
return this.props.ui.style.categories[category] || this.props.ui.style.categories['other']
|
||||
}
|
||||
|
||||
getNarrativeLinks(event) {
|
||||
const narrative = this.props.domain.narratives.find(nv => nv.id === event.narrative);
|
||||
if (narrative) return narrative.byId[event.id];
|
||||
return null;
|
||||
const narrative = this.props.domain.narratives.find(nv => nv.id === event.narrative)
|
||||
if (narrative) return narrative.byId[event.id]
|
||||
return null
|
||||
}
|
||||
|
||||
setNarrative(narrative) {
|
||||
// only handleSelect if narrative is not null
|
||||
if (!!narrative)
|
||||
this.handleSelect([ narrative.steps[0] ])
|
||||
this.props.actions.updateNarrative(narrative)
|
||||
}
|
||||
|
||||
moveInNarrative(amt) {
|
||||
const { current } = this.props.app.narrativeState
|
||||
const { narrative } = this.props.app
|
||||
|
||||
if (amt === 1) {
|
||||
this.handleSelect([ narrative.steps[current + 1] ])
|
||||
this.props.actions.incrementNarrativeCurrent()
|
||||
}
|
||||
if (amt === -1) {
|
||||
this.handleSelect([ narrative.steps[current - 1] ])
|
||||
this.props.actions.decrementNarrativeCurrent()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { actions, app, domain, ui } = this.props
|
||||
return (
|
||||
<div>
|
||||
<Toolbar
|
||||
onFilter={this.handleTagFilter}
|
||||
onSelectNarrative={this.handleSelectNarrative}
|
||||
actions={this.props.actions}
|
||||
isNarrative={!!app.narrative}
|
||||
methods={{
|
||||
onFilter: actions.updateTagFilters,
|
||||
onSelectNarrative: this.setNarrative
|
||||
}}
|
||||
/>
|
||||
<Map
|
||||
mapId="map"
|
||||
mapId='map'
|
||||
methods={{
|
||||
onSelect: this.handleSelect,
|
||||
onSelectNarrative: this.handleSelectNarrative,
|
||||
onSelectNarrative: this.setNarrative,
|
||||
getCategoryColor: this.getCategoryColor,
|
||||
}}
|
||||
/>
|
||||
<Timeline
|
||||
methods={{
|
||||
onSelect: this.handleSelect,
|
||||
onUpdateTimerange: this.updateTimerange,
|
||||
onUpdateTimerange: actions.updateTimeRange,
|
||||
getCategoryColor: category => this.getCategoryColor(category)
|
||||
}}
|
||||
/>
|
||||
{(this.props.app.narrative !== null)
|
||||
? <NarrativeCard
|
||||
onSelect={this.handleSelect}
|
||||
onSelectNarrative={this.handleSelectNarrative}
|
||||
/>
|
||||
: ''
|
||||
}
|
||||
<NarrativeCard
|
||||
methods={{
|
||||
onNext: () => this.moveInNarrative(1),
|
||||
onPrev: () => this.moveInNarrative(-1),
|
||||
onSelectNarrative: this.setNarrative
|
||||
}}
|
||||
/>
|
||||
<CardStack
|
||||
onViewSource={this.handleViewSource}
|
||||
onSelect={this.handleSelect}
|
||||
onHighlight={this.handleHighlight}
|
||||
onToggleCardstack={() => this.props.actions.updateSelected([])}
|
||||
onToggleCardstack={() => actions.updateSelected([])}
|
||||
getNarrativeLinks={event => this.getNarrativeLinks(event)}
|
||||
getCategoryColor={category => this.getCategoryColor(category)}
|
||||
/>
|
||||
<InfoPopUp
|
||||
ui={this.props.ui}
|
||||
app={this.props.app}
|
||||
toggle={() => this.props.actions.toggleInfoPopup()}
|
||||
ui={ui}
|
||||
app={app}
|
||||
toggle={() => actions.toggleInfoPopup()}
|
||||
/>
|
||||
<Notification
|
||||
isNotification={this.props.app.flags.isNotification}
|
||||
notifications={this.props.domain.notifications}
|
||||
onToggle={this.props.actions.markNotificationsRead}
|
||||
isNotification={app.flags.isNotification}
|
||||
notifications={domain.notifications}
|
||||
onToggle={actions.markNotificationsRead}
|
||||
/>
|
||||
{this.props.app.source ? (
|
||||
{app.source ? (
|
||||
<SourceOverlay
|
||||
source={this.props.app.source}
|
||||
source={app.source}
|
||||
onCancel={() => {
|
||||
this.props.actions.updateSource(null)}
|
||||
actions.updateSource(null)}
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<LoadingOverlay
|
||||
ui={this.props.app.flags.isFetchingDomain}
|
||||
language={this.props.app.language}
|
||||
ui={app.flags.isFetchingDomain}
|
||||
language={app.language}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function injectSource(id) {
|
||||
@@ -167,4 +180,4 @@ function injectSource(id) {
|
||||
export default connect(
|
||||
state => state,
|
||||
mapDispatchToProps,
|
||||
)(Dashboard);
|
||||
)(Dashboard)
|
||||
|
||||
@@ -33,8 +33,10 @@ class MapEvents extends React.Component {
|
||||
})
|
||||
|
||||
if (this.props.narrative) {
|
||||
const { byId } = this.props.narrative
|
||||
const eventsInNarrative = events.filter(e => byId.hasOwnProperty(e.id))
|
||||
const { steps } = this.props.narrative
|
||||
const onlyIfInNarrative = e => steps.map(s => s.id).includes(e.id)
|
||||
const eventsInNarrative = events.filter(onlyIfInNarrative)
|
||||
|
||||
if (eventsInNarrative.length <= 0) {
|
||||
styleProps = {
|
||||
...styleProps,
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
import React from 'react';
|
||||
import { Portal } from 'react-portal';
|
||||
import React from 'react'
|
||||
import { Portal } from 'react-portal'
|
||||
|
||||
class MapNarratives extends React.Component {
|
||||
|
||||
projectPoint(location) {
|
||||
const latLng = new L.LatLng(location[0], location[1]);
|
||||
const latLng = new L.LatLng(location[0], location[1])
|
||||
return {
|
||||
x: this.props.map.latLngToLayerPoint(latLng).x + this.props.mapTransformX,
|
||||
y: this.props.map.latLngToLayerPoint(latLng).y + this.props.mapTransformY
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
getNarrativeStyle(narrativeId) {
|
||||
const styleName = (narrativeId && narrativeId in this.props.narrativeProps)
|
||||
? narrativeId
|
||||
: 'default';
|
||||
return this.props.narrativeProps[styleName];
|
||||
: 'default'
|
||||
return this.props.narrativeProps[styleName]
|
||||
}
|
||||
|
||||
getStrokeWidth(narrative, step) {
|
||||
if (!step) return 0;
|
||||
return this.getNarrativeStyle(narrative.id).strokeWidth;
|
||||
if (!step) return 0
|
||||
return this.getNarrativeStyle(narrative.id).strokeWidth
|
||||
}
|
||||
|
||||
getStrokeDashArray(narrative, step) {
|
||||
if (!step) return 'none';
|
||||
return (this.getNarrativeStyle(narrative.id).style === 'dotted') ? "2px 5px" : 'none';
|
||||
if (!step) return 'none'
|
||||
return (this.getNarrativeStyle(narrative.id).style === 'dotted') ? "2px 5px" : 'none'
|
||||
}
|
||||
|
||||
getStroke(narrative, step) {
|
||||
if (!step || this.props.narrative === null) return 'none';
|
||||
return this.getNarrativeStyle(narrative.id).stroke;
|
||||
if (!step || this.props.narrative === null) return 'none'
|
||||
return this.getNarrativeStyle(narrative.id).stroke
|
||||
}
|
||||
|
||||
getStrokeOpacity(narrative, step) {
|
||||
if (this.props.narrative === null) return 0;
|
||||
if (!step || narrative.id !== this.props.narrative.id) return 0.1;
|
||||
return 1;
|
||||
if (this.props.narrative === null) return 0
|
||||
if (!step || narrative.id !== this.props.narrative.id) return 0.1
|
||||
return 1
|
||||
}
|
||||
|
||||
hasNoLocation(step) {
|
||||
@@ -44,14 +44,14 @@ class MapNarratives extends React.Component {
|
||||
}
|
||||
|
||||
renderNarrativeStep(allSteps, step, idx, n) {
|
||||
const { x, y } = this.projectPoint([step.latitude, step.longitude]);
|
||||
const step2 = allSteps[idx + 1];
|
||||
const { x, y } = this.projectPoint([step.latitude, step.longitude])
|
||||
const step2 = allSteps[idx + 1]
|
||||
|
||||
// don't draw if one of the steps has no location
|
||||
if (this.hasNoLocation(step) || this.hasNoLocation(step2))
|
||||
return null
|
||||
|
||||
const p2 = this.projectPoint([step2.latitude, step2.longitude]);
|
||||
const p2 = this.projectPoint([step2.latitude, step2.longitude])
|
||||
|
||||
return (
|
||||
<line
|
||||
@@ -70,11 +70,11 @@ class MapNarratives extends React.Component {
|
||||
}}
|
||||
>
|
||||
</line>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
renderNarrative(n) {
|
||||
const steps = n.steps.slice(0, n.steps.length - 1);
|
||||
const steps = n.steps.slice(0, n.steps.length - 1)
|
||||
|
||||
return (
|
||||
<g id={`narrative-${n.id.replace(/ /g,"_")}`} className="narrative">
|
||||
@@ -84,14 +84,14 @@ class MapNarratives extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.narrative === null) return (<div />);
|
||||
if (this.props.narrative === null) return (<div />)
|
||||
|
||||
return (
|
||||
<Portal node={this.props.svg}>
|
||||
{this.props.narratives.map(n => this.renderNarrative(n))}
|
||||
</Portal>
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default MapNarratives;
|
||||
export default MapNarratives
|
||||
|
||||
@@ -1,86 +1,65 @@
|
||||
import React from 'react';
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { selectActiveNarrative } from '../selectors'
|
||||
|
||||
class NarrativeCard extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
step: 0
|
||||
}
|
||||
}
|
||||
|
||||
goToPrevKeyFrame() {
|
||||
if (this.state.step > 0) {
|
||||
this.setState({ step: this.state.step - 1 });
|
||||
}
|
||||
}
|
||||
|
||||
goToNextKeyFrame() {
|
||||
if (this.state.step < this.props.narrative.steps.length - 1) {
|
||||
this.setState({ step: this.state.step + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const step = this.props.narrative.steps[this.state.step];
|
||||
this.props.onSelect([step]);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.narrative === this.props.narrative && this.state.step !== prevState.step) {
|
||||
const step = this.props.narrative.steps[this.state.step];
|
||||
this.props.onSelect([step]);
|
||||
} else if (prevProps.narrative !== this.props.narrative && this.props.narrative !== null) {
|
||||
this.setState({
|
||||
step: 0
|
||||
}, () => {
|
||||
const step = this.props.narrative.steps[this.state.step];
|
||||
this.props.onSelect([step]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderClose() {
|
||||
function NarrativeCard ({ narrative, methods }) {
|
||||
const { onSelectNarrative, onNext, onPrev } = methods
|
||||
function renderClose() {
|
||||
return (
|
||||
<button
|
||||
className="side-menu-burg is-active"
|
||||
onClick={() => { this.props.onSelectNarrative(null); }}
|
||||
className='side-menu-burg is-active'
|
||||
onClick={() => { onSelectNarrative(null) }}
|
||||
>
|
||||
<span></span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.narrative.steps[this.state.step]) {
|
||||
const steps = this.props.narrative.steps;
|
||||
const step = steps[this.state.step];
|
||||
|
||||
return (
|
||||
<div className="narrative-info">
|
||||
{this.renderClose()}
|
||||
<h3>{this.props.narrative.label}</h3>
|
||||
<p>{this.props.narrative.description}</p>
|
||||
<h6>
|
||||
<i className="material-icons left">location_on</i>
|
||||
{this.state.step + 1}/{steps.length}. {step.location}
|
||||
</h6>
|
||||
<div className="actions">
|
||||
<div className={`${(!this.state.step) ? 'disabled ' : ''} action`} onClick={() => this.goToPrevKeyFrame()}>←</div>
|
||||
<div className={`${(this.state.step >= this.props.narrative.steps.length - 1) ? 'disabled ' : ''} action`} onClick={() => this.goToNextKeyFrame()}>→</div>
|
||||
</div>
|
||||
function _renderActions(current, steps) {
|
||||
const prevExists = current !== 0
|
||||
const nextExists = current < steps.length - 1
|
||||
return (
|
||||
<div className='actions'>
|
||||
<div
|
||||
className={`${prevExists ? '' : 'disabled'} action`}
|
||||
onClick={prevExists ? onPrev : null}>←
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (<div/>);
|
||||
<div
|
||||
className={`${nextExists ? '' : 'disabled'} action`}
|
||||
onClick={nextExists ? onNext : null}>→
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// no display if no narrative
|
||||
if (!narrative) return null
|
||||
|
||||
const { steps, current } = narrative
|
||||
|
||||
if (steps[current]) {
|
||||
const step = steps[current]
|
||||
|
||||
return (
|
||||
<div className='narrative-info'>
|
||||
{renderClose()}
|
||||
<h3>{narrative.label}</h3>
|
||||
<p>{narrative.description}</p>
|
||||
<h6>
|
||||
<i className='material-icons left'>location_on</i>
|
||||
{current + 1}/{steps.length}. {step.location}
|
||||
</h6>
|
||||
{_renderActions(current, steps)}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
narrative: state.app.narrative
|
||||
narrative: selectActiveNarrative(state)
|
||||
}
|
||||
}
|
||||
export default connect(mapStateToProps)(NarrativeCard);
|
||||
export default connect(mapStateToProps)(NarrativeCard)
|
||||
|
||||
@@ -34,18 +34,20 @@ class Timeline extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isNarrative, app, ui } = this.props
|
||||
let classes = `timeline-wrapper ${(this.state.isFolded) ? ' folded' : ''}`;
|
||||
classes += (this.props.app.narrative !== null) ? ' narrative-mode' : '';
|
||||
classes += (app.narrative !== null) ? ' narrative-mode' : '';
|
||||
return (
|
||||
<div className={classes}>
|
||||
<TimelineHeader
|
||||
title={copy[this.props.app.language].timeline.info}
|
||||
date0={formatterWithYear(this.props.app.timerange[0])}
|
||||
date1={formatterWithYear(this.props.app.timerange[1])}
|
||||
title={copy[app.language].timeline.info}
|
||||
date0={formatterWithYear(app.timerange[0])}
|
||||
date1={formatterWithYear(app.timerange[1])}
|
||||
onClick={() => { this.onClickArrow(); }}
|
||||
hideInfo={isNarrative}
|
||||
/>
|
||||
<div className="timeline-content">
|
||||
<div id={this.props.ui.dom.timeline} className="timeline" />
|
||||
<div id={ui.dom.timeline} className="timeline" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -54,6 +56,7 @@ class Timeline extends React.Component {
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
isNarrative: !!state.app.narrative,
|
||||
domain: {
|
||||
events: state.domain.events,
|
||||
categories: selectors.selectCategories(state),
|
||||
|
||||
@@ -5,27 +5,24 @@ import * as selectors from '../selectors'
|
||||
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
|
||||
import Search from './Search.jsx';
|
||||
import TagListPanel from './TagListPanel.jsx';
|
||||
import ToolbarBottomActions from './ToolbarBottomActions.jsx';
|
||||
// import ToolbarBottomActions from './ToolbarBottomActions.jsx';
|
||||
import copy from '../js/data/copy.json';
|
||||
import { isNotNullNorUndefined, trimAndEllipse } from '../js/utilities.js';
|
||||
import { trimAndEllipse } from '../js/utilities.js';
|
||||
|
||||
class Toolbar extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
tabNum: -1
|
||||
};
|
||||
super(props)
|
||||
this.state = { _selected: -1 }
|
||||
}
|
||||
|
||||
toggleTab(tabNum) {
|
||||
this.setState({ tabNum: (this.state.tabNum === tabNum) ? -1 : tabNum });
|
||||
selectTab(selected) {
|
||||
const _selected = (this.state._selected === selected) ? -1 : selected
|
||||
this.setState({ _selected });
|
||||
}
|
||||
|
||||
renderClosePanel() {
|
||||
return (
|
||||
<div className="panel-header" onClick={() => this.toggleTab(-1)}>
|
||||
<div className="panel-header" onClick={() => this.selectTab(-1)}>
|
||||
<div className="caret"></div>
|
||||
</div>
|
||||
);
|
||||
@@ -49,11 +46,8 @@ class Toolbar extends React.Component {
|
||||
}
|
||||
|
||||
goToNarrative(narrative) {
|
||||
this.setState({
|
||||
tabNum: -1
|
||||
}, () => {
|
||||
this.props.onSelectNarrative(narrative);
|
||||
});
|
||||
this.selectTab(-1) // set all unselected within this component
|
||||
this.props.methods.onSelectNarrative(narrative);
|
||||
}
|
||||
|
||||
renderToolbarNarrativePanel() {
|
||||
@@ -64,7 +58,7 @@ class Toolbar extends React.Component {
|
||||
{this.props.narratives.map((narr) => {
|
||||
return (
|
||||
<div className="panel-action action">
|
||||
<button style={{ backgroundColor: '#000' }} onClick={() => { this.goToNarrative(narr); }}>
|
||||
<button style={{ backgroundColor: '#000' }} onClick={() => { this.goToNarrative(narr) }}>
|
||||
<p>{narr.label}</p>
|
||||
<p><small>{trimAndEllipse(narr.description, 120)}</small></p>
|
||||
</button>
|
||||
@@ -94,12 +88,12 @@ class Toolbar extends React.Component {
|
||||
return '';
|
||||
}
|
||||
|
||||
renderToolbarTab(tabNum, label) {
|
||||
const isActive = (this.state.tabNum === tabNum);
|
||||
renderToolbarTab(_selected, label) {
|
||||
const isActive = (this.state._selected === _selected);
|
||||
let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab';
|
||||
|
||||
return (
|
||||
<div className={classes} onClick={() => { this.toggleTab(tabNum); }}>
|
||||
<div className={classes} onClick={() => { this.selectTab(_selected); }}>
|
||||
<i className="material-icons">timeline</i>
|
||||
<div className="tab-caption">{label}</div>
|
||||
</div>
|
||||
@@ -118,20 +112,17 @@ class Toolbar extends React.Component {
|
||||
{this.renderToolbarTab(0, 'Focus stories')}
|
||||
{this.renderToolbarTab(1, 'Explore freely')}
|
||||
</div>
|
||||
<ToolbarBottomActions
|
||||
actions={this.props.actions}
|
||||
/>
|
||||
{/* <ToolbarBottomActions /> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderToolbarPanels() {
|
||||
let classes = (this.state.tabNum !== -1) ? 'toolbar-panels' : 'toolbar-panels folded';
|
||||
|
||||
let classes = (this.state._selected >= 0) ? 'toolbar-panels' : 'toolbar-panels folded';
|
||||
return (
|
||||
<div className={classes}>
|
||||
{this.renderClosePanel()}
|
||||
<Tabs selectedIndex={this.state.tabNum}>
|
||||
<Tabs selectedIndex={this.state._selected}>
|
||||
{this.renderToolbarNarrativePanel()}
|
||||
{this.renderToolbarTagPanel()}
|
||||
</Tabs>
|
||||
@@ -142,12 +133,12 @@ class Toolbar extends React.Component {
|
||||
renderToolbarNavs() {
|
||||
if (this.props.narratives) {
|
||||
return this.props.narratives.map((nar, idx) => {
|
||||
const isActive = (idx === this.state.tab);
|
||||
const isActive = (idx === this.state._selected);
|
||||
|
||||
let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab';
|
||||
|
||||
return (
|
||||
<div className={classes} onClick={() => { this.toggleTab(idx); }}>
|
||||
<div className={classes} onClick={() => { this.selectTab(idx); }}>
|
||||
<div className="tab-caption">{nar.label}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -168,15 +159,14 @@ class Toolbar extends React.Component {
|
||||
{this.renderToolbarTab(0, 'Narratives')}
|
||||
{(isTags) ? this.renderToolbarTab(1, 'Explore by tag') : ''}
|
||||
</div>
|
||||
<ToolbarBottomActions
|
||||
actions={this.props.actions}
|
||||
/>
|
||||
{/* <ToolbarBottomActions /> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const isNarrative = isNotNullNorUndefined(this.props.narrative);
|
||||
const { isNarrative } = this.props
|
||||
|
||||
return (
|
||||
<div id="toolbar-wrapper" className={`toolbar-wrapper ${(isNarrative) ? 'narrative-mode' : ''}`}>
|
||||
{this.renderToolbarTabs()}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
const TimelineHeader = ({ title, date0, date1, onClick }) => (
|
||||
<div className="timeline-header">
|
||||
<div className="timeline-toggle" onClick={() => onClick()}>
|
||||
<p><i className="arrow-down"></i></p>
|
||||
const TimelineHeader = ({ title, date0, date1, onClick, hideInfo }) => (
|
||||
<div className='timeline-header'>
|
||||
<div className='timeline-toggle' onClick={() => onClick()}>
|
||||
<p><i className='arrow-down'></i></p>
|
||||
</div>
|
||||
<div className="timeline-info">
|
||||
<div className={`timeline-info ${hideInfo ? 'hidden' : ''}`}>
|
||||
<p>{title}</p>
|
||||
<p>{date0} - {date1}</p>
|
||||
</div>
|
||||
|
||||
@@ -73,11 +73,41 @@ 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 function compareTimestamp (a, b) {
|
||||
return (parseTimestamp(a.timestamp) > parseTimestamp(b.timestamp));
|
||||
}
|
||||
|
||||
/**
|
||||
* Inset the full source represenation from 'allSources' into an event. The
|
||||
* function is 'curried' to allow easy use with maps. To use for a single
|
||||
* source, call with two sets of parentheses:
|
||||
* const src = insetSourceFrom(sources)(anEvent)
|
||||
*/
|
||||
export function insetSourceFrom(allSources) {
|
||||
return (event) => {
|
||||
let sources
|
||||
if (!event.sources) {
|
||||
sources = []
|
||||
} else {
|
||||
sources = event.sources.map(id => (
|
||||
allSources.hasOwnProperty(id) ? allSources[id] : null
|
||||
))
|
||||
}
|
||||
return {
|
||||
...event,
|
||||
sources
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Debugging function: put in place of a mapStateToProps function to
|
||||
* view that source modal by default
|
||||
*/
|
||||
function injectSource(id) {
|
||||
export function injectSource(id) {
|
||||
return state => ({
|
||||
...state,
|
||||
app: {
|
||||
@@ -86,4 +116,3 @@ function injectSource(id) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import initial from '../store/initial.js';
|
||||
import initial from '../store/initial.js'
|
||||
|
||||
import { parseDate } from '../js/utilities.js';
|
||||
import { parseDate } from '../js/utilities.js'
|
||||
|
||||
import {
|
||||
UPDATE_HIGHLIGHTED,
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
UPDATE_TAGFILTERS,
|
||||
UPDATE_TIMERANGE,
|
||||
UPDATE_NARRATIVE,
|
||||
INCREMENT_NARRATIVE_CURRENT,
|
||||
DECREMENT_NARRATIVE_CURRENT,
|
||||
UPDATE_SOURCE,
|
||||
RESET_ALLFILTERS,
|
||||
TOGGLE_LANGUAGE,
|
||||
@@ -18,63 +20,69 @@ import {
|
||||
TOGGLE_NOTIFICATIONS,
|
||||
FETCH_ERROR,
|
||||
FETCH_SOURCE_ERROR,
|
||||
} from '../actions';
|
||||
} from '../actions'
|
||||
|
||||
function updateHighlighted(appState, action) {
|
||||
return Object.assign({}, appState, {
|
||||
highlighted: action.highlighted
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function updateSelected(appState, action) {
|
||||
return Object.assign({}, appState, {
|
||||
selected: action.selected
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function updateNarrative(appState, action) {
|
||||
if (action.narrative === null) {
|
||||
return Object.assign({}, appState, {
|
||||
narrative: action.narrative,
|
||||
});
|
||||
} else {
|
||||
const dates = action.narrative.steps.map(n => parseDate(n.timestamp).getTime())
|
||||
let minDate = Math.min(...dates);
|
||||
let maxDate = Math.max(...dates);
|
||||
// Add some margin to the datetime extent
|
||||
minDate = minDate - ((maxDate - minDate) / 20);
|
||||
maxDate = maxDate + ((maxDate - minDate) / 20);
|
||||
return {
|
||||
...appState,
|
||||
narrative: action.narrative,
|
||||
narrativeState: {
|
||||
current: !!action.narrative ? 0 : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.assign({}, appState, {
|
||||
narrative: action.narrative,
|
||||
filters: Object.assign({}, appState.filters, {
|
||||
timerange: [new Date(minDate), new Date(maxDate)]
|
||||
}),
|
||||
});
|
||||
function incrementNarrativeCurrent(appState, action) {
|
||||
return {
|
||||
...appState,
|
||||
narrativeState: {
|
||||
current: appState.narrativeState.current += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function decrementNarrativeCurrent(appState, action) {
|
||||
return {
|
||||
...appState,
|
||||
narrativeState: {
|
||||
current: appState.narrativeState.current -= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateTagFilters(appState, action) {
|
||||
const tagFilters = appState.filters.tags.slice(0);
|
||||
const tagFilters = appState.filters.tags.slice(0)
|
||||
const nextActiveState = action.tag.active
|
||||
|
||||
function traverseNode(node) {
|
||||
const tagFilter = tagFilters.find(tF => tF.key === node.key);
|
||||
node.active = nextActiveState;
|
||||
if (!tagFilter) tagFilters.push(node);
|
||||
const tagFilter = tagFilters.find(tF => tF.key === node.key)
|
||||
node.active = nextActiveState
|
||||
if (!tagFilter) tagFilters.push(node)
|
||||
|
||||
if (node && Object.keys(node.children).length > 0) {
|
||||
Object.values(node.children).forEach((childNode) => { traverseNode(childNode); });
|
||||
Object.values(node.children).forEach((childNode) => { traverseNode(childNode) })
|
||||
}
|
||||
}
|
||||
|
||||
traverseNode(action.tag);
|
||||
traverseNode(action.tag)
|
||||
|
||||
return Object.assign({}, appState, {
|
||||
filters: Object.assign({}, appState.filters, {
|
||||
tags: tagFilters
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function updateTimeRange(appState, action) { // XXX
|
||||
@@ -82,7 +90,7 @@ function updateTimeRange(appState, action) { // XXX
|
||||
filters: Object.assign({}, appState.filters, {
|
||||
timerange: action.timerange
|
||||
}),
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function resetAllFilters(appState) { // XXX
|
||||
@@ -96,26 +104,26 @@ function resetAllFilters(appState) { // XXX
|
||||
],
|
||||
}),
|
||||
selected: [],
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function toggleLanguage(appState, action) {
|
||||
let otherLanguage = (appState.language === 'es-MX') ? 'en-US' : 'es-MX';
|
||||
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);
|
||||
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 updateSource(appState, action) {
|
||||
@@ -138,7 +146,7 @@ function toggleFetchingDomain(appState, action) {
|
||||
flags: Object.assign({}, appState.flags, {
|
||||
isFetchingDomain: !appState.flags.isFetchingDomain
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function toggleFetchingSources(appState, action) {
|
||||
@@ -146,7 +154,7 @@ function toggleFetchingSources(appState, action) {
|
||||
flags: Object.assign({}, appState.flags, {
|
||||
isFetchingSources: !appState.flags.isFetchingSources
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function toggleInfoPopup(appState, action) {
|
||||
@@ -154,7 +162,7 @@ function toggleInfoPopup(appState, action) {
|
||||
flags: Object.assign({}, appState.flags, {
|
||||
isInfopopup: !appState.flags.isInfopopup
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function toggleNotifications(appState, action) {
|
||||
@@ -162,7 +170,7 @@ function toggleNotifications(appState, action) {
|
||||
flags: Object.assign({}, appState.flags, {
|
||||
isNotification: !appState.flags.isNotification
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function fetchSourceError(appState, action) {
|
||||
@@ -180,38 +188,42 @@ function fetchSourceError(appState, action) {
|
||||
function app(appState = initial.app, action) {
|
||||
switch (action.type) {
|
||||
case UPDATE_HIGHLIGHTED:
|
||||
return updateHighlighted(appState, action);
|
||||
return updateHighlighted(appState, action)
|
||||
case UPDATE_SELECTED:
|
||||
return updateSelected(appState, action);
|
||||
return updateSelected(appState, action)
|
||||
case UPDATE_TAGFILTERS:
|
||||
return updateTagFilters(appState, action);
|
||||
return updateTagFilters(appState, action)
|
||||
case UPDATE_TIMERANGE:
|
||||
return updateTimeRange(appState, action);
|
||||
return updateTimeRange(appState, action)
|
||||
case UPDATE_NARRATIVE:
|
||||
return updateNarrative(appState, action);
|
||||
return updateNarrative(appState, action)
|
||||
case INCREMENT_NARRATIVE_CURRENT:
|
||||
return incrementNarrativeCurrent(appState, action)
|
||||
case DECREMENT_NARRATIVE_CURRENT:
|
||||
return decrementNarrativeCurrent(appState, action)
|
||||
case UPDATE_SOURCE:
|
||||
return updateSource(appState, action);
|
||||
return updateSource(appState, action)
|
||||
case RESET_ALLFILTERS:
|
||||
return resetAllFilters(appState, action);
|
||||
return resetAllFilters(appState, action)
|
||||
case TOGGLE_LANGUAGE:
|
||||
return toggleLanguage(appState, action);
|
||||
return toggleLanguage(appState, action)
|
||||
case TOGGLE_MAPVIEW:
|
||||
return toggleMapView(appState, action);
|
||||
return toggleMapView(appState, action)
|
||||
case FETCH_ERROR:
|
||||
return fetchError(appState, action);
|
||||
return fetchError(appState, action)
|
||||
case TOGGLE_FETCHING_DOMAIN:
|
||||
return toggleFetchingDomain(appState, action);
|
||||
return toggleFetchingDomain(appState, action)
|
||||
case TOGGLE_FETCHING_SOURCES:
|
||||
return toggleFetchingSources(appState, action);
|
||||
return toggleFetchingSources(appState, action)
|
||||
case TOGGLE_INFOPOPUP:
|
||||
return toggleInfoPopup(appState, action);
|
||||
return toggleInfoPopup(appState, action)
|
||||
case TOGGLE_NOTIFICATIONS:
|
||||
return toggleNotifications(appState, action);
|
||||
return toggleNotifications(appState, action)
|
||||
case FETCH_SOURCE_ERROR:
|
||||
return fetchSourceError(appState, action);
|
||||
return fetchSourceError(appState, action)
|
||||
default:
|
||||
return appState;
|
||||
return appState
|
||||
}
|
||||
}
|
||||
|
||||
export default app;
|
||||
export default app
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
box-sizing: border-box;
|
||||
margin: 1px 0 0 0;
|
||||
padding: 15px;
|
||||
border: 1px solid rgba(0, 0, 0, 0);
|
||||
border-radius: 3px;
|
||||
border: 1px solid $black;
|
||||
// border-radius: 3px;
|
||||
transition: 0.2 ease;
|
||||
background: $offwhite;
|
||||
background: $darkwhite;
|
||||
color: $darkgrey;
|
||||
box-shadow: 0 19px 19px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);
|
||||
font-size: $large;
|
||||
@@ -39,10 +39,13 @@
|
||||
.card-row, .card-col {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid $lightwhite;
|
||||
margin: 5px 0 10px 0;
|
||||
padding-bottom: 10px;
|
||||
|
||||
&.details {
|
||||
border-bottom: 1px solid $lightwhite;
|
||||
}
|
||||
|
||||
.card-cell {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -120,6 +123,7 @@
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.card-toggle p {
|
||||
@@ -197,6 +201,7 @@
|
||||
.summary {
|
||||
overflow: auto;
|
||||
margin-top: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.tag {
|
||||
@@ -204,4 +209,12 @@
|
||||
margin: 0;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: $offwhite;
|
||||
}
|
||||
|
||||
.card-row {
|
||||
border-color: darkgray;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
@import 'burger';
|
||||
@import 'card';
|
||||
|
||||
$card-width: 500px;
|
||||
$card-width: 370px;
|
||||
$narrative-info-max-height: 170px;
|
||||
$timeline-height: 170px;
|
||||
|
||||
.card-stack {
|
||||
position: absolute;
|
||||
@@ -9,11 +11,17 @@ $card-width: 500px;
|
||||
right: 10px;
|
||||
max-height: calc(100% - 208px);
|
||||
height: auto;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 19px 38px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);
|
||||
z-index: $header;
|
||||
color: white;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
&.narrative-mode {
|
||||
right: auto;
|
||||
left: 10px;
|
||||
top: $narrative-info-max-height + 12px;
|
||||
height: calc(100% - #{$narrative-info-max-height} - #{$timeline-height} - 12px);
|
||||
}
|
||||
|
||||
&.full-height {
|
||||
max-height: calc(100% - 20px);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
$narrative-info-width: 370px;
|
||||
|
||||
/*
|
||||
NARRATIVE INFO
|
||||
*/
|
||||
@@ -5,8 +7,9 @@ NARRATIVE INFO
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
height: auto;
|
||||
width: 370px;
|
||||
// height: auto;
|
||||
height: 170px;
|
||||
width: $narrative-info-width;
|
||||
box-sizing: border-box;
|
||||
padding: 15px;
|
||||
max-height: calc(100% - 250px);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
.timeline-wrapper {
|
||||
position: fixed;
|
||||
box-sizing: border-box;
|
||||
@@ -67,6 +66,10 @@
|
||||
}
|
||||
|
||||
.timeline-info {
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
width: calc(#{$card-width} - 20px);
|
||||
position: absolute;
|
||||
margin-top: -70px;
|
||||
margin-left: 10px;
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
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 getSelected = state => state.app.selected;
|
||||
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
|
||||
export const getActiveStep = state => state.app.narrativeState.current
|
||||
export const getSelected = state => state.app.selected
|
||||
export const getSites = (state) => {
|
||||
if (process.env.features.USE_SITES) return state.domain.sites;
|
||||
return [];
|
||||
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 [];
|
||||
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;
|
||||
export const getTimeRange = state => state.app.filters.timerange;
|
||||
export const getNotifications = state => state.domain.notifications
|
||||
export const getTagTree = state => state.domain.tags
|
||||
export const getTagsFilter = state => state.app.filters.tags
|
||||
export const getTimeRange = state => state.app.filters.timerange
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Some handy helpers
|
||||
*/
|
||||
const parseTimestamp = ts => d3.timeParse("%Y-%m-%dT%H:%M:%S")(ts);
|
||||
|
||||
/**
|
||||
* Given an event and all tags,
|
||||
@@ -30,11 +34,11 @@ const parseTimestamp = ts => d3.timeParse("%Y-%m-%dT%H:%M:%S")(ts);
|
||||
*/
|
||||
function isTaggedIn(event, tagFilters) {
|
||||
if (event.tags) {
|
||||
const tagsInEvent = event.tags.split(",");
|
||||
const tagsInEvent = event.tags.split(",")
|
||||
const isTagged = tagsInEvent.some((tag) => {
|
||||
return tagFilters.find(tF => (tF.key === tag && tF.active));
|
||||
});
|
||||
return isTagged;
|
||||
return tagFilters.find(tF => (tF.key === tag && tF.active))
|
||||
})
|
||||
return isTagged
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
@@ -48,7 +52,7 @@ function isNoTags(tagFilters) {
|
||||
tagFilters.length === 0
|
||||
|| !process.env.features.USE_TAGS
|
||||
|| tagFilters.every(t => !t.active)
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,7 +63,7 @@ function isTimeRangedIn(event, timeRange) {
|
||||
return (
|
||||
timeRange[0] < parseTimestamp(event.timestamp)
|
||||
&& parseTimestamp(event.timestamp) < timeRange[1]
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,64 +75,79 @@ export const selectEvents = createSelector(
|
||||
(events, tagFilters, timeRange) => {
|
||||
|
||||
return events.reduce((acc, event) => {
|
||||
const isTagged = isTaggedIn(event, tagFilters) || isNoTags(tagFilters);
|
||||
const isTimeRanged = isTimeRangedIn(event, timeRange);
|
||||
const isTagged = isTaggedIn(event, tagFilters) || isNoTags(tagFilters)
|
||||
const isTimeRanged = isTimeRangedIn(event, timeRange)
|
||||
|
||||
if (isTimeRanged && isTagged) {
|
||||
const eventClone = Object.assign({}, event);
|
||||
acc[event.id] = eventClone;
|
||||
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, getTagsFilter, getTimeRange],
|
||||
(events, narrativeMetadata, tagFilters, timeRange) => {
|
||||
[getEvents, getNarratives, getTagsFilter, getTimeRange, getSources],
|
||||
(events, narrativesMeta, tagFilters, timeRange, sources) => {
|
||||
|
||||
const narratives = {};
|
||||
events.forEach((evt) => {
|
||||
const isTagged = isTaggedIn(evt, tagFilters) || isNoTags(tagFilters);
|
||||
const isTimeRanged = isTimeRangedIn(evt, timeRange);
|
||||
const isInNarrative = evt.narratives.length > 0;
|
||||
const narratives = {}
|
||||
const narrativeSkeleton = id => ({ id, steps: [] })
|
||||
|
||||
evt.narratives.map(narrative => {
|
||||
if (!narratives[narrative]) {
|
||||
narratives[narrative] = { id: narrative, steps: [], byId: {} };
|
||||
}
|
||||
/* 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
|
||||
|
||||
if (isInNarrative) {
|
||||
narratives[narrative].steps.push(evt);
|
||||
narratives[narrative].byId[evt.id] = { next: null, prev: null };
|
||||
}
|
||||
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))
|
||||
})
|
||||
});
|
||||
})
|
||||
|
||||
Object.keys(narratives).forEach((key) => {
|
||||
const steps = narratives[key].steps;
|
||||
|
||||
steps.sort((a, b) => {
|
||||
return (parseTimestamp(a.timestamp) > parseTimestamp(b.timestamp));
|
||||
});
|
||||
/* sort steps by time */
|
||||
Object.keys(narratives).forEach(key => {
|
||||
const steps = narratives[key].steps
|
||||
|
||||
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;
|
||||
});
|
||||
steps.sort(compareTimestamp)
|
||||
|
||||
if (narrativeMetadata.find(n => n.id === key)) {
|
||||
narratives[key] = Object.assign(narrativeMetadata.find(n => n.id === key), narratives[key]);
|
||||
// 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),
|
||||
...narratives[key]
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
return Object.values(narratives);
|
||||
});
|
||||
return Object.values(narratives)
|
||||
})
|
||||
|
||||
/** 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 }
|
||||
: null
|
||||
)
|
||||
/**
|
||||
* Of all the filtered events, group them by location and return a list of
|
||||
* locations with at least one event in it, based on the time range and tags
|
||||
@@ -137,12 +156,12 @@ export const selectLocations = createSelector(
|
||||
[selectEvents],
|
||||
(events) => {
|
||||
|
||||
const selectedLocations = {};
|
||||
const selectedLocations = {}
|
||||
events.forEach(event => {
|
||||
const location = event.location;
|
||||
const location = event.location
|
||||
|
||||
if (selectedLocations[location]) {
|
||||
selectedLocations[location].events.push(event);
|
||||
selectedLocations[location].events.push(event)
|
||||
} else {
|
||||
selectedLocations[location] = {
|
||||
label: location,
|
||||
@@ -153,9 +172,11 @@ export const selectLocations = createSelector(
|
||||
}
|
||||
})
|
||||
|
||||
return Object.values(selectedLocations);
|
||||
return Object.values(selectedLocations)
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Of all the sources, select those that are relevant to the selected events.
|
||||
@@ -167,21 +188,7 @@ export const selectSelected = createSelector(
|
||||
return []
|
||||
}
|
||||
|
||||
// NB: return source object if exists, otherwise null
|
||||
const srcs = selected
|
||||
.map(e => e.sources)
|
||||
.map(_sources => {
|
||||
if (!_sources) return [];
|
||||
return _sources.map(id => (
|
||||
sources.hasOwnProperty(id) ? sources[id] : null
|
||||
))
|
||||
}
|
||||
)
|
||||
|
||||
return selected.map((s, idx) => ({
|
||||
...s,
|
||||
sources: srcs[idx]
|
||||
}))
|
||||
return selected.map(insetSourceFrom(sources))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -191,7 +198,7 @@ export const selectSelected = createSelector(
|
||||
export const selectCategories = createSelector(
|
||||
[getCategories],
|
||||
(categories) => categories
|
||||
);
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
@@ -201,23 +208,23 @@ export const selectCategories = createSelector(
|
||||
export const selectTagList = createSelector(
|
||||
[getTagTree],
|
||||
(tags) => {
|
||||
const tagList = [];
|
||||
let depth = 0;
|
||||
const tagList = []
|
||||
let depth = 0
|
||||
function traverseNode(node, depth) {
|
||||
node.active = (!node.hasOwnProperty('active')) ? false : node.active;
|
||||
node.depth = depth;
|
||||
node.active = (!node.hasOwnProperty('active')) ? false : node.active
|
||||
node.depth = depth
|
||||
|
||||
if (node.active) tagList.push(node)
|
||||
|
||||
if (Object.keys(node.children).length > 0) {
|
||||
Object.values(node.children).forEach((childNode) => {
|
||||
traverseNode(childNode, depth + 1);
|
||||
});
|
||||
traverseNode(childNode, depth + 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
if (tags && tags !== undefined) {
|
||||
if (tags.key && tags.children) traverseNode(tags, depth)
|
||||
}
|
||||
return tagList;
|
||||
return tagList
|
||||
}
|
||||
)
|
||||
|
||||
@@ -33,6 +33,9 @@ const initial = {
|
||||
selected: [],
|
||||
source: null,
|
||||
narrative: null,
|
||||
narrativeState: {
|
||||
current: null
|
||||
},
|
||||
filters: {
|
||||
timerange: [
|
||||
d3.timeParse("%Y-%m-%dT%H:%M:%S")("2013-02-23T12:00:00"),
|
||||
|
||||
Reference in New Issue
Block a user