mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-11 21:08:36 +03:00
Merge pull request #88 from forensic-architecture/source-overlay-refine
Source overlay refine
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
"marked": "^0.6.0",
|
||||
"normalizr": "^3.2.3",
|
||||
"object-hash": "^1.3.0",
|
||||
"ramda": "^0.26.1",
|
||||
"react": "^16.6.3",
|
||||
"react-dom": "^16.6.3",
|
||||
"react-image": "^1.5.1",
|
||||
|
||||
@@ -14,7 +14,7 @@ import InfoPopUp from './InfoPopup.jsx'
|
||||
import Timeline from './Timeline.jsx'
|
||||
import Notification from './Notification.jsx'
|
||||
|
||||
import { parseDate } from '../js/utilities'
|
||||
import { parseDate, injectSource } from '../js/utilities'
|
||||
|
||||
class Dashboard extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -60,8 +60,8 @@ class Dashboard extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
getCategoryColor(category='other') {
|
||||
return this.props.ui.style.categories[category] || this.props.ui.style.categories['other']
|
||||
getCategoryColor(category) {
|
||||
return this.props.ui.style.categories[category] || this.props.ui.style.categories['default']
|
||||
}
|
||||
|
||||
getNarrativeLinks(event) {
|
||||
@@ -171,5 +171,6 @@ function mapDispatchToProps(dispatch) {
|
||||
|
||||
export default connect(
|
||||
state => state,
|
||||
// state => injectSource("Youtube - Novodvirske Tank Separatist Patrol Video"),
|
||||
mapDispatchToProps,
|
||||
)(Dashboard)
|
||||
|
||||
@@ -25,7 +25,7 @@ class Md extends React.Component {
|
||||
render() {
|
||||
if (this.state.md && !this.state.error) {
|
||||
return (
|
||||
<div 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>
|
||||
|
||||
@@ -6,27 +6,36 @@ import Spinner from './presentational/Spinner'
|
||||
import NoSource from './presentational/NoSource'
|
||||
// TODO: move render functions into presentational components
|
||||
|
||||
function SourceOverlay ({ source, onCancel }) {
|
||||
function renderError() {
|
||||
class SourceOverlay extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
idx: 0
|
||||
}
|
||||
}
|
||||
|
||||
renderError() {
|
||||
return (
|
||||
<NoSource failedUrls={["NOT ALL SOURCES AVAILABLE IN APPLICATION YET"]} />
|
||||
)
|
||||
}
|
||||
|
||||
function renderImage(path) {
|
||||
renderImage(path) {
|
||||
return (
|
||||
<div className='source-image-container'>
|
||||
<Img
|
||||
className='source-image'
|
||||
src={path}
|
||||
loader={<div style={{ width: '400px', height: '400px' }}><Spinner /></div>}
|
||||
unloader={<NoSource failedUrls={source.paths} />}
|
||||
unloader={<NoSource failedUrls={this.props.source.paths} />}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function renderVideo(path) {
|
||||
renderVideo(path) {
|
||||
// NB: assume only one video
|
||||
return (
|
||||
<div className="media-player">
|
||||
@@ -39,26 +48,26 @@ function SourceOverlay ({ source, onCancel }) {
|
||||
)
|
||||
}
|
||||
|
||||
function renderText(path) {
|
||||
renderText(path) {
|
||||
return (
|
||||
<div className='source-text-container'>
|
||||
<Md
|
||||
path={path}
|
||||
loader={<Spinner />}
|
||||
unloader={renderError()}
|
||||
unloader={() => this.renderError()}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function renderNoSupport(ext) {
|
||||
renderNoSupport(ext) {
|
||||
return (
|
||||
<NoSource failedUrls={[`Application does not support extension: ${ext}`]} />
|
||||
)
|
||||
}
|
||||
|
||||
function toMedia(path) {
|
||||
toMedia(path) {
|
||||
let type;
|
||||
switch (true) {
|
||||
case /\.(png|jpg)$/.test(path):
|
||||
@@ -73,7 +82,7 @@ function SourceOverlay ({ source, onCancel }) {
|
||||
return { type, path }
|
||||
}
|
||||
|
||||
function getTypeCounts(media) {
|
||||
getTypeCounts(media) {
|
||||
let counts = { Image: 0, Video: 0, Text: 0 }
|
||||
media.forEach(m => {
|
||||
counts[m.type] += 1
|
||||
@@ -81,21 +90,21 @@ function SourceOverlay ({ source, onCancel }) {
|
||||
return counts
|
||||
}
|
||||
|
||||
function _renderPath(media) {
|
||||
_renderPath(media) {
|
||||
const { path, type } = media
|
||||
switch (type) {
|
||||
case 'Image':
|
||||
return renderImage(path)
|
||||
return this.renderImage(path)
|
||||
case 'Video':
|
||||
return renderVideo(path)
|
||||
return this.renderVideo(path)
|
||||
case 'Text':
|
||||
return renderText(path)
|
||||
return this.renderText(path)
|
||||
default:
|
||||
return renderNoSupport(path.split('.')[1])
|
||||
return this.renderNoSupport(path.split('.')[1])
|
||||
}
|
||||
}
|
||||
|
||||
function _renderCounts(counts) {
|
||||
_renderCounts(counts) {
|
||||
const strFor = type =>
|
||||
counts[type] > 0 ?
|
||||
`${counts[type]} ${type.toLowerCase()}${counts[type] > 1 ? 's': ''}`
|
||||
@@ -113,52 +122,84 @@ function SourceOverlay ({ source, onCancel }) {
|
||||
)
|
||||
}
|
||||
|
||||
function _renderContent(media) {
|
||||
_renderContent(media) {
|
||||
const el = document.querySelector(`.source-media-gallery`);
|
||||
const shiftW = (!!el) ? el.getBoundingClientRect().width : 0;
|
||||
return (
|
||||
<React.Fragment>
|
||||
{media.map(_renderPath)}
|
||||
</React.Fragment>
|
||||
<div className="source-media-gallery" style={{ transition: 'transform 0.2s ease', transform: `translate(${this.state.idx * -shiftW}px)`}}>
|
||||
{media.map((m) => this._renderPath(m))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof(source) !== 'object') {
|
||||
return renderError()
|
||||
onShiftGallery(shift) {
|
||||
if (this.state.idx === 0 && shift === -1) return;
|
||||
if (this.state.idx - 1 === this.props.source.paths.length && shift === 1) return
|
||||
this.setState({ idx: this.state.idx+shift });
|
||||
}
|
||||
const {id, url, title, paths, date, type, desc} = source
|
||||
const media = paths.map(toMedia)
|
||||
const counts = getTypeCounts(media)
|
||||
|
||||
_renderControls() {
|
||||
if (this.props.source.paths.length > 1) {
|
||||
return (
|
||||
<div className="media-gallery-controls">
|
||||
<div className="back" onClick={() => this.onShiftGallery(-1)}><svg><path d="M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z"></path></svg></div>
|
||||
<div className="next" onClick={() => this.onShiftGallery(1)}><svg><path d="M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z"></path></svg></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="media-gallery-controls"></div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mo-overlay" onClick={onCancel}>
|
||||
<div className="mo-container" onClick={(e) => { e.stopPropagation(); }}>
|
||||
<div className="mo-header">
|
||||
<div className="mo-header-close" onClick={onCancel}>
|
||||
<button className="side-menu-burg is-active"><span></span></button>
|
||||
render () {
|
||||
if (typeof(this.props.source) !== 'object') {
|
||||
return this.renderError()
|
||||
}
|
||||
const {id, url, title, paths, date, type, desc} = this.props.source
|
||||
const media = paths.map(this.toMedia)
|
||||
const counts = this.getTypeCounts(media)
|
||||
|
||||
return (
|
||||
<div className="mo-overlay" onClick={this.props.onCancel}>
|
||||
<div className="mo-container" onClick={(e) => { e.stopPropagation(); }}>
|
||||
<div className="mo-header">
|
||||
<div className="mo-header-close" onClick={this.props.onCancel}>
|
||||
<button className="side-menu-burg is-active"><span></span></button>
|
||||
</div>
|
||||
<div className="mo-header-text">{this.props.source.title}</div>
|
||||
</div>
|
||||
<div className="mo-header-text">{source.title}</div>
|
||||
</div>
|
||||
<div className="mo-media-container">
|
||||
{_renderContent(media)}
|
||||
</div>
|
||||
<div className="mo-meta-container">
|
||||
<div className="mo-box">
|
||||
{title? <p><b>{title}</b></p> : null}
|
||||
<div>{_renderCounts(counts)}</div>
|
||||
<hr />
|
||||
{type ? <h4>Media type</h4> : null}
|
||||
{type ? <p><i className="material-icons left">perm_media</i>{type}</p> : null}
|
||||
{date ? <h4>Date</h4> : null}
|
||||
{date ? <p><i className="material-icons left">today</i>{date}</p>: null}
|
||||
{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}
|
||||
{desc ? <hr /> : null}
|
||||
{desc ? <div>{desc}</div> : null}
|
||||
<div className="mo-media-container">
|
||||
{this._renderContent(media)}
|
||||
{this._renderControls()}
|
||||
</div>
|
||||
<div className="mo-meta-container">
|
||||
<div className="mo-box-title">
|
||||
<p>{`${this.state.idx+1} / ${paths.length}`}</p>
|
||||
{title? <p><b>{title}</b></p> : null}
|
||||
<div>{desc}</div>
|
||||
</div>
|
||||
|
||||
<div className="mo-box">
|
||||
<div>
|
||||
{type ? <h4>Media type</h4> : null}
|
||||
{type ? <p><i className="material-icons left">perm_media</i>{type}</p> : null}
|
||||
</div>
|
||||
<div>
|
||||
{date ? <h4>Date</h4> : 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}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default SourceOverlay
|
||||
|
||||
@@ -20,6 +20,7 @@ class Timeline extends React.Component {
|
||||
super(props);
|
||||
this.styleDatetime = this.styleDatetime.bind(this)
|
||||
this.getDatetimeX = this.getDatetimeX.bind(this)
|
||||
this.onApplyZoom = this.onApplyZoom.bind(this)
|
||||
this.svgRef = React.createRef()
|
||||
this.state = {
|
||||
isFolded: false,
|
||||
@@ -99,6 +100,7 @@ class Timeline extends React.Component {
|
||||
* 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;
|
||||
}
|
||||
@@ -153,7 +155,7 @@ class Timeline extends React.Component {
|
||||
|
||||
this.setState({ timerange: [domain0, domainF] }, () => {
|
||||
this.props.methods.onUpdateTimerange(this.state.timerange);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -284,9 +286,10 @@ class Timeline extends React.Component {
|
||||
onMoveTime={(dir) => { this.onMoveTime(dir) }}
|
||||
/>
|
||||
<TimelineZoomControls
|
||||
extent={this.getTimeScaleExtent()}
|
||||
zoomLevels={this.props.app.zoomLevels}
|
||||
dims={dims}
|
||||
onApplyZoom={(zoom) => { this.onApplyZoom(zoom); }}
|
||||
onApplyZoom={this.onApplyZoom}
|
||||
/>
|
||||
<TimelineLabels
|
||||
dims={dims}
|
||||
|
||||
@@ -20,14 +20,14 @@ class TimelineAxis extends React.Component {
|
||||
.tickPadding(5)
|
||||
.tickSize(this.props.dims.trackHeight)
|
||||
.tickFormat(d3.timeFormat('%d %b'));
|
||||
|
||||
|
||||
this.x1 =
|
||||
d3.axisBottom(this.props.scaleX)
|
||||
.ticks(10)
|
||||
.tickPadding(this.props.dims.margin_top)
|
||||
.tickSize(0)
|
||||
.tickFormat(d3.timeFormat('%H:%M'));
|
||||
|
||||
|
||||
if (!this.state.isInitialized) this.setState({ isInitialized: true });
|
||||
}
|
||||
|
||||
@@ -61,9 +61,9 @@ class TimelineAxis extends React.Component {
|
||||
className={`axis axisHourText`}
|
||||
>
|
||||
</g>
|
||||
</React.Fragment>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TimelineAxis;
|
||||
export default TimelineAxis;
|
||||
|
||||
@@ -59,4 +59,4 @@ class TimelineCategories extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default TimelineCategories;
|
||||
export default TimelineCategories;
|
||||
|
||||
@@ -14,7 +14,7 @@ function ToolbarBottomActions (props) {
|
||||
{/* isEnabled={this.props.viewFilters.routes} */}
|
||||
{/* /> */}
|
||||
<SitesIcon
|
||||
isEnabled={props.sites.enabled}
|
||||
isActive={props.sites.enabled}
|
||||
onClickHandler={props.sites.toggle}
|
||||
/>
|
||||
{/* <CoeventIcon */}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
const SitesIcon = ({ isEnabled, onClickHandler }) => {
|
||||
const classes = (isEnabled) ? 'action-button enabled' : 'action-button disabled';
|
||||
const SitesIcon = ({ isActive, isDisabled, onClickHandler }) => {
|
||||
let classes = (isActive) ? 'action-button enabled' : 'action-button';
|
||||
if (isDisabled) {
|
||||
classes = 'action-button disabled'
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -24,7 +24,9 @@ function NarrativeCard ({ narrative }) {
|
||||
|
||||
{/* <i className='material-icons left'>location_on</i> */}
|
||||
{/* {_renderActions(current, steps)} */}
|
||||
<p className='narrative-info-desc'>{narrative.description}</p>
|
||||
<div className='narrative-info-desc'>
|
||||
<p>{narrative.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -22,23 +22,23 @@ const TimelineLabels = ({ dims, timelabels }) => {
|
||||
y2="20"
|
||||
>
|
||||
</line>
|
||||
<text
|
||||
class="timeLabel0 timeLabel"
|
||||
x="5"
|
||||
y="15"
|
||||
>
|
||||
{formatterWithYear(timelabels[0])}
|
||||
</text>
|
||||
<text
|
||||
class="timelabelF timeLabel"
|
||||
x={dims.width - dims.width_controls - 5}
|
||||
y="15"
|
||||
style={{ textAnchor: 'end' }}
|
||||
>
|
||||
{formatterWithYear(timelabels[1])}
|
||||
</text>
|
||||
{/* <text */}
|
||||
{/* class="timeLabel0 timeLabel" */}
|
||||
{/* x="5" */}
|
||||
{/* y="15" */}
|
||||
{/* > */}
|
||||
{/* {formatterWithYear(timelabels[0])} */}
|
||||
{/* </text> */}
|
||||
{/* <text */}
|
||||
{/* class="timelabelF timeLabel" */}
|
||||
{/* x={dims.width - dims.width_controls - 5} */}
|
||||
{/* y="15" */}
|
||||
{/* style={{ textAnchor: 'end' }} */}
|
||||
{/* > */}
|
||||
{/* {formatterWithYear(timelabels[1])} */}
|
||||
{/* </text> */}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
export default TimelineLabels;
|
||||
export default TimelineLabels;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
const TimelineZoomControls = ({ zoomLevels, dims, onApplyZoom }) => {
|
||||
|
||||
const TimelineZoomControls = ({ extent, zoomLevels, dims, onApplyZoom }) => {
|
||||
function renderZoom(zoom, idx) {
|
||||
const isActive = (zoom.duration === extent)
|
||||
return (
|
||||
<text
|
||||
className={`zoom-level-button ${zoom.active ? 'active' : ''}`}
|
||||
className={`zoom-level-button ${isActive ? 'active' : ''}`}
|
||||
x="60"
|
||||
y={(idx * 15) + 20}
|
||||
onClick={() => onApplyZoom(zoom)}
|
||||
@@ -22,4 +22,4 @@ const TimelineZoomControls = ({ zoomLevels, dims, onApplyZoom }) => {
|
||||
);
|
||||
}
|
||||
|
||||
export default TimelineZoomControls;
|
||||
export default TimelineZoomControls;
|
||||
|
||||
@@ -108,13 +108,15 @@ export function insetSourceFrom(allSources) {
|
||||
* view that source modal by default
|
||||
*/
|
||||
export function injectSource(id) {
|
||||
return state => ({
|
||||
...state,
|
||||
app: {
|
||||
...state.app,
|
||||
source: state.domain.sources[id]
|
||||
return state => {
|
||||
return {
|
||||
...state,
|
||||
app: {
|
||||
...state.app,
|
||||
source: state.domain.sources[id]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function urlFromEnv(ext) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import initial from '../store/initial.js'
|
||||
import { parseDate } from '../js/utilities'
|
||||
|
||||
import {
|
||||
UPDATE_HIGHLIGHTED,
|
||||
@@ -35,49 +36,49 @@ function updateSelected(appState, action) {
|
||||
}
|
||||
|
||||
function updateNarrative(appState, action) {
|
||||
let minTime = appState.filters.timerange[0];
|
||||
let maxTime = appState.filters.timerange[1];
|
||||
let minTime = appState.filters.timerange[0]
|
||||
let maxTime = appState.filters.timerange[1]
|
||||
|
||||
let cornerBound0 = [180, 180];
|
||||
let cornerBound1 = [-180, -180];
|
||||
let cornerBound0 = [180, 180]
|
||||
let cornerBound1 = [-180, -180]
|
||||
|
||||
// Compute narrative time range and map bounds
|
||||
if (!!action.narrative) {
|
||||
minTime = parseDate('2100-01-01T00:00:00');
|
||||
maxTime = parseDate('1900-01-01T00:00:00');
|
||||
minTime = parseDate('2100-01-01T00:00:00')
|
||||
maxTime = parseDate('1900-01-01T00:00:00')
|
||||
|
||||
// Find max and mins coordinates of narrative events
|
||||
action.narrative.steps.forEach(step => {
|
||||
const stepTime = parseDate(step.timestamp);
|
||||
if (stepTime < minTime) minTime = stepTime;
|
||||
if (stepTime > maxTime) maxTime = stepTime;
|
||||
const stepTime = parseDate(step.timestamp)
|
||||
if (stepTime < minTime) minTime = stepTime
|
||||
if (stepTime > maxTime) maxTime = stepTime
|
||||
|
||||
if (!!step.longitude && !!step.latitude) {
|
||||
if (+step.longitude < cornerBound0[1]) cornerBound0[1] = +step.longitude;
|
||||
if (+step.longitude > cornerBound1[1]) cornerBound1[1] = +step.longitude;
|
||||
if (+step.latitude < cornerBound0[0]) cornerBound0[0] = +step.latitude;
|
||||
if (+step.latitude > cornerBound1[0]) cornerBound1[0] = +step.latitude;
|
||||
if (+step.longitude < cornerBound0[1]) cornerBound0[1] = +step.longitude
|
||||
if (+step.longitude > cornerBound1[1]) cornerBound1[1] = +step.longitude
|
||||
if (+step.latitude < cornerBound0[0]) cornerBound0[0] = +step.latitude
|
||||
if (+step.latitude > cornerBound1[0]) cornerBound1[0] = +step.latitude
|
||||
}
|
||||
});
|
||||
})
|
||||
// Adjust bounds to center around first event, while keeping visible all others
|
||||
// Takes first event, finds max ditance with first attempt bounds, and use this max distance
|
||||
// on the other side, both in latitude and longitude
|
||||
const first = action.narrative.steps[0];
|
||||
const first = action.narrative.steps[0]
|
||||
if (!!first.longitude && !!first.latitude) {
|
||||
const firstToLong0 = Math.abs(+first.longitude - cornerBound0[1]);
|
||||
const firstToLong1 = Math.abs(+first.longitude - cornerBound1[1]);
|
||||
const firstToLat0 = Math.abs(+first.latitude - cornerBound0[0]);
|
||||
const firstToLat1 = Math.abs(+first.latitude - cornerBound1[0]);
|
||||
const firstToLong0 = Math.abs(+first.longitude - cornerBound0[1])
|
||||
const firstToLong1 = Math.abs(+first.longitude - cornerBound1[1])
|
||||
const firstToLat0 = Math.abs(+first.latitude - cornerBound0[0])
|
||||
const firstToLat1 = Math.abs(+first.latitude - cornerBound1[0])
|
||||
|
||||
if (firstToLong0 > firstToLong1) cornerBound1[1] = +first.longitude + firstToLong0;
|
||||
if (firstToLong0 < firstToLong1) cornerBound0[1] = +first.longitude - firstToLong1;
|
||||
if (firstToLat0 > firstToLat1) cornerBound1[0] = +first.latitude + firstToLat0;
|
||||
if (firstToLat0 < firstToLat1) cornerBound0[0] = +first.latitude - firstToLat1;
|
||||
if (firstToLong0 > firstToLong1) cornerBound1[1] = +first.longitude + firstToLong0
|
||||
if (firstToLong0 < firstToLong1) cornerBound0[1] = +first.longitude - firstToLong1
|
||||
if (firstToLat0 > firstToLat1) cornerBound1[0] = +first.latitude + firstToLat0
|
||||
if (firstToLat0 < firstToLat1) cornerBound0[0] = +first.latitude - firstToLat1
|
||||
}
|
||||
|
||||
|
||||
// Add some buffer on both sides of the time extent
|
||||
minTime = new Date(minTime.getTime() - Math.abs((maxTime - minTime) / 10));
|
||||
maxTime = new Date(maxTime.getTime() + Math.abs((maxTime - minTime) / 10));
|
||||
minTime = new Date(minTime.getTime() - Math.abs((maxTime - minTime) / 10))
|
||||
maxTime = new Date(maxTime.getTime() + Math.abs((maxTime - minTime) / 10))
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -138,14 +139,14 @@ function updateTagFilters(appState, action) {
|
||||
function updateCategoryFilters(appState, action) {
|
||||
const categoryFilters = appState.filters.categories.slice(0)
|
||||
|
||||
const catFilter = categoryFilters.find(cF => cF.category === action.category.category);
|
||||
const catFilter = categoryFilters.find(cF => cF.category === action.category.category)
|
||||
|
||||
if (!catFilter) {
|
||||
categoryFilters.push(action.category)
|
||||
} else {
|
||||
catFilter.active = (!!action.category.active);
|
||||
catFilter.active = (!!action.category.active)
|
||||
}
|
||||
|
||||
|
||||
|
||||
return Object.assign({}, appState, {
|
||||
filters: Object.assign({}, appState.filters, {
|
||||
|
||||
@@ -3,22 +3,25 @@ import Joi from 'joi';
|
||||
const eventSchema = Joi.object().keys({
|
||||
id: Joi.string().required(),
|
||||
description: Joi.string().allow('').required(),
|
||||
date: Joi.string().required(),
|
||||
time: Joi.string().required(),
|
||||
date: Joi.string().allow(''),
|
||||
time: Joi.string().allow(''),
|
||||
time_precision: Joi.string().allow(''),
|
||||
location: Joi.string().allow('').required(),
|
||||
latitude: Joi.string().allow('').required(),
|
||||
longitude: Joi.string().allow('').required(),
|
||||
location: Joi.string().allow(''),
|
||||
latitude: Joi.string().allow(''),
|
||||
longitude: Joi.string().allow(''),
|
||||
type: Joi.string().allow(''),
|
||||
category: Joi.string().required(),
|
||||
narratives: Joi.array(),
|
||||
sources: Joi.array(),
|
||||
tags: Joi.array().allow(''),
|
||||
comments: Joi.string().allow(''),
|
||||
timestamp: Joi.string().required(),
|
||||
timestamp: Joi.string(),
|
||||
|
||||
// nested
|
||||
narrative___stepStyles: Joi.array(),
|
||||
});
|
||||
})
|
||||
.and('latitude', 'longitude')
|
||||
.and('date', 'time', 'timestamp')
|
||||
.or('timestamp', 'latitude')
|
||||
|
||||
export default eventSchema;
|
||||
export default eventSchema
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
@import 'card';
|
||||
|
||||
$card-width: 370px;
|
||||
$narrative-info-max-height: 170px;
|
||||
$narrative-info-max-height: 200px;
|
||||
$timeline-height: 170px;
|
||||
|
||||
.card-stack {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
max-height: calc(100% - 208px);
|
||||
max-height: calc(100% - 180px);
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
overflow-y: scroll;
|
||||
box-shadow: 0 19px 38px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);
|
||||
z-index: $header;
|
||||
color: white;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
$panel-width: 800px;
|
||||
$panel-width: 1000px;
|
||||
$panel-height: 700px;
|
||||
$vimeo-width: $panel-width - 100;
|
||||
$vimeo-height: $panel-height / 2;
|
||||
|
||||
$padding: 20px;
|
||||
$header-inset: 10px;
|
||||
|
||||
@@ -20,6 +21,7 @@ $header-inset: 10px;
|
||||
}
|
||||
|
||||
.mo-container {
|
||||
background-color: rgba(239, 239, 239, 0.9);
|
||||
// max-width: $panel-width;
|
||||
// min-width: $panel-width;
|
||||
// max-height: $panel-height;
|
||||
@@ -27,43 +29,30 @@ $header-inset: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-height: 80vh;
|
||||
height: 80vh;
|
||||
width: $panel-width;
|
||||
max-width: 90vw;
|
||||
box-shadow: 0 19px 19px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);
|
||||
|
||||
.mo-media-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.mo-header {
|
||||
min-height: 42px;
|
||||
max-height: 42px;
|
||||
margin-bottom: 2px;
|
||||
border-radius: 2px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: black;
|
||||
color: white;
|
||||
|
||||
.mo-header-close {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-left: $header-inset + 8px;
|
||||
.back, .next {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: $darkgrey;
|
||||
color: $offwhite;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 19px 19px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);
|
||||
svg path { fill: $offwhite; }
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.mo-header-text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: $padding;
|
||||
font-family: "Lato", Helvetica, sans-serif;
|
||||
.back {
|
||||
left: 10px;
|
||||
svg path { transform: translate(17px,15px)rotate(-90deg)}
|
||||
}
|
||||
.next {
|
||||
margin-left: calc(100% - 60px);
|
||||
right: 10px;
|
||||
svg path { transform: translate(17px,15px)rotate(90deg)}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,12 +83,17 @@ $header-inset: 10px;
|
||||
}
|
||||
|
||||
.mo-media-container {
|
||||
background-color: rgba(239, 239, 239, 0.9);
|
||||
flex: 1;
|
||||
/*display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;*/
|
||||
display: inline-block;
|
||||
overflow-x: hidden;
|
||||
box-sizing: border-box;
|
||||
min-width: 100%;
|
||||
width: 100%;
|
||||
max-height: 60vh;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
font-family: "Lato", Helvetica, sans-serif;
|
||||
|
||||
.media-player {
|
||||
@@ -112,6 +106,14 @@ $header-inset: 10px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.media-gallery-controls {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: -50%;
|
||||
}
|
||||
|
||||
// NB: topcushion seems to be necessary with certain overflows..
|
||||
&.topcushion {
|
||||
padding-top: 150px;
|
||||
@@ -119,23 +121,24 @@ $header-inset: 10px;
|
||||
}
|
||||
|
||||
.mo-meta-container {
|
||||
background-color: rgba(239, 239, 239, 0.9);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
min-height: 100px;
|
||||
min-width: $panel-width;
|
||||
max-width: $panel-height;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
min-height: 100px;
|
||||
width: 100%;
|
||||
padding: $padding;
|
||||
|
||||
.mo-box-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mo-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
max-width: $panel-width;
|
||||
width: 100%;
|
||||
padding: $padding 0;
|
||||
@@ -148,7 +151,7 @@ $header-inset: 10px;
|
||||
text-transform: uppercase;
|
||||
font-size: $xsmall;
|
||||
color: $darkwhite;
|
||||
font-weight: 100;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
p {
|
||||
@@ -160,7 +163,7 @@ $header-inset: 10px;
|
||||
font-size: $normal;
|
||||
color: $darkwhite;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: $large;
|
||||
@@ -223,19 +226,73 @@ $header-inset: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.source-image-container, .source-text-container {
|
||||
padding: $padding;
|
||||
display: inline-block;
|
||||
align-items: center;
|
||||
.source-media-gallery {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
transition: transform 0.6s ease 0s;
|
||||
width: 100%;
|
||||
// min-width: $panel-width - 30px;
|
||||
// min-height: $panel-height;
|
||||
}
|
||||
|
||||
.source-text-container {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: $lightwhite;
|
||||
box-sizing: border-box;
|
||||
padding: 0 calc(50% - 400px);
|
||||
overflow-y: scroll;
|
||||
font-family: 'Merriweather', Georgia, serif;
|
||||
line-height: 1.5em;
|
||||
|
||||
a {
|
||||
color: $darkgrey;
|
||||
border-bottom: 1px solid $red;
|
||||
&:hover { border-bottom: 1px solid $darkgrey; color: $darkgrey; }
|
||||
}
|
||||
|
||||
.md-container {
|
||||
width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.source-image-container, .media-player {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: calc(100% - 20px);
|
||||
height: 100%;
|
||||
min-width: calc(100% - 20px);
|
||||
margin: 0 10px;
|
||||
background: $lightwhite;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.media-player {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
max-height: 100%;
|
||||
padding: 20px 10%;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.source-image, .source-video {
|
||||
max-width: calc(100% - 20px);
|
||||
max-height: 350px !important;
|
||||
// height: 100%;
|
||||
padding: 10px;
|
||||
max-height: calc(100% - 20px);
|
||||
padding: 0px;
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.media-player {
|
||||
overflow-y: hidden;
|
||||
.video-react .video-react-progress-control {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.video-react .video-react-control {
|
||||
min-height: 100%;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
$narrative-info-width: 370px;
|
||||
$narrative-info-width: 386px;
|
||||
$timeline-height: 170px;
|
||||
|
||||
/*
|
||||
@@ -8,8 +8,8 @@ NARRATIVE INFO
|
||||
position: fixed;
|
||||
top: 30px;
|
||||
left: auto;
|
||||
right: 10px;
|
||||
height: 170px;
|
||||
right: 9px;
|
||||
height: 205px;
|
||||
width: $narrative-info-width;
|
||||
box-sizing: border-box;
|
||||
max-height: calc(100% - 250px);
|
||||
@@ -39,7 +39,8 @@ NARRATIVE INFO
|
||||
}
|
||||
|
||||
.narrative-info-desc {
|
||||
overflow: auto;
|
||||
height: 153px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
p {
|
||||
@@ -117,7 +118,7 @@ NARRATIVE INFO
|
||||
|
||||
&.right {
|
||||
// right: calc(#{$narrative-info-width} + 10px);
|
||||
right: 10px;
|
||||
right: 25px;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
|
||||
@@ -96,6 +96,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
transition: 0.2s ease;
|
||||
border: 1px solid $offwhite;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { mergeDeepLeft } from 'ramda'
|
||||
|
||||
const initial = {
|
||||
/*
|
||||
* The Domain or 'domain' of this state refers to the tree of data
|
||||
@@ -38,8 +40,8 @@ const initial = {
|
||||
},
|
||||
filters: {
|
||||
timerange: [
|
||||
d3.timeParse("%Y-%m-%dT%H:%M:%S")("2013-02-23T12:00:00"),
|
||||
d3.timeParse("%Y-%m-%dT%H:%M:%S")("2016-02-23T12:00:00")
|
||||
new Date(2013, 2, 23, 12),
|
||||
new Date(2016, 2, 23, 12)
|
||||
],
|
||||
mapBounds: null,
|
||||
tags: [],
|
||||
@@ -51,45 +53,18 @@ const initial = {
|
||||
sites: true
|
||||
},
|
||||
},
|
||||
base_uri: 'http://127.0.0.1:8000/', // Modify accordingly on production setup.
|
||||
isMobile: (/Mobi/.test(navigator.userAgent)),
|
||||
language: 'en-US',
|
||||
mapAnchor: process.env.MAP_ANCHOR,
|
||||
zoomLevels: [{
|
||||
label: '3 years',
|
||||
duration: 1576800,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '3 months',
|
||||
duration: 129600,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '3 days',
|
||||
duration: 4320,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '12 hours',
|
||||
duration: 720,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '2 hours',
|
||||
duration: 120,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '30 min',
|
||||
duration: 30,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '10 min',
|
||||
duration: 10,
|
||||
active: false
|
||||
}],
|
||||
mapAnchor: [31.356397, 34.784818],
|
||||
zoomLevels: [
|
||||
{ label: '3 years', duration: 1576800 },
|
||||
{ label: '3 months', duration: 129600 },
|
||||
{ label: '3 days', duration: 4320 },
|
||||
{ label: '12 hours', duration: 720 },
|
||||
{ label: '2 hours', duration: 120 },
|
||||
{ label: '30 min', duration: 30 },
|
||||
{ label: '10 min', duration: 10 }
|
||||
],
|
||||
flags: {
|
||||
isFetchingDomain: false,
|
||||
isFetchingSources: false,
|
||||
@@ -108,30 +83,13 @@ const initial = {
|
||||
ui: {
|
||||
style: {
|
||||
categories: {
|
||||
default: 'yellow',
|
||||
// Add here other categories to differentiate by color, like:
|
||||
alpha: '#00ff00',
|
||||
beta: '#ff0000',
|
||||
other: '#f3de2c'
|
||||
default: '#f3de2c',
|
||||
},
|
||||
narratives: {
|
||||
default: {
|
||||
opacity: 0.9,
|
||||
stroke: 'red',
|
||||
strokeWidth: 3
|
||||
},
|
||||
narrative_1: {
|
||||
opacity: 0.4,
|
||||
stroke: '#f18f01',
|
||||
strokeWidth: 3
|
||||
},
|
||||
// process.env.features.NARRATIVE_STEP_STYLES
|
||||
stepStyles: {
|
||||
Physical: {
|
||||
stroke: 'yellow',
|
||||
strokeWidth: 3,
|
||||
opacity: 0.9,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -143,4 +101,15 @@ const initial = {
|
||||
}
|
||||
};
|
||||
|
||||
export default initial;
|
||||
let appStore;
|
||||
if (process.env.store) {
|
||||
appStore = mergeDeepLeft(process.env.store, initial);
|
||||
} else {
|
||||
appStore = initial
|
||||
}
|
||||
|
||||
// NB: config.js dates get implicitly converted to strings in mergeDeepLeft
|
||||
appStore.app.filters.timerange[0] = new Date(appStore.app.filters.timerange[0])
|
||||
appStore.app.filters.timerange[1] = new Date(appStore.app.filters.timerange[1])
|
||||
|
||||
export default appStore
|
||||
|
||||
@@ -5059,6 +5059,11 @@ quick-lru@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
|
||||
|
||||
ramda@^0.26.1:
|
||||
version "0.26.1"
|
||||
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06"
|
||||
integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==
|
||||
|
||||
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80"
|
||||
|
||||
Reference in New Issue
Block a user