mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-12 05:18:34 +03:00
display narrative events only in narrative mode; make card more minimal
This commit is contained in:
BIN
src/assets/transparent.png
Normal file
BIN
src/assets/transparent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -21,19 +21,13 @@ class Card extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isHighlighted: false
|
||||
isOpen: false
|
||||
}
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.setState({
|
||||
isHighlighted: !this.state.isHighlighted
|
||||
}, () => {
|
||||
if (!this.state.isHighlighted) {
|
||||
this.props.onHighlight(this.props.event)
|
||||
} else {
|
||||
this.props.onHighlight(null)
|
||||
}
|
||||
isOpen: !this.state.isOpen
|
||||
})
|
||||
}
|
||||
|
||||
@@ -49,13 +43,14 @@ class Card extends React.Component {
|
||||
const categoryLabel = this.props.event.category
|
||||
const color = this.props.getCategoryColor(this.props.event.category)
|
||||
|
||||
return (
|
||||
<CardCategory
|
||||
categoryTitle={categoryTitle}
|
||||
categoryLabel={categoryLabel}
|
||||
color={color}
|
||||
/>
|
||||
)
|
||||
return null
|
||||
// return (
|
||||
// <CardCategory
|
||||
// categoryTitle={categoryTitle}
|
||||
// categoryLabel={categoryLabel}
|
||||
// color={color}
|
||||
// />
|
||||
// )
|
||||
}
|
||||
|
||||
renderSummary() {
|
||||
@@ -63,7 +58,7 @@ class Card extends React.Component {
|
||||
<CardSummary
|
||||
language={this.props.language}
|
||||
description={this.props.event.description}
|
||||
isHighlighted={this.state.isHighlighted}
|
||||
isOpen={this.state.isOpen}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -96,7 +91,7 @@ class Card extends React.Component {
|
||||
|
||||
const source_lang = copy[this.props.language].cardstack.sources
|
||||
return (
|
||||
<div className="card-col">
|
||||
<div className='card-col'>
|
||||
<h4>{source_lang}: </h4>
|
||||
{this.props.event.sources.map(source => (
|
||||
<CardSource
|
||||
@@ -136,49 +131,45 @@ class Card extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
renderHeader() {
|
||||
renderMain() {
|
||||
return (
|
||||
<div className="card-collapsed">
|
||||
<div className="card-row">
|
||||
<div className='card-container'>
|
||||
<div className='card-row details'>
|
||||
{this.renderTimestamp()}
|
||||
{this.renderLocation()}
|
||||
</div>
|
||||
{this.renderCategory()}
|
||||
<br/>
|
||||
{this.renderSummary()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
if (this.state.isHighlighted) {
|
||||
return (
|
||||
<div className="card-bottomhalf">
|
||||
{this.renderTags()}
|
||||
{this.renderSources()}
|
||||
{this.renderNarrative()}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return <div classname="card-bottomhalf"></div>
|
||||
}
|
||||
renderExtra() {
|
||||
return (
|
||||
<div className='card-bottomhalf'>
|
||||
{this.renderTags()}
|
||||
{this.renderSources()}
|
||||
{this.renderNarrative()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderCaret() {
|
||||
return (
|
||||
<CardCaret
|
||||
toggle={() => this.toggle()}
|
||||
isHighlighted={this.state.isHighlighted}
|
||||
isOpen={this.state.isOpen}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isSelected } = this.props
|
||||
return (
|
||||
<li className='event-card'>
|
||||
{this.renderHeader()}
|
||||
{this.renderContent()}
|
||||
{this.renderCaret()}
|
||||
<li className={`event-card ${isSelected ? 'selected' : ''}`}>
|
||||
{this.renderMain()}
|
||||
{this.state.isOpen ? this.renderExtra() : null}
|
||||
{isSelected ? this.renderCaret() : null}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,13 +9,18 @@ import {
|
||||
} from '../js/utilities.js'
|
||||
|
||||
class CardStack extends React.Component {
|
||||
renderCards(events) {
|
||||
return events.map(event => (
|
||||
renderCards(events, selections) {
|
||||
// if no selections provided, select all
|
||||
if (!selections)
|
||||
selections = events.map(e => true)
|
||||
|
||||
return events.map((event, idx) => (
|
||||
<Card
|
||||
event={event}
|
||||
sourceError={this.props.sourceError}
|
||||
language={this.props.language}
|
||||
isLoading={this.props.isLoading}
|
||||
isSelected={selections[idx]}
|
||||
getNarrativeLinks={this.props.getNarrativeLinks}
|
||||
getCategoryGroup={this.props.getCategoryGroup}
|
||||
getCategoryColor={this.props.getCategoryColor}
|
||||
@@ -37,7 +42,11 @@ class CardStack extends React.Component {
|
||||
|
||||
renderNarrativeCards() {
|
||||
const { narrative } = this.props
|
||||
return this.renderCards(narrative.steps)
|
||||
const showing = narrative.steps.slice(narrative.current)
|
||||
const selections = showing
|
||||
.map((_, idx) => (idx === 0))
|
||||
|
||||
return this.renderCards(showing, selections)
|
||||
}
|
||||
|
||||
renderCardStackHeader() {
|
||||
|
||||
@@ -34,18 +34,20 @@ class Timeline extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isNarrative, app, ui } = this.props
|
||||
let classes = `timeline-wrapper ${(this.state.isFolded) ? ' folded' : ''}`;
|
||||
classes += (this.props.app.narrative !== null) ? ' narrative-mode' : '';
|
||||
classes += (app.narrative !== null) ? ' narrative-mode' : '';
|
||||
return (
|
||||
<div className={classes}>
|
||||
<TimelineHeader
|
||||
title={copy[this.props.app.language].timeline.info}
|
||||
date0={formatterWithYear(this.props.app.timerange[0])}
|
||||
date1={formatterWithYear(this.props.app.timerange[1])}
|
||||
title={copy[app.language].timeline.info}
|
||||
date0={formatterWithYear(app.timerange[0])}
|
||||
date1={formatterWithYear(app.timerange[1])}
|
||||
onClick={() => { this.onClickArrow(); }}
|
||||
hideInfo={isNarrative}
|
||||
/>
|
||||
<div className="timeline-content">
|
||||
<div id={this.props.ui.dom.timeline} className="timeline" />
|
||||
<div id={ui.dom.timeline} className="timeline" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -54,6 +56,7 @@ class Timeline extends React.Component {
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
isNarrative: !!state.app.narrative,
|
||||
domain: {
|
||||
events: state.domain.events,
|
||||
categories: selectors.selectCategories(state),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
const TimelineHeader = ({ title, date0, date1, onClick }) => (
|
||||
<div className="timeline-header">
|
||||
<div className="timeline-toggle" onClick={() => onClick()}>
|
||||
<p><i className="arrow-down"></i></p>
|
||||
const TimelineHeader = ({ title, date0, date1, onClick, hideInfo }) => (
|
||||
<div className='timeline-header'>
|
||||
<div className='timeline-toggle' onClick={() => onClick()}>
|
||||
<p><i className='arrow-down'></i></p>
|
||||
</div>
|
||||
<div className="timeline-info">
|
||||
<div className={`timeline-info ${hideInfo ? 'hidden' : ''}`}>
|
||||
<p>{title}</p>
|
||||
<p>{date0} - {date1}</p>
|
||||
</div>
|
||||
|
||||
@@ -79,6 +79,29 @@ export function compareTimestamp (a, b) {
|
||||
return (parseTimestamp(a.timestamp) > parseTimestamp(b.timestamp));
|
||||
}
|
||||
|
||||
/**
|
||||
* Inset the full source represenation from 'allSources' into an event. The
|
||||
* function is 'curried' to allow easy use with maps. To use for a single
|
||||
* source, call with two sets of parentheses:
|
||||
* const src = insetSourceFrom(sources)(anEvent)
|
||||
*/
|
||||
export function insetSourceFrom(allSources) {
|
||||
return (event) => {
|
||||
let sources
|
||||
if (!event.sources) {
|
||||
sources = []
|
||||
} else {
|
||||
sources = event.sources.map(id => (
|
||||
allSources.hasOwnProperty(id) ? allSources[id] : null
|
||||
))
|
||||
}
|
||||
return {
|
||||
...event,
|
||||
sources
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Debugging function: put in place of a mapStateToProps function to
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
box-sizing: border-box;
|
||||
margin: 1px 0 0 0;
|
||||
padding: 15px;
|
||||
border: 1px solid rgba(0, 0, 0, 0);
|
||||
border-radius: 3px;
|
||||
border: 1px solid $black;
|
||||
// border-radius: 3px;
|
||||
transition: 0.2 ease;
|
||||
background: $offwhite;
|
||||
background: $darkwhite;
|
||||
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;
|
||||
@@ -39,10 +39,13 @@
|
||||
.card-row, .card-col {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid $lightwhite;
|
||||
margin: 5px 0 10px 0;
|
||||
padding-bottom: 10px;
|
||||
|
||||
&.details {
|
||||
border-bottom: 1px solid $lightwhite;
|
||||
}
|
||||
|
||||
.card-cell {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -120,6 +123,7 @@
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.card-toggle p {
|
||||
@@ -197,6 +201,7 @@
|
||||
.summary {
|
||||
overflow: auto;
|
||||
margin-top: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.tag {
|
||||
@@ -204,4 +209,12 @@
|
||||
margin: 0;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: $offwhite;
|
||||
}
|
||||
|
||||
.card-row {
|
||||
border-color: darkgray;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,17 +11,16 @@ $timeline-height: 170px;
|
||||
right: 10px;
|
||||
max-height: calc(100% - 208px);
|
||||
height: auto;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 19px 38px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);
|
||||
z-index: $header;
|
||||
color: white;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
&.narrative-mode {
|
||||
right: auto;
|
||||
left: 10px;
|
||||
top: $narrative-info-max-height + 12px;
|
||||
height: calc(100% - #{$narrative-info-max-height} - #{$timeline-height});
|
||||
height: calc(100% - #{$narrative-info-max-height} - #{$timeline-height} - 12px);
|
||||
}
|
||||
|
||||
&.full-height {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
.timeline-wrapper {
|
||||
position: fixed;
|
||||
box-sizing: border-box;
|
||||
@@ -67,6 +66,10 @@
|
||||
}
|
||||
|
||||
.timeline-info {
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
width: calc(#{$card-width} - 20px);
|
||||
position: absolute;
|
||||
margin-top: -70px;
|
||||
margin-left: 10px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createSelector} from 'reselect'
|
||||
import { parseTimestamp, compareTimestamp } from '../js/utilities'
|
||||
import { parseTimestamp, compareTimestamp, insetSourceFrom } from '../js/utilities'
|
||||
|
||||
// Input selectors
|
||||
export const getEvents = state => state.domain.events
|
||||
@@ -22,6 +22,8 @@ export const getTagTree = state => state.domain.tags
|
||||
export const getTagsFilter = state => state.app.filters.tags
|
||||
export const getTimeRange = state => state.app.filters.timerange
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Some handy helpers
|
||||
*/
|
||||
@@ -90,8 +92,8 @@ export const selectEvents = createSelector(
|
||||
* and if TAGS are being used, select them if their tags are enabled
|
||||
*/
|
||||
export const selectNarratives = createSelector(
|
||||
[getEvents, getNarratives, getTagsFilter, getTimeRange],
|
||||
(events, narrativesMeta, tagFilters, timeRange) => {
|
||||
[getEvents, getNarratives, getTagsFilter, getTimeRange, getSources],
|
||||
(events, narrativesMeta, tagFilters, timeRange, sources) => {
|
||||
|
||||
const narratives = {}
|
||||
const narrativeSkeleton = id => ({ id, steps: [] })
|
||||
@@ -109,7 +111,8 @@ export const selectNarratives = createSelector(
|
||||
|
||||
// add evt to steps
|
||||
if (isInNarrative)
|
||||
narratives[narrative].steps.push(evt)
|
||||
// NB: insetSourceFrom is a 'curried' function to allow with maps
|
||||
narratives[narrative].steps.push(insetSourceFrom(sources)(evt))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -173,6 +176,8 @@ export const selectLocations = createSelector(
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Of all the sources, select those that are relevant to the selected events.
|
||||
*/
|
||||
@@ -183,21 +188,7 @@ export const selectSelected = createSelector(
|
||||
return []
|
||||
}
|
||||
|
||||
// NB: return source object if exists, otherwise null
|
||||
const srcs = selected
|
||||
.map(e => e.sources)
|
||||
.map(_sources => {
|
||||
if (!_sources) return []
|
||||
return _sources.map(id => (
|
||||
sources.hasOwnProperty(id) ? sources[id] : null
|
||||
))
|
||||
}
|
||||
)
|
||||
|
||||
return selected.map((s, idx) => ({
|
||||
...s,
|
||||
sources: srcs[idx]
|
||||
}))
|
||||
return selected.map(insetSourceFrom(sources))
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user