Merge pull request #96 from forensic-architecture/fix/user-testing-bugs

Fix/user testing bugs
This commit is contained in:
Lachlan Kermode
2019-02-04 10:47:55 +00:00
committed by GitHub
26 changed files with 451 additions and 206 deletions

View File

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

View File

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

View File

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

View 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>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@
@import 'header';
@import 'cardstack';
@import 'narrativecard';
@import 'mediaoverlay';
@import 'sourceoverlay';
@import 'map';
@import 'timeline';
@import 'tag-filters';

View File

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

View File

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

View File

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

View File

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

View File

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