mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-13 05:48:36 +03:00
Using prettier for linting
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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} />
|
||||
));
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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" />;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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})`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user