mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-11 21:08:36 +03:00
Merge pull request #96 from forensic-architecture/fix/user-testing-bugs
Fix/user testing bugs
This commit is contained in:
@@ -20,19 +20,26 @@ module.exports = {
|
||||
},
|
||||
store: {
|
||||
app: {
|
||||
mapAnchor: [31.356397, 34.784818],
|
||||
filters: {
|
||||
// timerange: [
|
||||
// new Date(2015, 7, 9),
|
||||
// new Date(2015, 10, 6, 23)
|
||||
// ]
|
||||
}
|
||||
map: {
|
||||
anchor: [31.356397, 34.784818]
|
||||
},
|
||||
timeline: {
|
||||
range: [
|
||||
new Date(2014, 7, 9),
|
||||
new Date(2014, 10, 6, 23)
|
||||
],
|
||||
rangeLimits: [
|
||||
new Date(2014, 5, 9),
|
||||
new Date(2018, 1, 6, 23)
|
||||
]
|
||||
} }
|
||||
},
|
||||
ui: {
|
||||
style: {
|
||||
categories: {},
|
||||
shapes: {},
|
||||
narratives: {}
|
||||
narratives: {},
|
||||
selectedEvent: {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ class Card extends React.Component {
|
||||
<CardLocation
|
||||
language={this.props.language}
|
||||
location={this.props.event.location}
|
||||
isPrecise={(this.props.event.type === 'Structure')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -87,11 +88,27 @@ class Card extends React.Component {
|
||||
|
||||
// NB: should be internaionalized.
|
||||
renderTimestamp () {
|
||||
let timelabel = this.makeTimelabel(this.props.event.timestamp)
|
||||
|
||||
let precision = this.props.event.time_display
|
||||
if (precision === '_date_only') {
|
||||
precision = ''
|
||||
timelabel = timelabel.substring(0, 11)
|
||||
} else if (precision === '_approximate_date_only') {
|
||||
precision = ' (Approximate date)'
|
||||
timelabel = timelabel.substring(0, 11)
|
||||
} else if (precision === '_approximate_datetime') {
|
||||
precision = ' (Approximate datetime)'
|
||||
} else {
|
||||
timelabel = timelabel.substring(0, 11)
|
||||
}
|
||||
|
||||
return (
|
||||
<CardTimestamp
|
||||
makeTimelabel={(timestamp) => this.makeTimelabel(timestamp)}
|
||||
makeTimelabel={timelabel}
|
||||
language={this.props.language}
|
||||
timestamp={this.props.event.timestamp}
|
||||
timelabel={timelabel}
|
||||
precision={precision}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -143,9 +160,14 @@ class Card extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isSelected } = this.props
|
||||
const { isSelected, idx } = this.props
|
||||
|
||||
return (
|
||||
<li className={`event-card ${isSelected ? 'selected' : ''}`}>
|
||||
<li
|
||||
className={`event-card ${isSelected ? 'selected' : ''}`}
|
||||
id={`event-card-${idx}`}
|
||||
ref={this.props.innerRef}
|
||||
>
|
||||
{this.renderMain()}
|
||||
{this.state.isOpen ? this.renderExtra() : null}
|
||||
{isSelected ? this.renderCaret() : null}
|
||||
@@ -154,4 +176,5 @@ class Card extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default Card
|
||||
// The ref to each card will be used in CardStack for programmatic scrolling
|
||||
export default React.forwardRef((props, ref) => <Card innerRef={ref} {...props} />)
|
||||
|
||||
@@ -6,13 +6,62 @@ import Card from './Card.jsx'
|
||||
import copy from '../js/data/copy.json'
|
||||
|
||||
class CardStack extends React.Component {
|
||||
constructor () {
|
||||
super()
|
||||
this.refs = {}
|
||||
this.refCardStack = React.createRef()
|
||||
this.refCardStackContent = React.createRef()
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
const isNarrative = !!this.props.narrative
|
||||
|
||||
if (isNarrative) {
|
||||
this.scrollToCard()
|
||||
}
|
||||
}
|
||||
|
||||
scrollToCard () {
|
||||
const duration = 500
|
||||
const element = this.refCardStack.current
|
||||
const cardScroll = this.refs[this.props.narrative.current].current.offsetTop - 20
|
||||
|
||||
let start = element.scrollTop
|
||||
let change = cardScroll - start
|
||||
let currentTime = 0
|
||||
const increment = 20
|
||||
|
||||
// t = current time
|
||||
// b = start value
|
||||
// c = change in value
|
||||
// d = duration
|
||||
Math.easeInOutQuad = function (t, b, c, d) {
|
||||
t /= d / 2
|
||||
if (t < 1) return c / 2 * t * t + b
|
||||
t -= 1
|
||||
return -c / 2 * (t * (t - 2) - 1) + b
|
||||
}
|
||||
|
||||
const animateScroll = function () {
|
||||
currentTime += increment
|
||||
const val = Math.easeInOutQuad(currentTime, start, change, duration)
|
||||
element.scrollTop = val
|
||||
if (currentTime < duration) setTimeout(animateScroll, increment)
|
||||
}
|
||||
animateScroll()
|
||||
}
|
||||
|
||||
renderCards (events, selections) {
|
||||
// if no selections provided, select all
|
||||
if (!selections) { selections = events.map(e => true) }
|
||||
this.refs = []
|
||||
|
||||
return events.map((event, idx) => (
|
||||
<Card
|
||||
return events.map((event, idx) => {
|
||||
const thisRef = React.createRef()
|
||||
this.refs[idx] = thisRef
|
||||
return (<Card
|
||||
event={event}
|
||||
ref={thisRef}
|
||||
sourceError={this.props.sourceError}
|
||||
language={this.props.language}
|
||||
isLoading={this.props.isLoading}
|
||||
@@ -24,8 +73,8 @@ class CardStack extends React.Component {
|
||||
onViewSource={this.props.onViewSource}
|
||||
onHighlight={this.props.onHighlight}
|
||||
onSelect={this.props.onSelect}
|
||||
/>
|
||||
))
|
||||
/>)
|
||||
})
|
||||
}
|
||||
|
||||
renderSelectedCards () {
|
||||
@@ -38,9 +87,10 @@ class CardStack extends React.Component {
|
||||
|
||||
renderNarrativeCards () {
|
||||
const { narrative } = this.props
|
||||
const showing = narrative.steps.slice(narrative.current)
|
||||
const showing = narrative.steps
|
||||
|
||||
const selections = showing
|
||||
.map((_, idx) => (idx === 0))
|
||||
.map((_, idx) => (idx === narrative.current))
|
||||
|
||||
return this.renderCards(showing, selections)
|
||||
}
|
||||
@@ -74,7 +124,9 @@ class CardStack extends React.Component {
|
||||
|
||||
renderNarrativeContent () {
|
||||
return (
|
||||
<div id='card-stack-content' className='card-stack-content'>
|
||||
<div id='card-stack-content' className='card-stack-content'
|
||||
ref={this.refCardStackContent}
|
||||
>
|
||||
<ul>
|
||||
{this.renderNarrativeCards()}
|
||||
</ul>
|
||||
@@ -102,6 +154,7 @@ class CardStack extends React.Component {
|
||||
return (
|
||||
<div
|
||||
id='card-stack'
|
||||
ref={this.refCardStack}
|
||||
className={`card-stack narrative-mode
|
||||
${isCardstack ? '' : ' folded'}`
|
||||
}
|
||||
|
||||
39
src/components/CategoriesListPanel.jsx
Normal file
39
src/components/CategoriesListPanel.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react'
|
||||
import Checkbox from './presentational/Checkbox'
|
||||
import copy from '../js/data/copy.json'
|
||||
|
||||
export default (props) => {
|
||||
function onClickCheckbox (obj, type) {
|
||||
obj.active = !obj.active
|
||||
props.onCategoryFilter(obj)
|
||||
}
|
||||
|
||||
function renderCategoryTree () {
|
||||
return (
|
||||
<div>
|
||||
<h2>{copy[props.language].toolbar.categories}</h2>
|
||||
{props.categories.map(cat => {
|
||||
return (<li
|
||||
key={cat.category.replace(/ /g, '_')}
|
||||
className={'tag-filter active'}
|
||||
style={{ marginLeft: '20px' }}
|
||||
>
|
||||
<Checkbox
|
||||
label={cat.category}
|
||||
isActive={cat.active}
|
||||
onClickCheckbox={() => onClickCheckbox(cat, 'category')}
|
||||
/>
|
||||
</li>)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='react-innertabpanel'>
|
||||
<h2>{copy[props.language].toolbar.explore_by_category__title}</h2>
|
||||
<p>{copy[props.language].toolbar.explore_by_category__description}</p>
|
||||
{renderCategoryTree()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -92,6 +92,7 @@ class Dashboard extends React.Component {
|
||||
|
||||
render () {
|
||||
const { actions, app, domain, ui } = this.props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Toolbar
|
||||
@@ -171,6 +172,5 @@ function mapDispatchToProps (dispatch) {
|
||||
|
||||
export default connect(
|
||||
state => state,
|
||||
// state => injectSource("Youtube - Novodvirske Tank Separatist Patrol Video"),
|
||||
mapDispatchToProps
|
||||
)(Dashboard)
|
||||
|
||||
@@ -192,6 +192,7 @@ class Map extends React.Component {
|
||||
*/
|
||||
styleLocation (location) {
|
||||
const noEvents = location.events.length
|
||||
|
||||
return [
|
||||
null,
|
||||
() => noEvents > 1 ? <text className='location-count' dx='-3' dy='4'>{noEvents}</text> : null
|
||||
@@ -220,6 +221,7 @@ class Map extends React.Component {
|
||||
svg={this.svgRef.current}
|
||||
selected={this.props.app.selected}
|
||||
projectPoint={this.projectPoint}
|
||||
styles={this.props.ui.mapSelectedEvents}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -279,6 +281,7 @@ function mapStateToProps (state) {
|
||||
tiles: state.ui.tiles,
|
||||
dom: state.ui.dom,
|
||||
narratives: state.ui.style.narratives,
|
||||
mapSelectedEvents: state.ui.style.selectedEvents,
|
||||
shapes: state.ui.style.shapes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,16 +9,7 @@ import NoSource from './presentational/NoSource'
|
||||
class SourceOverlay extends React.Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
idx: 0
|
||||
}
|
||||
}
|
||||
|
||||
renderError () {
|
||||
return (
|
||||
<NoSource failedUrls={['NOT ALL SOURCES AVAILABLE IN APPLICATION YET']} />
|
||||
)
|
||||
this.state = { idx: 0 }
|
||||
}
|
||||
|
||||
renderImage (path) {
|
||||
@@ -27,7 +18,7 @@ class SourceOverlay extends React.Component {
|
||||
<Img
|
||||
className='source-image'
|
||||
src={path}
|
||||
loader={<div style={{ width: '400px', height: '400px' }}><Spinner /></div>}
|
||||
loader={<div className='source-image-loader'><Spinner /></div>}
|
||||
unloader={<NoSource failedUrls={this.props.source.paths} />}
|
||||
/>
|
||||
</div>
|
||||
@@ -35,7 +26,6 @@ class SourceOverlay extends React.Component {
|
||||
}
|
||||
|
||||
renderVideo (path) {
|
||||
// NB: assume only one video
|
||||
return (
|
||||
<div className='media-player'>
|
||||
<Player
|
||||
@@ -81,11 +71,13 @@ class SourceOverlay extends React.Component {
|
||||
}
|
||||
|
||||
getTypeCounts (media) {
|
||||
let counts = { Image: 0, Video: 0, Text: 0 }
|
||||
media.forEach(m => {
|
||||
counts[m.type] += 1
|
||||
})
|
||||
return counts
|
||||
return media.reduce(
|
||||
(acc, vl) => {
|
||||
acc[vl.type] += 1
|
||||
return acc
|
||||
},
|
||||
{ Image: 0, Video: 0, Text: 0 }
|
||||
)
|
||||
}
|
||||
|
||||
_renderPath (media) {
|
||||
@@ -122,26 +114,49 @@ class SourceOverlay extends React.Component {
|
||||
|
||||
_renderContent (media) {
|
||||
const el = document.querySelector(`.source-media-gallery`)
|
||||
const shiftW = (el) ? el.getBoundingClientRect().width : 0
|
||||
const shiftW = el ? el.getBoundingClientRect().width : 0
|
||||
return (
|
||||
<div className='source-media-gallery' style={{ transition: 'transform 0.2s ease', transform: `translate(${this.state.idx * -shiftW}px)` }}>
|
||||
<div className='source-media-gallery' style={{ transform: `translate(${this.state.idx * -shiftW}px)` }}>
|
||||
{media.map((m) => this._renderPath(m))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
onShiftGallery (shift) {
|
||||
// no more left
|
||||
if (this.state.idx === 0 && shift === -1) return
|
||||
if (this.state.idx - 1 === this.props.source.paths.length && shift === 1) return
|
||||
// no more right
|
||||
if (this.state.idx === this.props.source.paths.length - 1 && shift === 1) return
|
||||
this.setState({ idx: this.state.idx + shift })
|
||||
}
|
||||
|
||||
_renderControls () {
|
||||
const backArrow = this.state.idx !== 0 ? (
|
||||
<div
|
||||
className='back'
|
||||
onClick={() => this.onShiftGallery(-1)}
|
||||
>
|
||||
<svg>
|
||||
<path d='M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z' />
|
||||
</svg>
|
||||
</div>
|
||||
) : null
|
||||
const forwardArrow = this.state.idx < this.props.source.paths.length - 1 ? (
|
||||
<div
|
||||
className='next'
|
||||
onClick={() => this.onShiftGallery(1)}
|
||||
>
|
||||
<svg>
|
||||
<path d='M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z' />
|
||||
</svg>
|
||||
</div>
|
||||
) : null
|
||||
|
||||
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' /></svg></div>
|
||||
<div className='next' onClick={() => this.onShiftGallery(1)}><svg><path d='M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z' /></svg></div>
|
||||
{backArrow}
|
||||
{forwardArrow}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -159,12 +174,12 @@ class SourceOverlay extends React.Component {
|
||||
|
||||
return (
|
||||
<div className='mo-overlay' onClick={this.props.onCancel}>
|
||||
<div className='mo-container' onClick={(e) => { e.stopPropagation() }}>
|
||||
<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 /></button>
|
||||
</div>
|
||||
<div className='mo-header-text'>{this.props.source.title}</div>
|
||||
<div className='mo-header-text'>{this.props.source.title.substring(0, 200)}</div>
|
||||
</div>
|
||||
<div className='mo-media-container'>
|
||||
{this._renderContent(media)}
|
||||
|
||||
@@ -22,8 +22,7 @@ class TagListPanel extends React.Component {
|
||||
|
||||
onClickCheckbox (obj, type) {
|
||||
obj.active = !obj.active
|
||||
if (type === 'category') this.props.onCategoryFilter(obj)
|
||||
if (type === 'tag') this.props.onTagFilter(obj)
|
||||
this.props.onTagFilter(obj)
|
||||
}
|
||||
|
||||
createNodeComponent (node, depth) {
|
||||
@@ -70,34 +69,11 @@ class TagListPanel extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
renderCategoryTree () {
|
||||
return (
|
||||
<div>
|
||||
<h2>{copy[this.props.language].toolbar.categories}</h2>
|
||||
{this.props.categories.map(cat => {
|
||||
return (<li
|
||||
key={cat.category.replace(/ /g, '_')}
|
||||
className={'tag-filter active'}
|
||||
style={{ marginLeft: '20px' }}
|
||||
>
|
||||
<Checkbox
|
||||
label={cat.category}
|
||||
isActive={cat.active}
|
||||
onClickCheckbox={() => this.onClickCheckbox(cat, 'category')}
|
||||
/>
|
||||
</li>)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='react-innertabpanel'>
|
||||
<h2>{copy[this.props.language].toolbar.explore_by_tag__title}</h2>
|
||||
<p>{copy[this.props.language].toolbar.explore_by_tag__description}</p>
|
||||
{this.renderCategoryTree()}
|
||||
{this.renderTree()}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -172,9 +172,28 @@ class Timeline extends React.Component {
|
||||
const extent = this.getTimeScaleExtent()
|
||||
const newCentralTime = d3.timeMinute.offset(this.state.scaleX.domain()[0], extent / 2)
|
||||
|
||||
let newDomain0 = d3.timeMinute.offset(newCentralTime, -zoom.duration / 2)
|
||||
let newDomainF = d3.timeMinute.offset(newCentralTime, zoom.duration / 2)
|
||||
|
||||
if (this.props.app.timeline.rangeLimits) {
|
||||
// If the store contains absolute time limits,
|
||||
// make sure the zoom doesn't go over them
|
||||
const minDate = parseDate(this.props.app.timeline.rangeLimits[0])
|
||||
const maxDate = parseDate(this.props.app.timeline.rangeLimits[1])
|
||||
|
||||
if (newDomain0 < minDate) {
|
||||
newDomain0 = minDate
|
||||
newDomainF = d3.timeMinute.offset(newDomain0, zoom.duration)
|
||||
}
|
||||
if (newDomainF > maxDate) {
|
||||
newDomainF = maxDate
|
||||
newDomain0 = d3.timeMinute.offset(newDomainF, -zoom.duration)
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ timerange: [
|
||||
d3.timeMinute.offset(newCentralTime, -zoom.duration / 2),
|
||||
d3.timeMinute.offset(newCentralTime, zoom.duration / 2)
|
||||
newDomain0,
|
||||
newDomainF
|
||||
] }, () => {
|
||||
this.props.methods.onUpdateTimerange(this.state.timerange)
|
||||
})
|
||||
@@ -205,8 +224,18 @@ class Timeline extends React.Component {
|
||||
const timeShift = (drag0 - dragNow) / 1000
|
||||
|
||||
const { range } = this.props.app.timeline
|
||||
const newDomain0 = d3.timeSecond.offset(range[0], timeShift)
|
||||
const newDomainF = d3.timeSecond.offset(range[1], timeShift)
|
||||
let newDomain0 = d3.timeSecond.offset(range[0], timeShift)
|
||||
let newDomainF = d3.timeSecond.offset(range[1], timeShift)
|
||||
|
||||
if (this.props.app.timeline.rangeLimits) {
|
||||
// If the store contains absolute time limits,
|
||||
// make sure the zoom doesn't go over them
|
||||
const minDate = parseDate(this.props.app.timeline.rangeLimits[0])
|
||||
const maxDate = parseDate(this.props.app.timeline.rangeLimits[1])
|
||||
|
||||
newDomain0 = (newDomain0 < minDate) ? minDate : newDomain0
|
||||
newDomainF = (newDomainF > maxDate) ? maxDate : newDomainF
|
||||
}
|
||||
|
||||
// Updates components without updating timerange
|
||||
this.onSoftTimeRangeUpdate([newDomain0, newDomainF])
|
||||
@@ -290,15 +319,12 @@ class Timeline extends React.Component {
|
||||
dims={dims}
|
||||
onApplyZoom={this.onApplyZoom}
|
||||
/>
|
||||
{/* <Labels */}
|
||||
{/* dims={dims} */}
|
||||
{/* timelabels={this.state.timerange} */}
|
||||
{/* /> */}
|
||||
<Markers
|
||||
selected={this.props.app.selected}
|
||||
getEventX={this.getDatetimeX}
|
||||
getCategoryY={this.state.scaleY}
|
||||
transitionDuration={this.state.transitionDuration}
|
||||
styles={this.props.ui.styles}
|
||||
/>
|
||||
<Events
|
||||
datetimes={this.props.domain.datetimes}
|
||||
@@ -333,7 +359,8 @@ function mapStateToProps (state) {
|
||||
narrative: state.app.narrative
|
||||
},
|
||||
ui: {
|
||||
dom: state.ui.dom
|
||||
dom: state.ui.dom,
|
||||
styles: state.ui.style.selectedEvents
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as selectors from '../selectors'
|
||||
import { Tabs, TabPanel } from 'react-tabs'
|
||||
import Search from './Search.jsx'
|
||||
import TagListPanel from './TagListPanel.jsx'
|
||||
import CategoriesListPanel from './CategoriesListPanel.jsx'
|
||||
import ToolbarBottomActions from './ToolbarBottomActions.jsx'
|
||||
import copy from '../js/data/copy.json'
|
||||
import { trimAndEllipse } from '../js/utilities.js'
|
||||
@@ -71,6 +72,21 @@ class Toolbar extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
renderToolbarCategoriesPanel () {
|
||||
if (process.env.features.CATEGORIES_AS_TAGS) {
|
||||
return (
|
||||
<TabPanel>
|
||||
<CategoriesListPanel
|
||||
categories={this.props.categories}
|
||||
categoryFilters={this.props.categoryFilters}
|
||||
onCategoryFilter={this.props.methods.onCategoryFilter}
|
||||
language={this.props.language}
|
||||
/>
|
||||
</TabPanel>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
renderToolbarTagPanel () {
|
||||
if (process.env.features.USE_TAGS &&
|
||||
this.props.tags.children) {
|
||||
@@ -78,17 +94,14 @@ class Toolbar extends React.Component {
|
||||
<TabPanel>
|
||||
<TagListPanel
|
||||
tags={this.props.tags}
|
||||
categories={this.props.categories}
|
||||
tagFilters={this.props.tagFilters}
|
||||
categoryFilters={this.props.categoryFilters}
|
||||
onTagFilter={this.props.methods.onTagFilter}
|
||||
onCategoryFilter={this.props.methods.onCategoryFilter}
|
||||
language={this.props.language}
|
||||
/>
|
||||
</TabPanel>
|
||||
)
|
||||
}
|
||||
return ''
|
||||
return null
|
||||
}
|
||||
|
||||
renderToolbarTab (_selected, label, iconKey) {
|
||||
@@ -110,6 +123,7 @@ class Toolbar extends React.Component {
|
||||
{this.renderClosePanel()}
|
||||
<Tabs selectedIndex={this.state._selected}>
|
||||
{this.renderToolbarNarrativePanel()}
|
||||
{this.renderToolbarCategoriesPanel()}
|
||||
{this.renderToolbarTagPanel()}}
|
||||
</Tabs>
|
||||
</div>
|
||||
@@ -130,7 +144,7 @@ class Toolbar extends React.Component {
|
||||
)
|
||||
})
|
||||
}
|
||||
return ''
|
||||
return null
|
||||
}
|
||||
|
||||
renderToolbarTabs () {
|
||||
@@ -138,14 +152,17 @@ class Toolbar extends React.Component {
|
||||
if (process.env.title) title = process.env.title
|
||||
const narrativesLabel = copy[this.props.language].toolbar.narratives_label
|
||||
const tagsLabel = copy[this.props.language].toolbar.tags_label
|
||||
const categoriesLabel = 'Categories' // TODO:
|
||||
const isTags = this.props.tags && this.props.tags.children
|
||||
const isCategories = true
|
||||
|
||||
return (
|
||||
<div className='toolbar'>
|
||||
<div className='toolbar-header'><p>{title}</p></div>
|
||||
<div className='toolbar-tabs'>
|
||||
{this.renderToolbarTab(0, narrativesLabel, 'timeline')}
|
||||
{(isTags) ? this.renderToolbarTab(1, tagsLabel, 'style') : ''}
|
||||
{(isCategories) ? this.renderToolbarTab(1, categoriesLabel, 'widgets') : null}
|
||||
{(isTags) ? this.renderToolbarTab(2, tagsLabel, 'filter_list') : null}
|
||||
</div>
|
||||
<ToolbarBottomActions
|
||||
sites={{
|
||||
|
||||
@@ -3,13 +3,13 @@ import React from 'react'
|
||||
import copy from '../../../js/data/copy.json'
|
||||
import { isNotNullNorUndefined } from '../../../js/utilities'
|
||||
|
||||
const CardLocation = ({ language, location }) => {
|
||||
const CardLocation = ({ language, location, isPrecise }) => {
|
||||
if (isNotNullNorUndefined(location)) {
|
||||
return (
|
||||
<div className='card-cell location'>
|
||||
<p>
|
||||
<i className='material-icons left'>location_on</i>
|
||||
{location}
|
||||
{`${location}${(isPrecise) ? '' : ' (Approximated)'}`}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -3,18 +3,17 @@ import React from 'react'
|
||||
import copy from '../../../js/data/copy.json'
|
||||
import { isNotNullNorUndefined } from '../../../js/utilities'
|
||||
|
||||
const CardTimestamp = ({ makeTimelabel, language, timestamp }) => {
|
||||
const CardTimestamp = ({ timelabel, language, precision }) => {
|
||||
// const daytimeLang = copy[language].cardstack.timestamp
|
||||
// const estimatedLang = copy[language].cardstack.estimated
|
||||
const unknownLang = copy[language].cardstack.unknown_time
|
||||
|
||||
if (isNotNullNorUndefined(timestamp)) {
|
||||
const timelabel = makeTimelabel(timestamp)
|
||||
if (isNotNullNorUndefined(timelabel)) {
|
||||
return (
|
||||
<div className='card-cell timestamp'>
|
||||
<p>
|
||||
<i className='material-icons left'>today</i>
|
||||
{timelabel}
|
||||
{timelabel}{(precision !== '') ? ` - ${precision}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,20 +2,71 @@ import React from 'react'
|
||||
import { Portal } from 'react-portal'
|
||||
|
||||
function MapEvents ({ getCategoryColor, categories, projectPoint, styleLocation, narrative, onSelect, svg, locations }) {
|
||||
// function getLocationEventsDistribution (location) {
|
||||
// const eventCount = {}
|
||||
//
|
||||
// categories.forEach(cat => {
|
||||
// eventCount[cat.category] = []
|
||||
// })
|
||||
//
|
||||
// location.events.forEach((event) => {
|
||||
// ;
|
||||
// eventCount[event.category].push(event)
|
||||
// })
|
||||
//
|
||||
// return eventCount
|
||||
// }
|
||||
function getCoordinatesForPercent (radius, percent) {
|
||||
const x = radius * Math.cos(2 * Math.PI * percent)
|
||||
const y = radius * Math.sin(2 * Math.PI * percent)
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
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 = ({
|
||||
fill: getCategoryColor(locCategory),
|
||||
stroke: '#ffffff',
|
||||
strokeWidth: 0,
|
||||
fillOpacity: 0.85,
|
||||
...extraStyles
|
||||
})
|
||||
|
||||
const colorSlices = location.events.map(e => getCategoryColor(e.category))
|
||||
|
||||
let cumulativeAngleSweep = 0
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{colorSlices.map((color, idx) => {
|
||||
const r = 10
|
||||
|
||||
// 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)
|
||||
|
||||
cumulativeAngleSweep = (idx + 1) / colorSlices.length
|
||||
|
||||
const [endX, endY] = getCoordinatesForPercent(r, cumulativeAngleSweep)
|
||||
|
||||
// if the slices are less than 2, take the long arc
|
||||
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(' ')
|
||||
|
||||
const extraStyles = ({
|
||||
...styles,
|
||||
fill: color
|
||||
})
|
||||
|
||||
return (
|
||||
<path
|
||||
class='location-event-marker'
|
||||
id={`arc_${idx}`}
|
||||
d={arc}
|
||||
style={extraStyles}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
function renderLocation (location) {
|
||||
/**
|
||||
@@ -27,18 +78,6 @@ function MapEvents ({ getCategoryColor, categories, projectPoint, styleLocation,
|
||||
}
|
||||
*/
|
||||
const { x, y } = projectPoint([location.latitude, location.longitude])
|
||||
// const eventsByCategory = getLocationEventsDistribution(location);
|
||||
|
||||
const locCategory = location.events.length > 0 ? location.events[0].category : 'default'
|
||||
const customStyles = styleLocation ? styleLocation(location) : null
|
||||
const extraStyles = customStyles[0]
|
||||
const extraRender = customStyles[1]
|
||||
|
||||
const styles = ({
|
||||
fill: getCategoryColor(locCategory),
|
||||
fillOpacity: 1,
|
||||
...extraStyles
|
||||
})
|
||||
|
||||
// in narrative mode, only render events in narrative
|
||||
if (narrative) {
|
||||
@@ -51,19 +90,19 @@ function MapEvents ({ getCategoryColor, categories, projectPoint, styleLocation,
|
||||
}
|
||||
}
|
||||
|
||||
const customStyles = styleLocation ? styleLocation(location) : null
|
||||
const extraRender = (customStyles) ? customStyles[1] : null
|
||||
|
||||
return (
|
||||
<g
|
||||
className='location'
|
||||
transform={`translate(${x}, ${y})`}
|
||||
onClick={() => onSelect(location.events)}
|
||||
>
|
||||
<circle
|
||||
className='location-event-marker'
|
||||
r={7}
|
||||
style={styles}
|
||||
/>
|
||||
{renderLocationSlicesByCategory(location)}
|
||||
{extraRender ? extraRender() : null}
|
||||
</g>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react'
|
||||
import { Portal } from 'react-portal'
|
||||
// import { concatStatic } from 'rxjs/operator/concat'
|
||||
// import { single } from 'rxjs/operator/single'
|
||||
|
||||
function MapNarratives ({ styles, onSelectNarrative, svg, narrative, narratives, projectPoint }) {
|
||||
function getNarrativeStyle (narrativeId) {
|
||||
@@ -28,7 +30,7 @@ function MapNarratives ({ styles, onSelectNarrative, svg, narrative, narratives,
|
||||
// 0 if not in narrative mode, 1 if active narrative, 0.1 if inactive
|
||||
let styles = {
|
||||
strokeOpacity: (n === null) ? 0
|
||||
: (step && (n.id === narrative.id)) ? 1 : 0.1,
|
||||
: (step && (n.id === narrative.id)) ? 1 : 0.0,
|
||||
strokeWidth: 0,
|
||||
strokeDasharray: 'none',
|
||||
stroke: 'none'
|
||||
@@ -59,24 +61,67 @@ function MapNarratives ({ styles, onSelectNarrative, svg, narrative, narratives,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
const coord1 = {
|
||||
x: coord0.x - edge * Math.cos(-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)
|
||||
}
|
||||
|
||||
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
|
||||
}}
|
||||
/>)
|
||||
}
|
||||
|
||||
function _renderNarrativeStep (p1, p2, styles) {
|
||||
const { stroke, strokeWidth, strokeDasharray, strokeOpacity } = styles
|
||||
|
||||
return (
|
||||
<line
|
||||
className='narrative-step'
|
||||
x1={p1.x}
|
||||
x2={p2.x}
|
||||
y1={p1.y}
|
||||
y2={p2.y}
|
||||
markerStart='none'
|
||||
onClick={n => onSelectNarrative(n)}
|
||||
style={{
|
||||
strokeWidth,
|
||||
strokeDasharray,
|
||||
strokeOpacity,
|
||||
stroke
|
||||
}}
|
||||
/>
|
||||
<g>
|
||||
<line
|
||||
className='narrative-step'
|
||||
x1={p1.x}
|
||||
x2={p2.x}
|
||||
y1={p1.y}
|
||||
y2={p2.y}
|
||||
markerStart='none'
|
||||
onClick={n => onSelectNarrative(n)}
|
||||
style={{
|
||||
strokeWidth,
|
||||
strokeDasharray,
|
||||
strokeOpacity,
|
||||
stroke
|
||||
}}
|
||||
/>
|
||||
{(stroke !== 'none')
|
||||
? _renderNarrativeStepArrow(p1, p2, styles)
|
||||
: ''
|
||||
}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,21 +4,23 @@ import { Portal } from 'react-portal'
|
||||
class MapSelectedEvents extends React.Component {
|
||||
renderMarker (event) {
|
||||
const { x, y } = this.props.projectPoint([event.latitude, event.longitude])
|
||||
const styles = this.props.styles
|
||||
const r = styles ? styles.r : 24
|
||||
return (
|
||||
<g
|
||||
className='location-marker'
|
||||
transform={`translate(${x - 32}, ${y})`}
|
||||
transform={`translate(${x - r}, ${y})`}
|
||||
>
|
||||
<path
|
||||
className='leaflet-interactive'
|
||||
stroke='#ffffff'
|
||||
stroke={styles ? styles.stroke : '#ffffff'}
|
||||
stroke-opacity='1'
|
||||
stroke-width='3'
|
||||
stroke-width={styles ? styles['stroke-width'] : 2}
|
||||
stroke-linecap=''
|
||||
stroke-linejoin='round'
|
||||
stroke-dasharray='5,2'
|
||||
stroke-dasharray={styles ? styles['stroke-dasharray'] : '2,2'}
|
||||
fill='none'
|
||||
d='M0,0a32,32 0 1,0 64,0 a32,32 0 1,0 -64,0 '
|
||||
d={`M0,0a${r},${r} 0 1,0 ${r * 2},0 a${r},${r} 0 1,0 -${r * 2},0 `}
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import React from 'react'
|
||||
|
||||
const TimelineMarkers = ({ getEventX, getCategoryY, transitionDuration, selected }) => {
|
||||
const TimelineMarkers = ({ styles, getEventX, getCategoryY, transitionDuration, selected }) => {
|
||||
function renderMarker (event) {
|
||||
return (
|
||||
<circle
|
||||
className='timeline-marker'
|
||||
cx={0}
|
||||
cy={0}
|
||||
stroke={styles ? styles.stroke : '#ffffff'}
|
||||
stroke-opacity='1'
|
||||
stroke-width={styles ? styles['stroke-width'] : 2}
|
||||
stroke-linecap=''
|
||||
stroke-linejoin='round'
|
||||
stroke-dasharray={styles ? styles['stroke-dasharray'] : '2,2'}
|
||||
style={{
|
||||
'transform': `translate(${getEventX(event)}px, ${getCategoryY(event.category)}px)`,
|
||||
'-webkit-transition': `transform ${transitionDuration / 1000}s ease`,
|
||||
|
||||
@@ -111,10 +111,12 @@
|
||||
"narratives_label": "Narratives",
|
||||
"narrative_summary": "Here you can follow some curated stories we have found in the data.",
|
||||
"categories": "Categories",
|
||||
"tags": "Tags",
|
||||
"tags_label": "Tags",
|
||||
"explore_by_tag__title": "Explore by tag or category",
|
||||
"explore_by_tag__description": "Selecting tags or categories, you'll see only those events that are tagged accordingly. If you select nothing, as well as everything, all data will be displayed."
|
||||
"tags": "Filters",
|
||||
"tags_label": "Filters",
|
||||
"explore_by_tag__title": "Explore by filter",
|
||||
"explore_by_tag__description": "Selecting a filter will show you only those events that are annotated with the filter. If you select nothing, as well as everything, all data will be displayed.",
|
||||
"explore_by_category__title": "Explore by category",
|
||||
"explore_by_category__description": "Selecting a category will show you only the events in that category. If you select a filter on top, it will filter events only in the selected categories."
|
||||
|
||||
},
|
||||
"timeline": {
|
||||
|
||||
@@ -16,6 +16,7 @@ const eventSchema = Joi.object().keys({
|
||||
tags: Joi.array().allow(''),
|
||||
comments: Joi.string().allow(''),
|
||||
timestamp: Joi.string(),
|
||||
time_display: Joi.string().allow(''),
|
||||
|
||||
// nested
|
||||
narrative___stepStyles: Joi.array()
|
||||
|
||||
@@ -5,7 +5,8 @@ const siteSchema = Joi.object().keys({
|
||||
description: Joi.string().allow('').required(),
|
||||
site: Joi.string().required(),
|
||||
latitude: Joi.string().required(),
|
||||
longitude: Joi.string().required()
|
||||
longitude: Joi.string().required(),
|
||||
enabled: Joi.string().allow('')
|
||||
})
|
||||
|
||||
export default siteSchema
|
||||
|
||||
@@ -5,13 +5,19 @@
|
||||
border: 1px solid $black;
|
||||
// border-radius: 3px;
|
||||
transition: 0.2 ease;
|
||||
background: $darkwhite;
|
||||
background: $midwhite;
|
||||
color: $darkgrey;
|
||||
box-shadow: 0 19px 19px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);
|
||||
font-size: $large;
|
||||
line-height: $xxlarge;
|
||||
height: auto;
|
||||
opacity: 0.9;
|
||||
transition: background-color 0.4s;
|
||||
|
||||
&:hover {
|
||||
background: $lightwhite;
|
||||
transition: background-color 0.4s;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 0;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
@import 'header';
|
||||
@import 'cardstack';
|
||||
@import 'narrativecard';
|
||||
@import 'mediaoverlay';
|
||||
@import 'sourceoverlay';
|
||||
@import 'map';
|
||||
@import 'timeline';
|
||||
@import 'tag-filters';
|
||||
|
||||
@@ -170,10 +170,15 @@
|
||||
}
|
||||
|
||||
.location-event-marker {
|
||||
pointer-events: all !important;
|
||||
fill: $event_default;
|
||||
stroke-width: 0;
|
||||
}
|
||||
|
||||
.narrative-step-arrow {
|
||||
pointer-events: all !important;
|
||||
}
|
||||
|
||||
.path-polyline {
|
||||
stroke: $darkgrey;
|
||||
stroke-width: 2px;
|
||||
@@ -181,7 +186,8 @@
|
||||
|
||||
.location-count {
|
||||
z-index: 100;
|
||||
fill: #a4a4a4;
|
||||
font-weight: 900;
|
||||
fill: #d0d0d0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -122,17 +122,17 @@ NARRATIVE INFO
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-size: 60pt;
|
||||
font-size: 40pt;
|
||||
color: $offwhite;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&.disabled {
|
||||
color: $darkgrey;
|
||||
color: $midgrey;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: $darkgrey;
|
||||
color: $midwhite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
$panel-width: 1000px;
|
||||
$panel-height: 700px;
|
||||
$panel-height: 1000px;
|
||||
$vimeo-width: $panel-width - 100;
|
||||
$vimeo-height: $panel-height / 2;
|
||||
|
||||
@@ -22,20 +22,19 @@ $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;
|
||||
// min-height: $panel-height;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 80vh;
|
||||
height: $panel-height;
|
||||
max-height: calc(100% - 40px);
|
||||
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);
|
||||
|
||||
.back, .next {
|
||||
width: 30px;
|
||||
max-width: 30px;
|
||||
max-height: 30px;
|
||||
height: 30px;
|
||||
background: $darkgrey;
|
||||
color: $offwhite;
|
||||
@@ -84,15 +83,13 @@ $header-inset: 10px;
|
||||
|
||||
.mo-media-container {
|
||||
flex: 1;
|
||||
/*display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;*/
|
||||
display: inline-block;
|
||||
overflow-x: hidden;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-height: 60vh;
|
||||
max-height: calc(#{$panel-height} - 100px);
|
||||
padding: 20px;
|
||||
font-family: "Lato", Helvetica, sans-serif;
|
||||
|
||||
@@ -109,7 +106,7 @@ $header-inset: 10px;
|
||||
.media-gallery-controls {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: -50%;
|
||||
}
|
||||
@@ -127,12 +124,13 @@ $header-inset: 10px;
|
||||
box-sizing: border-box;
|
||||
min-height: 100px;
|
||||
width: 100%;
|
||||
padding: $padding;
|
||||
// padding: $padding;
|
||||
|
||||
.mo-box-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.mo-box {
|
||||
@@ -184,20 +182,6 @@ $header-inset: 10px;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.media-player {
|
||||
min-width: $vimeo-width;
|
||||
max-width: $vimeo-width;
|
||||
min-height: $vimeo-height;
|
||||
max-height: $vimeo-height;
|
||||
border: none;
|
||||
|
||||
iframe, video {
|
||||
width: $vimeo-width;
|
||||
height: $vimeo-height - 50;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.media-controls {
|
||||
padding: 0 50px;
|
||||
}
|
||||
@@ -230,22 +214,21 @@ $header-inset: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
transition: transform 0.6s ease 0s;
|
||||
width: 100%;
|
||||
// min-width: $panel-width - 30px;
|
||||
// min-height: $panel-height;
|
||||
margin: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.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;
|
||||
min-width: 100%;
|
||||
|
||||
a {
|
||||
color: $darkgrey;
|
||||
@@ -262,31 +245,33 @@ $header-inset: 10px;
|
||||
.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;
|
||||
padding: 20px;
|
||||
min-width: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.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;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.source-image, .source-video {
|
||||
max-width: calc(100% - 20px);
|
||||
max-height: calc(100% - 20px);
|
||||
padding: 0px;
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
max-width: calc(#{$panel-width} - 100px);
|
||||
max-height: $panel-height;
|
||||
margin: auto;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.source-image-loader {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.video-react .video-react-progress-control {
|
||||
@@ -295,4 +280,4 @@ $header-inset: 10px;
|
||||
|
||||
.video-react .video-react-control {
|
||||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
@@ -198,10 +198,6 @@
|
||||
stroke-dasharray: 1px 2px;
|
||||
}
|
||||
|
||||
.datetime {
|
||||
/*transition: transform 0.2s ease;*/
|
||||
}
|
||||
|
||||
.event {
|
||||
cursor: pointer;
|
||||
opacity: .7;
|
||||
@@ -213,9 +209,6 @@
|
||||
|
||||
.timeline-marker {
|
||||
fill: none;
|
||||
stroke: $offwhite;
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 5px 2px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export const getActiveNarrative = state => state.app.narrative
|
||||
export const getActiveStep = state => state.app.narrativeState.current
|
||||
export const getSelected = state => state.app.selected
|
||||
export const getSites = (state) => {
|
||||
if (process.env.features.USE_SITES) return state.domain.sites
|
||||
if (process.env.features.USE_SITES) return state.domain.sites.filter(s => !!(+s.enabled))
|
||||
return []
|
||||
}
|
||||
export const getSources = state => {
|
||||
|
||||
Reference in New Issue
Block a user