Merge pull request #88 from forensic-architecture/source-overlay-refine

Source overlay refine
This commit is contained in:
Lachlan Kermode
2019-01-17 17:14:39 +00:00
committed by GitHub
21 changed files with 344 additions and 251 deletions

View File

@@ -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",

View File

@@ -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)

View File

@@ -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>

View File

@@ -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

View File

@@ -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}

View File

@@ -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;

View File

@@ -59,4 +59,4 @@ class TimelineCategories extends React.Component {
}
}
export default TimelineCategories;
export default TimelineCategories;

View File

@@ -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 */}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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, {

View File

@@ -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

View File

@@ -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;

View File

@@ -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%;
}

View File

@@ -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 {

View File

@@ -96,6 +96,10 @@
}
}
&:hover {
cursor: pointer;
}
&:hover:not(.disabled) {
transition: 0.2s ease;
border: 1px solid $offwhite;

View File

@@ -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

View File

@@ -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"