Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/user testing bugs #96

Merged
merged 19 commits into from
Feb 4, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions example.config.js
Original file line number Diff line number Diff line change
@@ -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: {},
}
}
}
33 changes: 28 additions & 5 deletions src/components/Card.jsx
Original file line number Diff line number Diff line change
@@ -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} />)
67 changes: 60 additions & 7 deletions src/components/CardStack.jsx
Original file line number Diff line number Diff line change
@@ -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 changes: 39 additions & 0 deletions src/components/CategoriesListPanel.jsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
2 changes: 1 addition & 1 deletion src/components/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 3 additions & 0 deletions src/components/Map.jsx
Original file line number Diff line number Diff line change
@@ -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
}
}
63 changes: 39 additions & 24 deletions src/components/SourceOverlay.jsx
Original file line number Diff line number Diff line change
@@ -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,15 +18,14 @@ 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>
)
}

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)}
26 changes: 1 addition & 25 deletions src/components/TagListPanel.jsx
Original file line number Diff line number Diff line change
@@ -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>
)
45 changes: 36 additions & 9 deletions src/components/Timeline.jsx
Original file line number Diff line number Diff line change
@@ -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
}
}
}
29 changes: 23 additions & 6 deletions src/components/Toolbar.jsx
Original file line number Diff line number Diff line change
@@ -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,24 +72,36 @@ 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) {
return (
<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,22 +144,25 @@ class Toolbar extends React.Component {
)
})
}
return ''
return null
}

renderToolbarTabs () {
let title = copy[this.props.language].toolbar.title
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={{
4 changes: 2 additions & 2 deletions src/components/presentational/Card/Location.js
Original file line number Diff line number Diff line change
@@ -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>
)
7 changes: 3 additions & 4 deletions src/components/presentational/Card/Timestamp.js
Original file line number Diff line number Diff line change
@@ -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>
)
101 changes: 70 additions & 31 deletions src/components/presentational/Map/Events.jsx
Original file line number Diff line number Diff line change
@@ -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>

)
}

77 changes: 61 additions & 16 deletions src/components/presentational/Map/Narratives.jsx
Original file line number Diff line number Diff line change
@@ -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>
)
}

12 changes: 7 additions & 5 deletions src/components/presentational/Map/SelectedEvents.jsx
Original file line number Diff line number Diff line change
@@ -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>
)
8 changes: 7 additions & 1 deletion src/components/presentational/Timeline/Markers.js
Original file line number Diff line number Diff line change
@@ -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`,
10 changes: 6 additions & 4 deletions src/js/data/copy.json
Original file line number Diff line number Diff line change
@@ -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": {
1 change: 1 addition & 0 deletions src/reducers/schema/eventSchema.js
Original file line number Diff line number Diff line change
@@ -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()
3 changes: 2 additions & 1 deletion src/reducers/schema/siteSchema.js
Original file line number Diff line number Diff line change
@@ -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
8 changes: 7 additions & 1 deletion src/scss/card.scss
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 1 addition & 1 deletion src/scss/main.scss
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@
@import 'header';
@import 'cardstack';
@import 'narrativecard';
@import 'mediaoverlay';
@import 'sourceoverlay';
@import 'map';
@import 'timeline';
@import 'tag-filters';
8 changes: 7 additions & 1 deletion src/scss/map.scss
Original file line number Diff line number Diff line change
@@ -170,18 +170,24 @@
}

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

.location-count {
z-index: 100;
fill: #a4a4a4;
font-weight: 900;
fill: #d0d0d0;
}


6 changes: 3 additions & 3 deletions src/scss/narrativecard.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
71 changes: 28 additions & 43 deletions src/scss/mediaoverlay.scss → src/scss/sourceoverlay.scss
Original file line number Diff line number Diff line change
@@ -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%;
}
}
7 changes: 0 additions & 7 deletions src/scss/timeline.scss
Original file line number Diff line number Diff line change
@@ -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;
}

2 changes: 1 addition & 1 deletion src/selectors/index.js
Original file line number Diff line number Diff line change
@@ -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 => {