mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-11 04:48:36 +03:00
Merge pull request #98 from forensic-architecture/topic/fixes
Add key and optional cover
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -3,5 +3,10 @@ build/
|
||||
node_modules/
|
||||
config.js
|
||||
dev.config.js
|
||||
# ignore all covers but the default
|
||||
src/components/presentational/covers/
|
||||
!src/src/components/presentational/covers/Default.js
|
||||
|
||||
src/\.DS_Store
|
||||
|
||||
\.DS_Store
|
||||
|
||||
@@ -11,6 +11,7 @@ module.exports = {
|
||||
INCOMING_DATETIME_FORMAT: '%m/%d/%YT%H:%M',
|
||||
MAPBOX_TOKEN: 'pk.EXAMPLE_MAPBOX_TOKEN',
|
||||
features: {
|
||||
USE_COVER: false,
|
||||
USE_TAGS: false,
|
||||
USE_SEARCH: false,
|
||||
USE_SITES: true,
|
||||
@@ -32,16 +33,15 @@ module.exports = {
|
||||
new Date(2014, 5, 9),
|
||||
new Date(2018, 1, 6, 23)
|
||||
]
|
||||
} }
|
||||
},
|
||||
ui: {
|
||||
style: {
|
||||
categories: {},
|
||||
shapes: {},
|
||||
narratives: {},
|
||||
selectedEvent: {},
|
||||
}
|
||||
}
|
||||
},
|
||||
ui: {
|
||||
style: {
|
||||
categories: {},
|
||||
shapes: {},
|
||||
narratives: {},
|
||||
selectedEvent: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -291,6 +291,13 @@ export function markNotificationsRead () {
|
||||
}
|
||||
}
|
||||
|
||||
export const TOGGLE_COVER = 'TOGGLE_COVER'
|
||||
export function toggleCover () {
|
||||
return {
|
||||
type: TOGGLE_COVER
|
||||
}
|
||||
}
|
||||
|
||||
// ERRORS
|
||||
|
||||
export const FETCH_SOURCE_ERROR = 'FETCH_SOURCE_ERROR'
|
||||
|
||||
@@ -11,7 +11,6 @@ export default (props) => {
|
||||
function renderCategoryTree () {
|
||||
return (
|
||||
<div>
|
||||
<h2>{copy[props.language].toolbar.categories}</h2>
|
||||
{props.categories.map(cat => {
|
||||
return (<li
|
||||
key={cat.category.replace(/ /g, '_')}
|
||||
@@ -31,7 +30,7 @@ export default (props) => {
|
||||
|
||||
return (
|
||||
<div className='react-innertabpanel'>
|
||||
<h2>{copy[props.language].toolbar.explore_by_category__title}</h2>
|
||||
<h2>{copy[props.language].toolbar.categories}</h2>
|
||||
<p>{copy[props.language].toolbar.explore_by_category__description}</p>
|
||||
{renderCategoryTree()}
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,8 @@ import NarrativeControls from './presentational/Narrative/Controls.js'
|
||||
import InfoPopUp from './InfoPopup.jsx'
|
||||
import Timeline from './Timeline.jsx'
|
||||
import Notification from './Notification.jsx'
|
||||
import StaticPage from './StaticPage'
|
||||
import DefaultCover from './presentational/covers/Default'
|
||||
|
||||
import { parseDate } from '../js/utilities'
|
||||
|
||||
@@ -139,7 +141,9 @@ class Dashboard extends React.Component {
|
||||
<InfoPopUp
|
||||
ui={ui}
|
||||
app={app}
|
||||
toggle={() => actions.toggleInfoPopup()}
|
||||
methods={{
|
||||
onClose: actions.toggleInfoPopup
|
||||
}}
|
||||
/>
|
||||
<Notification
|
||||
isNotification={app.flags.isNotification}
|
||||
@@ -155,6 +159,13 @@ class Dashboard extends React.Component {
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{process.env.features.USE_COVER && (
|
||||
<StaticPage showing={app.flags.isCover}>
|
||||
{/* enable USE_COVER in config.js features, and customise your header */}
|
||||
{/* pass 'actions.toggleCover' as a prop to your custom header */}
|
||||
<DefaultCover showAppHandler={actions.toggleCover} />
|
||||
</StaticPage>
|
||||
)}
|
||||
<LoadingOverlay
|
||||
ui={app.flags.isFetchingDomain}
|
||||
language={app.language}
|
||||
|
||||
@@ -1,84 +1,44 @@
|
||||
import React from 'react'
|
||||
import copy from '../js/data/copy.json'
|
||||
// NB: should we make this componetn part of a future feature?
|
||||
|
||||
export default class InfoPopUp extends React.Component {
|
||||
renderView2DCopy () {
|
||||
return copy[this.props.app.language].legend.view2d.paragraphs.map(paragraph => <p>{paragraph}</p>)
|
||||
export default ({ ui, app, methods }) => {
|
||||
function renderIntro () {
|
||||
return copy[app.language].legend.default.intro.map(txt => <p>{txt}</p>)
|
||||
}
|
||||
|
||||
renderCategoryColors () {
|
||||
const colors = copy[this.props.app.language].legend.view2d.colors.slice(0)
|
||||
colors.reverse()
|
||||
return (
|
||||
<div className='legend-labels' style={{ 'margin-left': '-10px' }}>
|
||||
{colors.map((color, idx) => {
|
||||
return (
|
||||
<div className='label' style={{ 'margin-left': `${idx * 5}` }}>
|
||||
<div className={`color-category ${color.class}`} />
|
||||
{color.label}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
function renderCategoryColors () {
|
||||
const categories = Object.keys(ui.style.categories).filter(label => label !== 'default')
|
||||
|
||||
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>
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
renderView2DLegend () {
|
||||
function renderView2DLegend () {
|
||||
return (
|
||||
<div className={`infopopup ${(this.props.app.flags.isInfopopup) ? '' : 'hidden'}`}>
|
||||
<button onClick={() => this.props.toggle()} className='side-menu-burg over-white is-active'><span /></button>
|
||||
{this.renderView2DCopy()}
|
||||
<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 className='legend'>
|
||||
<div className='legend-section' style={{ 'height': '100px' }}>
|
||||
<svg x='0px' y='0px' width='100px' height='100px' viewBox='0 0 100 100' enableBackground='new 0 0 100 100'>
|
||||
<circle fill='#D2CD28' cx='50' cy='50' r='50' />
|
||||
<circle fill='#662770' cx='50' cy='50' r='40' />
|
||||
<circle fill='#2F409A' cx='50' cy='50' r='30' />
|
||||
<circle fill='#256C36' cx='50' cy='50' r='20' />
|
||||
<circle fill='#FF0000' cx='50' cy='50' r='10' />
|
||||
</svg>
|
||||
{this.renderCategoryColors()}
|
||||
</div>
|
||||
<div className='legend-section'>
|
||||
<svg x='0px' y='0px' width='100px' height='30px' viewBox='0 0 100 30' enableBackground='new 0 0 100 30'>
|
||||
<line fill='none' stroke='#2F409A' strokeDasharray='4,4' x1='30' y1='15' x2='70' y2='15' />
|
||||
<circle fill='2F409A' fillOpacity='0.2' stroke='#2F409A' strokeDasharray='4,4' cx='80' cy='15' r='10' />
|
||||
<circle fill='2F409A' fillOpacity='0.2' stroke='#2F409A' strokeDasharray='4,4' cx='20' cy='15' r='10' />
|
||||
</svg>
|
||||
<div className='legend-labels'>
|
||||
<div className='label'>Comunicaciones</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='legend-section'>
|
||||
<svg x='0px' y='0px' width='100px' height='30px' viewBox='0 0 100 30' enableBackground='new 0 0 100 30'>
|
||||
<circle opacity='0.3' fill='#FF0000' cx='50' cy='15' r='15' />
|
||||
</svg>
|
||||
<div className='legend-labels'>
|
||||
<div className='label'>Ataques</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='legend-section'>
|
||||
<svg x='0px' y='0px' width='100px' height='30px' viewBox='0 40 100 30' enableBackground='new 0 0 100 70'>
|
||||
<polyline fill='none' stroke='#000000' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' stroke-miterlimit='10' points='
|
||||
8.376,63.723 47.287,63.723 60,46 106,46 ' />
|
||||
<line stroke='#000000' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' x1='33.723' y1='59.663' x2='39.069' y2='63.723' />
|
||||
<line stroke='#000000' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' x1='33.723' y1='67.782' x2='39.069' y2='63.723' />
|
||||
<line stroke='#000000' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' x1='78.849' y1='41.94' x2='84.195' y2='46' />
|
||||
<line stroke='#000000' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' x1='78.849' y1='50.06' x2='84.195' y2='46' />
|
||||
</svg>
|
||||
<div className='legend-labels'>
|
||||
<div className='label'>Rutas de bus</div>
|
||||
</div>
|
||||
<div className='legend-container'>
|
||||
{renderCategoryColors()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div>{this.renderView2DLegend()}</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div>{renderView2DLegend()}</div>
|
||||
)
|
||||
}
|
||||
|
||||
9
src/components/StaticPage.js
Normal file
9
src/components/StaticPage.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
export default ({ showing, children }) => {
|
||||
return (
|
||||
<div className={`cover-container ${showing ? 'showing' : ''}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -63,7 +63,6 @@ class TagListPanel extends React.Component {
|
||||
renderTree () {
|
||||
return (
|
||||
<div>
|
||||
<h2>{copy[this.props.language].toolbar.tags}</h2>
|
||||
{this.state.treeComponents.map(c => c)}
|
||||
</div>
|
||||
)
|
||||
@@ -72,7 +71,7 @@ class TagListPanel extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div className='react-innertabpanel'>
|
||||
<h2>{copy[this.props.language].toolbar.explore_by_tag__title}</h2>
|
||||
<h2>{copy[this.props.language].toolbar.tags}</h2>
|
||||
<p>{copy[this.props.language].toolbar.explore_by_tag__description}</p>
|
||||
{this.renderTree()}
|
||||
</div>
|
||||
|
||||
16
src/components/presentational/covers/Default.js
Normal file
16
src/components/presentational/covers/Default.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
|
||||
export default ({ showAppHandler }) => (
|
||||
<div className='default-cover-container'>
|
||||
<h3>Here's an example cover.</h3>
|
||||
<p>Replace it with a more descriptive one:</p>
|
||||
<ul>
|
||||
<li>Create a new component in <code>components/presentational/covers</code>.</li>
|
||||
<li>Import in in <code>components/Dashboard.jsx</code> in the <code>render</code> function.</li>
|
||||
</ul>
|
||||
<br /><br />
|
||||
<div>
|
||||
<button onClick={showAppHandler}>Go to app</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -90,6 +90,14 @@
|
||||
{ "class": "category_group03", "label": "Category Group 03" },
|
||||
{ "class": "other", "label": "Other categories" }
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"header": "Navigating the Platform",
|
||||
"intro": [
|
||||
"Each event represents an occurence that is distinct in either time, 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.",
|
||||
"Narratives compose events to reveal logical threads that emerge from them. Transition to narrative mode by selecting a narrative from the top left dashboard icon."
|
||||
]
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
|
||||
@@ -126,3 +126,13 @@ export function urlFromEnv (ext) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleFlagAC (flag) {
|
||||
return (appState) => ({
|
||||
...appState,
|
||||
flags: {
|
||||
...appState.flags,
|
||||
[flag]: !appState.flags[flag]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* global d3 */
|
||||
import initial from '../store/initial.js'
|
||||
import { parseDate } from '../js/utilities'
|
||||
import { parseDate, toggleFlagAC } from '../js/utilities'
|
||||
|
||||
import {
|
||||
UPDATE_HIGHLIGHTED,
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
TOGGLE_FETCHING_SOURCES,
|
||||
TOGGLE_INFOPOPUP,
|
||||
TOGGLE_NOTIFICATIONS,
|
||||
TOGGLE_COVER,
|
||||
FETCH_ERROR,
|
||||
FETCH_SOURCE_ERROR
|
||||
} from '../actions'
|
||||
@@ -189,16 +190,6 @@ function toggleLanguage (appState, action) {
|
||||
})
|
||||
}
|
||||
|
||||
function toggleSites (appState, action) {
|
||||
return {
|
||||
...appState,
|
||||
flags: {
|
||||
...appState.flags,
|
||||
isShowingSites: !appState.flags.isShowingSites
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateSource (appState, action) {
|
||||
return {
|
||||
...appState,
|
||||
@@ -214,37 +205,12 @@ function fetchError (state, action) {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFetchingDomain (appState, action) {
|
||||
return Object.assign({}, appState, {
|
||||
flags: Object.assign({}, appState.flags, {
|
||||
isFetchingDomain: !appState.flags.isFetchingDomain
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function toggleFetchingSources (appState, action) {
|
||||
return Object.assign({}, appState, {
|
||||
flags: Object.assign({}, appState.flags, {
|
||||
isFetchingSources: !appState.flags.isFetchingSources
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function toggleInfoPopup (appState, action) {
|
||||
return Object.assign({}, appState, {
|
||||
flags: Object.assign({}, appState.flags, {
|
||||
isInfopopup: !appState.flags.isInfopopup
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function toggleNotifications (appState, action) {
|
||||
return Object.assign({}, appState, {
|
||||
flags: Object.assign({}, appState.flags, {
|
||||
isNotification: !appState.flags.isNotification
|
||||
})
|
||||
})
|
||||
}
|
||||
const toggleSites = toggleFlagAC('isShowingSites')
|
||||
const toggleFetchingDomain = toggleFlagAC('isFetchingDomain')
|
||||
const toggleFetchingSources = toggleFlagAC('isFetchingSources')
|
||||
const toggleInfoPopup = toggleFlagAC('isInfopopup')
|
||||
const toggleNotifications = toggleFlagAC('isNotification')
|
||||
const toggleCover = toggleFlagAC('isCover')
|
||||
|
||||
function fetchSourceError (appState, action) {
|
||||
return {
|
||||
@@ -278,20 +244,24 @@ function app (appState = initial.app, action) {
|
||||
return updateSource(appState, action)
|
||||
case RESET_ALLFILTERS:
|
||||
return resetAllFilters(appState, action)
|
||||
/* toggles */
|
||||
case TOGGLE_LANGUAGE:
|
||||
return toggleLanguage(appState, action)
|
||||
case TOGGLE_SITES:
|
||||
return toggleSites(appState, action)
|
||||
return toggleSites(appState)
|
||||
case TOGGLE_FETCHING_DOMAIN:
|
||||
return toggleFetchingDomain(appState)
|
||||
case TOGGLE_FETCHING_SOURCES:
|
||||
return toggleFetchingSources(appState)
|
||||
case TOGGLE_INFOPOPUP:
|
||||
return toggleInfoPopup(appState)
|
||||
case TOGGLE_NOTIFICATIONS:
|
||||
return toggleNotifications(appState)
|
||||
case TOGGLE_COVER:
|
||||
return toggleCover(appState)
|
||||
/* errors */
|
||||
case FETCH_ERROR:
|
||||
return fetchError(appState, action)
|
||||
case TOGGLE_FETCHING_DOMAIN:
|
||||
return toggleFetchingDomain(appState, action)
|
||||
case TOGGLE_FETCHING_SOURCES:
|
||||
return toggleFetchingSources(appState, action)
|
||||
case TOGGLE_INFOPOPUP:
|
||||
return toggleInfoPopup(appState, action)
|
||||
case TOGGLE_NOTIFICATIONS:
|
||||
return toggleNotifications(appState, action)
|
||||
case FETCH_SOURCE_ERROR:
|
||||
return fetchSourceError(appState, action)
|
||||
default:
|
||||
|
||||
26
src/scss/cover.scss
Normal file
26
src/scss/cover.scss
Normal file
@@ -0,0 +1,26 @@
|
||||
.cover-container {
|
||||
position: absolute;
|
||||
top: -100%;
|
||||
left: 0;
|
||||
background-color: black;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.95;
|
||||
transition: top 0.4s ease;
|
||||
z-index: $overheader + 1;
|
||||
|
||||
color: $offwhite;
|
||||
|
||||
&.showing {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.default-cover-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -24,87 +24,52 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.legend-section {
|
||||
width: 300px;
|
||||
padding-left: 60px;
|
||||
height: 40px;
|
||||
display: inline-block;
|
||||
|
||||
svg {
|
||||
width: 100px;
|
||||
float: left;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.legend-labels {
|
||||
float: left;
|
||||
display: inline-block;
|
||||
width: calc(100% - 100px);
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: $xsmall;
|
||||
margin-top: 10px;
|
||||
margin-left: 10px;
|
||||
|
||||
.color-category {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 10px;
|
||||
display: inline-block;
|
||||
margin: 0px 5px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
.legend-labels .label {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.legend-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
h2 {
|
||||
display: flex;
|
||||
font-size: 12pt;
|
||||
letter-spacing: 2px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.side-menu-burg {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
margin-bottom: 3px;
|
||||
padding-left: 80px;
|
||||
.legend-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
line-height: 15px;
|
||||
height: 15px;
|
||||
font-size: $normal;
|
||||
}
|
||||
.legend-section {
|
||||
width: 300px;
|
||||
height: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.color-marker {
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
svg {
|
||||
width: 60px;
|
||||
float: left;
|
||||
margin: 0 10px 0 0;
|
||||
border-radius: 15px;
|
||||
|
||||
&.victims { background-color: #C90500; }
|
||||
&.military { background-color: #319C31; }
|
||||
&.nonstate { background-color: #AC28AC; }
|
||||
&.state-police { background-color: #0000BF; }
|
||||
&.iguala-municipal-police { background-color: #00558D; }
|
||||
&.federal-police { background-color: #5756A2; }
|
||||
&.huitzuco-municipal-police { background-color: #4ECAC1; }
|
||||
&.cocula-municipal-police { background-color: #095959; }
|
||||
&.ambulance { background-color: #ffffff; }
|
||||
&.other { background-color: #D3CE2A; }
|
||||
&.drivers { background-color: #822519; }
|
||||
&.communications { background-color: #a6a6a6; }
|
||||
&.GIEI { background-color: #ffffff; }
|
||||
&.PGR { background-color: #000000; }
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.legend-labels {
|
||||
display: flex;
|
||||
|
||||
.label {
|
||||
font-size: $xsmall;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,3 +16,4 @@
|
||||
@import 'notification';
|
||||
@import 'scene';
|
||||
@import 'mediaplayer';
|
||||
@import 'cover';
|
||||
|
||||
@@ -52,7 +52,7 @@ const initial = {
|
||||
map: {
|
||||
anchor: [31.356397, 34.784818],
|
||||
startZoom: 11,
|
||||
minZoom: 7,
|
||||
minZoom: 6,
|
||||
maxZoom: 18,
|
||||
bounds: null,
|
||||
maxBounds: [[180, -180], [-180, 180]]
|
||||
@@ -84,9 +84,9 @@ const initial = {
|
||||
flags: {
|
||||
isFetchingDomain: false,
|
||||
isFetchingSources: false,
|
||||
|
||||
isCover: true,
|
||||
isCardstack: true,
|
||||
isInfopopup: false,
|
||||
isInfopopup: true,
|
||||
isShowingSites: true
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user