mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-08 03:18:36 +03:00
Merge branch 'develop' into feature/simplified-card-props
This commit is contained in:
@@ -305,6 +305,13 @@ export function toggleInfoPopup () {
|
||||
}
|
||||
}
|
||||
|
||||
export const TOGGLE_INTROPOPUP = 'TOGGLE_INTROPOPUP'
|
||||
export function toggleIntroPopup () {
|
||||
return {
|
||||
type: TOGGLE_INTROPOPUP
|
||||
}
|
||||
}
|
||||
|
||||
export const TOGGLE_NOTIFICATIONS = 'TOGGLE_NOTIFICATIONS'
|
||||
export function toggleNotifications () {
|
||||
return {
|
||||
|
||||
@@ -94,9 +94,11 @@
|
||||
"default": {
|
||||
"header": "Navigating the Platform",
|
||||
"intro": [
|
||||
"Open source research by [Bellingcat](https://bellingcat.com).<br/>Software and spatialisation by [Forensic Architecture](https://forensic-architecture.org).",
|
||||
"Each event represents an occurence that is distinct in time or space, or both. An event is represented by a coloured circle on both the map and the timeline.",
|
||||
"Select an event to reveal its content and sources. You can filter events by category or other specified filters in the top left toolbar."
|
||||
"Each small **dot** represents a **datapoint**, or incident. Click on a dot to see details. Hover over a larger ‘**cluster**’ dot to see how many events it represents.",
|
||||
"Zoom in either with a mouse-scroll or by clicking a ‘cluster’ dot.",
|
||||
"Use **filters** and **categories** to segment the data. Selecting certain filters and categories will show only the datapoints that relate to them. If no filters or categories are selected, all the datapoints are displayed.",
|
||||
"Selecting more than one filter will introduce colour-coded datapoints, which allow you to compare types of incident across time and space. This feature works up to a maximum of six filters.",
|
||||
"Use the left and right arrows to move back and forward through time. Use the handles on the right to select a date range."
|
||||
],
|
||||
"notation": "Combinations of colours within a circle indicate multiple events in a single location.",
|
||||
"arrows": "Use the left/right arrows on the keboard to move back and forth between events in time."
|
||||
|
||||
@@ -1,110 +1,13 @@
|
||||
import React from 'react'
|
||||
import marked from 'marked'
|
||||
import Popup from './presentational/Popup'
|
||||
import copy from '../common/data/copy.json'
|
||||
|
||||
export default ({ ui, app, methods }) => {
|
||||
function renderIntro () {
|
||||
var introCopy = copy[app.language].legend.default.intro
|
||||
if (process.env.store.text && process.env.store.text.introCopy) introCopy = process.env.store.text.introCopy
|
||||
return introCopy.map(txt => <p dangerouslySetInnerHTML={{ __html: marked(txt) }} />)
|
||||
}
|
||||
|
||||
function renderHalfWithDot () {
|
||||
// extract category colors from store for combined display.
|
||||
const categoryKeys = Object.keys(ui.style.categories)
|
||||
let firstFill = 'red'
|
||||
let secondFill = 'blue'
|
||||
if (categoryKeys.length >= 1) {
|
||||
firstFill = ui.style.categories[categoryKeys[0]]
|
||||
}
|
||||
if (categoryKeys.length >= 2) {
|
||||
secondFill = ui.style.categories[categoryKeys[1]]
|
||||
}
|
||||
|
||||
return [
|
||||
<style>{`.svg-demo { max-width: 30px } .first { fill: ${firstFill} } .second { fill: ${secondFill} } .demo-text { font-size: 9pt; color: white; font-weight:900 }`}</style>,
|
||||
<svg viewBox='0 0 30 30' className='svg-demo'>
|
||||
<g className='location demo-element' transform='translate(15,15)'>
|
||||
<path className='location-event-marker first' id='arc_0' d='M 10 0 A 10 10 0 0 1 -10 1.2246467991473533e-15 L 0 0 L 10 0 Z' />
|
||||
<path class='location-event-marker second' id='arc_1' d='M -10 1.2246467991473533e-15 A 10 10 0 0 1 10 -2.4492935982947065e-15 L 0 0 L -10 1.2246467991473533e-15 Z' />
|
||||
<text class='location-count demo-text' dx='-4' dy='4'>2</text>
|
||||
</g>
|
||||
</svg>
|
||||
]
|
||||
}
|
||||
|
||||
function renderCategoryColors () {
|
||||
const categories = Object.keys(ui.style.categories).filter(label => label !== 'default')
|
||||
categories.reverse()
|
||||
return categories.map(category => (
|
||||
<div className='legend-section'>
|
||||
<svg x='0px' y='0px' width='50px' height='20px' viewBox='0 0 100 30' enableBackground='new 0 0 100 30'>
|
||||
<circle opacity='1' fill={ui.style.categories[category]} cx='50' cy='15' r='15' />
|
||||
</svg>
|
||||
<div className='legend-labels'>
|
||||
<div className='label'>{category}</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
function renderArrow (strokeFill) {
|
||||
return (
|
||||
<svg x='-10px' y='0px' width='100px' height='30px' viewBox='0 40 100 30' enableBackground='new 0 0 100 70'>
|
||||
<polyline fill='none' stroke={strokeFill} strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' stroke-miterlimit='10' points='
|
||||
8.376,63.723 47.287,63.723 60,46 80,46 ' />
|
||||
<line stroke={strokeFill} strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' x1='78.849' y1='41.94' x2='84.195' y2='46' />
|
||||
<line stroke={strokeFill} strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' x1='78.849' y1='50.06' x2='84.195' y2='46' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function renderView2DLegend () {
|
||||
return (
|
||||
<div className={`infopopup ${(app.flags.isInfopopup) ? '' : 'hidden'}`}>
|
||||
<div className='legend-header'>
|
||||
<button onClick={methods.onClose} className='side-menu-burg over-white is-active'><span /></button>
|
||||
<h2>{copy[app.language].legend.default.header}</h2>
|
||||
</div>
|
||||
{renderIntro()}
|
||||
<div>
|
||||
{renderCategoryColors()}
|
||||
</div>
|
||||
<br />
|
||||
<div className='legend'>
|
||||
<div className='legend-container'>
|
||||
<div className='legend-item one'>
|
||||
{renderHalfWithDot()}
|
||||
</div>
|
||||
<div className='legend-item three'>
|
||||
{copy[app.language].legend.default.notation}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<p>{copy[app.language].legend.default.arrows}</p>
|
||||
</div>
|
||||
|
||||
{
|
||||
ui.style.arrows ? (
|
||||
|
||||
Object.keys(ui.style.arrows).map(arrowName => (
|
||||
<div className='legend-section'>
|
||||
{renderArrow(ui.style.arrows[arrowName])}
|
||||
<div className='legend-labels'>
|
||||
<div className='label'>{arrowName}</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : null
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>{renderView2DLegend()}</div>
|
||||
)
|
||||
}
|
||||
export default ({ isOpen, onClose, language, styles }) => (
|
||||
<Popup
|
||||
title={copy[language].legend.default.header}
|
||||
content={copy[language].legend.default.intro}
|
||||
onClose={onClose}
|
||||
isOpen={isOpen}
|
||||
styles={styles}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -13,7 +13,8 @@ import Toolbar from './Toolbar/Layout'
|
||||
import CardStack from './CardStack.jsx'
|
||||
// import {CardStack} from '@forensic-architecture/design-system'
|
||||
import NarrativeControls from './presentational/Narrative/Controls.js'
|
||||
import InfoPopUp from './InfoPopup.jsx'
|
||||
import InfoPopup from './InfoPopup.jsx'
|
||||
import Popup from './presentational/Popup'
|
||||
import Timeline from './Timeline.jsx'
|
||||
import Notification from './Notification.jsx'
|
||||
import StateOptions from './StateOptions.jsx'
|
||||
@@ -239,7 +240,7 @@ class Dashboard extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { actions, app, domain, ui, features } = this.props
|
||||
const { actions, app, domain, features } = this.props
|
||||
if (isMobile || window.innerWidth < 600) {
|
||||
const msg = 'This platform is not suitable for mobile. Please re-visit the site on a device with a larger screen.'
|
||||
return (
|
||||
@@ -259,6 +260,13 @@ class Dashboard extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
const popupStyles = {
|
||||
fontSize: 24,
|
||||
height: `calc(100vh - ${app.timeline.dimensions.height}px)`,
|
||||
width: '40vw',
|
||||
bottom: app.timeline.dimensions.height
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Toolbar
|
||||
@@ -310,12 +318,19 @@ class Dashboard extends React.Component {
|
||||
onSelectNarrative: this.setNarrative
|
||||
}}
|
||||
/>
|
||||
<InfoPopUp
|
||||
ui={ui}
|
||||
app={app}
|
||||
methods={{
|
||||
onClose: actions.toggleInfoPopup
|
||||
}}
|
||||
<InfoPopup
|
||||
language={app.language}
|
||||
styles={popupStyles}
|
||||
isOpen={app.flags.isInfopopup}
|
||||
onClose={actions.toggleInfoPopup}
|
||||
/>
|
||||
<Popup
|
||||
title={process.env.display_title}
|
||||
theme='dark'
|
||||
isOpen={app.flags.isIntropopup}
|
||||
onClose={actions.toggleIntroPopup}
|
||||
content={app.intro}
|
||||
styles={popupStyles}
|
||||
/>
|
||||
{app.debug ? <Notification
|
||||
isNotification={app.flags.isNotification}
|
||||
|
||||
@@ -176,7 +176,9 @@ class TemplateCover extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='md-container' dangerouslySetInnerHTML={{ __html: marked(this.props.cover.description) }} />
|
||||
{Array.isArray(this.props.cover.description)
|
||||
? this.props.cover.description.map(e => <div className='md-container' dangerouslySetInnerHTML={{ __html: marked(e) }} />)
|
||||
: <div className='md-container' dangerouslySetInnerHTML={{ __html: marked(this.props.cover.description) }} />}
|
||||
|
||||
{videos ? (
|
||||
<div className='hero'>
|
||||
|
||||
21
src/components/presentational/Popup.js
Normal file
21
src/components/presentational/Popup.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
import marked from 'marked'
|
||||
|
||||
export default ({
|
||||
content = [],
|
||||
styles = {},
|
||||
isOpen = true,
|
||||
onClose,
|
||||
title,
|
||||
theme = 'light'
|
||||
}) => (
|
||||
<div>
|
||||
<div className={`infopopup ${isOpen ? '' : 'hidden'} ${theme === 'dark' ? 'dark' : 'light'}`} style={styles}>
|
||||
<div className='legend-header'>
|
||||
<button onClick={onClose} className='side-menu-burg over-white is-active'><span /></button>
|
||||
<h2>{title}</h2>
|
||||
</div>
|
||||
{content.map(t => <div dangerouslySetInnerHTML={{ __html: marked(t) }} />)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
TOGGLE_FETCHING_DOMAIN,
|
||||
TOGGLE_FETCHING_SOURCES,
|
||||
TOGGLE_INFOPOPUP,
|
||||
TOGGLE_INTROPOPUP,
|
||||
TOGGLE_NOTIFICATIONS,
|
||||
TOGGLE_COVER,
|
||||
FETCH_ERROR,
|
||||
@@ -205,6 +206,7 @@ const toggleSites = toggleFlagAC('isShowingSites')
|
||||
const toggleFetchingDomain = toggleFlagAC('isFetchingDomain')
|
||||
const toggleFetchingSources = toggleFlagAC('isFetchingSources')
|
||||
const toggleInfoPopup = toggleFlagAC('isInfopopup')
|
||||
const toggleIntroPopup = toggleFlagAC('isIntropopup')
|
||||
const toggleNotifications = toggleFlagAC('isNotification')
|
||||
const toggleCover = toggleFlagAC('isCover')
|
||||
|
||||
@@ -287,6 +289,8 @@ function app (appState = initial.app, action) {
|
||||
return toggleFetchingSources(appState)
|
||||
case TOGGLE_INFOPOPUP:
|
||||
return toggleInfoPopup(appState)
|
||||
case TOGGLE_INTROPOPUP:
|
||||
return toggleIntroPopup(appState)
|
||||
case TOGGLE_NOTIFICATIONS:
|
||||
return toggleNotifications(appState)
|
||||
case TOGGLE_COVER:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
$event_default: red;
|
||||
|
||||
$offwhite: #efefef;
|
||||
$offwhite-transparent: rgba(239,239,239, 0.9);
|
||||
$lightwhite: #dfdfdf;
|
||||
$midwhite: #a0a0a0;
|
||||
$darkwhite: darken($midwhite, 15%);
|
||||
@@ -10,6 +11,7 @@ $green: rgb(61, 241, 79);
|
||||
$midgrey: rgb(44, 44, 44);
|
||||
$darkgrey: #232323;
|
||||
$black: #000000;
|
||||
$black-transparent: rgba(0,0,0,0.7);
|
||||
|
||||
// Category colors
|
||||
$default: red;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
box-shadow: 10px -10px 38px rgba(0, 0, 0, 0.3), 10px 15px 12px rgba(0, 0, 0, 0.22);
|
||||
color: $darkgrey;
|
||||
position: absolute;
|
||||
background: $offwhite;
|
||||
background: $offwhite-transparent;
|
||||
bottom: $timeline-height;
|
||||
left: $toolbar-width;
|
||||
border: 3px solid $offwhite;
|
||||
@@ -23,6 +23,24 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.side-menu-burg {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 10px;
|
||||
&.light {
|
||||
&.is-active span:after,
|
||||
&.is-active span:before {
|
||||
background: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background: $black-transparent;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -38,11 +56,6 @@
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.side-menu-burg {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.legend-container {
|
||||
height: 100%;
|
||||
|
||||
@@ -93,6 +93,7 @@ const initial = {
|
||||
isCover: true,
|
||||
isCardstack: true,
|
||||
isInfopopup: false,
|
||||
isIntropopup: false,
|
||||
isShowingSites: true
|
||||
},
|
||||
cover: {
|
||||
@@ -170,4 +171,6 @@ if (process.env.store) {
|
||||
appStore.app.timeline.range[0] = new Date(appStore.app.timeline.range[0])
|
||||
appStore.app.timeline.range[1] = new Date(appStore.app.timeline.range[1])
|
||||
|
||||
appStore.app.flags.isIntropopup = !!appStore.app.intro
|
||||
|
||||
export default appStore
|
||||
|
||||
Reference in New Issue
Block a user