Using prettier for linting

This commit is contained in:
Zac Ioannidis
2020-12-08 13:13:50 +00:00
parent fa329066e4
commit 81e00fd917
111 changed files with 3986 additions and 3294 deletions

View File

@@ -1,13 +1,11 @@
import '../scss/main.scss'
import React from 'react'
import Layout from './Layout'
import "../scss/main.scss";
import React from "react";
import Layout from "./Layout";
class App extends React.Component {
render () {
return (
<Layout />
)
render() {
return <Layout />;
}
}
export default App
export default App;

View File

@@ -1,82 +1,85 @@
import copy from '../common/data/copy.json'
import React from 'react'
import copy from "../common/data/copy.json";
import React from "react";
import CardCustomField from './presentational/Card/CustomField'
import CardTime from './presentational/Card/Time'
import CardLocation from './presentational/Card/Location'
import CardCaret from './presentational/Card/Caret'
import CardSummary from './presentational/Card/Summary'
import CardSource from './presentational/Card/Source'
import { makeNiceDate } from '../common/utilities'
import CardCustomField from "./presentational/Card/CustomField";
import CardTime from "./presentational/Card/Time";
import CardLocation from "./presentational/Card/Location";
import CardCaret from "./presentational/Card/Caret";
import CardSummary from "./presentational/Card/Summary";
import CardSource from "./presentational/Card/Source";
import { makeNiceDate } from "../common/utilities";
class Card extends React.Component {
constructor (props) {
super(props)
constructor(props) {
super(props);
this.state = {
isOpen: false
}
isOpen: false,
};
}
toggle () {
toggle() {
this.setState({
isOpen: !this.state.isOpen
})
isOpen: !this.state.isOpen,
});
}
makeTimelabel (datetime) {
return makeNiceDate(datetime)
makeTimelabel(datetime) {
return makeNiceDate(datetime);
}
handleCardSelect (e) {
if (!e.target.className.includes('arrow-down')) {
const selectedEventFormat = this.props.idx > 0 ? [this.props.event] : this.props.event
this.props.onSelect(selectedEventFormat, this.props.idx)
handleCardSelect(e) {
if (!e.target.className.includes("arrow-down")) {
const selectedEventFormat =
this.props.idx > 0 ? [this.props.event] : this.props.event;
this.props.onSelect(selectedEventFormat, this.props.idx);
}
}
renderSummary () {
renderSummary() {
return (
<CardSummary
language={this.props.language}
description={this.props.event.description}
isOpen={this.state.isOpen}
/>
)
);
}
renderLocation () {
renderLocation() {
return (
<CardLocation
language={this.props.language}
location={this.props.event.location}
isPrecise={(!this.props.event.type || this.props.event.type === 'Structure')}
isPrecise={
!this.props.event.type || this.props.event.type === "Structure"
}
/>
)
);
}
renderSources () {
renderSources() {
if (this.props.sourceError) {
return <div>ERROR: something went wrong loading sources, TODO:</div>
return <div>ERROR: something went wrong loading sources, TODO:</div>;
}
const sourceLang = copy[this.props.language].cardstack.sources
const sourceLang = copy[this.props.language].cardstack.sources;
return (
<div className='card-col'>
<div className="card-col">
<h4>{sourceLang}: </h4>
{this.props.event.sources.map(source => (
{this.props.event.sources.map((source) => (
<CardSource
isLoading={this.props.isLoading}
source={source}
onClickHandler={source => this.props.onViewSource(source)}
onClickHandler={(source) => this.props.onViewSource(source)}
/>
))}
</div>
)
);
}
// NB: should be internaionalized.
renderTime () {
let timelabel = this.makeTimelabel(this.props.event.datetime)
renderTime() {
const timelabel = this.makeTimelabel(this.props.event.datetime);
// let precision = this.props.event.time_display
// if (precision === '_date_only') {
@@ -97,54 +100,46 @@ class Card extends React.Component {
language={this.props.language}
timelabel={timelabel}
/>
)
);
}
renderCustomFields () {
return this.props.features.CUSTOM_EVENT_FIELDS
.map(field => {
const value = this.props.event[field.key]
return value ? (
<CardCustomField field={field} value={this.props.event[field.key]} />
) : null
})
renderCustomFields() {
return this.props.features.CUSTOM_EVENT_FIELDS.map((field) => {
const value = this.props.event[field.key];
return value ? (
<CardCustomField field={field} value={this.props.event[field.key]} />
) : null;
});
}
renderMain () {
renderMain() {
return (
<div className='card-container'>
<div className='card-row details'>
<div className="card-container">
<div className="card-row details">
{this.renderTime()}
{this.renderLocation()}
</div>
{this.renderSummary()}
{this.renderCustomFields()}
</div>
)
);
}
renderExtra () {
return (
<div className='card-bottomhalf'>
{this.renderSources()}
</div>
)
renderExtra() {
return <div className="card-bottomhalf">{this.renderSources()}</div>;
}
renderCaret () {
renderCaret() {
return this.props.features.USE_SOURCES ? (
<CardCaret
toggle={() => this.toggle()}
isOpen={this.state.isOpen}
/>
) : null
<CardCaret toggle={() => this.toggle()} isOpen={this.state.isOpen} />
) : null;
}
render () {
const { isSelected, idx } = this.props
render() {
const { isSelected, idx } = this.props;
return (
<li
className={`event-card ${isSelected ? 'selected' : ''}`}
className={`event-card ${isSelected ? "selected" : ""}`}
id={`event-card-${idx}`}
ref={this.props.innerRef}
onClick={(e) => this.handleCardSelect(e)}
@@ -153,9 +148,11 @@ class Card extends React.Component {
{this.state.isOpen ? this.renderExtra() : null}
{this.renderCaret()}
</li>
)
);
}
}
// The ref to each card will be used in CardStack for programmatic scrolling
export default React.forwardRef((props, ref) => <Card innerRef={ref} {...props} />)
export default React.forwardRef((props, ref) => (
<Card innerRef={ref} {...props} />
));

View File

@@ -29,8 +29,8 @@ class CardStack extends React.Component {
const cardScroll = this.refs[this.props.narrative.current].current
.offsetTop;
let start = element.scrollTop;
let change = cardScroll - start;
const start = element.scrollTop;
const change = cardScroll - start;
let currentTime = 0;
const increment = 20;

View File

@@ -1,32 +1,57 @@
import React from 'react'
import React from "react";
const Icon = ({ iconType }) => {
if (iconType === 'personas') {
if (iconType === "personas") {
return (
<svg x='0px' y='0px' width='40px' height='40px' viewBox='0 0 40 40' enableBackground='new 0 0 40 40'>
<path d='M15.464,17.713' />
<path d='M5.526,17.713c-1.537,0.595-3,1.472-4.314,2.637l1.114,17.081h16.338' />
<path d='M12.283,15.522c-1.707,0.661-3.332,1.636-4.792,2.93l1.238,18.979h18.153' />
<circle cx='27.432' cy='8.876' r='6.877' />
<path d='M21.297,13.088c-1.896,0.733-3.702,1.817-5.326,3.256l1.375,21.087h20.17l1.376-21.087c-1.624-1.438-3.43-2.522-5.326-3.256' />
<path d='M20.968,6.547c-0.926-0.554-2.006-0.877-3.163-0.877c-3.418,0-6.188,2.771-6.188,6.188c0,2.811,1.875,5.18,4.441,5.935' />
<path d='M12.38,8.881c-0.738-0.361-1.564-0.57-2.441-0.57c-3.076,0-5.57,2.494-5.57,5.57c0,1.983,1.04,3.72,2.601,4.707' />
<svg
x="0px"
y="0px"
width="40px"
height="40px"
viewBox="0 0 40 40"
enableBackground="new 0 0 40 40"
>
<path d="M15.464,17.713" />
<path d="M5.526,17.713c-1.537,0.595-3,1.472-4.314,2.637l1.114,17.081h16.338" />
<path d="M12.283,15.522c-1.707,0.661-3.332,1.636-4.792,2.93l1.238,18.979h18.153" />
<circle cx="27.432" cy="8.876" r="6.877" />
<path d="M21.297,13.088c-1.896,0.733-3.702,1.817-5.326,3.256l1.375,21.087h20.17l1.376-21.087c-1.624-1.438-3.43-2.522-5.326-3.256" />
<path d="M20.968,6.547c-0.926-0.554-2.006-0.877-3.163-0.877c-3.418,0-6.188,2.771-6.188,6.188c0,2.811,1.875,5.18,4.441,5.935" />
<path d="M12.38,8.881c-0.738-0.361-1.564-0.57-2.441-0.57c-3.076,0-5.57,2.494-5.57,5.57c0,1.983,1.04,3.72,2.601,4.707" />
</svg>
)
} else if (iconType === 'tipos') {
);
} else if (iconType === "tipos") {
return (
<svg x='0px' y='0px' width='40px' height='40px' viewBox='0 0 40 40' enableBackground='new 0 0 40 40'>
<path strokeDasharray='3, 4' d='M22.326,5.346
<svg
x="0px"
y="0px"
width="40px"
height="40px"
viewBox="0 0 40 40"
enableBackground="new 0 0 40 40"
>
<path
strokeDasharray="3, 4"
d="M22.326,5.346
c-2.154-2.081-5.082-3.367-8.314-3.367c-6.614,0-11.976,5.361-11.976,11.974c0,6.613,5.361,11.977,11.976,11.977
c0.228,0,0.449-0.021,0.674-0.034' />
<circle cx='23' cy='17.288' r='11.975' />
<circle strokeDasharray='3, 4' cx='25.987' cy='26.926' r='11.976' />
c0.228,0,0.449-0.021,0.674-0.034"
/>
<circle cx="23" cy="17.288" r="11.975" />
<circle strokeDasharray="3, 4" cx="25.987" cy="26.926" r="11.976" />
</svg>
)
} else if (iconType === 'hardware') {
);
} else if (iconType === "hardware") {
return (
<svg x='0px' y='0px' width='40px' height='40px' viewBox='0 0 40 40' enableBackground='new 0 0 40 40'>
<path d='M20,1.695C12.571,1.696,6.286,2.019,5.272,2.452C5.253,2.458,5.233,2.466,5.215,2.474
<svg
x="0px"
y="0px"
width="40px"
height="40px"
viewBox="0 0 40 40"
enableBackground="new 0 0 40 40"
>
<path
d="M20,1.695C12.571,1.696,6.286,2.019,5.272,2.452C5.253,2.458,5.233,2.466,5.215,2.474
c-0.01,0.004-0.019,0.008-0.027,0.012C4.38,2.831,3.803,4.256,3.802,5.907v3.502H2.926H1.175c-0.241,0-0.438,0.196-0.438,0.438
v0.875v5.254c0,0.242,0.196,0.438,0.438,0.438h1.751c0.242,0,0.438-0.195,0.438-0.438V11.16h0.438v15.324h5.691
c0.242,0,0.438,0.195,0.438,0.438v1.751c0,0.241-0.195,0.438-0.438,0.438H3.802v3.063c0,0.626,0.167,1.203,0.438,1.515v3.74
@@ -38,46 +63,83 @@ const Icon = ({ iconType }) => {
V7.22c0,0.242-0.195,0.438-0.438,0.438H4.991c-0.242,0-0.438-0.196-0.438-0.438V5.469C4.553,4.261,4.945,3.28,5.429,3.28z
M5.553,8.534h28.895c0.483,0,0.876,0.392,0.876,0.875v13.134c0,0.484-0.393,0.876-0.876,0.876h-3.466c0,0-0.863,0.613-0.912,0.613
H9.931c-0.113,0-0.225-0.022-0.33-0.065l-0.778-0.548h-3.27c-0.483,0-0.875-0.392-0.875-0.876V9.409
C4.678,8.926,5.069,8.534,5.553,8.534L5.553,8.534z' />
C4.678,8.926,5.069,8.534,5.553,8.534L5.553,8.534z"
/>
</svg>
)
} else if (iconType === 'escenas') {
);
} else if (iconType === "escenas") {
return (
<svg className='scenes' x='0px' y='0px' width='40px' height='40px' viewBox='0 0 40 40' enableBackground='new 0 0 40 40'>
<path d='M36.729,14.743v13.15l-14.225,6.693V21.438L36.729,14.743 M38.732,11.045L20.5,19.625v18.662l18.232-8.58V11.045
L38.732,11.045z' />
<path d='M4.271,14.743l14.225,6.695v13.148L4.271,27.894V14.743 M2.268,11.045v18.662l18.232,8.58V19.625L2.268,11.045L2.268,11.045
z' />
<path d='M20.5,4.844l13.289,6.202L20.5,17.247L7.209,11.046L20.5,4.844 M20.5,2.537L2.268,11.045L20.5,19.554l18.232-8.509
L20.5,2.537L20.5,2.537z' />
<svg
className="scenes"
x="0px"
y="0px"
width="40px"
height="40px"
viewBox="0 0 40 40"
enableBackground="new 0 0 40 40"
>
<path
d="M36.729,14.743v13.15l-14.225,6.693V21.438L36.729,14.743 M38.732,11.045L20.5,19.625v18.662l18.232-8.58V11.045
L38.732,11.045z"
/>
<path
d="M4.271,14.743l14.225,6.695v13.148L4.271,27.894V14.743 M2.268,11.045v18.662l18.232,8.58V19.625L2.268,11.045L2.268,11.045
z"
/>
<path
d="M20.5,4.844l13.289,6.202L20.5,17.247L7.209,11.046L20.5,4.844 M20.5,2.537L2.268,11.045L20.5,19.554l18.232-8.509
L20.5,2.537L20.5,2.537z"
/>
</svg>
)
} else if (iconType === 'docs') {
);
} else if (iconType === "docs") {
return (
<svg x='0px' y='0px' width='40px' height='40px' viewBox='0 0 40 40' enableBackground='new 0 0 40 40'>
<path d='M31.543,5.987V3.158
c0-1.103-0.095-1.197-1.197-1.197H4.791c-1.103,0-1.198,0.095-1.198,1.197V32.84c0,1.103,0.095,1.197,1.198,1.197h2.829' />
<path d='M35.57,36.866
c0,1.103-0.096,1.198-1.198,1.198H8.817c-1.103,0-1.198-0.096-1.198-1.198V7.185c0-1.103,0.095-1.197,1.198-1.197h25.555
c1.103,0,1.198,0.095,1.198,1.197V36.866z' />
<path d='M58.755,29.633' />
<path d='M21.86,40.072' />
<path d='M-22.755,58.555' />
<line x1='11.612' y1='11.977' x2='31.577' y2='11.977' />
<line x1='11.612' y1='17.966' x2='31.577' y2='17.966' />
<line x1='11.612' y1='29.945' x2='31.577' y2='29.945' />
<line x1='11.612' y1='23.955' x2='31.577' y2='23.955' />
<svg
x="0px"
y="0px"
width="40px"
height="40px"
viewBox="0 0 40 40"
enableBackground="new 0 0 40 40"
>
<path
d="M31.543,5.987V3.158
c0-1.103-0.095-1.197-1.197-1.197H4.791c-1.103,0-1.198,0.095-1.198,1.197V32.84c0,1.103,0.095,1.197,1.198,1.197h2.829"
/>
<path
d="M35.57,36.866
c0,1.103-0.096,1.198-1.198,1.198H8.817c-1.103,0-1.198-0.096-1.198-1.198V7.185c0-1.103,0.095-1.197,1.198-1.197h25.555
c1.103,0,1.198,0.095,1.198,1.197V36.866z"
/>
<path d="M58.755,29.633" />
<path d="M21.86,40.072" />
<path d="M-22.755,58.555" />
<line x1="11.612" y1="11.977" x2="31.577" y2="11.977" />
<line x1="11.612" y1="17.966" x2="31.577" y2="17.966" />
<line x1="11.612" y1="29.945" x2="31.577" y2="29.945" />
<line x1="11.612" y1="23.955" x2="31.577" y2="23.955" />
</svg>
)
} else if (iconType === 'search') {
);
} else if (iconType === "search") {
return (
<svg x='0px' y='0px' width='40px' height='40px' viewBox='0 0 40 40' enableBackground='new 0 0 40 40'>
<circle cx='18.306' cy='18.307' r='13.856' />
<path strokeLinecap='round' strokeLinejoin='round' d='M28.24,28.24
l8.346,8.346L28.24,28.24z' />
<svg
x="0px"
y="0px"
width="40px"
height="40px"
viewBox="0 0 40 40"
enableBackground="new 0 0 40 40"
>
<circle cx="18.306" cy="18.307" r="13.856" />
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M28.24,28.24
l8.346,8.346L28.24,28.24z"
/>
</svg>
)
);
}
}
};
export default Icon
export default Icon;

View File

@@ -1,6 +1,6 @@
import React from 'react'
import Popup from './presentational/Popup'
import copy from '../common/data/copy.json'
import React from "react";
import Popup from "./presentational/Popup";
import copy from "../common/data/copy.json";
export default ({ isOpen, onClose, language, styles }) => (
<Popup
@@ -10,4 +10,4 @@ export default ({ isOpen, onClose, language, styles }) => (
isOpen={isOpen}
styles={styles}
/>
)
);

View File

@@ -1,304 +1,321 @@
/* global alert, Event */
import React from 'react'
import React from "react";
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import * as actions from '../actions'
import * as selectors from '../selectors'
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import * as actions from "../actions";
import * as selectors from "../selectors";
import MediaOverlay from './Overlay/Media'
import LoadingOverlay from './Overlay/Loading'
import Map from './Map.jsx'
import Toolbar from './Toolbar/Layout'
import CardStack from './CardStack.jsx'
import MediaOverlay from "./Overlay/Media";
import LoadingOverlay from "./Overlay/Loading";
import Map from "./Map.jsx";
import Toolbar from "./Toolbar/Layout";
import CardStack from "./CardStack.jsx";
// import {CardStack} from '@forensic-architecture/design-system'
import NarrativeControls from './presentational/Narrative/Controls.js'
import InfoPopup from './InfoPopup.jsx'
import Popup from './presentational/Popup'
import Timeline from './Timeline.jsx'
import Notification from './Notification.jsx'
import StateOptions from './StateOptions.jsx'
import StaticPage from './StaticPage'
import TemplateCover from './TemplateCover'
import NarrativeControls from "./presentational/Narrative/Controls.js";
import InfoPopup from "./InfoPopup.jsx";
import Popup from "./presentational/Popup";
import Timeline from "./Timeline.jsx";
import Notification from "./Notification.jsx";
import StateOptions from "./StateOptions.jsx";
import StaticPage from "./StaticPage";
import TemplateCover from "./TemplateCover";
import colors from '../common/global'
import { binarySearch, insetSourceFrom } from '../common/utilities'
import { isMobileOnly } from 'react-device-detect'
import Search from './Search.jsx'
import colors from "../common/global";
import { binarySearch, insetSourceFrom } from "../common/utilities";
import { isMobileOnly } from "react-device-detect";
import Search from "./Search.jsx";
class Dashboard extends React.Component {
constructor (props) {
super(props)
constructor(props) {
super(props);
this.handleViewSource = this.handleViewSource.bind(this)
this.handleHighlight = this.handleHighlight.bind(this)
this.setNarrative = this.setNarrative.bind(this)
this.setNarrativeFromFilters = this.setNarrativeFromFilters.bind(this)
this.handleSelect = this.handleSelect.bind(this)
this.getCategoryColor = this.getCategoryColor.bind(this)
this.findEventIdx = this.findEventIdx.bind(this)
this.onKeyDown = this.onKeyDown.bind(this)
this.selectNarrativeStep = this.selectNarrativeStep.bind(this)
this.handleViewSource = this.handleViewSource.bind(this);
this.handleHighlight = this.handleHighlight.bind(this);
this.setNarrative = this.setNarrative.bind(this);
this.setNarrativeFromFilters = this.setNarrativeFromFilters.bind(this);
this.handleSelect = this.handleSelect.bind(this);
this.getCategoryColor = this.getCategoryColor.bind(this);
this.findEventIdx = this.findEventIdx.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.selectNarrativeStep = this.selectNarrativeStep.bind(this);
}
componentDidMount () {
componentDidMount() {
if (!this.props.app.isMobile) {
this.props.actions.fetchDomain()
.then(domain =>
this.props.actions.updateDomain({
domain,
features: this.props.features
}))
this.props.actions.fetchDomain().then((domain) =>
this.props.actions.updateDomain({
domain,
features: this.props.features,
})
);
}
// NOTE: hack to get the timeline to always show. Not entirely sure why
// this is necessary.
window.dispatchEvent(new Event('resize'))
window.dispatchEvent(new Event("resize"));
}
handleHighlight (highlighted) {
this.props.actions.updateHighlighted((highlighted) || null)
handleHighlight(highlighted) {
this.props.actions.updateHighlighted(highlighted || null);
}
handleViewSource (source) {
this.props.actions.updateSource(source)
handleViewSource(source) {
this.props.actions.updateSource(source);
}
findEventIdx (theEvent) {
const { events } = this.props.domain
return binarySearch(
events,
theEvent,
(theev, otherev) => {
return theev.datetime - otherev.datetime
}
)
findEventIdx(theEvent) {
const { events } = this.props.domain;
return binarySearch(events, theEvent, (theev, otherev) => {
return theev.datetime - otherev.datetime;
});
}
handleSelect (selected, axis) {
const matchedEvents = []
const TIMELINE_AXIS = 0
handleSelect(selected, axis) {
const matchedEvents = [];
const TIMELINE_AXIS = 0;
if (axis === TIMELINE_AXIS) {
matchedEvents.push(selected)
matchedEvents.push(selected);
// find in events
const { events } = this.props.domain
const idx = this.findEventIdx(selected)
const { events } = this.props.domain;
const idx = this.findEventIdx(selected);
// check events before
let ptr = idx - 1
let ptr = idx - 1;
while (
ptr >= 0 &&
(events[idx].datetime).getTime() === (events[ptr].datetime).getTime()
events[idx].datetime.getTime() === events[ptr].datetime.getTime()
) {
if (events[ptr].id !== selected.id) {
matchedEvents.push(events[ptr])
matchedEvents.push(events[ptr]);
}
ptr -= 1
ptr -= 1;
}
// check events after
ptr = idx + 1
ptr = idx + 1;
while (
ptr < events.length &&
(events[idx].datetime).getTime() === (events[ptr].datetime).getTime()
events[idx].datetime.getTime() === events[ptr].datetime.getTime()
) {
if (events[ptr].id !== selected.id) {
matchedEvents.push(events[ptr])
matchedEvents.push(events[ptr]);
}
ptr += 1
ptr += 1;
}
} else { // Map..
const std = { ...selected }
delete std.sources
Object.values(std).forEach(ev => matchedEvents.push(ev))
}
this.props.actions.updateSelected(matchedEvents)
}
getCategoryColor (category) {
if (!this.props.features.USE_CATEGORIES) { return colors.fallbackEventColor }
const cat = this.props.ui.style.categories[category]
if (cat) {
return cat
} else {
return this.props.ui.style.categories['default']
// Map..
const std = { ...selected };
delete std.sources;
Object.values(std).forEach((ev) => matchedEvents.push(ev));
}
this.props.actions.updateSelected(matchedEvents);
}
getCategoryColor(category) {
if (!this.props.features.USE_CATEGORIES) {
return colors.fallbackEventColor;
}
const cat = this.props.ui.style.categories[category];
if (cat) {
return cat;
} else {
return this.props.ui.style.categories.default;
}
}
setNarrative (narrative) {
setNarrative(narrative) {
// only handleSelect if narrative is not null and has associated events
if (narrative && narrative.steps.length >= 1) {
this.handleSelect([ narrative.steps[0] ])
this.handleSelect([narrative.steps[0]]);
}
this.props.actions.updateNarrative(narrative)
this.props.actions.updateNarrative(narrative);
}
setNarrativeFromFilters (withSteps) {
const { app, domain } = this.props
let activeFilters = app.associations.filters
setNarrativeFromFilters(withSteps) {
const { app, domain } = this.props;
let activeFilters = app.associations.filters;
if (activeFilters.length === 0) {
alert('No filters selected, cant narrativise')
return
alert("No filters selected, cant narrativise");
return;
}
activeFilters = activeFilters.map(f => ({ name: f }))
activeFilters = activeFilters.map((f) => ({ name: f }));
const evs = domain.events.filter(ev => {
let hasOne = false
const evs = domain.events.filter((ev) => {
let hasOne = false;
// add event if it has at least one matching filter
for (let i = 0; i < activeFilters.length; i++) {
if (ev.associations.includes(activeFilters[i].name)) {
hasOne = true
break
hasOne = true;
break;
}
}
if (hasOne) return true
return false
})
if (hasOne) return true;
return false;
});
if (evs.length === 0) {
alert('No associated events, cant narrativise')
return
alert("No associated events, cant narrativise");
return;
}
const name = activeFilters.map(f => f.name).join('-')
const desc = activeFilters.map(f => f.description).join('\n\n')
const name = activeFilters.map((f) => f.name).join("-");
const desc = activeFilters.map((f) => f.description).join("\n\n");
this.setNarrative({
id: name,
label: name,
description: desc,
withLines: withSteps,
steps: evs.map(insetSourceFrom(domain.sources))
})
steps: evs.map(insetSourceFrom(domain.sources)),
});
}
selectNarrativeStep (idx) {
selectNarrativeStep(idx) {
// Try to find idx if event passed rather than number
if (typeof idx !== 'number') {
let e = idx[0] || idx
if (typeof idx !== "number") {
const e = idx[0] || idx;
if (this.props.app.associations.narrative) {
const { steps } = this.props.app.associations.narrative
const { steps } = this.props.app.associations.narrative;
// choose the first event at a given location
const locationEventId = e.id
const narrativeIdxObj = steps.find(s => s.id === locationEventId)
let narrativeIdx = steps.indexOf(narrativeIdxObj)
const locationEventId = e.id;
const narrativeIdxObj = steps.find((s) => s.id === locationEventId);
const narrativeIdx = steps.indexOf(narrativeIdxObj);
if (narrativeIdx > -1) {
idx = narrativeIdx
idx = narrativeIdx;
}
}
}
const { narrative } = this.props.app.associations
if (narrative === null) return
const { narrative } = this.props.app.associations;
if (narrative === null) return;
if (idx < narrative.steps.length && idx >= 0) {
const step = narrative.steps[idx]
const step = narrative.steps[idx];
this.handleSelect([step])
this.props.actions.updateNarrativeStepIdx(idx)
this.handleSelect([step]);
this.props.actions.updateNarrativeStepIdx(idx);
}
}
onKeyDown (e) {
const { narrative, selected } = this.props.app
const { events } = this.props.domain
onKeyDown(e) {
const { narrative, selected } = this.props.app;
const { events } = this.props.domain;
const prev = idx => {
const prev = (idx) => {
if (narrative === null) {
this.handleSelect(events[idx - 1], 0)
this.handleSelect(events[idx - 1], 0);
} else {
this.selectNarrativeStep(this.props.narrativeIdx - 1)
this.selectNarrativeStep(this.props.narrativeIdx - 1);
}
}
const next = idx => {
};
const next = (idx) => {
if (narrative === null) {
this.handleSelect(events[idx + 1], 0)
this.handleSelect(events[idx + 1], 0);
} else {
this.selectNarrativeStep(this.props.narrativeIdx + 1)
this.selectNarrativeStep(this.props.narrativeIdx + 1);
}
}
};
if (selected.length > 0) {
const ev = selected[selected.length - 1]
const idx = this.findEventIdx(ev)
const ev = selected[selected.length - 1];
const idx = this.findEventIdx(ev);
switch (e.keyCode) {
case 37: // left arrow
case 38: // up arrow
if (idx <= 0) return
prev(idx)
break
if (idx <= 0) return;
prev(idx);
break;
case 39: // right arrow
case 40: // down arrow
if (idx < 0 || idx >= this.props.domain.length - 1) return
next(idx)
break
if (idx < 0 || idx >= this.props.domain.length - 1) return;
next(idx);
break;
default:
}
}
}
renderIntroPopup (isMobile, styles) {
const { app, actions } = this.props
renderIntroPopup(isMobile, styles) {
const { app, actions } = this.props;
const extraContent = isMobile ? <div style={{ position: 'relative', bottom: 0 }}>
<h3 style={{ color: 'var(--error-red)' }}>This platform is not suitable for mobile.<br /><br />Please re-visit the site on a device with a larger screen.</h3>
</div> : null
const extraContent = isMobile ? (
<div style={{ position: "relative", bottom: 0 }}>
<h3 style={{ color: "var(--error-red)" }}>
This platform is not suitable for mobile.
<br />
<br />
Please re-visit the site on a device with a larger screen.
</h3>
</div>
) : null;
return <Popup
title='Introduction to the platform'
theme='dark'
isOpen={app.flags.isIntropopup}
onClose={actions.toggleIntroPopup}
content={app.intro}
styles={styles}
isMobile={isMobile}
>
{extraContent}
</Popup>
return (
<Popup
title="Introduction to the platform"
theme="dark"
isOpen={app.flags.isIntropopup}
onClose={actions.toggleIntroPopup}
content={app.intro}
styles={styles}
isMobile={isMobile}
>
{extraContent}
</Popup>
);
}
render () {
const { actions, app, domain, features } = this.props
const dateHeight = 80
const padding = 2
const checkMobile = (isMobileOnly || window.innerWidth < 600)
render() {
const { actions, app, domain, features } = this.props;
const dateHeight = 80;
const padding = 2;
const checkMobile = isMobileOnly || window.innerWidth < 600;
const popupStyles = {
height: checkMobile ? '100vh' : 'fit-content',
display: checkMobile ? 'block' : 'table',
width: checkMobile ? '100vw' : window.innerWidth > 768 ? '60vw' : `calc(100vw - var(--toolbar-width))`,
maxWidth: checkMobile ? '100vw' : 600,
maxHeight: checkMobile ? '100vh' : window.innerHeight > 768 ? `calc(100vh - ${app.timeline.dimensions.height}px - ${dateHeight}px)` : `100vh`,
left: checkMobile ? padding : 'var(--toolbar-width)',
height: checkMobile ? "100vh" : "fit-content",
display: checkMobile ? "block" : "table",
width: checkMobile
? "100vw"
: window.innerWidth > 768
? "60vw"
: "calc(100vw - var(--toolbar-width))",
maxWidth: checkMobile ? "100vw" : 600,
maxHeight: checkMobile
? "100vh"
: window.innerHeight > 768
? `calc(100vh - ${app.timeline.dimensions.height}px - ${dateHeight}px)`
: "100vh",
left: checkMobile ? padding : "var(--toolbar-width)",
top: 0,
overflowY: 'scroll'
}
overflowY: "scroll",
};
if (checkMobile) {
const msg = 'This platform is not suitable for mobile. Please re-visit the site on a device with a larger screen.'
const msg =
"This platform is not suitable for mobile. Please re-visit the site on a device with a larger screen.";
return (
<div>
{(features.USE_COVER && !app.intro) && (
{features.USE_COVER && !app.intro && (
<StaticPage showing={app.flags.isCover}>
{/* enable USE_COVER in config.js features, and customise your header */}
{/* pass 'actions.toggleCover' as a prop to your custom header */}
<TemplateCover showAppHandler={() => {
/* eslint-disable no-undef */
alert(msg)
/* eslint-enable no-undef */
}} />
<TemplateCover
showAppHandler={() => {
/* eslint-disable no-undef */
alert(msg);
/* eslint-enable no-undef */
}}
/>
</StaticPage>
)}
{app.intro && <>
{this.renderIntroPopup(true, popupStyles)}
</>}
{app.intro && <>{this.renderIntroPopup(true, popupStyles)}</>}
{!app.intro && !features.USE_COVER && (
<div className='fixedTooSmallMessage'>{msg}</div>
<div className="fixedTooSmallMessage">{msg}</div>
)}
</div>
)
);
}
return (
@@ -307,9 +324,11 @@ class Dashboard extends React.Component {
isNarrative={!!app.associations.narrative}
methods={{
onTitle: actions.toggleCover,
onSelectFilter: filters => actions.toggleAssociations('filters', filters),
onCategoryFilter: categories => actions.toggleAssociations('categories', categories),
onSelectNarrative: this.setNarrative
onSelectFilter: (filters) =>
actions.toggleAssociations("filters", filters),
onCategoryFilter: (categories) =>
actions.toggleAssociations("categories", categories),
onSelectNarrative: this.setNarrative,
}}
/>
<Map
@@ -317,39 +336,54 @@ class Dashboard extends React.Component {
methods={{
onSelectNarrative: this.setNarrative,
getCategoryColor: this.getCategoryColor,
onSelect: app.associations.narrative ? this.selectNarrativeStep : ev => this.handleSelect(ev, 1)
onSelect: app.associations.narrative
? this.selectNarrativeStep
: (ev) => this.handleSelect(ev, 1),
}}
/>
<Timeline
onKeyDown={this.onKeyDown}
methods={{
onSelect: app.associations.narrative ? this.selectNarrativeStep : ev => this.handleSelect(ev, 0),
onSelect: app.associations.narrative
? this.selectNarrativeStep
: (ev) => this.handleSelect(ev, 0),
onUpdateTimerange: actions.updateTimeRange,
getCategoryColor: this.getCategoryColor
getCategoryColor: this.getCategoryColor,
}}
/>
<CardStack
timelineDims={app.timeline.dimensions}
onViewSource={this.handleViewSource}
onSelect={app.associations.narrative ? this.selectNarrativeStep : () => null}
onSelect={
app.associations.narrative ? this.selectNarrativeStep : () => null
}
onHighlight={this.handleHighlight}
onToggleCardstack={() => actions.updateSelected([])}
getCategoryColor={this.getCategoryColor}
/>
<StateOptions
showing={this.props.narratives && this.props.narratives.length !== 0 && !app.associations.narrative && app.associations.filters.length > 0}
showing={
this.props.narratives &&
this.props.narratives.length !== 0 &&
!app.associations.narrative &&
app.associations.filters.length > 0
}
timelineDims={app.timeline.dimensions}
onClickHandler={this.setNarrativeFromFilters}
/>
<NarrativeControls
narrative={app.associations.narrative ? {
...app.associations.narrative,
current: this.props.narrativeIdx
} : null}
narrative={
app.associations.narrative
? {
...app.associations.narrative,
current: this.props.narrativeIdx,
}
: null
}
methods={{
onNext: () => this.selectNarrativeStep(this.props.narrativeIdx + 1),
onPrev: () => this.selectNarrativeStep(this.props.narrativeIdx - 1),
onSelectNarrative: this.setNarrative
onSelectNarrative: this.setNarrative,
}}
/>
<InfoPopup
@@ -359,24 +393,27 @@ class Dashboard extends React.Component {
onClose={actions.toggleInfoPopup}
/>
{this.renderIntroPopup(false, popupStyles)}
{app.debug ? <Notification
isNotification={app.flags.isNotification}
notifications={domain.notifications}
onToggle={actions.markNotificationsRead}
/> : null}
{features.USE_SEARCH && (<Search
narrative={app.narrative}
queryString={app.searchQuery}
events={domain.events}
onSearchRowClick={this.handleSelect}
/>)}
{app.debug ? (
<Notification
isNotification={app.flags.isNotification}
notifications={domain.notifications}
onToggle={actions.markNotificationsRead}
/>
) : null}
{features.USE_SEARCH && (
<Search
narrative={app.narrative}
queryString={app.searchQuery}
events={domain.events}
onSearchRowClick={this.handleSelect}
/>
)}
{app.source ? (
<MediaOverlay
source={app.source}
onCancel={() => {
actions.updateSource(null)
}
}
actions.updateSource(null);
}}
/>
) : null}
<LoadingOverlay
@@ -388,26 +425,29 @@ class Dashboard extends React.Component {
<StaticPage showing={app.flags.isCover}>
{/* enable USE_COVER in config.js features, and customise your header */}
{/* pass 'actions.toggleCover' as a prop to your custom header */}
<TemplateCover showing={app.flags.isCover} showAppHandler={actions.toggleCover} />
<TemplateCover
showing={app.flags.isCover}
showAppHandler={actions.toggleCover}
/>
</StaticPage>
)}
</div>
)
);
}
}
function mapDispatchToProps (dispatch) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch)
}
actions: bindActionCreators(actions, dispatch),
};
}
export default connect(
state => ({
(state) => ({
...state,
narrativeIdx: selectors.selectNarrativeIdx(state),
narratives: selectors.selectNarratives(state),
selected: selectors.selectSelected(state)
selected: selectors.selectSelected(state),
}),
mapDispatchToProps
)(Dashboard)
)(Dashboard);

View File

@@ -1,255 +1,302 @@
/* global L, Event */
import React from 'react'
import { Portal } from 'react-portal'
import Supercluster from 'supercluster'
import React from "react";
import { Portal } from "react-portal";
import Supercluster from "supercluster";
import { connect } from 'react-redux'
import * as selectors from '../selectors'
import { connect } from "react-redux";
import * as selectors from "../selectors";
import 'leaflet'
import "leaflet";
import Sites from './presentational/Map/Sites.jsx'
import Shapes from './presentational/Map/Shapes.jsx'
import Events from './presentational/Map/Events.jsx'
import Clusters from './presentational/Map/Clusters.jsx'
import SelectedEvents from './presentational/Map/SelectedEvents.jsx'
import Narratives from './presentational/Map/Narratives'
import DefsMarkers from './presentational/Map/DefsMarkers.jsx'
import LoadingOverlay from '../components/Overlay/Loading'
import Sites from "./presentational/Map/Sites.jsx";
import Shapes from "./presentational/Map/Shapes.jsx";
import Events from "./presentational/Map/Events.jsx";
import Clusters from "./presentational/Map/Clusters.jsx";
import SelectedEvents from "./presentational/Map/SelectedEvents.jsx";
import Narratives from "./presentational/Map/Narratives";
import DefsMarkers from "./presentational/Map/DefsMarkers.jsx";
import LoadingOverlay from "../components/Overlay/Loading";
import { mapClustersToLocations, isIdentical, isLatitude, isLongitude, calculateTotalClusterPoints, calcClusterSize } from '../common/utilities'
import {
mapClustersToLocations,
isIdentical,
isLatitude,
isLongitude,
calculateTotalClusterPoints,
calcClusterSize,
} from "../common/utilities";
// NB: important constants for map, TODO: make statics
const supportedMapboxMap = ['streets', 'satellite']
const defaultToken = 'your_token'
const supportedMapboxMap = ["streets", "satellite"];
const defaultToken = "your_token";
class Map extends React.Component {
constructor () {
super()
this.projectPoint = this.projectPoint.bind(this)
this.onClusterSelect = this.onClusterSelect.bind(this)
this.loadClusterData = this.loadClusterData.bind(this)
this.getClusterChildren = this.getClusterChildren.bind(this)
this.svgRef = React.createRef()
this.map = null
this.superclusterIndex = null
constructor() {
super();
this.projectPoint = this.projectPoint.bind(this);
this.onClusterSelect = this.onClusterSelect.bind(this);
this.loadClusterData = this.loadClusterData.bind(this);
this.getClusterChildren = this.getClusterChildren.bind(this);
this.svgRef = React.createRef();
this.map = null;
this.superclusterIndex = null;
this.state = {
mapTransformX: 0,
mapTransformY: 0,
indexLoaded: false,
clusters: []
}
this.styleLocation = this.styleLocation.bind(this)
clusters: [],
};
this.styleLocation = this.styleLocation.bind(this);
}
componentDidMount () {
componentDidMount() {
if (this.map === null) {
this.initializeMap()
this.initializeMap();
}
window.dispatchEvent(new Event('resize'))
window.dispatchEvent(new Event("resize"));
}
componentWillReceiveProps (nextProps) {
componentWillReceiveProps(nextProps) {
if (!isIdentical(nextProps.domain.locations, this.props.domain.locations)) {
this.loadClusterData(nextProps.domain.locations)
this.loadClusterData(nextProps.domain.locations);
}
// Set appropriate zoom for narrative
const { bounds } = nextProps.app.map
if (!isIdentical(bounds, this.props.app.map.bounds) &&
bounds !== null) {
this.map.fitBounds(bounds)
const { bounds } = nextProps.app.map;
if (!isIdentical(bounds, this.props.app.map.bounds) && bounds !== null) {
this.map.fitBounds(bounds);
} else {
if (!isIdentical(nextProps.app.selected, this.props.app.selected)) {
// Fly to first of events selected
const eventPoint = (nextProps.app.selected.length > 0) ? nextProps.app.selected[0] : null
const eventPoint =
nextProps.app.selected.length > 0 ? nextProps.app.selected[0] : null;
if (eventPoint !== null && eventPoint.latitude && eventPoint.longitude) {
if (
eventPoint !== null &&
eventPoint.latitude &&
eventPoint.longitude
) {
// this.map.setView([eventPoint.latitude, eventPoint.longitude])
this.map.setView([eventPoint.latitude, eventPoint.longitude], this.map.getZoom(), {
'animate': true,
'pan': {
'duration': 0.7
this.map.setView(
[eventPoint.latitude, eventPoint.longitude],
this.map.getZoom(),
{
animate: true,
pan: {
duration: 0.7,
},
}
})
);
}
}
}
}
initializeMap () {
initializeMap() {
/**
* Creates a Leaflet map and a tilelayer for the map background
*/
const { map: mapConfig, cluster: clusterConfig } = this.props.app
const { map: mapConfig, cluster: clusterConfig } = this.props.app;
const map =
L.map(this.props.ui.dom.map)
.setView(mapConfig.anchor, mapConfig.startZoom)
.setMinZoom(mapConfig.minZoom)
.setMaxZoom(mapConfig.maxZoom)
.setMaxBounds(mapConfig.maxBounds)
const map = L.map(this.props.ui.dom.map)
.setView(mapConfig.anchor, mapConfig.startZoom)
.setMinZoom(mapConfig.minZoom)
.setMaxZoom(mapConfig.maxZoom)
.setMaxBounds(mapConfig.maxBounds);
// Initialize supercluster index
this.superclusterIndex = new Supercluster(clusterConfig)
this.superclusterIndex = new Supercluster(clusterConfig);
let firstLayer
let firstLayer;
if ((supportedMapboxMap.indexOf(this.props.ui.tiles) !== -1) && process.env.MAPBOX_TOKEN && process.env.MAPBOX_TOKEN !== defaultToken) {
if (
supportedMapboxMap.indexOf(this.props.ui.tiles) !== -1 &&
process.env.MAPBOX_TOKEN &&
process.env.MAPBOX_TOKEN !== defaultToken
) {
firstLayer = L.tileLayer(
`http://a.tiles.mapbox.com/v4/mapbox.${this.props.ui.tiles}/{z}/{x}/{y}@2x.png?access_token=${process.env.MAPBOX_TOKEN}`
)
} else if (process.env.MAPBOX_TOKEN && process.env.MAPBOX_TOKEN !== defaultToken) {
);
} else if (
process.env.MAPBOX_TOKEN &&
process.env.MAPBOX_TOKEN !== defaultToken
) {
firstLayer = L.tileLayer(
`http://a.tiles.mapbox.com/styles/v1/${this.props.ui.tiles}/tiles/{z}/{x}/{y}?access_token=${process.env.MAPBOX_TOKEN}`
)
);
} else {
firstLayer = L.tileLayer(
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
)
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
);
}
firstLayer.addTo(map)
firstLayer.addTo(map);
map.keyboard.disable()
map.zoomControl.remove()
map.keyboard.disable();
map.zoomControl.remove();
map.on('moveend', () => {
this.updateClusters()
this.alignLayers()
})
map.on("moveend", () => {
this.updateClusters();
this.alignLayers();
});
map.on('move zoomend viewreset', () => this.alignLayers())
map.on('zoomstart', () => { if (this.svgRef.current !== null) this.svgRef.current.classList.add('hide') })
map.on('zoomend', () => { if (this.svgRef.current !== null) this.svgRef.current.classList.remove('hide') })
window.addEventListener('resize', () => { this.alignLayers() })
map.on("move zoomend viewreset", () => this.alignLayers());
map.on("zoomstart", () => {
if (this.svgRef.current !== null)
this.svgRef.current.classList.add("hide");
});
map.on("zoomend", () => {
if (this.svgRef.current !== null)
this.svgRef.current.classList.remove("hide");
});
window.addEventListener("resize", () => {
this.alignLayers();
});
this.map = map
this.map = map;
}
getMapDetails () {
const bounds = this.map.getBounds()
const bbox = [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()]
const zoom = this.map.getZoom()
return [bbox, zoom]
getMapDetails() {
const bounds = this.map.getBounds();
const bbox = [
bounds.getWest(),
bounds.getSouth(),
bounds.getEast(),
bounds.getNorth(),
];
const zoom = this.map.getZoom();
return [bbox, zoom];
}
updateClusters () {
const [bbox, zoom] = this.getMapDetails()
updateClusters() {
const [bbox, zoom] = this.getMapDetails();
if (this.superclusterIndex && this.state.indexLoaded) {
this.setState({
clusters: this.superclusterIndex.getClusters(bbox, zoom)
})
clusters: this.superclusterIndex.getClusters(bbox, zoom),
});
}
}
loadClusterData (locations) {
loadClusterData(locations) {
if (locations && locations.length > 0 && this.superclusterIndex) {
const convertedLocations = locations.reduce((acc, loc) => {
const { longitude, latitude } = loc
const validCoordinates = isLatitude(latitude) && isLongitude(longitude)
const { longitude, latitude } = loc;
const validCoordinates = isLatitude(latitude) && isLongitude(longitude);
if (validCoordinates) {
const feature = {
type: 'Feature',
type: "Feature",
properties: {
cluster: false,
id: loc.label
id: loc.label,
},
geometry: {
type: 'Point',
coordinates: [longitude, latitude]
}
}
acc.push(feature)
type: "Point",
coordinates: [longitude, latitude],
},
};
acc.push(feature);
}
return acc
}, [])
this.superclusterIndex.load(convertedLocations)
return acc;
}, []);
this.superclusterIndex.load(convertedLocations);
this.setState({ indexLoaded: true }, () => {
this.updateClusters()
})
this.updateClusters();
});
} else {
this.setState({ clusters: [] })
this.setState({ clusters: [] });
}
}
getClusterChildren (clusterId) {
getClusterChildren(clusterId) {
if (this.superclusterIndex) {
try {
const children = this.superclusterIndex.getLeaves(clusterId, Infinity, 0)
return mapClustersToLocations(children, this.props.domain.locations)
const children = this.superclusterIndex.getLeaves(
clusterId,
Infinity,
0
);
return mapClustersToLocations(children, this.props.domain.locations);
} catch (err) {
return []
return [];
}
}
return []
return [];
}
getSelectedClusters () {
const { selected } = this.props.app
const selectedIds = selected.map(sl => sl.id)
getSelectedClusters() {
const { selected } = this.props.app;
const selectedIds = selected.map((sl) => sl.id);
if (this.state.clusters && this.state.clusters.length > 0) {
return this.state.clusters.reduce((acc, cl) => {
if (cl.properties.cluster) {
const children = this.getClusterChildren(cl.properties.cluster_id)
const children = this.getClusterChildren(cl.properties.cluster_id);
if (children && children.length > 0) {
children.forEach(child => {
const clusterPresent = acc.findIndex(item => item.id === cl.id) >= 0
children.forEach((child) => {
const clusterPresent =
acc.findIndex((item) => item.id === cl.id) >= 0;
if (selectedIds.includes(child.id) && !clusterPresent) {
acc.push(cl)
acc.push(cl);
}
})
});
}
}
return acc
}, [])
return acc;
}, []);
}
return []
return [];
}
alignLayers () {
const mapNode = document.querySelector('.leaflet-map-pane')
if (mapNode === null) return { transformX: 0, transformY: 0 }
alignLayers() {
const mapNode = document.querySelector(".leaflet-map-pane");
if (mapNode === null) return { transformX: 0, transformY: 0 };
// We'll get the transform of the leaflet container,
// which will let us offset the SVG by the same quantity
const transform = window
.getComputedStyle(mapNode)
.getPropertyValue('transform')
.getPropertyValue("transform");
// Offset with leaflet map transform boundaries
this.setState({
mapTransformX: +transform.split(',')[4],
mapTransformY: +transform.split(',')[5].split(')')[0]
})
mapTransformX: +transform.split(",")[4],
mapTransformY: +transform.split(",")[5].split(")")[0],
});
}
projectPoint (location) {
const latLng = new L.LatLng(location[0], location[1])
projectPoint(location) {
const latLng = new L.LatLng(location[0], location[1]);
return {
x: this.map.latLngToLayerPoint(latLng).x + this.state.mapTransformX,
y: this.map.latLngToLayerPoint(latLng).y + this.state.mapTransformY
}
y: this.map.latLngToLayerPoint(latLng).y + this.state.mapTransformY,
};
}
onClusterSelect ({ id, latitude, longitude }) {
const expansionZoom = Math.max(this.superclusterIndex.getClusterExpansionZoom(parseInt(id)), this.superclusterIndex.options.minZoom)
const zoomLevelsToSkip = 2
const zoomToFly = Math.max(expansionZoom + zoomLevelsToSkip, this.props.app.cluster.maxZoom)
this.map.flyTo(new L.LatLng(latitude, longitude), zoomToFly)
onClusterSelect({ id, latitude, longitude }) {
const expansionZoom = Math.max(
this.superclusterIndex.getClusterExpansionZoom(parseInt(id)),
this.superclusterIndex.options.minZoom
);
const zoomLevelsToSkip = 2;
const zoomToFly = Math.max(
expansionZoom + zoomLevelsToSkip,
this.props.app.cluster.maxZoom
);
this.map.flyTo(new L.LatLng(latitude, longitude), zoomToFly);
}
getClientDims () {
const boundingClient = document.querySelector(`#${this.props.ui.dom.map}`).getBoundingClientRect()
getClientDims() {
const boundingClient = document
.querySelector(`#${this.props.ui.dom.map}`)
.getBoundingClientRect();
return {
width: boundingClient.width,
height: boundingClient.height
}
height: boundingClient.height,
};
}
renderTiles () {
const pane = this.map.getPanes().overlayPane
const { width, height } = this.getClientDims()
renderTiles() {
const pane = this.map.getPanes().overlayPane;
const { width, height } = this.getClientDims();
return this.map ? (
<Portal node={pane}>
@@ -257,24 +304,27 @@ class Map extends React.Component {
ref={this.svgRef}
width={width}
height={height}
style={{ transform: `translate3d(${-this.state.mapTransformX}px, ${-this.state.mapTransformY}px, 0)` }}
className='leaflet-svg'
style={{
transform: `translate3d(${-this.state.mapTransformX}px, ${-this
.state.mapTransformY}px, 0)`,
}}
className="leaflet-svg"
/>
</Portal>
) : null
) : null;
}
renderSites () {
renderSites() {
return (
<Sites
sites={this.props.domain.sites}
projectPoint={this.projectPoint}
isEnabled={this.props.app.views.sites}
/>
)
);
}
renderShapes () {
renderShapes() {
return (
<Shapes
svg={this.svgRef.current}
@@ -282,22 +332,26 @@ class Map extends React.Component {
projectPoint={this.projectPoint}
styles={this.props.ui.shapes}
/>
)
);
}
renderNarratives () {
const hasNarratives = this.props.domain.narratives.length > 0
renderNarratives() {
const hasNarratives = this.props.domain.narratives.length > 0;
return (
<Narratives
svg={this.svgRef.current}
narratives={hasNarratives ? this.props.domain.narratives : [this.props.app.narrative]}
narratives={
hasNarratives
? this.props.domain.narratives
: [this.props.app.narrative]
}
projectPoint={this.projectPoint}
narrative={this.props.app.narrative}
styles={this.props.ui.narratives}
onSelectNarrative={this.props.methods.onSelectNarrative}
features={this.props.features}
/>
)
);
}
/**
@@ -309,22 +363,27 @@ class Map extends React.Component {
* at the second index is an optional additional component that renders in
* the <g/> div.
*/
styleLocation (location) {
return [null, null]
styleLocation(location) {
return [null, null];
}
styleCluster (cluster) {
return [null, null]
styleCluster(cluster) {
return [null, null];
}
renderEvents () {
renderEvents() {
/*
Uncomment below to filter out the locations already present in a cluster.
Leaving these lines commented out renders all the locations on the map, regardless of whether or not they are clustered
*/
const individualClusters = this.state.clusters.filter(cl => !cl.properties.cluster)
const filteredLocations = mapClustersToLocations(individualClusters, this.props.domain.locations)
const individualClusters = this.state.clusters.filter(
(cl) => !cl.properties.cluster
);
const filteredLocations = mapClustersToLocations(
individualClusters,
this.props.domain.locations
);
return (
<Events
svg={this.svgRef.current}
@@ -343,11 +402,13 @@ class Map extends React.Component {
filterColors={this.props.ui.filterColors}
features={this.props.features}
/>
)
);
}
renderClusters () {
const allClusters = this.state.clusters.filter(cl => cl.properties.cluster)
renderClusters() {
const allClusters = this.state.clusters.filter(
(cl) => cl.properties.cluster
);
return (
<Clusters
svg={this.svgRef.current}
@@ -360,34 +421,37 @@ class Map extends React.Component {
getClusterChildren={this.getClusterChildren}
filterColors={this.props.ui.filterColors}
/>
)
);
}
renderSelected () {
const selectedClusters = this.getSelectedClusters()
const totalMarkers = []
renderSelected() {
const selectedClusters = this.getSelectedClusters();
const totalMarkers = [];
this.props.app.selected.forEach(s => {
const { latitude, longitude } = s
this.props.app.selected.forEach((s) => {
const { latitude, longitude } = s;
totalMarkers.push({
latitude,
longitude,
radius: this.props.ui.eventRadius
})
})
radius: this.props.ui.eventRadius,
});
});
const totalClusterPoints = calculateTotalClusterPoints(this.state.clusters)
const totalClusterPoints = calculateTotalClusterPoints(this.state.clusters);
selectedClusters.forEach(cl => {
selectedClusters.forEach((cl) => {
if (cl.properties.cluster) {
const { coordinates } = cl.geometry
const { coordinates } = cl.geometry;
totalMarkers.push({
latitude: String(coordinates[1]),
longitude: String(coordinates[0]),
radius: calcClusterSize(cl.properties.point_count, totalClusterPoints)
})
radius: calcClusterSize(
cl.properties.point_count,
totalClusterPoints
),
});
}
})
});
return (
<SelectedEvents
@@ -396,22 +460,24 @@ class Map extends React.Component {
projectPoint={this.projectPoint}
styles={this.props.ui.mapSelectedEvents}
/>
)
);
}
renderMarkers () {
renderMarkers() {
return (
<Portal node={this.svgRef.current}>
<DefsMarkers />
</Portal>
)
);
}
render () {
const { isShowingSites, isFetchingDomain } = this.props.app.flags
const classes = this.props.app.narrative ? 'map-wrapper narrative-mode' : 'map-wrapper'
render() {
const { isShowingSites, isFetchingDomain } = this.props.app.flags;
const classes = this.props.app.narrative
? "map-wrapper narrative-mode"
: "map-wrapper";
const innerMap = this.map ? (
<React.Fragment>
<>
{this.renderTiles()}
{this.renderMarkers()}
{isShowingSites ? this.renderSites() : null}
@@ -420,14 +486,11 @@ class Map extends React.Component {
{this.renderEvents()}
{this.renderClusters()}
{this.renderSelected()}
</React.Fragment>
) : null
</>
) : null;
return (
<div className={classes}
onKeyDown={this.props.onKeyDown}
tabIndex='0'
>
<div className={classes} onKeyDown={this.props.onKeyDown} tabIndex="0">
<div id={this.props.ui.dom.map} />
<LoadingOverlay
isLoading={this.props.app.loading || isFetchingDomain}
@@ -436,18 +499,18 @@ class Map extends React.Component {
/>
{innerMap}
</div>
)
);
}
}
function mapStateToProps (state) {
function mapStateToProps(state) {
return {
domain: {
locations: selectors.selectLocations(state),
narratives: selectors.selectNarratives(state),
categories: selectors.getCategories(state),
sites: selectors.selectSites(state),
shapes: selectors.selectShapes(state)
shapes: selectors.selectShapes(state),
},
app: {
views: state.app.associations.views,
@@ -461,8 +524,8 @@ function mapStateToProps (state) {
coloringSet: state.app.associations.coloringSet,
flags: {
isShowingSites: state.app.flags.isShowingSites,
isFetchingDomain: state.app.flags.isFetchingDomain
}
isFetchingDomain: state.app.flags.isFetchingDomain,
},
},
ui: {
tiles: state.ui.tiles,
@@ -472,10 +535,10 @@ function mapStateToProps (state) {
shapes: state.ui.style.shapes,
eventRadius: state.ui.eventRadius,
radial: state.ui.style.clusters.radial,
filterColors: state.ui.coloring.colors
filterColors: state.ui.coloring.colors,
},
features: selectors.getFeatures(state)
}
features: selectors.getFeatures(state),
};
}
export default connect(mapStateToProps)(Map)
export default connect(mapStateToProps)(Map);

View File

@@ -1,69 +1,71 @@
import React from 'react'
import React from "react";
export default class Notification extends React.Component {
constructor (props) {
super()
constructor(props) {
super();
this.state = {
isExtended: false
}
isExtended: false,
};
}
toggleDetails () {
this.setState({ isExtended: !this.state.isExtended })
toggleDetails() {
this.setState({ isExtended: !this.state.isExtended });
}
renderItems (items) {
if (!items) return ''
renderItems(items) {
if (!items) return "";
return (
<div>
{items.map((item) => {
if (item.error) {
return (<p>{item.error.message}</p>)
return <p>{item.error.message}</p>;
}
return ''
return "";
})}
</div>
)
);
}
renderNotificationContent (notification) {
let { type, message, items } = notification
renderNotificationContent(notification) {
const { type, message, items } = notification;
return (
<div>
<div className={`message ${type}`}>
{message}
</div>
<div className={`message ${type}`}>{message}</div>
<div className={`details ${this.state.isExtended}`}>
{(items !== null) ? this.renderItems(items) : ''}
{items !== null ? this.renderItems(items) : ""}
</div>
</div>
)
);
}
render () {
if (!this.props.notifications) return null
const notificationsToRender = this.props.notifications.filter(n => !('isRead' in n && n.isRead))
render() {
if (!this.props.notifications) return null;
const notificationsToRender = this.props.notifications.filter(
(n) => !("isRead" in n && n.isRead)
);
if (notificationsToRender.length > 0) {
return (
<div className={`notification-wrapper`}>
<div className="notification-wrapper">
{this.props.notifications.map((notification) => {
return (
<div className='notification' onClick={() => this.toggleDetails()}>
<div
className="notification"
onClick={() => this.toggleDetails()}
>
<button
onClick={this.props.onToggle}
className='side-menu-burg over-white is-active'
className="side-menu-burg over-white is-active"
>
<span />
</button>
{this.renderNotificationContent(notification)}
</div>
)
})
}
);
})}
</div>
)
);
}
return (<div />)
return <div />;
}
}

View File

@@ -1,76 +1,94 @@
import React from 'react'
import { Player } from 'video-react'
import Img from 'react-image'
import Md from './Md'
import Spinner from '../presentational/Spinner'
import NoSource from '../presentational/NoSource'
import React from "react";
import { Player } from "video-react";
import Img from "react-image";
import Md from "./Md";
import Spinner from "../presentational/Spinner";
import NoSource from "../presentational/NoSource";
export default ({ media, viewIdx, translations, switchLanguage, langIdx }) => {
const el = document.querySelector(`.source-media-gallery`)
const shiftW = el ? el.getBoundingClientRect().width : 0
const el = document.querySelector(".source-media-gallery");
const shiftW = el ? el.getBoundingClientRect().width : 0;
function renderMedia (media) {
let { path, type, poster } = media
function renderMedia(media) {
const { path, type, poster } = media;
switch (type) {
case 'Image':
case "Image":
return (
<div className='source-image-container'>
<div className="source-image-container">
<Img
className='source-image'
className="source-image"
src={path}
loader={<div className='source-image-loader'><Spinner /></div>}
unloader={<NoSource failedUrls={[ path ]} />}
onClick={() => window.open(path, '_blank')}
loader={
<div className="source-image-loader">
<Spinner />
</div>
}
unloader={<NoSource failedUrls={[path]} />}
onClick={() => window.open(path, "_blank")}
/>
</div>
)
case 'Video':
);
case "Video":
return (
<div className='media-player'>
<div className='banner-trans right-overlay'>
{translations ? translations.map((trans, idx) => (
langIdx !== idx + 1 ? (
<div className='trans-button' onClick={() => switchLanguage(idx + 1)}>{trans.code}</div>
) : (
<div className='trans-button' onClick={() => switchLanguage(0)}>EN</div>
)
)) : null}
<div className="media-player">
<div className="banner-trans right-overlay">
{translations
? translations.map((trans, idx) =>
langIdx !== idx + 1 ? (
<div
className="trans-button"
onClick={() => switchLanguage(idx + 1)}
>
{trans.code}
</div>
) : (
<div
className="trans-button"
onClick={() => switchLanguage(0)}
>
EN
</div>
)
)
: null}
</div>
<Player
poster={poster}
className='source-video'
className="source-video"
playsInline
src={path}
/>
</div>
)
case 'Text':
);
case "Text":
return (
<div className='source-text-container'>
<div className="source-text-container">
<Md
path={path}
loader={<Spinner />}
unloader={() => this.renderError()}
/>
</div>
)
case 'Document':
return (
<iframe className='source-document' src={path} />
)
);
case "Document":
return <iframe className="source-document" src={path} />;
default:
return (
<NoSource failedUrls={[`Application does not support extension: ${path.split('.')[1]}`]} />
)
<NoSource
failedUrls={[
`Application does not support extension: ${path.split(".")[1]}`,
]}
/>
);
}
}
return (
<div
className='source-media-gallery'
className="source-media-gallery"
style={{ transform: `translate(${viewIdx * -shiftW}px)` }}
>
{media.map((m) => renderMedia(m))}
</div>
)
}
);
};

View File

@@ -1,36 +1,30 @@
import React from 'react'
import React from "react";
export default ({ viewIdx, paths, onShiftHandler }) => {
const backArrow = viewIdx !== 0 ? (
<div
className='back'
onClick={() => onShiftHandler(-1)}
>
<div className='centerer'>
<i className='material-icons'>arrow_left</i>
const backArrow =
viewIdx !== 0 ? (
<div className="back" onClick={() => onShiftHandler(-1)}>
<div className="centerer">
<i className="material-icons">arrow_left</i>
</div>
</div>
</div>
) : null
const forwardArrow = viewIdx < paths.length - 1 ? (
<div
className='next'
onClick={() => onShiftHandler(1)}
>
<div className='centerer'>
<i className='material-icons'>arrow_right</i>
) : null;
const forwardArrow =
viewIdx < paths.length - 1 ? (
<div className="next" onClick={() => onShiftHandler(1)}>
<div className="centerer">
<i className="material-icons">arrow_right</i>
</div>
</div>
</div>
) : null
) : null;
if (paths.length > 1) {
return (
<div className='media-gallery-controls'>
<div className="media-gallery-controls">
{backArrow}
{forwardArrow}
</div>
)
);
}
return (
<div className='media-gallery-controls' />
)
}
return <div className="media-gallery-controls" />;
};

View File

@@ -1,21 +1,23 @@
import React from 'react'
import copy from '../../common/data/copy.json'
import React from "react";
import copy from "../../common/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

@@ -1,36 +1,41 @@
/* global fetch */
import React from 'react'
import PropTypes from 'prop-types'
import marked from 'marked'
import React from "react";
import PropTypes from "prop-types";
import marked from "marked";
class Md extends React.Component {
constructor (props) {
super(props)
this.state = { md: null, error: null }
constructor(props) {
super(props);
this.state = { md: null, error: null };
}
componentDidMount () {
componentDidMount() {
fetch(this.props.path)
.then(resp => resp.text())
.then(text => {
if (text.length <= 0) { throw new Error() }
.then((resp) => resp.text())
.then((text) => {
if (text.length <= 0) {
throw new Error();
}
this.setState({ md: marked(text) })
this.setState({ md: marked(text) });
})
.catch(() => {
this.setState({ error: true })
})
this.setState({ error: true });
});
}
render () {
render() {
if (this.state.md && !this.state.error) {
return (
<div className='md-container' dangerouslySetInnerHTML={{ __html: this.state.md }} />
)
<div
className="md-container"
dangerouslySetInnerHTML={{ __html: this.state.md }}
/>
);
} else if (this.state.error) {
return this.props.unloader || <div>Error: couldn't load source</div>
return this.props.unloader || <div>Error: couldn't load source</div>;
} else {
return this.props.loader
return this.props.loader;
}
}
}
@@ -38,7 +43,7 @@ class Md extends React.Component {
Md.propTypes = {
loader: PropTypes.func,
unloader: PropTypes.func.isRequired,
path: PropTypes.string.isRequired
}
path: PropTypes.string.isRequired,
};
export default Md
export default Md;

View File

@@ -1,8 +1,8 @@
import React from 'react'
import marked from 'marked'
import Content from './Content'
import Controls from './Controls'
import { selectTypeFromPathWithPoster } from '../../common/utilities'
import React from "react";
import marked from "marked";
import Content from "./Content";
import Controls from "./Controls";
import { selectTypeFromPathWithPoster } from "../../common/utilities";
/*
* Inside the SourceOverlay, both the currently displaying media and language
@@ -10,95 +10,124 @@ import { selectTypeFromPathWithPoster } from '../../common/utilities'
* state.
*/
class SourceOverlay extends React.Component {
constructor () {
super()
this.state = { mediaIdx: 0, langIdx: 0 }
this.onShiftGallery = this.onShiftGallery.bind(this)
constructor() {
super();
this.state = { mediaIdx: 0, langIdx: 0 };
this.onShiftGallery = this.onShiftGallery.bind(this);
}
getTypeCounts (media) {
getTypeCounts(media) {
return media.reduce(
(acc, vl) => {
acc[vl.type] += 1
return acc
acc[vl.type] += 1;
return acc;
},
{ Image: 0, Video: 0, Text: 0 }
)
);
}
onShiftGallery (shift) {
onShiftGallery(shift) {
// no more left
if (this.state.mediaIdx === 0 && shift === -1) return
if (this.state.mediaIdx === 0 && shift === -1) return;
// no more right
if (this.state.mediaIdx === this.props.source.paths.length - 1 && shift === 1) return
this.setState({ mediaIdx: this.state.mediaIdx + shift })
if (
this.state.mediaIdx === this.props.source.paths.length - 1 &&
shift === 1
)
return;
this.setState({ mediaIdx: this.state.mediaIdx + shift });
}
switchLanguage (idx) {
this.setState({ langIdx: idx })
switchLanguage(idx) {
this.setState({ langIdx: idx });
}
renderContent (source) {
const { url, title, paths, date, type, poster, description } = source
const shortenedTitle = title.substring(0, 100)
renderContent(source) {
const { url, title, paths, date, type, poster, description } = source;
const shortenedTitle = title.substring(0, 100);
return (
<React.Fragment>
<div className='mo-banner'>
<div className='mo-banner-close' onClick={this.props.onCancel}>
<i className='material-icons'>close</i>
<>
<div className="mo-banner">
<div className="mo-banner-close" onClick={this.props.onCancel}>
<i className="material-icons">close</i>
</div>
<h3 className='mo-banner-content'>{shortenedTitle}</h3>
<h3 className="mo-banner-content">{shortenedTitle}</h3>
</div>
<div className='mo-container' onClick={e => e.stopPropagation()}>
<div className='mo-media-container'>
<div className="mo-container" onClick={(e) => e.stopPropagation()}>
<div className="mo-media-container">
<Content
switchLanguage={(lang) => this.switchLanguage(lang)}
translations={this.props.translations}
langIdx={this.state.langIdx}
media={paths.map(p => selectTypeFromPathWithPoster(p, poster))}
media={paths.map((p) => selectTypeFromPathWithPoster(p, poster))}
viewIdx={this.state.mediaIdx}
/>
</div>
</div>
<div className='mo-footer'>
<Controls paths={paths} viewIdx={this.state.mediaIdx} onShiftHandler={this.onShiftGallery} />
<div className="mo-footer">
<Controls
paths={paths}
viewIdx={this.state.mediaIdx}
onShiftHandler={this.onShiftGallery}
/>
<div className='mo-meta-container'>
{description ? <div className='mo-box-desc'>
<div className='md-container' dangerouslySetInnerHTML={{ __html: marked(description) }} />
</div> : null}
<div className="mo-meta-container">
{description ? (
<div className="mo-box-desc">
<div
className="md-container"
dangerouslySetInnerHTML={{ __html: marked(description) }}
/>
</div>
) : null}
{(type || date || url) ? (
<div className='mo-box'>
{type || date || url ? (
<div className="mo-box">
<div>
{type ? <h4>Evidence type</h4> : null}
{type ? <p><i className='material-icons left'>perm_media</i>{type}</p> : null}
{type ? (
<p>
<i className="material-icons left">perm_media</i>
{type}
</p>
) : null}
</div>
<div>
{date ? <h4>Date Published</h4> : null}
{date ? <p><i className='material-icons left'>today</i>{date}</p> : null}
{date ? (
<p>
<i className="material-icons left">today</i>
{date}
</p>
) : null}
</div>
<div>
{url ? <h4>Link</h4> : null}
{url ? <span><i className='material-icons left'>link</i><a href={url} target='_blank'>Link to original URL</a></span> : null}
{url ? (
<span>
<i className="material-icons left">link</i>
<a href={url} target="_blank">
Link to original URL
</a>
</span>
) : null}
</div>
</div>
) : null}
</div>
</div>
</React.Fragment>
)
</>
);
}
renderIntlContent () {
const { langIdx } = this.state
const { translations, source } = this.props
let translated = null
renderIntlContent() {
const { langIdx } = this.state;
const { translations, source } = this.props;
let translated = null;
if (translations && translations.length && langIdx > 0) {
translated = translations[langIdx - 1]
translated = translations[langIdx - 1];
}
if (translated) {
translated = {
@@ -106,24 +135,24 @@ class SourceOverlay extends React.Component {
poster: source.poster,
// NOTE: this is to allow a slightly nicer syntax when using the Media
// overlay in cover videos.
paths: translated.file ? [translated.file] : translated.paths
}
paths: translated.file ? [translated.file] : translated.paths,
};
}
return this.renderContent(langIdx === 0 ? source : translated)
return this.renderContent(langIdx === 0 ? source : translated);
}
render () {
if (typeof (this.props.source) !== 'object') {
return this.renderError()
render() {
if (typeof this.props.source !== "object") {
return this.renderError();
}
return (
<div className={`mo-overlay ${this.props.opaque ? 'opaque' : ''}`}>
<div className={`mo-overlay ${this.props.opaque ? "opaque" : ""}`}>
{this.renderIntlContent()}
</div>
)
);
}
}
export default SourceOverlay
export default SourceOverlay;

View File

@@ -1,76 +1,100 @@
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 '../scss/search.scss'
import "../scss/search.scss";
import SearchRow from './SearchRow.jsx'
import SearchRow from "./SearchRow.jsx";
class Search extends React.Component {
constructor (props) {
super(props)
constructor(props) {
super(props);
this.state = {
isFolded: true
}
this.onButtonClick = this.onButtonClick.bind(this)
this.updateSearchQuery = this.updateSearchQuery.bind(this)
isFolded: true,
};
this.onButtonClick = this.onButtonClick.bind(this);
this.updateSearchQuery = this.updateSearchQuery.bind(this);
}
onButtonClick () {
this.setState(prevState => {
return { isFolded: !prevState.isFolded }
})
onButtonClick() {
this.setState((prevState) => {
return { isFolded: !prevState.isFolded };
});
}
updateSearchQuery (e) {
let queryString = e.target.value
this.props.actions.updateSearchQuery(queryString)
updateSearchQuery(e) {
const queryString = e.target.value;
this.props.actions.updateSearchQuery(queryString);
}
render () {
let searchResults
render() {
let searchResults;
const searchAttributes = ['description', 'location', 'category', 'date']
const searchAttributes = ["description", "location", "category", "date"];
if (!this.props.queryString) {
searchResults = []
searchResults = [];
} else {
searchResults = this.props.events.filter(event =>
searchAttributes.some(attribute => event[attribute].toLowerCase().includes(this.props.queryString.toLowerCase()))
)
searchResults = this.props.events.filter((event) =>
searchAttributes.some((attribute) =>
event[attribute]
.toLowerCase()
.includes(this.props.queryString.toLowerCase())
)
);
}
return (
<div class={'search-outer-container' + (this.props.narrative ? ' narrative-mode ' : '')}>
<div id='search-bar-icon-container' onClick={this.onButtonClick}>
<i className='material-icons'>search</i>
<div
class={
"search-outer-container" +
(this.props.narrative ? " narrative-mode " : "")
}
>
<div id="search-bar-icon-container" onClick={this.onButtonClick}>
<i className="material-icons">search</i>
</div>
<div class={'search-bar-overlay' + (this.state.isFolded ? ' folded' : '')}>
<div class='search-input-container'>
<input class='search-bar-input' onChange={this.updateSearchQuery} type='text' />
<i id='close-search-overlay' className='material-icons' onClick={this.onButtonClick} >close</i>
<div
class={"search-bar-overlay" + (this.state.isFolded ? " folded" : "")}
>
<div class="search-input-container">
<input
class="search-bar-input"
onChange={this.updateSearchQuery}
type="text"
/>
<i
id="close-search-overlay"
className="material-icons"
onClick={this.onButtonClick}
>
close
</i>
</div>
<div class='search-results'>
{searchResults.map(result => {
return <SearchRow onSearchRowClick={this.props.onSearchRowClick} eventObj={result} query={this.props.queryString} />
<div class="search-results">
{searchResults.map((result) => {
return (
<SearchRow
onSearchRowClick={this.props.onSearchRowClick}
eventObj={result}
query={this.props.queryString}
/>
);
})}
</div>
</div>
</div>
)
);
}
}
function mapDispatchToProps (dispatch) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch)
}
actions: bindActionCreators(actions, dispatch),
};
}
export default connect(
state => state,
mapDispatchToProps
)(Search)
export default connect((state) => state, mapDispatchToProps)(Search);

View File

@@ -1,40 +1,62 @@
import React from 'react'
import React from "react";
const SearchRow = ({ query, eventObj, onSearchRowClick }) => {
const { description, location, date } = eventObj
function getHighlightedText (text, highlight) {
const { description, location, date } = eventObj;
function getHighlightedText(text, highlight) {
// Split text on highlight term, include term itself into parts, ignore case
const parts = text.split(new RegExp(`(${highlight})`, 'gi'))
return <span>{ parts.map(part => part.toLowerCase() === highlight.toLowerCase() ? <span style={{ backgroundColor: 'yellow', color: 'black' }}>{part}</span> : part) }</span>
const parts = text.split(new RegExp(`(${highlight})`, "gi"));
return (
<span>
{parts.map((part) =>
part.toLowerCase() === highlight.toLowerCase() ? (
<span style={{ backgroundColor: "yellow", color: "black" }}>
{part}
</span>
) : (
part
)
)}
</span>
);
}
function getShortDescription (text, searchQuery) {
var regexp = new RegExp(`(([^ ]* ){0,6}[a-zA-Z]*${searchQuery.toLowerCase()}[a-zA-Z]*( [^ ]*){0,5})`, 'gm')
let parts = text.toLowerCase().match(regexp)
for (var x = 0; x < (parts ? parts.length : 0); x++) {
parts[x] = '...' + parts[x]
function getShortDescription(text, searchQuery) {
const regexp = new RegExp(
`(([^ ]* ){0,6}[a-zA-Z]*${searchQuery.toLowerCase()}[a-zA-Z]*( [^ ]*){0,5})`,
"gm"
);
const parts = text.toLowerCase().match(regexp);
for (let x = 0; x < (parts ? parts.length : 0); x++) {
parts[x] = "..." + parts[x];
}
const firstLine = [text.match('(([^ ]* ){0,10})', 'm')[0]]
return parts || firstLine
const firstLine = [text.match("(([^ ]* ){0,10})", "m")[0]];
return parts || firstLine;
}
return (
<div className='search-row' onClick={() => onSearchRowClick([eventObj])}>
<div className='location-date-container'>
<div className='date-container'>
<i className='material-icons'>event</i>
<div className="search-row" onClick={() => onSearchRowClick([eventObj])}>
<div className="location-date-container">
<div className="date-container">
<i className="material-icons">event</i>
<p>{getHighlightedText(date, query)}</p>
</div>
<div className='location-container'>
<i className='material-icons'>location_on</i>
<div className="location-container">
<i className="material-icons">location_on</i>
<p>{getHighlightedText(location, query)}</p>
</div>
</div>
<p>{getShortDescription(description, query).map(match => {
return <span>{getHighlightedText(match, query)}...<br /></span>
})}</p>
<p>
{getShortDescription(description, query).map((match) => {
return (
<span>
{getHighlightedText(match, query)}...
<br />
</span>
);
})}
</p>
</div>
)
}
);
};
export default SearchRow
export default SearchRow;

View File

@@ -1,19 +1,28 @@
import React, { useState } from 'react'
import React, { useState } from "react";
export default ({ showing, onClickHandler, timelineDims }) => {
if (!showing) {
return null
return null;
}
const [checked, setChecked] = useState(false)
const handleCheck = () => setChecked(!checked)
const onNarrativise = () => onClickHandler(checked)
const [checked, setChecked] = useState(false);
const handleCheck = () => setChecked(!checked);
const onNarrativise = () => onClickHandler(checked);
return <div className='stateoptions-panel' style={{ bottom: timelineDims.height }}>
<div>
<div className='button' onClick={onNarrativise}>Narrativise</div>
<label for='withlines'>Connect by lines</label>
<input name='withlines' onClick={handleCheck} checked={checked} type='checkbox' />
return (
<div className="stateoptions-panel" style={{ bottom: timelineDims.height }}>
<div>
<div className="button" onClick={onNarrativise}>
Narrativise
</div>
<label for="withlines">Connect by lines</label>
<input
name="withlines"
onClick={handleCheck}
checked={checked}
type="checkbox"
/>
</div>
</div>
</div>
}
);
};

View File

@@ -1,9 +1,9 @@
import React from 'react'
import React from "react";
export default ({ showing, children }) => {
return (
<div className={`cover-container ${showing ? 'showing' : ''}`}>
<div className={`cover-container ${showing ? "showing" : ""}`}>
{children}
</div>
)
}
);
};

View File

@@ -1,11 +1,11 @@
import React from 'react'
import { connect } from 'react-redux'
import { Player } from 'video-react'
import marked from 'marked'
import MediaOverlay from './Overlay/Media'
import falogo from '../assets/fa-logo.png'
import bcatlogo from '../assets/bellingcat-logo.png'
const MEDIA_HIDDEN = -2
import React from "react";
import { connect } from "react-redux";
import { Player } from "video-react";
import marked from "marked";
import MediaOverlay from "./Overlay/Media";
import falogo from "../assets/fa-logo.png";
import bcatlogo from "../assets/bellingcat-logo.png";
const MEDIA_HIDDEN = -2;
/**
* Manages the presentation of props that come in from the store's app.cover.
@@ -14,211 +14,260 @@ const MEDIA_HIDDEN = -2
* a couple of weird offset calculations... but it works for the time being.
*/
class TemplateCover extends React.Component {
constructor (props) {
super(props)
constructor(props) {
super(props);
this.state = {
video: MEDIA_HIDDEN,
featureLang: 0
}
featureLang: 0,
};
}
getVideo (index, headerEndIndex) {
getVideo(index, headerEndIndex) {
if (index < headerEndIndex) {
return this.props.cover.headerVideos[index]
return this.props.cover.headerVideos[index];
} else if (index >= 0) {
return this.props.cover.videos[index - headerEndIndex]
return this.props.cover.videos[index - headerEndIndex];
} else {
return null
return null;
}
}
onVideoClickHandler (index) {
const buffer = this.props.cover.headerVideos ? this.props.cover.headerVideos.length : 0
onVideoClickHandler(index) {
const buffer = this.props.cover.headerVideos
? this.props.cover.headerVideos.length
: 0;
return () => {
this.setState({
video: index + buffer
})
}
video: index + buffer,
});
};
}
renderFeature () {
const { featureVideo } = this.props.cover
const { featureLang } = this.state
const { translations } = featureVideo
const source = featureLang === 0
? featureVideo
: {
...translations[featureLang - 1],
poster: featureVideo.poster
}
renderFeature() {
const { featureVideo } = this.props.cover;
const { featureLang } = this.state;
const { translations } = featureVideo;
const source =
featureLang === 0
? featureVideo
: {
...translations[featureLang - 1],
poster: featureVideo.poster,
};
return (
<div>
<div className='banner-trans right-overlay'>
{translations && translations.map((trans, idx) => {
const langIdx = idx + 1 // default lang idx is 0
if (featureLang !== langIdx) {
return <div onClick={() => this.setState({ featureLang: langIdx })} className='trans-button'>{trans.code}</div>
} else {
return <div onClick={() => this.setState({ featureLang: 0 })} className='trans-button'>ENG</div>
}
})}
<div className="banner-trans right-overlay">
{translations &&
translations.map((trans, idx) => {
const langIdx = idx + 1; // default lang idx is 0
if (featureLang !== langIdx) {
return (
<div
onClick={() => this.setState({ featureLang: langIdx })}
className="trans-button"
>
{trans.code}
</div>
);
} else {
return (
<div
onClick={() => this.setState({ featureLang: 0 })}
className="trans-button"
>
ENG
</div>
);
}
})}
</div>
<Player
className='source-video'
className="source-video"
poster={source.poster}
playsInline
src={source.file}
/>
</div>
)
);
}
renderHeaderVideos () {
const { headerVideos } = this.props.cover
renderHeaderVideos() {
const { headerVideos } = this.props.cover;
return (
<div className='row'>
{ headerVideos.slice(0, 2).map((media, index) => (
<div className='cell plain' onClick={() => this.setState({ video: index })}>
<div className="row">
{headerVideos.slice(0, 2).map((media, index) => (
<div
className="cell plain"
onClick={() => this.setState({ video: index })}
>
{media.buttonTitle}
</div>
)) }
))}
</div>
)
);
}
renderButton (button, yellow) {
renderButton(button, yellow) {
return (
<div className='row'>
<a className={`cell ${yellow ? 'yellow' : 'plain'}`} href={button.href}>
<div className="row">
<a className={`cell ${yellow ? "yellow" : "plain"}`} href={button.href}>
{button.title}
</a>
</div>
)
);
}
renderMediaOverlay () {
const video = this.getVideo(this.state.video, this.props.cover.headerVideos ? this.props.cover.headerVideos.length : 0)
renderMediaOverlay() {
const video = this.getVideo(
this.state.video,
this.props.cover.headerVideos ? this.props.cover.headerVideos.length : 0
);
return (
<MediaOverlay
opaque
source={
{
title: video.title,
desc: video.desc,
paths: [video.file],
poster: video.poster
}}
source={{
title: video.title,
desc: video.desc,
paths: [video.file],
poster: video.poster,
}}
translations={video.translations}
onCancel={() => this.setState({ video: MEDIA_HIDDEN })}
/>
)
);
}
render () {
render() {
if (!this.props.cover) {
return (
<div className='default-cover-container'>
You haven't specified any cover props. Put them in the values that overwrite the store in <code>app.cover</code>
<div className="default-cover-container">
You haven't specified any cover props. Put them in the values that
overwrite the store in <code>app.cover</code>
</div>
)
);
}
const { videos, footerButton } = this.props.cover
const { showing } = this.props
const { videos, footerButton } = this.props.cover;
const { showing } = this.props;
return (
<div className='default-cover-container'>
<div className={showing ? 'cover-header' : 'cover-header minimized'}>
<a className='cover-logo-container' href='https://forensic-architecture.org'>
<img className='cover-logo' src={falogo} />
<div className="default-cover-container">
<div className={showing ? "cover-header" : "cover-header minimized"}>
<a
className="cover-logo-container"
href="https://forensic-architecture.org"
>
<img className="cover-logo" src={falogo} />
</a>
<a className='cover-logo-container' href='https://bellingcat.com'>
<img className='cover-logo' src={bcatlogo} />
<a className="cover-logo-container" href="https://bellingcat.com">
<img className="cover-logo" src={bcatlogo} />
</a>
</div>
<div className='cover-content'>
{
this.props.cover.bgVideo ? (
<div className={`fullscreen-bg ${!this.props.showing ? 'hidden' : ''}`}>
<video
loop
muted
autoPlay
preload='auto'
className='fullscreen-bg__video'
>
<source src={this.props.cover.bgVideo} type='video/mp4' />
</video>
</div>
) : null
}
<h2 style={{ margin: 0 }} dangerouslySetInnerHTML={{ __html: marked(this.props.cover.title) }} />
{
this.props.cover.subtitle ? (
<h3 style={{ marginTop: 0 }}>{this.props.cover.subtitle}</h3>
) : null
}
{
this.props.cover.subsubtitle ? (
<h5>{this.props.cover.subsubtitle}</h5>
) : null
}
<div className="cover-content">
{this.props.cover.bgVideo ? (
<div
className={`fullscreen-bg ${!this.props.showing ? "hidden" : ""}`}
>
<video
loop
muted
autoPlay
preload="auto"
className="fullscreen-bg__video"
>
<source src={this.props.cover.bgVideo} type="video/mp4" />
</video>
</div>
) : null}
<h2
style={{ margin: 0 }}
dangerouslySetInnerHTML={{ __html: marked(this.props.cover.title) }}
/>
{this.props.cover.subtitle ? (
<h3 style={{ marginTop: 0 }}>{this.props.cover.subtitle}</h3>
) : null}
{this.props.cover.subsubtitle ? (
<h5>{this.props.cover.subsubtitle}</h5>
) : null}
{this.props.cover.featureVideo ? this.renderFeature() : null}
<div className='hero thin'>
<div className="hero thin">
{this.props.cover.headerVideos ? this.renderHeaderVideos() : null}
{this.props.cover.headerButton ? this.renderButton(this.props.cover.headerButton) : null}
<div className='row'>
<div className='cell yellow' onClick={this.props.showAppHandler}>
{this.props.cover.headerButton
? this.renderButton(this.props.cover.headerButton)
: null}
<div className="row">
<div className="cell yellow" onClick={this.props.showAppHandler}>
{this.props.cover.exploreButton}
</div>
</div>
</div>
{Array.isArray(this.props.cover.description)
? this.props.cover.description.map(e => <div className='md-container' dangerouslySetInnerHTML={{ __html: marked(e) }} />)
: <div className='md-container' dangerouslySetInnerHTML={{ __html: marked(this.props.cover.description) }} />}
{Array.isArray(this.props.cover.description) ? (
this.props.cover.description.map((e) => (
<div
className="md-container"
dangerouslySetInnerHTML={{ __html: marked(e) }}
/>
))
) : (
<div
className="md-container"
dangerouslySetInnerHTML={{
__html: marked(this.props.cover.description),
}}
/>
)}
{videos ? (
<div className='hero'>
<div className='row'>
<div className="hero">
<div className="row">
{/* NOTE: only take first four videos, drop any others for style reasons */}
{ videos && videos.slice(0, 2).map((media, index) => (
<div className='cell small' onClick={this.onVideoClickHandler(index)} >
{media.buttonTitle}<br />{media.buttonSubtitle}
</div>
)) }
{videos &&
videos.slice(0, 2).map((media, index) => (
<div
className="cell small"
onClick={this.onVideoClickHandler(index)}
>
{media.buttonTitle}
<br />
{media.buttonSubtitle}
</div>
))}
</div>
<div className='row'>
{ videos.length > 2 && this.props.cover.videos.slice(2, 4).map((media, index) => (
<div className='cell small' onClick={this.onVideoClickHandler(index + 2)} >
{media.buttonTitle}<br />{media.buttonSubtitle}
</div>
)) }
<div className="row">
{videos.length > 2 &&
this.props.cover.videos.slice(2, 4).map((media, index) => (
<div
className="cell small"
onClick={this.onVideoClickHandler(index + 2)}
>
{media.buttonTitle}
<br />
{media.buttonSubtitle}
</div>
))}
</div>
</div>
) : null}
{footerButton ? (
<div className='hero'>
<div className='row'>
{this.renderButton(footerButton)}
</div>
<div className="hero">
<div className="row">{this.renderButton(footerButton)}</div>
</div>
) : null}
</div>
{
this.state.video !== MEDIA_HIDDEN ? this.renderMediaOverlay() : null }
{this.state.video !== MEDIA_HIDDEN ? this.renderMediaOverlay() : null}
</div>
)
);
}
}
function mapStateToProps (state) {
function mapStateToProps(state) {
return {
cover: state.app.cover
}
cover: state.app.cover,
};
}
export default connect(mapStateToProps)(TemplateCover)
export default connect(mapStateToProps)(TemplateCover);

View File

@@ -1,29 +1,29 @@
import React from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import * as d3 from 'd3'
import * as selectors from '../selectors'
import { setLoading, setNotLoading } from '../actions'
import hash from 'object-hash'
import React from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import * as d3 from "d3";
import * as selectors from "../selectors";
import { setLoading, setNotLoading } from "../actions";
import hash from "object-hash";
import copy from '../common/data/copy.json'
import Header from './presentational/Timeline/Header'
import Axis from './TimelineAxis.jsx'
import Clip from './presentational/Timeline/Clip'
import Handles from './presentational/Timeline/Handles.js'
import ZoomControls from './presentational/Timeline/ZoomControls.js'
import Markers from './presentational/Timeline/Markers.js'
import Events from './presentational/Timeline/Events.js'
import Categories from './TimelineCategories.jsx'
import copy from "../common/data/copy.json";
import Header from "./presentational/Timeline/Header";
import Axis from "./TimelineAxis.jsx";
import Clip from "./presentational/Timeline/Clip";
import Handles from "./presentational/Timeline/Handles.js";
import ZoomControls from "./presentational/Timeline/ZoomControls.js";
import Markers from "./presentational/Timeline/Markers.js";
import Events from "./presentational/Timeline/Events.js";
import Categories from "./TimelineCategories.jsx";
class Timeline extends React.Component {
constructor (props) {
super(props)
this.styleDatetime = this.styleDatetime.bind(this)
this.getDatetimeX = this.getDatetimeX.bind(this)
this.getY = this.getY.bind(this)
this.onApplyZoom = this.onApplyZoom.bind(this)
this.svgRef = React.createRef()
constructor(props) {
super(props);
this.styleDatetime = this.styleDatetime.bind(this);
this.getDatetimeX = this.getDatetimeX.bind(this);
this.getY = this.getY.bind(this);
this.onApplyZoom = this.onApplyZoom.bind(this);
this.svgRef = React.createRef();
this.state = {
isFolded: false,
dims: props.dimensions,
@@ -31,103 +31,128 @@ class Timeline extends React.Component {
scaleY: null,
timerange: [null, null], // two datetimes
dragPos0: null,
transitionDuration: 300
}
transitionDuration: 300,
};
}
componentDidMount () {
this.addEventListeners()
componentDidMount() {
this.addEventListeners();
}
componentWillReceiveProps (nextProps) {
componentWillReceiveProps(nextProps) {
if (hash(nextProps) !== hash(this.props)) {
this.setState({
timerange: nextProps.app.timeline.range,
scaleX: this.makeScaleX()
})
scaleX: this.makeScaleX(),
});
}
if ((hash(nextProps.domain.categories) !== hash(this.props.domain.categories)) || hash(nextProps.dimensions) !== hash(this.props.dimensions)) {
const { trackHeight, marginTop } = nextProps.dimensions
if (
hash(nextProps.domain.categories) !==
hash(this.props.domain.categories) ||
hash(nextProps.dimensions) !== hash(this.props.dimensions)
) {
const { trackHeight, marginTop } = nextProps.dimensions;
this.setState({
scaleY: this.makeScaleY(nextProps.domain.categories, trackHeight, marginTop)
})
scaleY: this.makeScaleY(
nextProps.domain.categories,
trackHeight,
marginTop
),
});
}
if (nextProps.dimensions.trackHeight !== this.props.dimensions.trackHeight) {
this.computeDims()
if (
nextProps.dimensions.trackHeight !== this.props.dimensions.trackHeight
) {
this.computeDims();
}
}
addEventListeners () {
window.addEventListener('resize', () => { this.computeDims() })
let element = document.querySelector('.timeline-wrapper')
addEventListeners() {
window.addEventListener("resize", () => {
this.computeDims();
});
const element = document.querySelector(".timeline-wrapper");
if (element !== null) {
element.addEventListener('transitionend', (event) => {
this.computeDims()
})
element.addEventListener("transitionend", (event) => {
this.computeDims();
});
}
}
makeScaleX () {
return d3.scaleTime()
makeScaleX() {
return d3
.scaleTime()
.domain(this.state.timerange)
.range([this.state.dims.marginLeft, this.state.dims.width - this.state.dims.width_controls])
.range([
this.state.dims.marginLeft,
this.state.dims.width - this.state.dims.width_controls,
]);
}
makeScaleY (categories, trackHeight, marginTop) {
const { features } = this.props
makeScaleY(categories, trackHeight, marginTop) {
const { features } = this.props;
if (features.GRAPH_NONLOCATED && features.GRAPH_NONLOCATED.categories) {
categories = categories.filter(cat => !features.GRAPH_NONLOCATED.categories.includes(cat.id))
categories = categories.filter(
(cat) => !features.GRAPH_NONLOCATED.categories.includes(cat.id)
);
}
const extraPadding = 0
const catHeight = categories.length > 2 ? trackHeight / categories.length : trackHeight / (categories.length + 1)
const extraPadding = 0;
const catHeight =
categories.length > 2
? trackHeight / categories.length
: trackHeight / (categories.length + 1);
const catsYpos = categories.map((g, i) => {
return ((i + 1) * catHeight) + marginTop + (extraPadding / 2)
})
const catMap = categories.map(c => c.id)
return (i + 1) * catHeight + marginTop + extraPadding / 2;
});
const catMap = categories.map((c) => c.id);
return (cat) => {
const idx = catMap.indexOf(cat)
return catsYpos[idx]
}
const idx = catMap.indexOf(cat);
return catsYpos[idx];
};
}
componentDidUpdate (prevProps, prevState) {
componentDidUpdate(prevProps, prevState) {
if (prevState.timerange !== this.state.timerange) {
this.setState({ scaleX: this.makeScaleX() })
this.setState({ scaleX: this.makeScaleX() });
}
}
/**
* Returns the time scale (x) extent in minutes
*/
getTimeScaleExtent () {
if (!this.state.scaleX) return 0
const timeDomain = this.state.scaleX.domain()
return (timeDomain[1].getTime() - timeDomain[0].getTime()) / 60000
getTimeScaleExtent() {
if (!this.state.scaleX) return 0;
const timeDomain = this.state.scaleX.domain();
return (timeDomain[1].getTime() - timeDomain[0].getTime()) / 60000;
}
onClickArrow () {
onClickArrow() {
this.setState((prevState, props) => {
return { isFolded: !prevState.isFolded }
})
return { isFolded: !prevState.isFolded };
});
}
computeDims () {
const dom = this.props.ui.dom.timeline
computeDims() {
const dom = this.props.ui.dom.timeline;
if (document.querySelector(`#${dom}`) !== null) {
const boundingClient = document.querySelector(`#${dom}`).getBoundingClientRect()
const boundingClient = document
.querySelector(`#${dom}`)
.getBoundingClientRect();
this.setState({
dims: {
...this.props.dimensions,
width: boundingClient.width
this.setState(
{
dims: {
...this.props.dimensions,
width: boundingClient.width,
},
},
() => {
this.setState({ scaleX: this.makeScaleX() });
}
},
() => {
this.setState({ scaleX: this.makeScaleX() })
})
);
}
}
@@ -135,34 +160,37 @@ class Timeline extends React.Component {
* Shift time range by moving forward or backwards
* @param {String} direction: 'forward' / 'backwards'
*/
onMoveTime (direction) {
const extent = this.getTimeScaleExtent()
const newCentralTime = d3.timeMinute.offset(this.state.scaleX.domain()[0], extent / 2)
onMoveTime(direction) {
const extent = this.getTimeScaleExtent();
const newCentralTime = d3.timeMinute.offset(
this.state.scaleX.domain()[0],
extent / 2
);
// if forward
let domain0 = newCentralTime
let domainF = d3.timeMinute.offset(newCentralTime, extent)
let domain0 = newCentralTime;
let domainF = d3.timeMinute.offset(newCentralTime, extent);
// if backwards
if (direction === 'backwards') {
domain0 = d3.timeMinute.offset(newCentralTime, -extent)
domainF = newCentralTime
if (direction === "backwards") {
domain0 = d3.timeMinute.offset(newCentralTime, -extent);
domainF = newCentralTime;
}
this.setState({ timerange: [domain0, domainF] }, () => {
this.props.methods.onUpdateTimerange(this.state.timerange)
})
this.props.methods.onUpdateTimerange(this.state.timerange);
});
}
onCenterTime (newCentralTime) {
const extent = this.getTimeScaleExtent()
onCenterTime(newCentralTime) {
const extent = this.getTimeScaleExtent();
const domain0 = d3.timeMinute.offset(newCentralTime, -extent / 2)
const domainF = d3.timeMinute.offset(newCentralTime, +extent / 2)
const domain0 = d3.timeMinute.offset(newCentralTime, -extent / 2);
const domainF = d3.timeMinute.offset(newCentralTime, +extent / 2);
this.setState({ timerange: [domain0, domainF] }, () => {
this.props.methods.onUpdateTimerange(this.state.timerange)
})
this.props.methods.onUpdateTimerange(this.state.timerange);
});
}
/**
@@ -170,119 +198,132 @@ class Timeline extends React.Component {
* WITHOUT updating the store, or data shown.
* Used for updates in the middle of a transition, for performance purposes
*/
onSoftTimeRangeUpdate (timerange) {
this.setState({ timerange })
onSoftTimeRangeUpdate(timerange) {
this.setState({ timerange });
}
/**
* Apply zoom level to timeline
* @param {object} zoom: zoom level from zoomLevels
*/
onApplyZoom (zoom) {
const extent = this.getTimeScaleExtent()
const newCentralTime = d3.timeMinute.offset(this.state.scaleX.domain()[0], extent / 2)
const { rangeLimits } = this.props.app.timeline
onApplyZoom(zoom) {
const extent = this.getTimeScaleExtent();
const newCentralTime = d3.timeMinute.offset(
this.state.scaleX.domain()[0],
extent / 2
);
const { rangeLimits } = this.props.app.timeline;
let newDomain0 = d3.timeMinute.offset(newCentralTime, -zoom.duration / 2)
let newDomainF = d3.timeMinute.offset(newCentralTime, zoom.duration / 2)
let newDomain0 = d3.timeMinute.offset(newCentralTime, -zoom.duration / 2);
let newDomainF = d3.timeMinute.offset(newCentralTime, zoom.duration / 2);
if (rangeLimits) {
// If the store contains absolute time limits,
// make sure the zoom doesn't go over them
const minDate = rangeLimits[0]
const maxDate = rangeLimits[1]
const minDate = rangeLimits[0];
const maxDate = rangeLimits[1];
if (newDomain0 < minDate) {
newDomain0 = minDate
newDomainF = d3.timeMinute.offset(newDomain0, zoom.duration)
newDomain0 = minDate;
newDomainF = d3.timeMinute.offset(newDomain0, zoom.duration);
}
if (newDomainF > maxDate) {
newDomainF = maxDate
newDomain0 = d3.timeMinute.offset(newDomainF, -zoom.duration)
newDomainF = maxDate;
newDomain0 = d3.timeMinute.offset(newDomainF, -zoom.duration);
}
}
this.setState({ timerange: [
newDomain0,
newDomainF
] }, () => {
this.props.methods.onUpdateTimerange(this.state.timerange)
})
this.setState(
{
timerange: [newDomain0, newDomainF],
},
() => {
this.props.methods.onUpdateTimerange(this.state.timerange);
}
);
}
toggleTransition (isTransition) {
this.setState({ transitionDuration: (isTransition) ? 300 : 0 })
toggleTransition(isTransition) {
this.setState({ transitionDuration: isTransition ? 300 : 0 });
}
/*
* Setup drag behavior
*/
onDragStart () {
d3.event.sourceEvent.stopPropagation()
this.setState({
dragPos0: d3.event.x
}, () => {
this.toggleTransition(false)
})
onDragStart() {
d3.event.sourceEvent.stopPropagation();
this.setState(
{
dragPos0: d3.event.x,
},
() => {
this.toggleTransition(false);
}
);
}
/*
* Drag and update
*/
onDrag () {
const drag0 = this.state.scaleX.invert(this.state.dragPos0).getTime()
const dragNow = this.state.scaleX.invert(d3.event.x).getTime()
const timeShift = (drag0 - dragNow) / 1000
onDrag() {
const drag0 = this.state.scaleX.invert(this.state.dragPos0).getTime();
const dragNow = this.state.scaleX.invert(d3.event.x).getTime();
const timeShift = (drag0 - dragNow) / 1000;
const { range, rangeLimits } = this.props.app.timeline
let newDomain0 = d3.timeSecond.offset(range[0], timeShift)
let newDomainF = d3.timeSecond.offset(range[1], timeShift)
const { range, rangeLimits } = this.props.app.timeline;
let newDomain0 = d3.timeSecond.offset(range[0], timeShift);
let newDomainF = d3.timeSecond.offset(range[1], timeShift);
if (rangeLimits) {
// If the store contains absolute time limits,
// make sure the zoom doesn't go over them
const minDate = rangeLimits[0]
const maxDate = rangeLimits[1]
const minDate = rangeLimits[0];
const maxDate = rangeLimits[1];
newDomain0 = (newDomain0 < minDate) ? minDate : newDomain0
newDomainF = (newDomainF > maxDate) ? maxDate : newDomainF
newDomain0 = newDomain0 < minDate ? minDate : newDomain0;
newDomainF = newDomainF > maxDate ? maxDate : newDomainF;
}
// Updates components without updating timerange
this.onSoftTimeRangeUpdate([newDomain0, newDomainF])
this.onSoftTimeRangeUpdate([newDomain0, newDomainF]);
}
/**
* Stop dragging and update data
*/
onDragEnd () {
this.toggleTransition(true)
this.props.methods.onUpdateTimerange(this.state.timerange)
onDragEnd() {
this.toggleTransition(true);
this.props.methods.onUpdateTimerange(this.state.timerange);
}
getDatetimeX (datetime) {
return this.state.scaleX(datetime)
getDatetimeX(datetime) {
return this.state.scaleX(datetime);
}
getY (event) {
const { features, domain } = this.props
const { USE_CATEGORIES, GRAPH_NONLOCATED } = features
const { categories } = domain
const categoriesExist = USE_CATEGORIES && categories && categories.length > 0
getY(event) {
const { features, domain } = this.props;
const { USE_CATEGORIES, GRAPH_NONLOCATED } = features;
const { categories } = domain;
const categoriesExist =
USE_CATEGORIES && categories && categories.length > 0;
if (!categoriesExist) {
return this.state.dims.trackHeight / 2
return this.state.dims.trackHeight / 2;
}
const { category } = event
const { category } = event;
if (GRAPH_NONLOCATED && GRAPH_NONLOCATED.categories.includes(category)) {
const { project } = event
return this.state.dims.marginTop + domain.projects[project].offset + this.props.ui.eventRadius
const { project } = event;
return (
this.state.dims.marginTop +
domain.projects[project].offset +
this.props.ui.eventRadius
);
}
if (!this.state.scaleY) return 0
if (!this.state.scaleY) return 0;
return this.state.scaleY(category)
return this.state.scaleY(category);
}
/**
@@ -294,39 +335,44 @@ class Timeline extends React.Component {
* at the second index is an optional additional component that renders in
* the <g/> div.
*/
styleDatetime (timestamp, category) {
return [null, null]
styleDatetime(timestamp, category) {
return [null, null];
}
render () {
const { isNarrative, app } = this.props
let classes = `timeline-wrapper ${(this.state.isFolded) ? ' folded' : ''}`
classes += (app.narrative !== null) ? ' narrative-mode' : ''
const { dims } = this.state
const foldedStyle = { bottom: this.state.isFolded ? -dims.height : 0 }
const heightStyle = { height: dims.height }
const extraStyle = { ...heightStyle, ...foldedStyle }
const contentHeight = { height: dims.contentHeight }
const { categories } = this.props.domain
render() {
const { isNarrative, app } = this.props;
let classes = `timeline-wrapper ${this.state.isFolded ? " folded" : ""}`;
classes += app.narrative !== null ? " narrative-mode" : "";
const { dims } = this.state;
const foldedStyle = { bottom: this.state.isFolded ? -dims.height : 0 };
const heightStyle = { height: dims.height };
const extraStyle = { ...heightStyle, ...foldedStyle };
const contentHeight = { height: dims.contentHeight };
const { categories } = this.props.domain;
return (
<div className={classes} style={extraStyle} onKeyDown={this.props.onKeyDown} tabIndex='1'>
<div
className={classes}
style={extraStyle}
onKeyDown={this.props.onKeyDown}
tabIndex="1"
>
<Header
title={copy[this.props.app.language].timeline.info}
from={this.state.timerange[0]}
to={this.state.timerange[1]}
onClick={() => { this.onClickArrow() }}
onClick={() => {
this.onClickArrow();
}}
hideInfo={isNarrative}
/>
<div className='timeline-content' style={heightStyle}>
<div id={this.props.ui.dom.timeline} className='timeline' style={contentHeight} >
<svg
ref={this.svgRef}
width={dims.width}
style={contentHeight}
>
<Clip
dims={dims}
/>
<div className="timeline-content" style={heightStyle}>
<div
id={this.props.ui.dom.timeline}
className="timeline"
style={contentHeight}
>
<svg ref={this.svgRef} width={dims.width} style={contentHeight}>
<Clip dims={dims} />
<Axis
dims={dims}
extent={this.getTimeScaleExtent()}
@@ -335,17 +381,30 @@ class Timeline extends React.Component {
/>
<Categories
dims={dims}
getCategoryY={category => this.getY({ category, project: null })}
onDragStart={() => { this.onDragStart() }}
onDrag={() => { this.onDrag() }}
onDragEnd={() => { this.onDragEnd() }}
categories={categories.map(c => c.id)}
getCategoryY={(category) =>
this.getY({ category, project: null })
}
onDragStart={() => {
this.onDragStart();
}}
onDrag={() => {
this.onDrag();
}}
onDragEnd={() => {
this.onDragEnd();
}}
categories={categories.map((c) => c.id)}
features={this.props.features}
fallbackLabel={copy[this.props.app.language].timeline.default_categories_label}
fallbackLabel={
copy[this.props.app.language].timeline
.default_categories_label
}
/>
<Handles
dims={dims}
onMoveTime={(dir) => { this.onMoveTime(dir) }}
onMoveTime={(dir) => {
this.onMoveTime(dir);
}}
/>
<ZoomControls
extent={this.getTimeScaleExtent()}
@@ -356,7 +415,7 @@ class Timeline extends React.Component {
<Markers
dims={dims}
selected={this.props.app.selected}
getEventX={ev => this.getDatetimeX(ev.datetime)}
getEventX={(ev) => this.getDatetimeX(ev.datetime)}
getEventY={this.getY}
categories={categories}
transitionDuration={this.state.transitionDuration}
@@ -372,11 +431,11 @@ class Timeline extends React.Component {
narrative={this.props.app.narrative}
getDatetimeX={this.getDatetimeX}
getY={this.getY}
getHighlights={group => {
if (group === 'None') {
return []
getHighlights={(group) => {
if (group === "None") {
return [];
}
return categories.map(c => c.group === group)
return categories.map((c) => c.group === group);
}}
getCategoryColor={this.props.methods.getCategoryColor}
transitionDuration={this.state.transitionDuration}
@@ -393,48 +452,45 @@ class Timeline extends React.Component {
</div>
</div>
</div>
)
);
}
}
function mapStateToProps (state) {
function mapStateToProps(state) {
return {
dimensions: selectors.selectDimensions(state),
isNarrative: !!state.app.associations.narrative,
domain: {
events: selectors.selectStackedEvents(state),
projects: selectors.selectProjects(state),
categories: (state => {
const allcats = selectors.getCategories(state)
const active = selectors.getActiveCategories(state)
return allcats.filter(c => active.includes(c.id))
categories: ((state) => {
const allcats = selectors.getCategories(state);
const active = selectors.getActiveCategories(state);
return allcats.filter((c) => active.includes(c.id));
})(state),
narratives: state.domain.narratives
narratives: state.domain.narratives,
},
app: {
selected: state.app.selected,
language: state.app.language,
timeline: state.app.timeline,
narrative: state.app.associations.narrative,
coloringSet: state.app.associations.coloringSet
coloringSet: state.app.associations.coloringSet,
},
ui: {
dom: state.ui.dom,
styles: state.ui.style.selectedEvents,
eventRadius: state.ui.eventRadius,
filterColors: state.ui.coloring.colors
filterColors: state.ui.coloring.colors,
},
features: selectors.getFeatures(state)
}
features: selectors.getFeatures(state),
};
}
function mapDispatchToProps (dispatch) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({ setLoading, setNotLoading }, dispatch)
}
actions: bindActionCreators({ setLoading, setNotLoading }, dispatch),
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Timeline)
export default connect(mapStateToProps, mapDispatchToProps)(Timeline);

View File

@@ -1,85 +1,85 @@
import React from 'react'
import * as d3 from 'd3'
import { setD3Locale } from '../common/utilities'
import React from "react";
import * as d3 from "d3";
import { setD3Locale } from "../common/utilities";
const TEXT_HEIGHT = 15
setD3Locale(d3)
const TEXT_HEIGHT = 15;
setD3Locale(d3);
class TimelineAxis extends React.Component {
constructor () {
super()
this.xAxis0Ref = React.createRef()
this.xAxis1Ref = React.createRef()
constructor() {
super();
this.xAxis0Ref = React.createRef();
this.xAxis1Ref = React.createRef();
this.state = {
isInitialized: false
}
isInitialized: false,
};
}
componentDidUpdate () {
let fstFmt, sndFmt
componentDidUpdate() {
let fstFmt, sndFmt;
// 10yrs
if (this.props.extent > 5256000) {
fstFmt = '%Y'
sndFmt = ''
// 1yr
fstFmt = "%Y";
sndFmt = "";
// 1yr
} else if (this.props.extent > 43200) {
sndFmt = '%d %b'
fstFmt = ''
sndFmt = "%d %b";
fstFmt = "";
} else {
sndFmt = '%d %b'
fstFmt = '%H:%M'
sndFmt = "%d %b";
fstFmt = "%H:%M";
}
let { marginTop, contentHeight } = this.props.dims
const { marginTop, contentHeight } = this.props.dims;
if (this.props.scaleX) {
this.x0 =
d3.axisBottom(this.props.scaleX)
.ticks(10)
.tickPadding(0)
.tickSize(contentHeight - TEXT_HEIGHT - marginTop)
.tickFormat(d3.timeFormat(fstFmt))
this.x0 = d3
.axisBottom(this.props.scaleX)
.ticks(10)
.tickPadding(0)
.tickSize(contentHeight - TEXT_HEIGHT - marginTop)
.tickFormat(d3.timeFormat(fstFmt));
this.x1 =
d3.axisBottom(this.props.scaleX)
.ticks(10)
.tickPadding(marginTop)
.tickSize(0)
.tickFormat(d3.timeFormat(sndFmt))
this.x1 = d3
.axisBottom(this.props.scaleX)
.ticks(10)
.tickPadding(marginTop)
.tickSize(0)
.tickFormat(d3.timeFormat(sndFmt));
if (!this.state.isInitialized) this.setState({ isInitialized: true })
if (!this.state.isInitialized) this.setState({ isInitialized: true });
}
if (this.state.isInitialized) {
d3.select(this.xAxis0Ref.current)
.transition()
.duration(this.props.transitionDuration)
.call(this.x0)
.call(this.x0);
d3.select(this.xAxis1Ref.current)
.transition()
.duration(this.props.transitionDuration)
.call(this.x1)
.call(this.x1);
}
}
render () {
render() {
return (
<React.Fragment>
<>
<g
ref={this.xAxis0Ref}
transform={`translate(0, ${this.props.dims.marginTop})`}
clipPath={`url(#clip)`}
className={`axis xAxis`}
clipPath="url(#clip)"
className="axis xAxis"
/>
<g
ref={this.xAxis1Ref}
transform={`translate(0, ${this.props.dims.marginTop})`}
clipPath={`url(#clip)`}
className={`axis xAxis`}
clipPath="url(#clip)"
className="axis xAxis"
/>
</React.Fragment>
)
</>
);
}
}
export default TimelineAxis
export default TimelineAxis;

View File

@@ -1,76 +1,84 @@
import React from 'react'
import * as d3 from 'd3'
import React from "react";
import * as d3 from "d3";
class TimelineCategories extends React.Component {
constructor (props) {
super(props)
this.grabRef = React.createRef()
constructor(props) {
super(props);
this.grabRef = React.createRef();
this.state = {
isInitialized: false
}
isInitialized: false,
};
}
componentDidUpdate () {
componentDidUpdate() {
if (!this.state.isInitialized) {
const drag = d3.drag()
.on('start', this.props.onDragStart)
.on('drag', this.props.onDrag)
.on('end', this.props.onDragEnd)
const drag = d3
.drag()
.on("start", this.props.onDragStart)
.on("drag", this.props.onDrag)
.on("end", this.props.onDragEnd);
d3.select(this.grabRef.current)
.call(drag)
d3.select(this.grabRef.current).call(drag);
this.setState({ isInitialized: true })
this.setState({ isInitialized: true });
}
}
renderCategory (cat, idx) {
const { features, dims } = this.props
const strokeWidth = 1 // dims.trackHeight / (this.props.categories.length + 1)
if (features.GRAPH_NONLOCATED &&
renderCategory(cat, idx) {
const { features, dims } = this.props;
const strokeWidth = 1; // dims.trackHeight / (this.props.categories.length + 1)
if (
features.GRAPH_NONLOCATED &&
features.GRAPH_NONLOCATED.categories &&
features.GRAPH_NONLOCATED.categories.includes(cat)) {
return null
features.GRAPH_NONLOCATED.categories.includes(cat)
) {
return null;
}
return (
<React.Fragment>
<>
<g
class='tick'
class="tick"
style={{ strokeWidth }}
opacity='0.5'
opacity="0.5"
transform={`translate(0,${this.props.getCategoryY(cat)})`}
>
<line x1={dims.marginLeft} x2={dims.width - dims.width_controls} />
</g>
<g class='tick' opacity='1' transform={`translate(0,${this.props.getCategoryY(cat)})`}>
<text x={dims.marginLeft - 5} dy='0.32em'>{cat}</text>
<g
class="tick"
opacity="1"
transform={`translate(0,${this.props.getCategoryY(cat)})`}
>
<text x={dims.marginLeft - 5} dy="0.32em">
{cat}
</text>
</g>
</React.Fragment>
)
</>
);
}
render () {
const { dims, categories, fallbackLabel } = this.props
const categoriesExist = categories && categories.length > 0
render() {
const { dims, categories, fallbackLabel } = this.props;
const categoriesExist = categories && categories.length > 0;
const renderedCategories = categoriesExist
? this.props.categories.map((cat, idx) => this.renderCategory(cat, idx))
: this.renderCategory(fallbackLabel, 0)
: this.renderCategory(fallbackLabel, 0);
return (
<g class='yAxis'>
<g class="yAxis">
{renderedCategories}
<rect
ref={this.grabRef}
class='drag-grabber'
class="drag-grabber"
x={dims.marginLeft}
y={dims.marginTop}
width={dims.width - dims.marginLeft - dims.width_controls}
height={dims.contentHeight}
/>
</g>
)
);
}
}
export default TimelineCategories
export default TimelineCategories;

View File

@@ -1,37 +1,35 @@
import React from 'react'
import React from "react";
import SitesIcon from '../presentational/Icons/Sites'
import CoverIcon from '../presentational/Icons/Cover'
import InfoIcon from '../presentational/Icons/Info'
import SitesIcon from "../presentational/Icons/Sites";
import CoverIcon from "../presentational/Icons/Cover";
import InfoIcon from "../presentational/Icons/Info";
function BottomActions (props) {
function renderToggles () {
function BottomActions(props) {
function renderToggles() {
return [
<div className='bottom-action-block'>
{props.features.USE_SITES ? <SitesIcon
isActive={props.sites.enabled}
onClickHandler={props.sites.toggle}
/> : null}
<div className="bottom-action-block">
{props.features.USE_SITES ? (
<SitesIcon
isActive={props.sites.enabled}
onClickHandler={props.sites.toggle}
/>
) : null}
</div>,
<div className='botttom-action-block'>
<div className="botttom-action-block">
<InfoIcon
isActive={props.info.enabled}
onClickHandler={props.info.toggle}
/>
</div>,
<div className='botttom-action-block'>
{props.features.USE_COVER ? <CoverIcon
onClickHandler={props.cover.toggle}
/> : null}
</div>
]
<div className="botttom-action-block">
{props.features.USE_COVER ? (
<CoverIcon onClickHandler={props.cover.toggle} />
) : null}
</div>,
];
}
return (
<div className='bottom-actions'>
{renderToggles()}
</div>
)
return <div className="bottom-actions">{renderToggles()}</div>;
}
export default BottomActions
export default BottomActions;

View File

@@ -1,39 +1,47 @@
import React from 'react'
import marked from 'marked'
import Checkbox from '../presentational/Checkbox'
import copy from '../../common/data/copy.json'
import React from "react";
import marked from "marked";
import Checkbox from "../presentational/Checkbox";
import copy from "../../common/data/copy.json";
export default ({
categories,
activeCategories,
onCategoryFilter,
language
language,
}) => {
function renderCategoryTree () {
function renderCategoryTree() {
return (
<div>
{categories.map(cat => {
return (<li
key={cat.id.replace(/ /g, '_')}
className={'filter-filter active'}
style={{ marginLeft: '20px' }}
>
<Checkbox
label={cat.id}
isActive={activeCategories.includes(cat.id)}
onClickCheckbox={() => onCategoryFilter(cat.id)}
/>
</li>)
{categories.map((cat) => {
return (
<li
key={cat.id.replace(/ /g, "_")}
className="filter-filter active"
style={{ marginLeft: "20px" }}
>
<Checkbox
label={cat.id}
isActive={activeCategories.includes(cat.id)}
onClickCheckbox={() => onCategoryFilter(cat.id)}
/>
</li>
);
})}
</div>
)
);
}
return (
<div className='react-innertabpanel'>
<div className="react-innertabpanel">
<h2>{copy[language].toolbar.categories}</h2>
<p dangerouslySetInnerHTML={{ __html: marked(copy[language].toolbar.explore_by_category__description) }} />
<p
dangerouslySetInnerHTML={{
__html: marked(
copy[language].toolbar.explore_by_category__description
),
}}
/>
{renderCategoryTree()}
</div>
)
}
);
};

View File

@@ -1,63 +1,69 @@
import React from 'react'
import Checkbox from '../presentational/Checkbox'
import marked from 'marked'
import copy from '../../common/data/copy.json'
import { getFilterIdxFromColorSet } from '../../common/utilities'
import React from "react";
import Checkbox from "../presentational/Checkbox";
import marked from "marked";
import copy from "../../common/data/copy.json";
import { getFilterIdxFromColorSet } from "../../common/utilities";
/** recursively get an array of node keys to toggle */
function getFiltersToToggle (filter, activeFilters) {
const [key, children] = filter
function getFiltersToToggle(filter, activeFilters) {
const [key, children] = filter;
// base case: no children to recurse through
if (children === {}) return [key]
if (children === {}) return [key];
const turningOff = activeFilters.includes(key)
let childKeys = Object.entries(children)
.flatMap(filter => getFiltersToToggle(filter, activeFilters))
.filter(child => activeFilters.includes(child) === turningOff)
const turningOff = activeFilters.includes(key);
const childKeys = Object.entries(children)
.flatMap((filter) => getFiltersToToggle(filter, activeFilters))
.filter((child) => activeFilters.includes(child) === turningOff);
childKeys.push(key)
return childKeys
childKeys.push(key);
return childKeys;
}
function aggregatePaths (filters) {
function insertPath (children = {}, [headOfPath, ...remainder]) {
let childKey = Object.keys(children).find(key => key === headOfPath)
if (!childKey) children[headOfPath] = {}
if (remainder.length > 0) insertPath(children[headOfPath], remainder)
return children
function aggregatePaths(filters) {
function insertPath(children = {}, [headOfPath, ...remainder]) {
const childKey = Object.keys(children).find((key) => key === headOfPath);
if (!childKey) children[headOfPath] = {};
if (remainder.length > 0) insertPath(children[headOfPath], remainder);
return children;
}
const allPaths = []
filters.forEach(filterItem => allPaths.push(filterItem.filter_paths))
const allPaths = [];
filters.forEach((filterItem) => allPaths.push(filterItem.filter_paths));
let aggregatedPaths = allPaths.reduce((children, path) => insertPath(children, path), {})
return aggregatedPaths
const aggregatedPaths = allPaths.reduce(
(children, path) => insertPath(children, path),
{}
);
return aggregatedPaths;
}
function FilterListPanel ({
function FilterListPanel({
filters,
activeFilters,
onSelectFilter,
language,
coloringSet,
filterColors
filterColors,
}) {
function createNodeComponent (filter, depth) {
const [key, children] = filter
const matchingKeys = getFiltersToToggle(filter, activeFilters)
const idxFromColorSet = getFilterIdxFromColorSet(key, coloringSet)
const assignedColor = idxFromColorSet !== -1 && activeFilters.includes(key) ? filterColors[idxFromColorSet] : ''
function createNodeComponent(filter, depth) {
const [key, children] = filter;
const matchingKeys = getFiltersToToggle(filter, activeFilters);
const idxFromColorSet = getFilterIdxFromColorSet(key, coloringSet);
const assignedColor =
idxFromColorSet !== -1 && activeFilters.includes(key)
? filterColors[idxFromColorSet]
: "";
const styles = ({
const styles = {
color: assignedColor,
marginLeft: `${depth * 20}px`
})
marginLeft: `${depth * 20}px`,
};
return (
<li
key={key.replace(/ /g, '_')}
className={'filter-filter'}
key={key.replace(/ /g, "_")}
className="filter-filter"
style={{ ...styles }}
>
<Checkbox
@@ -67,29 +73,37 @@ function FilterListPanel ({
color={assignedColor}
/>
{Object.keys(children).length > 0
? Object.entries(children).map(filter => createNodeComponent(filter, depth + 1))
? Object.entries(children).map((filter) =>
createNodeComponent(filter, depth + 1)
)
: null}
</li>
)
);
}
function renderTree (filters) {
const aggregatedFilterPaths = aggregatePaths(filters)
function renderTree(filters) {
const aggregatedFilterPaths = aggregatePaths(filters);
return (
<div>
{Object.entries(aggregatedFilterPaths).map(filter => createNodeComponent(filter, 1))}
{Object.entries(aggregatedFilterPaths).map((filter) =>
createNodeComponent(filter, 1)
)}
</div>
)
);
}
return (
<div className='react-innertabpanel'>
<div className="react-innertabpanel">
<h2>{copy[language].toolbar.filters}</h2>
<p dangerouslySetInnerHTML={{ __html: marked(copy[language].toolbar.explore_by_filter__description) }} />
<p
dangerouslySetInnerHTML={{
__html: marked(copy[language].toolbar.explore_by_filter__description),
}}
/>
{renderTree(filters)}
</div>
)
);
}
export default FilterListPanel
export default FilterListPanel;

View File

@@ -1,105 +1,122 @@
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as actions from '../../actions'
import * as selectors from '../../selectors'
import React from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import * as actions from "../../actions";
import * as selectors from "../../selectors";
import { Tabs, TabPanel } from 'react-tabs'
import FilterListPanel from './FilterListPanel'
import CategoriesListPanel from './CategoriesListPanel'
import BottomActions from './BottomActions'
import copy from '../../common/data/copy.json'
import { trimAndEllipse, getImmediateFilterParent, getFilterSiblings, getFilterParents } from '../../common/utilities.js'
import { Tabs, TabPanel } from "react-tabs";
import FilterListPanel from "./FilterListPanel";
import CategoriesListPanel from "./CategoriesListPanel";
import BottomActions from "./BottomActions";
import copy from "../../common/data/copy.json";
import {
trimAndEllipse,
getImmediateFilterParent,
getFilterSiblings,
getFilterParents,
} from "../../common/utilities.js";
class Toolbar extends React.Component {
constructor (props) {
super(props)
this.onSelectFilter = this.onSelectFilter.bind(this)
this.state = { _selected: -1 }
constructor(props) {
super(props);
this.onSelectFilter = this.onSelectFilter.bind(this);
this.state = { _selected: -1 };
}
selectTab (selected) {
const _selected = (this.state._selected === selected) ? -1 : selected
this.setState({ _selected })
selectTab(selected) {
const _selected = this.state._selected === selected ? -1 : selected;
this.setState({ _selected });
}
onSelectFilter (key, matchingKeys) {
const { filters, activeFilters, coloringSet, maxNumOfColors } = this.props
onSelectFilter(key, matchingKeys) {
const { filters, activeFilters, coloringSet, maxNumOfColors } = this.props;
const parent = getImmediateFilterParent(filters, key)
const isTurningOff = activeFilters.includes(key)
const parent = getImmediateFilterParent(filters, key);
const isTurningOff = activeFilters.includes(key);
if (!isTurningOff) {
const flattenedColoringSet = coloringSet.flatMap(f => f)
const newColoringSet = matchingKeys.filter(k => flattenedColoringSet.indexOf(k) === -1)
const flattenedColoringSet = coloringSet.flatMap((f) => f);
const newColoringSet = matchingKeys.filter(
(k) => flattenedColoringSet.indexOf(k) === -1
);
const updatedColoringSet = [...coloringSet, newColoringSet]
const updatedColoringSet = [...coloringSet, newColoringSet];
if (updatedColoringSet.length <= maxNumOfColors) {
this.props.actions.updateColoringSet(updatedColoringSet)
this.props.actions.updateColoringSet(updatedColoringSet);
}
} else {
const newColoringSets = coloringSet.map(set => (
set.filter(s => {
return !matchingKeys.includes(s)
const newColoringSets = coloringSet.map((set) =>
set.filter((s) => {
return !matchingKeys.includes(s);
})
))
this.props.actions.updateColoringSet(newColoringSets.filter(item => item.length !== 0))
);
this.props.actions.updateColoringSet(
newColoringSets.filter((item) => item.length !== 0)
);
}
if (isTurningOff) {
if (parent && activeFilters.includes(parent)) {
const siblings = getFilterSiblings(filters, parent, key)
let siblingsOff = true
for (let sibling of siblings) {
const siblings = getFilterSiblings(filters, parent, key);
let siblingsOff = true;
for (const sibling of siblings) {
if (activeFilters.includes(sibling)) {
siblingsOff = false
break
siblingsOff = false;
break;
}
}
if (siblingsOff) {
const grandparentsOn = getFilterParents(filters, key).filter(filt => activeFilters.includes(filt))
matchingKeys = matchingKeys.concat(grandparentsOn)
const grandparentsOn = getFilterParents(filters, key).filter((filt) =>
activeFilters.includes(filt)
);
matchingKeys = matchingKeys.concat(grandparentsOn);
}
}
}
this.props.methods.onSelectFilter(matchingKeys)
this.props.methods.onSelectFilter(matchingKeys);
}
renderClosePanel () {
renderClosePanel() {
return (
<div className='panel-header' onClick={() => this.selectTab(-1)}>
<div className='caret' />
<div className="panel-header" onClick={() => this.selectTab(-1)}>
<div className="caret" />
</div>
)
);
}
goToNarrative (narrative) {
this.selectTab(-1) // set all unselected within this component
this.props.methods.onSelectNarrative(narrative)
goToNarrative(narrative) {
this.selectTab(-1); // set all unselected within this component
this.props.methods.onSelectNarrative(narrative);
}
renderToolbarNarrativePanel () {
renderToolbarNarrativePanel() {
return (
<TabPanel>
<h2>{copy[this.props.language].toolbar.narrative_panel_title}</h2>
<p>{copy[this.props.language].toolbar.narrative_summary}</p>
{this.props.narratives.map((narr) => {
return (
<div className='panel-action action'>
<button onClick={() => { this.goToNarrative(narr) }}>
<div className="panel-action action">
<button
onClick={() => {
this.goToNarrative(narr);
}}
>
<p>{narr.id}</p>
<p><small>{trimAndEllipse(narr.desc, 120)}</small></p>
<p>
<small>{trimAndEllipse(narr.desc, 120)}</small>
</p>
</button>
</div>
)
);
})}
</TabPanel>
)
);
}
renderToolbarCategoriesPanel () {
renderToolbarCategoriesPanel() {
if (this.props.features.CATEGORIES_AS_FILTERS) {
return (
<TabPanel>
@@ -110,11 +127,11 @@ class Toolbar extends React.Component {
language={this.props.language}
/>
</TabPanel>
)
);
}
}
renderToolbarFilterPanel () {
renderToolbarFilterPanel() {
return (
<TabPanel>
<FilterListPanel
@@ -126,106 +143,135 @@ class Toolbar extends React.Component {
filterColors={this.props.filterColors}
/>
</TabPanel>
)
);
}
renderToolbarTab (_selected, label, iconKey) {
const isActive = (this.state._selected === _selected)
let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab'
renderToolbarTab(_selected, label, iconKey) {
const isActive = this.state._selected === _selected;
const classes = isActive ? "toolbar-tab active" : "toolbar-tab";
return (
<div className={classes} onClick={() => { this.selectTab(_selected) }}>
<i className='material-icons'>{iconKey}</i>
<div className='tab-caption'>{label}</div>
<div
className={classes}
onClick={() => {
this.selectTab(_selected);
}}
>
<i className="material-icons">{iconKey}</i>
<div className="tab-caption">{label}</div>
</div>
)
);
}
renderToolbarPanels () {
const { features, narratives } = this.props
let classes = (this.state._selected >= 0) ? 'toolbar-panels' : 'toolbar-panels folded'
renderToolbarPanels() {
const { features, narratives } = this.props;
const classes =
this.state._selected >= 0 ? "toolbar-panels" : "toolbar-panels folded";
return (
<div className={classes}>
{this.renderClosePanel()}
<Tabs selectedIndex={this.state._selected}>
{narratives && narratives.length !== 0 ? this.renderToolbarNarrativePanel() : null}
{features.CATEGORIES_AS_FILTERS ? this.renderToolbarCategoriesPanel() : null}
{narratives && narratives.length !== 0
? this.renderToolbarNarrativePanel()
: null}
{features.CATEGORIES_AS_FILTERS
? this.renderToolbarCategoriesPanel()
: null}
{features.USE_ASSOCIATIONS ? this.renderToolbarFilterPanel() : null}
</Tabs>
</div>
)
);
}
renderToolbarNavs () {
renderToolbarNavs() {
if (this.props.narratives) {
return this.props.narratives.map((nar, idx) => {
const isActive = (idx === this.state._selected)
const isActive = idx === this.state._selected;
let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab'
const classes = isActive ? "toolbar-tab active" : "toolbar-tab";
return (
<div className={classes} onClick={() => { this.selectTab(idx) }}>
<div className='tab-caption'>{nar.label}</div>
<div
className={classes}
onClick={() => {
this.selectTab(idx);
}}
>
<div className="tab-caption">{nar.label}</div>
</div>
)
})
);
});
}
return null
return null;
}
renderToolbarTabs () {
const { features, narratives } = this.props
const narrativesExist = narratives && narratives.length !== 0
let title = copy[this.props.language].toolbar.title
if (process.env.display_title) title = process.env.display_title
const narrativesLabel = copy[this.props.language].toolbar.narratives_label
const filtersLabel = copy[this.props.language].toolbar.filters_label
const categoriesLabel = 'Categories' // TODO:
renderToolbarTabs() {
const { features, narratives } = this.props;
const narrativesExist = narratives && narratives.length !== 0;
let title = copy[this.props.language].toolbar.title;
if (process.env.display_title) title = process.env.display_title;
const narrativesLabel = copy[this.props.language].toolbar.narratives_label;
const filtersLabel = copy[this.props.language].toolbar.filters_label;
const categoriesLabel = "Categories"; // TODO:
const narrativesIdx = 0
const categoriesIdx = narrativesExist ? 1 : 0
const filtersIdx = (narrativesExist && features.CATEGORIES_AS_FILTERS) ? 2 : (
narrativesExist || features.CATEGORIES_AS_FILTERS ? 1 : 0
)
const narrativesIdx = 0;
const categoriesIdx = narrativesExist ? 1 : 0;
const filtersIdx =
narrativesExist && features.CATEGORIES_AS_FILTERS
? 2
: narrativesExist || features.CATEGORIES_AS_FILTERS
? 1
: 0;
return (
<div className='toolbar'>
<div className='toolbar-header'onClick={this.props.methods.onTitle}><p>{title}</p></div>
<div className='toolbar-tabs'>
{narrativesExist ? this.renderToolbarTab(narrativesIdx, narrativesLabel, 'timeline') : null}
{features.CATEGORIES_AS_FILTERS ? this.renderToolbarTab(categoriesIdx, categoriesLabel, 'widgets') : null}
{features.USE_ASSOCIATIONS ? this.renderToolbarTab(filtersIdx, filtersLabel, 'filter_list') : null}
<div className="toolbar">
<div className="toolbar-header" onClick={this.props.methods.onTitle}>
<p>{title}</p>
</div>
<div className="toolbar-tabs">
{narrativesExist
? this.renderToolbarTab(narrativesIdx, narrativesLabel, "timeline")
: null}
{features.CATEGORIES_AS_FILTERS
? this.renderToolbarTab(categoriesIdx, categoriesLabel, "widgets")
: null}
{features.USE_ASSOCIATIONS
? this.renderToolbarTab(filtersIdx, filtersLabel, "filter_list")
: null}
</div>
<BottomActions
info={{
enabled: this.props.infoShowing,
toggle: this.props.actions.toggleInfoPopup
toggle: this.props.actions.toggleInfoPopup,
}}
sites={{
enabled: this.props.sitesShowing,
toggle: this.props.actions.toggleSites
toggle: this.props.actions.toggleSites,
}}
cover={{
toggle: this.props.actions.toggleCover
toggle: this.props.actions.toggleCover,
}}
features={this.props.features}
/>
</div>
)
);
}
render () {
const { isNarrative } = this.props
render() {
const { isNarrative } = this.props;
return (
<div id='toolbar-wrapper' className={`toolbar-wrapper ${(isNarrative) ? 'narrative-mode' : ''}`}>
<div
id="toolbar-wrapper"
className={`toolbar-wrapper ${isNarrative ? "narrative-mode" : ""}`}
>
{this.renderToolbarTabs()}
{this.renderToolbarPanels()}
</div>
)
);
}
}
function mapStateToProps (state) {
function mapStateToProps(state) {
return {
filters: selectors.getFilters(state),
categories: selectors.getCategories(state),
@@ -240,14 +286,14 @@ function mapStateToProps (state) {
coloringSet: state.app.associations.coloringSet,
maxNumOfColors: state.ui.coloring.maxNumOfColors,
filterColors: state.ui.coloring.colors,
features: selectors.getFeatures(state)
}
features: selectors.getFeatures(state),
};
}
function mapDispatchToProps (dispatch) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch)
}
actions: bindActionCreators(actions, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Toolbar)
export default connect(mapStateToProps, mapDispatchToProps)(Toolbar);

View File

@@ -1,82 +1,80 @@
import React from 'react'
import Checkbox from '../presentational/Checkbox'
import React from "react";
import Checkbox from "../presentational/Checkbox";
function SelectFilter (props) {
function isActive () {
function SelectFilter(props) {
function isActive() {
if (props.isCategory) {
return props.categoryFilters.includes(props.filter.id)
return props.categoryFilters.includes(props.filter.id);
}
return props.filterFilters.includes(props.filter.id)
return props.filterFilters.includes(props.filter.id);
}
function onClickFilter () {
function onClickFilter() {
if (isActive()) {
props.filter({
filters: props.filterFilters.filter(element => element !== props.filter.id)
})
filters: props.filterFilters.filter(
(element) => element !== props.filter.id
),
});
} else {
props.filter({
filters: props.filterFilters.concat(props.filter.id)
})
filters: props.filterFilters.concat(props.filter.id),
});
}
}
function onClickCategory () {
function onClickCategory() {
if (isActive()) {
props.filter({
categories: props.categoryFilters.filter(element => element !== props.filter.id)
})
categories: props.categoryFilters.filter(
(element) => element !== props.filter.id
),
});
} else {
props.filter({
categories: props.categoryFilters.concat(props.filter.id)
})
categories: props.categoryFilters.concat(props.filter.id),
});
}
}
function renderFilter () {
const filter = props.filter
let classes = (isActive()) ? 'filter-filter active' : 'filter-filter'
let label = `${filter.name} ( ${filter.mentions} )`
function renderFilter() {
const filter = props.filter;
const classes = isActive() ? "filter-filter active" : "filter-filter";
let label = `${filter.name} ( ${filter.mentions} )`;
if (props.isShowTree) {
label = `${filter.group} > ${filter.subgroup} > ${filter.name} ( ${filter.mentions} )`
label = `${filter.group} > ${filter.subgroup} > ${filter.name} ( ${filter.mentions} )`;
}
return (
<li
key={props.filter.id}
className={classes}
>
<li key={props.filter.id} className={classes}>
<Checkbox
isActive={isActive()}
label={label}
onClickCheckbox={() => onClickFilter()}
/>
</li>
)
);
}
function renderCategory () {
const category = props.categories[props.filter.id]
let classes = (isActive()) ? 'filter-filter active' : 'filter-filter'
function renderCategory() {
const category = props.categories[props.filter.id];
const classes = isActive() ? "filter-filter active" : "filter-filter";
if (category) {
return (
<li
key={props.filter.id}
className={classes}
>
<li key={props.filter.id} className={classes}>
<Checkbox
isActive={isActive()}
label={`${category.name} ( ${category.counts} )`}
onClickCheckbox={onClickCategory}
/>
</li>
)
);
}
return (<div />)
return <div />;
}
if (props.isCategory) return (renderCategory())
return (renderFilter())
if (props.isCategory) return renderCategory();
return renderFilter();
}
export default SelectFilter
export default SelectFilter;

View File

@@ -1,17 +1,15 @@
import React from 'react'
import React from "react";
const CardCaret = ({ isOpen, toggle }) => {
let classes = (isOpen)
? 'arrow-down'
: 'arrow-down folded'
const classes = isOpen ? "arrow-down" : "arrow-down folded";
return (
<div className='card-toggle' onClick={toggle}>
<div className="card-toggle" onClick={toggle}>
<p>
<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 { capitalize } from '../../../common/utilities.js'
import { capitalize } from "../../../common/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>
{capitalize(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,14 +1,14 @@
import React from 'react'
import marked from 'marked'
import React from "react";
import marked from "marked";
const CardCustomField = ({ field, value }) => (
<div className='card-cell'>
<div className="card-cell">
<p>
<i className='material-icons left'>{field.icon}</i>
<b>{field.title ? `${field.title}: ` : '- '}</b>
{field.kind === 'text' ? value : marked(`[${value}](${field.value})`)}
<i className="material-icons left">{field.icon}</i>
<b>{field.title ? `${field.title}: ` : "- "}</b>
{field.kind === "text" ? value : marked(`[${value}](${field.value})`)}
</p>
</div>
)
);
export default CardCustomField
export default CardCustomField;

View File

@@ -1,28 +1,28 @@
import React from 'react'
import React from "react";
import copy from '../../../common/data/copy.json'
import copy from "../../../common/data/copy.json";
const CardLocation = ({ language, location, isPrecise }) => {
if (location !== '') {
if (location !== "") {
return (
<div className='card-cell location'>
<div className="card-cell location">
<p>
<i className='material-icons left'>location_on</i>
{`${location}${(isPrecise) ? '' : ' (Approximated)'}`}
<i className="material-icons left">location_on</i>
{`${location}${isPrecise ? "" : " (Approximated)"}`}
</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,79 +1,79 @@
import React from 'react'
import Img from 'react-image'
import Spinner from '../Spinner'
import { typeForPath } from '../../../common/utilities'
import React from "react";
import Img from "react-image";
import Spinner from "../Spinner";
import { typeForPath } from "../../../common/utilities";
const CardSource = ({ source, isLoading, onClickHandler }) => {
function renderIconText (type) {
function renderIconText(type) {
switch (type) {
case 'Eyewitness Testimony':
return 'visibility'
case 'Government Data':
return 'public'
case 'Satellite Imagery':
return 'satellite'
case 'Second-Hand Testimony':
return 'visibility_off'
case 'Video':
return 'videocam'
case 'Photo':
return 'photo'
case 'Photobook':
return 'photo_album'
case 'Document':
return 'picture_as_pdf'
case "Eyewitness Testimony":
return "visibility";
case "Government Data":
return "public";
case "Satellite Imagery":
return "satellite";
case "Second-Hand Testimony":
return "visibility_off";
case "Video":
return "videocam";
case "Photo":
return "photo";
case "Photobook":
return "photo_album";
case "Document":
return "picture_as_pdf";
default:
return 'help'
return "help";
}
}
if (!source) {
return (
<div className='card-source'>
<div className="card-source">
<div>Error: this source was not found</div>
</div>
)
);
}
const isImgUrl = /\.(jpg|png)$/
let thumbnail = source.thumbnail
const isImgUrl = /\.(jpg|png)$/;
let thumbnail = source.thumbnail;
if (!thumbnail || thumbnail === '' || !thumbnail.match(isImgUrl)) {
if (!thumbnail || thumbnail === "" || !thumbnail.match(isImgUrl)) {
// default to first image in paths, null if no images
const imgs = source.paths.filter(p => p.match(isImgUrl))
thumbnail = imgs.length > 0 ? imgs[0] : null
const imgs = source.paths.filter((p) => p.match(isImgUrl));
thumbnail = imgs.length > 0 ? imgs[0] : null;
}
if (source.type === '' && source.paths.length >= 1) {
source.type = typeForPath(source.paths[0])
if (source.type === "" && source.paths.length >= 1) {
source.type = typeForPath(source.paths[0]);
}
const fallbackIcon = (
<i className='material-icons source-icon'>
{renderIconText(source.type)}
</i>
)
<i className="material-icons source-icon">{renderIconText(source.type)}</i>
);
return (
<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.title ? source.title : source.id}</p>
</div>
)}
<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.title ? source.title : source.id}</p>
</div>
)}
</div>
)
}
);
};
export default CardSource
export default CardSource;

View File

@@ -1,18 +1,18 @@
import React from 'react'
import React from "react";
import copy from '../../../common/data/copy.json'
import copy from "../../../common/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,32 +1,33 @@
import React from 'react'
import React from "react";
import copy from '../../../common/data/copy.json'
import { isNotNullNorUndefined } from '../../../common/utilities'
import copy from "../../../common/data/copy.json";
import { isNotNullNorUndefined } from "../../../common/utilities";
const CardTime = ({ timelabel, language, precision }) => {
// const daytimeLang = copy[language].cardstack.timestamp
// const estimatedLang = copy[language].cardstack.estimated
const unknownLang = copy[language].cardstack.unknown_time
const unknownLang = copy[language].cardstack.unknown_time;
if (isNotNullNorUndefined(timelabel)) {
return (
<div className='card-cell timestamp'>
<div className="card-cell timestamp">
<p>
<i className='material-icons left'>today</i>
{timelabel}{(precision && precision !== '') ? ` - ${precision}` : ''}
<i className="material-icons left">today</i>
{timelabel}
{precision && precision !== "" ? ` - ${precision}` : ""}
</p>
</div>
)
);
} else {
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>
{unknownLang}
</p>
</div>
)
);
}
}
};
export default CardTime
export default CardTime;

View File

@@ -1,17 +1,17 @@
import React from 'react'
import React from "react";
export default ({ label, isActive, onClickCheckbox, color }) => {
const styles = ({
background: isActive ? color : 'none',
border: `1px solid ${color}`
})
const styles = {
background: isActive ? color : "none",
border: `1px solid ${color}`,
};
return (
<div className={(isActive) ? 'item active' : 'item'}>
<div className={isActive ? "item active" : "item"}>
<span style={{ color: color }}>{label}</span>
<button onClick={onClickCheckbox}>
<div className='checkbox' style={styles} />
<div className="checkbox" style={styles} />
</button>
</div>
)
}
);
};

View File

@@ -1,20 +1,43 @@
import React from 'react'
import React from "react";
const CoeventIcon = ({ isEnabled, toggleMapViews }) => {
return (
<button
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' />
<button 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>
</button>
)
}
);
};
export default CoeventIcon
export default CoeventIcon;

View File

@@ -1,21 +1,16 @@
import React from 'react'
import React from "react";
const CoverIcon = ({ 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'
classes = "action-button disabled";
}
return (
<button
className={classes}
onClick={onClickHandler}
>
<i class='material-icons'>
home
</i>
<button className={classes} onClick={onClickHandler}>
<i class="material-icons">home</i>
</button>
)
}
);
};
export default CoverIcon
export default CoverIcon;

View File

@@ -1,21 +1,16 @@
import React from 'react'
import React from "react";
const CoverIcon = ({ 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'
classes = "action-button disabled";
}
return (
<button
className={classes}
onClick={onClickHandler}
>
<i class='material-icons'>
info
</i>
<button className={classes} onClick={onClickHandler}>
<i class="material-icons">info</i>
</button>
)
}
);
};
export default CoverIcon
export default CoverIcon;

View File

@@ -1,11 +1,23 @@
import React from 'react'
import React from "react";
export default ({ isActive, isDisabled, onClickHandler }) => {
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>
)
}
);
};

View File

@@ -1,16 +1,21 @@
import React from 'react'
import React from "react";
const RouteIcon = ({ isEnabled, toggleMapViews }) => {
return (
<button
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 ' />
<button 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>
</button>
)
}
);
};
export default RouteIcon
export default RouteIcon;

View File

@@ -1,21 +1,16 @@
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'
classes = "action-button disabled";
}
return (
<button
className={classes}
onClick={onClickHandler}
>
<i class='material-icons'>
location_on
</i>
<button className={classes} onClick={onClickHandler}>
<i class="material-icons">location_on</i>
</button>
)
}
);
};
export default SitesIcon
export default SitesIcon;

View File

@@ -63,7 +63,7 @@ function Cluster({
return (
<g
className={"cluster-event"}
className="cluster-event"
transform={`translate(${x}, ${y})`}
onClick={(e) => onClick({ id: clusterId, latitude, longitude })}
onMouseEnter={() => setHovered(true)}
@@ -75,7 +75,7 @@ function Cluster({
styles={{
...styles,
}}
className={"cluster-event-marker"}
className="cluster-event-marker"
/>
{hovered ? renderHover(cluster) : null}
</g>

View File

@@ -1,35 +1,41 @@
import React from 'react'
import { getCoordinatesForPercent } from '../../../common/utilities'
import React from "react";
import { getCoordinatesForPercent } from "../../../common/utilities";
function ColoredMarkers ({ radius, colorPercentMap, styles, className }) {
let cumulativeAngleSweep = 0
const colors = Object.keys(colorPercentMap)
function ColoredMarkers({ radius, colorPercentMap, styles, className }) {
let cumulativeAngleSweep = 0;
const colors = Object.keys(colorPercentMap);
return (
<React.Fragment>
<>
{colors.map((color, idx) => {
const colorPercent = colorPercentMap[color]
const colorPercent = colorPercentMap[color];
const [startX, startY] = getCoordinatesForPercent(radius, cumulativeAngleSweep)
const [startX, startY] = getCoordinatesForPercent(
radius,
cumulativeAngleSweep
);
cumulativeAngleSweep += colorPercent
cumulativeAngleSweep += colorPercent;
const [endX, endY] = getCoordinatesForPercent(radius, cumulativeAngleSweep)
const [endX, endY] = getCoordinatesForPercent(
radius,
cumulativeAngleSweep
);
// if the slices are less than 2, take the long arc
const largeArcFlag = (colors.length === 1) || colorPercent > 0.5 ? 1 : 0
const largeArcFlag = colors.length === 1 || colorPercent > 0.5 ? 1 : 0;
// create an array and join it just for code readability
const arc = [
`M ${startX} ${startY}`, // Move
`A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endX} ${endY}`, // Arc
`L 0 0 `, // Line
`L ${startX} ${startY} Z` // Line
].join(' ')
"L 0 0 ", // Line
`L ${startX} ${startY} Z`, // Line
].join(" ");
const extraStyles = ({
const extraStyles = {
...styles,
fill: color
})
fill: color,
};
return (
<path
@@ -38,10 +44,10 @@ function ColoredMarkers ({ radius, colorPercentMap, styles, className }) {
d={arc}
style={extraStyles}
/>
)
);
})}
</React.Fragment>
)
</>
);
}
export default ColoredMarkers
export default ColoredMarkers;

View File

@@ -1,14 +1,30 @@
import React from 'react'
import React from "react";
const MapDefsMarkers = () => (
<defs>
<marker id='arrow' viewBox='0 0 6 6' refX='3' refY='3' markerWidth='6' markerHeight='6' orient='auto'>
<path d='M0,3v-3l6,3l-6,3z' style={{ fill: 'red' }} />
<marker
id="arrow"
viewBox="0 0 6 6"
refX="3"
refY="3"
markerWidth="6"
markerHeight="6"
orient="auto"
>
<path d="M0,3v-3l6,3l-6,3z" style={{ fill: "red" }} />
</marker>
<marker id='arrow-off' viewBox='0 0 6 6' refX='3' refY='3' markerWidth='6' markerHeight='6' orient='auto'>
<path d='M0,3v-3l6,3l-6,3z' style={{ fill: 'black', fillOpacity: 0.2 }} />
<marker
id="arrow-off"
viewBox="0 0 6 6"
refX="3"
refY="3"
markerWidth="6"
markerHeight="6"
orient="auto"
>
<path d="M0,3v-3l6,3l-6,3z" style={{ fill: "black", fillOpacity: 0.2 }} />
</marker>
</defs>
)
);
export default MapDefsMarkers
export default MapDefsMarkers;

View File

@@ -1,10 +1,15 @@
import React from 'react'
import { Portal } from 'react-portal'
import colors from '../../../common/global.js'
import ColoredMarkers from './ColoredMarkers.jsx'
import { calcOpacity, getCoordinatesForPercent, calculateColorPercentages, zipColorsToPercentages } from '../../../common/utilities'
import React from "react";
import { Portal } from "react-portal";
import colors from "../../../common/global.js";
import ColoredMarkers from "./ColoredMarkers.jsx";
import {
calcOpacity,
getCoordinatesForPercent,
calculateColorPercentages,
zipColorsToPercentages,
} from "../../../common/utilities";
function MapEvents ({
function MapEvents({
getCategoryColor,
categories,
projectPoint,
@@ -17,110 +22,120 @@ function MapEvents ({
eventRadius,
coloringSet,
filterColors,
features
features,
}) {
function handleEventSelect (e, location) {
const events = e.shiftKey ? selected.concat(location.events) : location.events
onSelect(events)
function handleEventSelect(e, location) {
const events = e.shiftKey
? selected.concat(location.events)
: location.events;
onSelect(events);
}
function renderBorder () {
function renderBorder() {
return (
<React.Fragment>
{<circle
class='event-hover'
cx='0'
cy='0'
r='10'
<>
<circle
class="event-hover"
cx="0"
cy="0"
r="10"
stroke={colors.primaryHighlight}
fill-opacity='0.0'
/>}
</React.Fragment>
)
fill-opacity="0.0"
/>
</>
);
}
function renderLocationSlicesByAssociation (location) {
const colorPercentages = calculateColorPercentages([location], coloringSet)
function renderLocationSlicesByAssociation(location) {
const colorPercentages = calculateColorPercentages([location], coloringSet);
let styles = ({
const styles = {
stroke: colors.darkBackground,
strokeWidth: 0,
fillOpacity: narrative ? 1 : calcOpacity(location.events.length)
})
fillOpacity: narrative ? 1 : calcOpacity(location.events.length),
};
return (
<ColoredMarkers
radius={eventRadius}
colorPercentMap={zipColorsToPercentages(filterColors, colorPercentages)}
styles={{
...styles
...styles,
}}
className={'location-event-marker'}
className="location-event-marker"
/>
)
);
}
function renderLocationSlicesByCategory (location) {
const locCategory = location.events.length > 0 ? location.events[0].category : 'default'
const customStyles = styleLocation ? styleLocation(location) : null
const extraStyles = customStyles[0]
function renderLocationSlicesByCategory(location) {
const locCategory =
location.events.length > 0 ? location.events[0].category : "default";
const customStyles = styleLocation ? styleLocation(location) : null;
const extraStyles = customStyles[0];
let styles = ({
const styles = {
fill: getCategoryColor(locCategory),
stroke: colors.darkBackground,
strokeWidth: 0,
fillOpacity: narrative ? 1 : calcOpacity(location.events.length),
...extraStyles
})
...extraStyles,
};
const colorSlices = location.events.map(e => e.colour ? e.colour : getCategoryColor(e.category))
const colorSlices = location.events.map((e) =>
e.colour ? e.colour : getCategoryColor(e.category)
);
let cumulativeAngleSweep = 0
let cumulativeAngleSweep = 0;
return (
<React.Fragment>
<>
{colorSlices.map((color, idx) => {
const r = eventRadius
const r = eventRadius;
// Based on the number of events in each location,
// create a slice per event filled with its category color
const [startX, startY] = getCoordinatesForPercent(r, cumulativeAngleSweep)
const [startX, startY] = getCoordinatesForPercent(
r,
cumulativeAngleSweep
);
cumulativeAngleSweep = (idx + 1) / colorSlices.length
cumulativeAngleSweep = (idx + 1) / colorSlices.length;
const [endX, endY] = getCoordinatesForPercent(r, cumulativeAngleSweep)
const [endX, endY] = getCoordinatesForPercent(
r,
cumulativeAngleSweep
);
// if the slices are less than 2, take the long arc
const largeArcFlag = (colorSlices.length === 1) ? 1 : 0
const largeArcFlag = colorSlices.length === 1 ? 1 : 0;
// create an array and join it just for code readability
const arc = [
`M ${startX} ${startY}`, // Move
`A ${r} ${r} 0 ${largeArcFlag} 1 ${endX} ${endY}`, // Arc
`L 0 0 `, // Line
`L ${startX} ${startY} Z` // Line
].join(' ')
"L 0 0 ", // Line
`L ${startX} ${startY} Z`, // Line
].join(" ");
const extraStyles = ({
const extraStyles = {
...styles,
fill: color
})
fill: color,
};
return (
<path
class='location-event-marker'
class="location-event-marker"
id={`arc_${idx}`}
d={arc}
style={extraStyles}
/>
)
);
})}
</React.Fragment>
)
</>
);
}
function renderLocation (location) {
function renderLocation(location) {
/**
{
events: [...],
@@ -129,53 +144,55 @@ function MapEvents ({
longitude: '32.2'
}
*/
if (!location.latitude || !location.longitude) return null
const { x, y } = projectPoint([location.latitude, location.longitude])
if (!location.latitude || !location.longitude) return null;
const { x, y } = projectPoint([location.latitude, location.longitude]);
// in narrative mode, only render events in narrative
// TODO: move this to a selector
if (narrative) {
const { steps } = narrative
const onlyIfInNarrative = e => steps.map(s => s.id).includes(e.id)
const eventsInNarrative = location.events.filter(onlyIfInNarrative)
const { steps } = narrative;
const onlyIfInNarrative = (e) => steps.map((s) => s.id).includes(e.id);
const eventsInNarrative = location.events.filter(onlyIfInNarrative);
if (eventsInNarrative.length <= 0) {
return null
return null;
}
}
const customStyles = styleLocation ? styleLocation(location) : null
const extraRender = () => (
<React.Fragment>
{customStyles[1]}
</React.Fragment>
)
const customStyles = styleLocation ? styleLocation(location) : null;
const extraRender = () => <>{customStyles[1]}</>;
const isSelected = selected.reduce((acc, event) => {
return acc || (event.latitude === location.latitude && event.longitude === location.longitude)
}, false)
return (
acc ||
(event.latitude === location.latitude &&
event.longitude === location.longitude)
);
}, false);
return (
<g
className={`location-event ${narrative ? 'no-hover' : ''}`}
className={`location-event ${narrative ? "no-hover" : ""}`}
transform={`translate(${x}, ${y})`}
onClick={(e) => handleEventSelect(e, location)}
>
{features.COLOR_BY_ASSOCIATION ? renderLocationSlicesByAssociation(location) : null}
{features.COLOR_BY_CATEGORY ? renderLocationSlicesByCategory(location) : null}
{features.COLOR_BY_ASSOCIATION
? renderLocationSlicesByAssociation(location)
: null}
{features.COLOR_BY_CATEGORY
? renderLocationSlicesByCategory(location)
: null}
{extraRender ? extraRender() : null}
{isSelected ? null : renderBorder()}
</g>
)
);
}
return (
<Portal node={svg}>
<g className='event-locations'>
{locations.map(renderLocation)}
</g>
<g className="event-locations">{locations.map(renderLocation)}</g>
</Portal>
)
);
}
export default MapEvents
export default MapEvents;

View File

@@ -1,206 +1,208 @@
import React from 'react'
import { Portal } from 'react-portal'
import React from "react";
import { Portal } from "react-portal";
// import { concatStatic } from 'rxjs/operator/concat'
// import { single } from 'rxjs/operator/single'
const defaultStyles = {
strokeOpacity: 1,
strokeWidth: 0,
strokeDasharray: 'none',
stroke: 'none'
}
strokeDasharray: "none",
stroke: "none",
};
function MapNarratives ({
function MapNarratives({
styles,
onSelectNarrative,
svg,
narrative,
narratives,
projectPoint,
features
features,
}) {
function getNarrativeStyle (narrativeId) {
const styleName = (narrativeId && narrativeId in styles)
? narrativeId
: 'default'
return styles[styleName]
function getNarrativeStyle(narrativeId) {
const styleName =
narrativeId && narrativeId in styles ? narrativeId : "default";
return styles[styleName];
}
const narrativesExist = narratives && narratives.length !== 0
const narrativesExist = narratives && narratives.length !== 0;
function hasNoLocation (step) {
return (step.latitude === '' || step.longitude === '')
function hasNoLocation(step) {
return step.latitude === "" || step.longitude === "";
}
function _renderNarrativeStepArrow (p1, p2, styles) {
const distance = Math.sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y))
const theta = Math.atan2(p2.y - p1.y, p2.x - p1.x) // Angle of narrative step line
const alpha = Math.atan2(1, 2) // Angle of arrow overture
const edge = 10 // Arrow edge length
const offset = (distance < 24) ? distance / 2 : 24
function _renderNarrativeStepArrow(p1, p2, styles) {
const distance = Math.sqrt(
(p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y)
);
const theta = Math.atan2(p2.y - p1.y, p2.x - p1.x); // Angle of narrative step line
const alpha = Math.atan2(1, 2); // Angle of arrow overture
const edge = 10; // Arrow edge length
const offset = distance < 24 ? distance / 2 : 24;
// Arrow corners
const coord0 = {
x: p2.x - offset * Math.cos(theta),
y: p2.y - offset * Math.sin(theta)
}
y: p2.y - offset * Math.sin(theta),
};
const coord1 = {
x: coord0.x - edge * Math.cos(-theta - alpha),
y: coord0.y + edge * Math.sin(-theta - alpha)
}
y: coord0.y + edge * Math.sin(-theta - alpha),
};
const coord2 = {
x: coord0.x - edge * Math.cos(-theta + alpha),
y: coord0.y + edge * Math.sin(-theta + alpha)
}
y: coord0.y + edge * Math.sin(-theta + alpha),
};
return (<path
className='narrative-step-arrow'
d={`
return (
<path
className="narrative-step-arrow"
d={`
M ${coord0.x} ${coord0.y}
L ${coord1.x} ${coord1.y}
L ${coord2.x} ${coord2.y} Z
`}
style={{
...styles,
fillOpacity: styles.strokeOpacity,
fill: styles.stroke
}}
/>)
style={{
...styles,
fillOpacity: styles.strokeOpacity,
fill: styles.stroke,
}}
/>
);
}
function _renderNarrativeStep (p1, p2, styles) {
const { stroke, strokeWidth, strokeDasharray, strokeOpacity } = styles
function _renderNarrativeStep(p1, p2, styles) {
const { stroke, strokeWidth, strokeDasharray, strokeOpacity } = styles;
return (
<g>
<line
className='narrative-step'
className="narrative-step"
x1={p1.x}
x2={p2.x}
y1={p1.y}
y2={p2.y}
markerStart='none'
onClick={n => onSelectNarrative(n)}
markerStart="none"
onClick={(n) => onSelectNarrative(n)}
style={{
strokeWidth,
strokeDasharray,
strokeOpacity,
stroke
stroke,
}}
/>
{(stroke !== 'none')
? _renderNarrativeStepArrow(p1, p2, styles)
: ''
}
{stroke !== "none" ? _renderNarrativeStepArrow(p1, p2, styles) : ""}
</g>
)
);
}
function renderBetweenSteps (step1, step2, extraStyles) {
function renderBetweenSteps(step1, step2, extraStyles) {
// don't draw if one of the steps has no location, or not in narrative
if (hasNoLocation(step1) || hasNoLocation(step2)) {
return null
return null;
}
// don't draw if something else is up
if (!step1 || !step2) {
return null
return null;
}
const p1 = projectPoint([step1.latitude, step1.longitude])
const p2 = projectPoint([step2.latitude, step2.longitude])
const p1 = projectPoint([step1.latitude, step1.longitude]);
const p2 = projectPoint([step2.latitude, step2.longitude]);
return _renderNarrativeStep(p1, p2, {
...defaultStyles,
...(extraStyles || {})
})
...(extraStyles || {}),
});
}
function renderFullNarrative (n) {
function renderFullNarrative(n) {
if (n === null || n.id !== narrative.id) {
return null
return null;
}
const arrows = []
const arrows = [];
for (let idx = 0; idx < n.steps.length - 1; idx += 1) {
const step1 = n.steps[idx]
const step2 = n.steps[idx + 1]
arrows.push(renderBetweenSteps(step1, step2, getNarrativeStyle(n.id)))
const step1 = n.steps[idx];
const step2 = n.steps[idx + 1];
arrows.push(renderBetweenSteps(step1, step2, getNarrativeStyle(n.id)));
}
return arrows
return arrows;
}
function renderBetweenMarked (n) {
function renderBetweenMarked(n) {
// this function should only be called if features.NARRATIVE_STEP_STYLES
// is true, and thus there is a 'stepStyles' attributes in events
if (n === null || n.id !== narrative.id) {
return null
return null;
}
const arrows = []
const arrows = [];
let lastMarked = null
let lastMarked = null;
if (narrativesExist) {
for (let idx = 0; idx < n.steps.length; idx += 1) {
const step = n.steps[idx]
const step = n.steps[idx];
if (lastMarked) {
arrows.push(renderBetweenSteps(
lastMarked,
step,
n.withLines ? { strokeWidth: '1px', stroke: step.colour } : {})
)
arrows.push(
renderBetweenSteps(
lastMarked,
step,
n.withLines ? { strokeWidth: "1px", stroke: step.colour } : {}
)
);
}
lastMarked = step
lastMarked = step;
}
} else {
for (let idx = 0; idx < n.steps.length; idx += 1) {
const step = n.steps[idx]
const _idx = step.narratives.indexOf(n.id)
const stepStyle = step.narrative___stepStyles[_idx]
const step = n.steps[idx];
const _idx = step.narratives.indexOf(n.id);
const stepStyle = step.narrative___stepStyles[_idx];
if (stepStyle !== 'None') {
if (stepStyle !== "None") {
if (lastMarked) {
arrows.push(renderBetweenSteps(lastMarked, step, styles.stepStyles[stepStyle]))
arrows.push(
renderBetweenSteps(lastMarked, step, styles.stepStyles[stepStyle])
);
}
lastMarked = step
lastMarked = step;
}
}
}
return arrows
return arrows;
}
function renderNarrative (n) {
const narrativeId = `narrative-${n.id.replace(/ /g, '_')}`
function renderNarrative(n) {
const narrativeId = `narrative-${n.id.replace(/ /g, "_")}`;
const body = narrativesExist
? renderBetweenMarked(n)
: (features.NARRATIVE_STEP_STYLES
? renderBetweenMarked(n)
: renderFullNarrative(n))
: features.NARRATIVE_STEP_STYLES
? renderBetweenMarked(n)
: renderFullNarrative(n);
return (
<g id={narrativeId} className='narrative'>
<g id={narrativeId} className="narrative">
{body}
</g>
)
);
}
// don't render in explore mode
if (narrative === null) {
return null
return null;
}
return (
<Portal node={svg}>
<g className='narratives'>
{narratives.map(renderNarrative)}
</g>
<g className="narratives">{narratives.map(renderNarrative)}</g>
</Portal>
)
);
}
export default MapNarratives
export default MapNarratives;

View File

@@ -1,38 +1,38 @@
import React from 'react'
import { Portal } from 'react-portal'
import colors from '../../../common/global.js'
import React from "react";
import { Portal } from "react-portal";
import colors from "../../../common/global.js";
class MapSelectedEvents extends React.Component {
renderMarker (marker) {
const { x, y } = this.props.projectPoint([marker.latitude, marker.longitude])
const styles = this.props.styles
const r = marker.radius ? marker.radius + 5 : 24
renderMarker(marker) {
const { x, y } = this.props.projectPoint([
marker.latitude,
marker.longitude,
]);
const styles = this.props.styles;
const r = marker.radius ? marker.radius + 5 : 24;
return (
<g
className='location-marker'
transform={`translate(${x - r}, ${y})`}
>
<g className="location-marker" transform={`translate(${x - r}, ${y})`}>
<path
className='leaflet-interactive'
className="leaflet-interactive"
stroke={styles ? styles.stroke : colors.primaryHighlight}
stroke-opacity='1'
stroke-width={styles ? styles['stroke-width'] : 2}
stroke-linecap=''
stroke-linejoin='round'
stroke-dasharray={styles ? styles['stroke-dasharray'] : '2,2'}
fill='none'
stroke-opacity="1"
stroke-width={styles ? styles["stroke-width"] : 2}
stroke-linecap=""
stroke-linejoin="round"
stroke-dasharray={styles ? styles["stroke-dasharray"] : "2,2"}
fill="none"
d={`M0,0a${r},${r} 0 1,0 ${r * 2},0 a${r},${r} 0 1,0 -${r * 2},0 `}
/>
</g>
)
);
}
render () {
render() {
return (
<Portal node={this.props.svg}>
{this.props.selected.map(s => this.renderMarker(s))}
{this.props.selected.map((s) => this.renderMarker(s))}
</Portal>
)
);
}
}
export default MapSelectedEvents
export default MapSelectedEvents;

View File

@@ -1,49 +1,47 @@
import React from 'react'
import { Portal } from 'react-portal'
import React from "react";
import { Portal } from "react-portal";
function MapShapes ({ svg, shapes, projectPoint, styles }) {
function renderShape (shape) {
const lineCoords = []
const points = shape.points
.map(projectPoint)
function MapShapes({ svg, shapes, projectPoint, styles }) {
function renderShape(shape) {
const lineCoords = [];
const points = shape.points.map(projectPoint);
points.forEach((p1, idx) => {
if (idx < shape.points.length - 1) {
const p2 = points[idx + 1]
const p2 = points[idx + 1];
lineCoords.push({
x1: p1.x,
y1: p1.y,
x2: p2.x,
y2: p2.y
})
y2: p2.y,
});
}
})
});
return lineCoords.map(coords => {
const shapeStyles = (shape.name in styles)
? styles[shape.name]
: styles.default
return lineCoords.map((coords) => {
const shapeStyles =
shape.name in styles ? styles[shape.name] : styles.default;
return (
<line
id={`${shape.name}_style`}
markerStart='none'
markerStart="none"
{...coords}
style={shapeStyles}
/>
)
})
);
});
}
if (!shapes || !shapes.length) return null
if (!shapes || !shapes.length) return null;
return (
<Portal node={svg}>
<g id={`shapes-layer`} className='narrative'>
<g id="shapes-layer" className="narrative">
{shapes.map(renderShape)}
</g>
</Portal>
)
);
}
export default MapShapes
export default MapShapes;

View File

@@ -1,24 +1,25 @@
import React from 'react'
import React from "react";
function MapSites ({ sites, projectPoint }) {
function renderSite (site) {
const { x, y } = projectPoint([site.latitude, site.longitude])
function MapSites({ sites, projectPoint }) {
function renderSite(site) {
const { x, y } = projectPoint([site.latitude, site.longitude]);
return (<div
className='leaflet-tooltip site-label leaflet-zoom-animated leaflet-tooltip-top'
style={{ opacity: 1, transform: `translate3d(calc(${x}px - 50%), ${y - 25}px, 0px)` }}>
{site.site}
</div>
)
return (
<div
className="leaflet-tooltip site-label leaflet-zoom-animated leaflet-tooltip-top"
style={{
opacity: 1,
transform: `translate3d(calc(${x}px - 50%), ${y - 25}px, 0px)`,
}}
>
{site.site}
</div>
);
}
if (!sites || !sites.length) return null
if (!sites || !sites.length) return null;
return (
<div className='sites-layer'>
{sites.map(renderSite)}
</div>
)
return <div className="sites-layer">{sites.map(renderSite)}</div>;
}
export default MapSites
export default MapSites;

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React from "react";
export default ({ isDisabled, direction, onClickHandler }) => {
return (
@@ -6,11 +6,9 @@ export default ({ isDisabled, direction, onClickHandler }) => {
className={`narrative-adjust ${direction}`}
onClick={!isDisabled ? onClickHandler : null}
>
<i
className={`material-icons ${isDisabled ? 'disabled' : ''}`}
>
<i className={`material-icons ${isDisabled ? "disabled" : ""}`}>
{`chevron_${direction}`}
</i>
</div>
)
}
);
};

View File

@@ -1,17 +1,17 @@
import React from 'react'
import { connect } from 'react-redux'
import { selectActiveNarrative } from '../../../selectors'
import React from "react";
import { connect } from "react-redux";
import { selectActiveNarrative } from "../../../selectors";
function NarrativeCard ({ narrative }) {
function NarrativeCard({ narrative }) {
// no display if no narrative
const { steps, current } = narrative
const { steps, current } = narrative;
if (steps[current]) {
return (
<div className='narrative-info'>
<div className='narrative-info-header'>
<div className='count-container'>
<div className='count'>
<div className="narrative-info">
<div className="narrative-info-header">
<div className="count-container">
<div className="count">
{current + 1}/{steps.length}
</div>
</div>
@@ -22,19 +22,19 @@ function NarrativeCard ({ narrative }) {
{/* <i className='material-icons left'>location_on</i> */}
{/* {_renderActions(current, steps)} */}
<div className='narrative-info-desc'>
<div className="narrative-info-desc">
<p>{narrative.description}</p>
</div>
</div>
)
);
} else {
return null
return null;
}
}
function mapStateToProps (state) {
function mapStateToProps(state) {
return {
narrative: selectActiveNarrative(state)
}
narrative: selectActiveNarrative(state),
};
}
export default connect(mapStateToProps)(NarrativeCard)
export default connect(mapStateToProps)(NarrativeCard);

View File

@@ -1,17 +1,12 @@
import React from 'react'
import React from "react";
export default ({ onClickHandler, closeMsg }) => {
return (
<div
className='narrative-close'
onClick={onClickHandler}
>
<button
className='side-menu-burg is-active'
>
<div className="narrative-close" onClick={onClickHandler}>
<button className="side-menu-burg is-active">
<span />
</button>
<div className='close-text'>{closeMsg}</div>
<div className="close-text">{closeMsg}</div>
</div>
)
}
);
};

View File

@@ -1,32 +1,32 @@
import React from 'react'
import Card from './Card'
import Adjust from './Adjust'
import Close from './Close'
import React from "react";
import Card from "./Card";
import Adjust from "./Adjust";
import Close from "./Close";
export default ({ narrative, methods }) => {
if (!narrative) return null
if (!narrative) return null;
const { current, steps } = narrative
const prevExists = current !== 0
const nextExists = current < steps.length - 1
const { current, steps } = narrative;
const prevExists = current !== 0;
const nextExists = current < steps.length - 1;
return (
<React.Fragment>
<>
<Card narrative={narrative} />
<Adjust
isDisabled={!prevExists}
direction='left'
direction="left"
onClickHandler={methods.onPrev}
/>
<Adjust
isDisabled={!nextExists}
direction='right'
direction="right"
onClickHandler={methods.onNext}
/>
<Close
onClickHandler={() => methods.onSelectNarrative(null)}
closeMsg='-- exit from narrative --'
closeMsg="-- exit from narrative --"
/>
</React.Fragment>
)
}
</>
);
};

View File

@@ -1,16 +1,19 @@
import React from 'react'
import React from "react";
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>
<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>
<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,7 +1,7 @@
import React from 'react'
import marked from 'marked'
import React from "react";
import marked from "marked";
const fontSize = window.innerWidth > 1000 ? 14 : 18
const fontSize = window.innerWidth > 1000 ? 14 : 18;
export default ({
content = [],
@@ -9,18 +9,30 @@ export default ({
isOpen = true,
onClose,
title,
theme = 'light',
theme = "light",
isMobile = false,
children
children,
}) => (
<div>
<div className={`infopopup ${isOpen ? '' : 'hidden'} ${theme === 'dark' ? 'dark' : 'light'} ${isMobile ? 'mobile' : ''}`} style={{ ...styles, fontSize }}>
<div className='legend-header'>
<button onClick={onClose} className='side-menu-burg over-white is-active'><span /></button>
<div
className={`infopopup ${isOpen ? "" : "hidden"} ${
theme === "dark" ? "dark" : "light"
} ${isMobile ? "mobile" : ""}`}
style={{ ...styles, fontSize }}
>
<div className="legend-header">
<button
onClick={onClose}
className="side-menu-burg over-white is-active"
>
<span />
</button>
<h2>{title}</h2>
</div>
{content.map(t => <div dangerouslySetInnerHTML={{ __html: marked(t) }} />)}
{content.map((t) => (
<div dangerouslySetInnerHTML={{ __html: marked(t) }} />
))}
{children}
</div>
</div>
)
);

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 className='double-bounce' />
<div className={`spinner ${small ? "small" : ""}`}>
<div className="double-bounce-overlay" />
<div className="double-bounce" />
</div>
)
}
);
};
export default Spinner
export default Spinner;

View File

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

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React from "react";
export default ({
highlights,
@@ -9,35 +9,35 @@ export default ({
height,
onSelect,
styleProps,
extraRender
extraRender,
}) => {
if (highlights.length === 0) {
return (
<rect
onClick={onSelect}
className='event'
className="event"
x={x}
y={y}
style={styleProps}
width={width}
height={height}
/>
)
);
}
const sectionHeight = height / highlights.length
const sectionHeight = height / highlights.length;
return (
<React.Fragment>
<>
{highlights.map((h, idx) => (
<rect
onClick={onSelect}
className='event'
className="event"
x={x}
y={y - sectionHeight + (idx * sectionHeight) + (sectionHeight / 2)}
y={y - sectionHeight + idx * sectionHeight + sectionHeight / 2}
style={{ ...styleProps, opacity: h ? 0.3 : 0.1 }}
width={width}
height={sectionHeight}
/>
))}
</React.Fragment>
)
}
</>
);
};

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React from "react";
export default ({
category,
@@ -8,17 +8,17 @@ export default ({
r,
onSelect,
styleProps,
extraRender
extraRender,
}) => {
if (!y) return null
if (!y) return null;
return (
<circle
onClick={onSelect}
className='event'
className="event"
cx={x}
cy={y}
style={styleProps}
r={r}
/>
)
}
);
};

View File

@@ -1,18 +1,10 @@
import React from 'react'
import React from "react";
export default ({
x,
y,
r,
transform,
onSelect,
styleProps,
extraRender
}) => {
export default ({ x, y, r, transform, onSelect, styleProps, extraRender }) => {
return (
<rect
onClick={onSelect}
className='event'
className="event"
x={x}
y={y - r}
style={styleProps}
@@ -20,5 +12,5 @@ export default ({
height={r}
transform={`rotate(45, ${x}, ${y})`}
/>
)
}
);
};

View File

@@ -1,23 +1,17 @@
import React from 'react'
import React from "react";
export default ({
x,
y,
r,
transform,
onSelect,
styleProps,
extraRender
}) => {
const s = r * 2 / 3
export default ({ x, y, r, transform, onSelect, styleProps, extraRender }) => {
const s = (r * 2) / 3;
return (
<polygon
onClick={onSelect}
className='event'
className="event"
x={x}
y={y - r}
style={styleProps}
points={`${x},${y + s} ${x - s},${y - s} ${x + s},${y} ${x - s},${y} ${x + s},${y - s}`}
points={`${x},${y + s} ${x - s},${y - s} ${x + s},${y} ${x - s},${y} ${
x + s
},${y - s}`}
/>
)
}
);
};

View File

@@ -1,74 +1,89 @@
import React from 'react'
import DatetimeBar from './DatetimeBar'
import DatetimeSquare from './DatetimeSquare'
import DatetimeStar from './DatetimeStar'
import Project from './Project'
import ColoredMarkers from '../Map/ColoredMarkers.jsx'
import React from "react";
import DatetimeBar from "./DatetimeBar";
import DatetimeSquare from "./DatetimeSquare";
import DatetimeStar from "./DatetimeStar";
import Project from "./Project";
import ColoredMarkers from "../Map/ColoredMarkers.jsx";
import {
calcOpacity,
getEventCategories,
zipColorsToPercentages,
calculateColorPercentages,
isLatitude,
isLongitude } from '../../../common/utilities'
isLongitude,
} from "../../../common/utilities";
function renderDot (event, styles, props) {
const colorPercentages = calculateColorPercentages([event], props.coloringSet)
function renderDot(event, styles, props) {
const colorPercentages = calculateColorPercentages(
[event],
props.coloringSet
);
return (
<g
className={'timeline-event'}
className="timeline-event"
onClick={props.onSelect}
transform={`translate(${props.x}, ${props.y})`}
>
<ColoredMarkers
radius={props.eventRadius}
colorPercentMap={zipColorsToPercentages(props.filterColors, colorPercentages)}
colorPercentMap={zipColorsToPercentages(
props.filterColors,
colorPercentages
)}
styles={{
...styles
...styles,
}}
className={'event'}
className="event"
/>
</g>
)
);
}
function renderBar (event, styles, props) {
function renderBar(event, styles, props) {
const fillOpacity = props.features.GRAPH_NONLOCATED
? event.projectOffset >= 0 ? styles.opacity : 0.5
: calcOpacity(1)
? event.projectOffset >= 0
? styles.opacity
: 0.5
: calcOpacity(1);
return <DatetimeBar
onSelect={props.onSelect}
category={event.category}
events={[event]}
x={props.x}
y={props.dims.marginTop}
width={props.eventRadius / 4}
height={props.dims.trackHeight}
styleProps={{ ...styles, fillOpacity }}
highlights={props.highlights}
/>
return (
<DatetimeBar
onSelect={props.onSelect}
category={event.category}
events={[event]}
x={props.x}
y={props.dims.marginTop}
width={props.eventRadius / 4}
height={props.dims.trackHeight}
styleProps={{ ...styles, fillOpacity }}
highlights={props.highlights}
/>
);
}
function renderDiamond (event, styles, props) {
return <DatetimeSquare
onSelect={props.onSelect}
x={props.x}
y={props.y}
r={1.8 * props.eventRadius}
styleProps={styles}
/>
function renderDiamond(event, styles, props) {
return (
<DatetimeSquare
onSelect={props.onSelect}
x={props.x}
y={props.y}
r={1.8 * props.eventRadius}
styleProps={styles}
/>
);
}
function renderStar (event, styles, props) {
return <DatetimeStar
onSelect={props.onSelect}
x={props.x}
y={props.y}
r={1.8 * props.eventRadius}
styleProps={{ ...styles, fillRule: 'nonzero' }}
transform='rotate(90)'
/>
function renderStar(event, styles, props) {
return (
<DatetimeStar
onSelect={props.onSelect}
x={props.x}
y={props.y}
r={1.8 * props.eventRadius}
styleProps={{ ...styles, fillRule: "nonzero" }}
transform="rotate(90)"
/>
);
}
const TimelineEvents = ({
@@ -88,95 +103,105 @@ const TimelineEvents = ({
setNotLoading,
eventRadius,
filterColors,
coloringSet
coloringSet,
}) => {
const narIds = narrative ? narrative.steps.map(s => s.id) : []
const narIds = narrative ? narrative.steps.map((s) => s.id) : [];
function renderEvent (acc, event) {
function renderEvent(acc, event) {
if (narrative) {
if (!(narIds.includes(event.id))) {
return null
if (!narIds.includes(event.id)) {
return null;
}
}
const isDot = (isLatitude(event.latitude) && isLongitude(event.longitude)) || (features.GRAPH_NONLOCATED && event.projectOffset !== -1)
const isDot =
(isLatitude(event.latitude) && isLongitude(event.longitude)) ||
(features.GRAPH_NONLOCATED && event.projectOffset !== -1);
let renderShape = isDot ? renderDot : renderBar
let renderShape = isDot ? renderDot : renderBar;
if (event.shape) {
if (event.shape === 'bar') {
renderShape = renderBar
} else if (event.shape === 'diamond') {
renderShape = renderDiamond
} else if (event.shape === 'star') {
renderShape = renderStar
if (event.shape === "bar") {
renderShape = renderBar;
} else if (event.shape === "diamond") {
renderShape = renderDiamond;
} else if (event.shape === "star") {
renderShape = renderStar;
} else {
renderShape = renderDot
renderShape = renderDot;
}
}
// if an event has multiple categories, it should be rendered on each of
// those timelines: so we create as many event 'shadows' as there are
// categories
const evShadows = getEventCategories(event, categories).map(cat => {
const y = getY({ ...event, category: cat.id })
const evShadows = getEventCategories(event, categories).map((cat) => {
const y = getY({ ...event, category: cat.id });
let colour = event.colour ? event.colour : getCategoryColor(cat.id)
const colour = event.colour ? event.colour : getCategoryColor(cat.id);
const styles = {
fill: colour,
fillOpacity: y > 0 ? calcOpacity(1) : 0,
transition: `transform ${transitionDuration / 1000}s ease`
}
transition: `transform ${transitionDuration / 1000}s ease`,
};
return { y, styles }
})
return { y, styles };
});
function getRender (y, styles) {
function getRender(y, styles) {
return renderShape(event, styles, {
x: getDatetimeX(event.datetime),
y,
eventRadius,
onSelect: () => onSelect(event),
dims,
highlights: features.HIGHLIGHT_GROUPS ? getHighlights(event.filters[features.HIGHLIGHT_GROUPS.filterIndexIndicatingGroup]) : [],
highlights: features.HIGHLIGHT_GROUPS
? getHighlights(
event.filters[
features.HIGHLIGHT_GROUPS.filterIndexIndicatingGroup
]
)
: [],
features,
filterColors,
coloringSet
})
coloringSet,
});
}
if (evShadows.length === 0) {
acc.push(getRender(getY(event), { fill: getCategoryColor(null) }))
acc.push(getRender(getY(event), { fill: getCategoryColor(null) }));
} else {
evShadows.forEach(evShadow => {
acc.push(getRender(evShadow.y, evShadow.styles))
})
evShadows.forEach((evShadow) => {
acc.push(getRender(evShadow.y, evShadow.styles));
});
}
return acc
return acc;
}
let renderProjects = () => null
let renderProjects = () => null;
if (features.GRAPH_NONLOCATED) {
renderProjects = function () {
return <React.Fragment>
{Object.values(projects).map(project => <Project
{...project}
eventRadius={eventRadius}
onClick={() => console.log(project)}
getX={getDatetimeX}
dims={dims}
colour={getCategoryColor(project.category)}
/>)}
</React.Fragment>
}
return (
<>
{Object.values(projects).map((project) => (
<Project
{...project}
eventRadius={eventRadius}
onClick={() => console.log(project)}
getX={getDatetimeX}
dims={dims}
colour={getCategoryColor(project.category)}
/>
))}
</>
);
};
}
return (
<g
clipPath={'url(#clip)'}
>
<g clipPath="url(#clip)">
{renderProjects()}
{events.reduce(renderEvent, [])}
</g>
)
}
);
};
export default TimelineEvents
export default TimelineEvents;

View File

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

View File

@@ -1,20 +1,24 @@
import React from 'react'
import { makeNiceDate } from '../../../common/utilities'
import React from "react";
import { makeNiceDate } from "../../../common/utilities";
const TimelineHeader = ({ title, from, to, onClick, hideInfo }) => {
const d0 = from && makeNiceDate(from)
const d1 = to && makeNiceDate(to)
const d0 = from && makeNiceDate(from);
const d1 = to && makeNiceDate(to);
return (
<div className='timeline-header'>
<div className='timeline-toggle' onClick={() => onClick()}>
<p><i className='arrow-down' /></p>
<div className="timeline-header">
<div className="timeline-toggle" onClick={() => onClick()}>
<p>
<i className="arrow-down" />
</p>
</div>
<div className={`timeline-info ${hideInfo ? 'hidden' : ''}`}>
<div className={`timeline-info ${hideInfo ? "hidden" : ""}`}>
<p>{title}</p>
<p>{d0} - {d1}</p>
<p>
{d0} - {d1}
</p>
</div>
</div>
)
}
);
};
export default TimelineHeader
export default TimelineHeader;

View File

@@ -1,39 +1,35 @@
import React from 'react'
import React from "react";
const TimelineLabels = ({ dims, timelabels }) => {
return (
<g>
<line
class='axisBoundaries'
class="axisBoundaries"
x1={dims.marginLeft}
x2={dims.marginLeft}
y1='10'
y2='20'
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'
y1="10"
y2="20"
/>
<text
class='timeLabel0 timeLabel'
x='5'
y='15'
>
<text class="timeLabel0 timeLabel" x="5" y="15">
{timelabels[0]}
</text>
<text
class='timelabelF timeLabel'
class="timelabelF timeLabel"
x={dims.width - dims.width_controls - 5}
y='135'
style={{ textAnchor: 'end' }}
y="135"
style={{ textAnchor: "end" }}
>
{timelabels[1]}
</text>
</g>
)
}
);
};
export default TimelineLabels
export default TimelineLabels;

View File

@@ -1,6 +1,10 @@
import React from 'react'
import colors from '../../../common/global'
import { getEventCategories, isLatitude, isLongitude } from '../../../common/utilities'
import React from "react";
import colors from "../../../common/global";
import {
getEventCategories,
isLatitude,
isLongitude,
} from "../../../common/utilities";
const TimelineMarkers = ({
styles,
@@ -11,79 +15,83 @@ const TimelineMarkers = ({
transitionDuration,
selected,
dims,
features
features,
}) => {
function renderMarker (acc, event) {
function renderCircle (y) {
return <circle
className='timeline-marker'
cx={0}
cy={0}
stroke={styles ? styles.stroke : colors.primaryHighlight}
stroke-opacity='1'
stroke-width={styles ? styles['stroke-width'] : 1}
stroke-linejoin='round'
stroke-dasharray={styles ? styles['stroke-dasharray'] : '2,2'}
style={{
'transform': `translate(${getEventX(event)}px, ${y}px)`,
'-webkit-transition': `transform ${transitionDuration / 1000}s ease`,
'-moz-transition': 'none',
'opacity': 1
}}
r={eventRadius * 2}
/>
function renderMarker(acc, event) {
function renderCircle(y) {
return (
<circle
className="timeline-marker"
cx={0}
cy={0}
stroke={styles ? styles.stroke : colors.primaryHighlight}
stroke-opacity="1"
stroke-width={styles ? styles["stroke-width"] : 1}
stroke-linejoin="round"
stroke-dasharray={styles ? styles["stroke-dasharray"] : "2,2"}
style={{
transform: `translate(${getEventX(event)}px, ${y}px)`,
"-webkit-transition": `transform ${
transitionDuration / 1000
}s ease`,
"-moz-transition": "none",
opacity: 1,
}}
r={eventRadius * 2}
/>
);
}
function renderBar () {
return <rect
className='timeline-marker'
x={0}
y={dims.marginTop}
width={eventRadius / 1.5}
height={dims.contentHeight - 55}
stroke={styles ? styles.stroke : colors.primaryHighlight}
stroke-opacity='1'
stroke-width={styles ? styles['stroke-width'] : 1}
stroke-dasharray={styles ? styles['stroke-dasharray'] : '2,2'}
style={{
'transform': `translate(${getEventX(event)}px)`,
'opacity': 0.7
}}
/>
function renderBar() {
return (
<rect
className="timeline-marker"
x={0}
y={dims.marginTop}
width={eventRadius / 1.5}
height={dims.contentHeight - 55}
stroke={styles ? styles.stroke : colors.primaryHighlight}
stroke-opacity="1"
stroke-width={styles ? styles["stroke-width"] : 1}
stroke-dasharray={styles ? styles["stroke-dasharray"] : "2,2"}
style={{
transform: `translate(${getEventX(event)}px)`,
opacity: 0.7,
}}
/>
);
}
const isDot = (isLatitude(event.latitude) && isLongitude(event.longitude)) || (features.GRAPH_NONLOCATED && event.projectOffset !== -1)
const evShadows = getEventCategories(event, categories).map(cat => getEventY({ ...event, category: cat.id }))
const isDot =
(isLatitude(event.latitude) && isLongitude(event.longitude)) ||
(features.GRAPH_NONLOCATED && event.projectOffset !== -1);
const evShadows = getEventCategories(event, categories).map((cat) =>
getEventY({ ...event, category: cat.id })
);
function renderMarkerForEvent (y) {
function renderMarkerForEvent(y) {
switch (event.shape) {
case 'circle':
case 'diamond':
case 'star':
acc.push(renderCircle(y))
break
case 'bar':
acc.push(renderBar(y))
break
case "circle":
case "diamond":
case "star":
acc.push(renderCircle(y));
break;
case "bar":
acc.push(renderBar(y));
break;
default:
return isDot ? acc.push(renderCircle(y)) : acc.push(renderBar(y))
return isDot ? acc.push(renderCircle(y)) : acc.push(renderBar(y));
}
}
if (evShadows.length > 0) {
evShadows.forEach(renderMarkerForEvent)
evShadows.forEach(renderMarkerForEvent);
} else {
renderMarkerForEvent(getEventY(event))
renderMarkerForEvent(getEventY(event));
}
return acc
return acc;
}
return (
<g
clipPath={'url(#clip)'}
>
{selected.reduce(renderMarker, [])}
</g>
)
}
return <g clipPath="url(#clip)">{selected.reduce(renderMarker, [])}</g>;
};
export default TimelineMarkers
export default TimelineMarkers;

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React from "react";
export default ({
offset,
@@ -10,17 +10,19 @@ export default ({
dims,
colour,
eventRadius,
onClick
onClick,
}) => {
const length = getX(end) - getX(start)
if (offset === undefined) return null
return <rect
onClick={onClick}
className='project'
x={getX(start)}
y={dims.marginTop + offset}
width={length}
style={{ fill: colour, fillOpacity: 0.2 }}
height={2 * eventRadius}
/>
}
const length = getX(end) - getX(start);
if (offset === undefined) return null;
return (
<rect
onClick={onClick}
className="project"
x={getX(start)}
y={dims.marginTop + offset}
width={length}
style={{ fill: colour, fillOpacity: 0.2 }}
height={2 * eventRadius}
/>
);
};

View File

@@ -1,45 +1,47 @@
import React from 'react'
import React from "react";
const DEFAULT_ZOOM_LEVELS = [
{ label: '20 years', duration: 10512000 },
{ label: '2 years', duration: 1051200 },
{ label: '3 months', duration: 129600 },
{ label: '3 days', duration: 4320 },
{ label: '12 hours', duration: 720 },
{ label: '1 hour', duration: 60 }
]
{ label: "20 years", duration: 10512000 },
{ label: "2 years", duration: 1051200 },
{ label: "3 months", duration: 129600 },
{ label: "3 days", duration: 4320 },
{ label: "12 hours", duration: 720 },
{ label: "1 hour", duration: 60 },
];
function zoomIsActive (duration, extent, max) {
function zoomIsActive(duration, extent, max) {
if (duration >= max && extent >= max) {
return true
return true;
}
return duration === extent
return duration === extent;
}
const TimelineZoomControls = ({ extent, zoomLevels, dims, onApplyZoom }) => {
function renderZoom (zoom, idx) {
const max = zoomLevels.reduce((acc, vl) => acc.duration < vl.duration ? vl : acc)
const isActive = zoomIsActive(zoom.duration, extent, max.duration)
function renderZoom(zoom, idx) {
const max = zoomLevels.reduce((acc, vl) =>
acc.duration < vl.duration ? vl : acc
);
const isActive = zoomIsActive(zoom.duration, extent, max.duration);
return (
<text
className={`zoom-level-button ${isActive ? 'active' : ''}`}
x='60'
y={(idx * 15) + 20}
className={`zoom-level-button ${isActive ? "active" : ""}`}
x="60"
y={idx * 15 + 20}
onClick={() => onApplyZoom(zoom)}
>
{zoom.label}
</text>
)
);
}
if (zoomLevels.length === 0) {
zoomLevels = DEFAULT_ZOOM_LEVELS
zoomLevels = DEFAULT_ZOOM_LEVELS;
}
return (
<g transform={`translate(${dims.width - dims.width_controls}, 0)`}>
{zoomLevels.map((z, idx) => renderZoom(z, idx))}
</g>
)
}
);
};
export default TimelineZoomControls
export default TimelineZoomControls;