mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-11 12:58:35 +03:00
Merge pull request #58 from forensic-architecture/topic/source-async
Topic/source async
This commit is contained in:
@@ -1,18 +1,20 @@
|
||||
module.exports = {
|
||||
title: 'Example',
|
||||
title: 'example',
|
||||
SERVER_ROOT: 'http://localhost:4040',
|
||||
EVENT_EXT: '/api/example/export_events/rows',
|
||||
EVENT_EXT: '/api/example/export_events/deeprows',
|
||||
CATEGORY_EXT: '/api/example/export_categories/rows',
|
||||
SOURCES_EXT: '/api/example/export_events/ids',
|
||||
NARRATIVE_EXT: '/api/example/export_narratives/ids',
|
||||
NARRATIVE_EXT: '/api/example/export_narratives/rows',
|
||||
SOURCES_EXT: '/api/example/export_sources/deepids',
|
||||
TAGS_EXT: '/api/example/export_tags/tree',
|
||||
SITES_EXT: '/api/example/export_sites/rows',
|
||||
MAP_ANCHOR: [31.356397, 34.784818],
|
||||
INCOMING_DATETIME_FORMAT: '%m/%d/%YT%H:%M',
|
||||
MAPBOX_TOKEN: 'SOME_MAPBOX_TOKEN',
|
||||
MAPBOX_TOKEN: 'pk.EXAMPLE_MAPBOX_TOKEN',
|
||||
features: {
|
||||
USE_TAGS: false,
|
||||
USE_SEARCH: false,
|
||||
USE_SITES: false
|
||||
USE_SITES: true,
|
||||
USE_SOURCES: true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"object-hash": "^1.3.0",
|
||||
"react": "^16.6.3",
|
||||
"react-dom": "^16.6.3",
|
||||
"react-image": "^1.5.1",
|
||||
"react-portal": "^4.2.0",
|
||||
"react-redux": "^5.0.4",
|
||||
"react-tabs": "^1.0.0",
|
||||
@@ -27,6 +28,7 @@
|
||||
"redux-thunk": "^2.2.0",
|
||||
"reselect": "^3.0.1",
|
||||
"uuid": "^3.1.0",
|
||||
"video-react": "^0.13.1",
|
||||
"video.js": "^5.19.2",
|
||||
"whatwg-fetch": "^2.0.3"
|
||||
},
|
||||
|
||||
@@ -220,6 +220,14 @@ export function resetAllFilters() {
|
||||
}
|
||||
}
|
||||
|
||||
export const UPDATE_SOURCE = "UPDATE_SOURCE"
|
||||
export function updateSource(source) {
|
||||
return {
|
||||
type: UPDATE_SOURCE,
|
||||
source
|
||||
}
|
||||
}
|
||||
|
||||
// UI
|
||||
|
||||
export const TOGGLE_FETCHING_DOMAIN = 'TOGGLE_FETCHING_DOMAIN'
|
||||
|
||||
BIN
src/assets/placeholder-image.jpg
Normal file
BIN
src/assets/placeholder-image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -30,9 +30,9 @@ class Card extends React.Component {
|
||||
isHighlighted: !this.state.isHighlighted
|
||||
}, () => {
|
||||
if (!this.state.isHighlighted) {
|
||||
this.props.highlight(this.props.event);
|
||||
this.props.onHighlight(this.props.event);
|
||||
} else {
|
||||
this.props.highlight(null);
|
||||
this.props.onHighlight(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -69,6 +69,9 @@ class Card extends React.Component {
|
||||
}
|
||||
|
||||
renderTags() {
|
||||
if (!this.props.tags || (this.props.tags && this.props.tags.length === 0)) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<CardTags
|
||||
tags={this.props.tags || []}
|
||||
@@ -87,16 +90,23 @@ class Card extends React.Component {
|
||||
}
|
||||
|
||||
renderSources() {
|
||||
return this.props.event.sources.map(source => (
|
||||
<CardSource
|
||||
isLoading={this.props.isLoading}
|
||||
language={this.props.language}
|
||||
source={{
|
||||
...source,
|
||||
error: this.props.sourceError
|
||||
}}
|
||||
/>
|
||||
))
|
||||
if (this.props.sourceError) {
|
||||
return <div>ERROR: something went wrong loading sources, TODO:</div>
|
||||
}
|
||||
|
||||
const source_lang = copy[this.props.language].cardstack.sources
|
||||
return (
|
||||
<div className="card-col">
|
||||
<h4>{source_lang}: </h4>
|
||||
{this.props.event.sources.map(source => (
|
||||
<CardSource
|
||||
isLoading={this.props.isLoading}
|
||||
source={source}
|
||||
onClickHandler={source => this.props.onViewSource(source)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// NB: should be internaionalized.
|
||||
@@ -117,7 +127,7 @@ class Card extends React.Component {
|
||||
|
||||
return (
|
||||
<CardNarrative
|
||||
select={(event) => this.props.select([event])}
|
||||
select={(event) => this.props.onSelect([event])}
|
||||
makeTimelabel={(timestamp) => this.makeTimelabel(timestamp)}
|
||||
next={links.next}
|
||||
prev={links.prev}
|
||||
|
||||
@@ -27,8 +27,9 @@ class CardStack extends React.Component {
|
||||
getCategoryGroup={this.props.getCategoryGroup}
|
||||
getCategoryColor={this.props.getCategoryColor}
|
||||
getCategoryLabel={this.props.getCategoryLabel}
|
||||
highlight={this.props.onHighlight}
|
||||
select={this.props.onSelect}
|
||||
onViewSource={this.props.onViewSource}
|
||||
onHighlight={this.props.onHighlight}
|
||||
onSelect={this.props.onSelect}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import * as actions from '../actions';
|
||||
|
||||
import SourceOverlay from './SourceOverlay.jsx';
|
||||
import LoadingOverlay from './presentational/LoadingOverlay';
|
||||
import Map from './Map.jsx';
|
||||
import Toolbar from './Toolbar.jsx';
|
||||
@@ -19,6 +20,7 @@ class Dashboard extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleViewSource = this.handleViewSource.bind(this)
|
||||
this.handleHighlight = this.handleHighlight.bind(this);
|
||||
this.handleSelect = this.handleSelect.bind(this);
|
||||
this.handleSelectNarrative = this.handleSelectNarrative.bind(this);
|
||||
@@ -46,6 +48,10 @@ class Dashboard extends React.Component {
|
||||
return this.eventsById[eventId];
|
||||
}
|
||||
|
||||
handleViewSource(source) {
|
||||
this.props.actions.updateSource(source)
|
||||
}
|
||||
|
||||
handleSelect(selected) {
|
||||
if (selected) {
|
||||
let eventsToSelect = selected.map(event => this.getEventById(event.id));
|
||||
@@ -108,6 +114,7 @@ class Dashboard extends React.Component {
|
||||
: ''
|
||||
}
|
||||
<CardStack
|
||||
onViewSource={this.handleViewSource}
|
||||
onSelect={this.handleSelect}
|
||||
onHighlight={this.handleHighlight}
|
||||
onToggleCardstack={() => this.props.actions.updateSelected([])}
|
||||
@@ -124,6 +131,14 @@ class Dashboard extends React.Component {
|
||||
notifications={this.props.domain.notifications}
|
||||
onToggle={this.props.actions.markNotificationsRead}
|
||||
/>
|
||||
{this.props.app.source ? (
|
||||
<SourceOverlay
|
||||
source={this.props.app.source}
|
||||
onCancel={() => {
|
||||
this.props.actions.updateSource(null)}
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<LoadingOverlay
|
||||
ui={this.props.app.flags.isFetchingDomain}
|
||||
language={this.props.app.language}
|
||||
|
||||
@@ -35,7 +35,7 @@ class MapNarratives extends React.Component {
|
||||
|
||||
getStrokeOpacity(narrative, step) {
|
||||
if (this.props.narrative === null) return 0;
|
||||
if (!step || narrative.id !== this.props.narrative.id) return 0.2;
|
||||
if (!step || narrative.id !== this.props.narrative.id) return 0.1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -53,8 +53,6 @@ class MapNarratives extends React.Component {
|
||||
y1={y}
|
||||
y2={p2.y}
|
||||
markerStart="none"
|
||||
markerEnd="url(#arrow)"
|
||||
midMarker="url(#arrow)"
|
||||
onClick={() => this.props.onSelectNarrative(n)}
|
||||
style={{
|
||||
strokeWidth: this.getStrokeWidth(n, step),
|
||||
@@ -88,4 +86,4 @@ class MapNarratives extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default MapNarratives;
|
||||
export default MapNarratives;
|
||||
|
||||
122
src/components/SourceOverlay.jsx
Normal file
122
src/components/SourceOverlay.jsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import React from 'react'
|
||||
import Img from 'react-image'
|
||||
import { Player } from 'video-react'
|
||||
import Spinner from './presentational/Spinner'
|
||||
import NoSource from './presentational/NoSource'
|
||||
|
||||
class SourceOverlay extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.renderVideo = this.renderVideo.bind(this)
|
||||
this.renderPhoto = this.renderPhoto.bind(this)
|
||||
this.renderPhotobook = this.renderPhotobook.bind(this)
|
||||
this.renderTestimony = this.renderTestimony.bind(this)
|
||||
}
|
||||
|
||||
renderVideo() {
|
||||
// NB: assume only one video
|
||||
return (
|
||||
<div className="media-player">
|
||||
<Player
|
||||
className='source-video'
|
||||
playsInline
|
||||
src={this.props.source.paths[0]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderPhoto() {
|
||||
return (
|
||||
<div className='source-image-container'>
|
||||
<Img
|
||||
className='source-image'
|
||||
src={this.props.source.paths}
|
||||
loader={<Spinner />}
|
||||
unloader={<NoSource failedUrls={this.props.source.paths} />}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderPhotobook() {
|
||||
return (
|
||||
<div className='source-image-container'>
|
||||
{this.props.source.paths.map((url, idx) => (
|
||||
<Img
|
||||
key={idx}
|
||||
className='source-image'
|
||||
src={url}
|
||||
loader={<Spinner />}
|
||||
unloader={<NoSource failedUrls={[this.props.source.path]} />}
|
||||
/>
|
||||
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderError() {
|
||||
return (
|
||||
<NoSource failedUrls={["NOT ALL SOURCES AVAILABLE IN APPLICATION YET"]} />
|
||||
)
|
||||
}
|
||||
|
||||
renderTestimony() {
|
||||
return (
|
||||
<div>
|
||||
<a href={`${this.props.source.path}.docx`}>Download Testimony</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
_renderSwitch() {
|
||||
switch(this.props.source.type) {
|
||||
case 'Video':
|
||||
return this.renderVideo()
|
||||
case 'Photo':
|
||||
return this.renderPhoto()
|
||||
case 'Photobook':
|
||||
return this.renderPhotobook()
|
||||
case 'Eyewitness Testimony':
|
||||
return this.renderTestimony()
|
||||
default:
|
||||
return this.renderError()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (typeof(this.props.source) !== 'object') {
|
||||
return this.renderError()
|
||||
}
|
||||
const {id, url, title, date, type, affil_1, affil_2} = this.props.source
|
||||
return (
|
||||
<div className="mo-overlay">
|
||||
<div className="mo-container">
|
||||
<div className="mo-header">
|
||||
<div className="mo-header-close" onClick={this.props.onCancel}>
|
||||
<button className="side-menu-burg is-active"><span></span></button>
|
||||
</div>
|
||||
<div className="mo-header-text">{this.props.source.id}</div>
|
||||
</div>
|
||||
<div className="mo-media-container">
|
||||
{this._renderSwitch()}
|
||||
</div>
|
||||
<div className="mo-meta-container">
|
||||
<div className="mo-box">
|
||||
{id ? <div><b>{id}</b></div> : null}
|
||||
{title? <div><b>{title}</b></div> : null}
|
||||
<hr/>
|
||||
{type ? <div>Type: <span className="indent">{type}</span></div> : null}
|
||||
{date ? <div>Date:<span className="indent">{date}</span></div> : null}
|
||||
<hr/>
|
||||
{url ? <div><a href={url} target="_blank">Link to original URL</a></div> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default SourceOverlay
|
||||
@@ -1,33 +1,77 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Spinner from './Spinner'
|
||||
import Img from 'react-image'
|
||||
|
||||
import copy from '../../js/data/copy.json'
|
||||
|
||||
function renderSource(source) {
|
||||
return source.error ? (
|
||||
<div><small>{source.error}</small></div>
|
||||
) : (
|
||||
<div>
|
||||
<p>{source.id}</p>
|
||||
const CardSource = ({ source, isLoading, onClickHandler }) => {
|
||||
function renderIconText(type) {
|
||||
switch(type) {
|
||||
case 'Eyewitness Testimony':
|
||||
return 'visibility'
|
||||
case 'Government Data':
|
||||
return 'public'
|
||||
case 'Satellite Imagery':
|
||||
return 'satellite'
|
||||
case 'Second-Hand Testimony':
|
||||
return 'visibility_off'
|
||||
case 'Video':
|
||||
return 'videocam'
|
||||
case 'Photo':
|
||||
return 'photo'
|
||||
case 'Photobook':
|
||||
return 'photo_album'
|
||||
default:
|
||||
return 'help'
|
||||
}
|
||||
}
|
||||
|
||||
if (!source) {
|
||||
return (
|
||||
<div className="card-source">
|
||||
<div>Error: this source was not found</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
let thumbnail = source.thumbnail
|
||||
if (!thumbnail || thumbnail === '') {
|
||||
// default to first image in paths, null if no images
|
||||
const imgs = source.paths.filter(p => p.match(/\.(jpg|png)$/))
|
||||
thumbnail = imgs.length > 0 ? imgs[0] : null
|
||||
}
|
||||
|
||||
console.log(!!thumbnail)
|
||||
console.log(thumbnail)
|
||||
|
||||
return (
|
||||
<div className="card-source">
|
||||
{isLoading
|
||||
? <Spinner/>
|
||||
: (
|
||||
<div className="source-row" onClick={() => onClickHandler(source)}>
|
||||
{!!thumbnail ? (
|
||||
<img className="source-icon" src={thumbnail} width={30} height={30} />
|
||||
) : (
|
||||
<i className="material-icons source-icon">
|
||||
{renderIconText(source.type)}
|
||||
</i>
|
||||
)}
|
||||
<p>{source.id}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CardSource = ({ source, language, isLoading, error }) => {
|
||||
const source_lang = copy[language].cardstack.source
|
||||
|
||||
function renderContent() {
|
||||
return isLoading
|
||||
? <Spinner/>
|
||||
: renderSource(source)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card-row card-cell source">
|
||||
<h4>{source_lang}: </h4>
|
||||
{renderContent()}
|
||||
</div>
|
||||
)
|
||||
CardSource.propTypes = {
|
||||
source: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.string
|
||||
}),
|
||||
isLoading: PropTypes.bool,
|
||||
onClickHandler: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default CardSource
|
||||
|
||||
16
src/components/presentational/NoSource.js
Normal file
16
src/components/presentational/NoSource.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
const NoSource = ({ failedUrls }) => {
|
||||
return (
|
||||
<div className="no-source-container">
|
||||
<div className="no-source-row">
|
||||
<i className="material-icons no-source-icon">
|
||||
error
|
||||
</i>
|
||||
<div>No media found, as the original media has not yet been uploaded to the platform.</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NoSource;
|
||||
@@ -63,7 +63,7 @@
|
||||
"incident_type": "Tipo de acción",
|
||||
"description": "Hechos",
|
||||
"people": "Personas en el evento",
|
||||
"source": "Fuente",
|
||||
"sources": "Fuentes",
|
||||
"category": "Según el testimonio de",
|
||||
"communication": "Comunicación",
|
||||
"transmitter": "Transmisor",
|
||||
@@ -138,7 +138,7 @@
|
||||
"description": "Summary",
|
||||
"tags": "Tags",
|
||||
"notags": "No known tags for this event.",
|
||||
"source": "Source",
|
||||
"sources": "Sources",
|
||||
"unknown_source": "The information for this source could not be retrieved.",
|
||||
"category": "Category",
|
||||
"communication": "Communication",
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
UPDATE_TAGFILTERS,
|
||||
UPDATE_TIMERANGE,
|
||||
UPDATE_NARRATIVE,
|
||||
UPDATE_SOURCE,
|
||||
RESET_ALLFILTERS,
|
||||
TOGGLE_LANGUAGE,
|
||||
TOGGLE_MAPVIEW,
|
||||
@@ -117,6 +118,13 @@ function toggleMapView(appState, action) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateSource(appState, action) {
|
||||
return {
|
||||
...appState,
|
||||
source: action.source
|
||||
}
|
||||
}
|
||||
|
||||
function fetchError(state, action) {
|
||||
return {
|
||||
...state,
|
||||
@@ -181,6 +189,8 @@ function app(appState = initial.app, action) {
|
||||
return updateTimeRange(appState, action);
|
||||
case UPDATE_NARRATIVE:
|
||||
return updateNarrative(appState, action);
|
||||
case UPDATE_SOURCE:
|
||||
return updateSource(appState, action);
|
||||
case RESET_ALLFILTERS:
|
||||
return resetAllFilters(appState, action);
|
||||
case TOGGLE_LANGUAGE:
|
||||
|
||||
@@ -2,12 +2,13 @@ import Joi from 'joi';
|
||||
|
||||
const sourceSchema = Joi.object().keys({
|
||||
id: Joi.string().required(),
|
||||
path: Joi.string().required(),
|
||||
thumbnail: Joi.string().allow(''),
|
||||
paths: Joi.array().required(),
|
||||
type: Joi.string().allow(''),
|
||||
affil_1: Joi.string().allow(''),
|
||||
affil_2: Joi.string().allow(''),
|
||||
url: Joi.string().allow(''),
|
||||
title: Joi.string().allow(''),
|
||||
desc: Joi.string().allow(''),
|
||||
parent: Joi.string().allow(''),
|
||||
author: Joi.string().allow(''),
|
||||
date: Joi.string().allow(''),
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
.card-cell {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
min-width: 80px;
|
||||
@@ -57,6 +57,43 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-source {
|
||||
margin: 0;
|
||||
padding: 2px 0;
|
||||
border-radius: 3px;
|
||||
|
||||
.source-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding: 8px 15px;
|
||||
border-left: 5px solid $darkgrey;
|
||||
background: linear-gradient(to right, $darkgrey 50%, transparent 50%);
|
||||
background-size: 200% 100%;
|
||||
background-position: right bottom;
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
|
||||
&:hover {
|
||||
background-color: $darkgrey;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
.material-icons {
|
||||
color: white;
|
||||
}
|
||||
background-position: left bottom;
|
||||
transition: all 2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.source-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 24px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-cell {
|
||||
a {
|
||||
transition: color 0.2s;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
@import 'header';
|
||||
@import 'cardstack';
|
||||
@import 'narrativecard';
|
||||
@import 'mediaoverlay';
|
||||
@import 'map';
|
||||
@import 'timeline';
|
||||
@import 'tag-filters';
|
||||
@@ -14,3 +15,4 @@
|
||||
@import 'infopopup';
|
||||
@import 'notification';
|
||||
@import 'scene';
|
||||
@import 'mediaplayer';
|
||||
|
||||
162
src/scss/mediaoverlay.scss
Normal file
162
src/scss/mediaoverlay.scss
Normal file
@@ -0,0 +1,162 @@
|
||||
$panel-width: 800px;
|
||||
$panel-height: 700px;
|
||||
$vimeo-width: $panel-width - 100;
|
||||
$vimeo-height: $panel-height / 2;
|
||||
|
||||
$header-inset: 10px;
|
||||
|
||||
.mo-overlay {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(239, 239, 239, 0.5);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.mo-header {
|
||||
min-height: 38px;
|
||||
max-height: 38px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
background-color: black;
|
||||
color: white;
|
||||
|
||||
.mo-header-close {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-left: $header-inset + 8px;
|
||||
}
|
||||
|
||||
.mo-header-text {
|
||||
flex: 1;
|
||||
margin-left: -4em;
|
||||
margin-right: $header-inset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: right;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
.mo-media-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.mo-media-container {
|
||||
// padding-top: 3*$header-inset;
|
||||
font-family: "Lato", Helvetica, sans-serif;
|
||||
// max-height: $vimeo-height;
|
||||
min-width: 100%;
|
||||
max-height: 500px;
|
||||
|
||||
.media-player {
|
||||
width: 100%;
|
||||
max-width: $vimeo-width;
|
||||
}
|
||||
}
|
||||
|
||||
.mo-meta-container {
|
||||
padding: 3*$header-inset;
|
||||
min-height: 100px;
|
||||
min-width: $panel-width;
|
||||
max-width: $panel-height;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.mo-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: $panel-width - (6*$header-inset);
|
||||
min-width: $panel-width - (6*$header-inset);
|
||||
border: 1px solid rgb(189,189,189);
|
||||
padding: $header-inset;
|
||||
}
|
||||
|
||||
.indent {
|
||||
margin-left: 2*$header-inset;
|
||||
}
|
||||
}
|
||||
|
||||
.mo-controls {
|
||||
color: white;
|
||||
width: $vimeo-width;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/* source overlay specific styles */
|
||||
.no-source-container {
|
||||
border: 1px solid black;
|
||||
padding: 2em;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.no-source-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 0.7em;
|
||||
// min-width: 150px;
|
||||
// max-width: 150px;
|
||||
|
||||
.no-source-icon {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.source-image-container {
|
||||
padding: 0 25px;
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.source-image, .source-video {
|
||||
max-width: calc(100% - 20px);
|
||||
max-height: 350px !important;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.media-player {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
1
src/scss/mediaplayer.scss
Normal file
1
src/scss/mediaplayer.scss
Normal file
@@ -0,0 +1 @@
|
||||
@import '~video-react/styles/scss/video-react';
|
||||
@@ -166,10 +166,16 @@ export const selectSelected = createSelector(
|
||||
if (selected.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// NB: return source object if exists, otherwise null
|
||||
const srcs = selected
|
||||
.map(e => e.sources)
|
||||
.map(_sources =>
|
||||
_sources.map(id => sources[id])
|
||||
.map(_sources => {
|
||||
if (!_sources) return [];
|
||||
return _sources.map(id => (
|
||||
sources.hasOwnProperty(id) ? sources[id] : null
|
||||
))
|
||||
}
|
||||
)
|
||||
|
||||
return selected.map((s, idx) => ({
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
const initial = {
|
||||
/*
|
||||
* The Domain or 'domain' of this state refers to the tree of data
|
||||
* available for render and display.
|
||||
* Selections and filters in the 'app' subtree will operate the domain
|
||||
* in mapStateToProps of the Dashboard, and deterimne which items
|
||||
* in the domain will get rendered by React
|
||||
*/
|
||||
* The Domain or 'domain' of this state refers to the tree of data
|
||||
* available for render and display.
|
||||
* Selections and filters in the 'app' subtree will operate the domain
|
||||
* in mapStateToProps of the Dashboard, and deterimne which items
|
||||
* in the domain will get rendered by React
|
||||
*/
|
||||
domain: {
|
||||
events: [],
|
||||
narratives: [],
|
||||
locations: [],
|
||||
categories: [],
|
||||
sources: {},
|
||||
sites: [],
|
||||
tags: {},
|
||||
notifications: [],
|
||||
},
|
||||
|
||||
/*
|
||||
* The 'app' subtree of this state determines the data and information to be
|
||||
* displayed.
|
||||
* It may refer to those the user interacts with, by selecting,
|
||||
* fitlering and so on, which ultimately operate on the data to be displayed.
|
||||
* Additionally, some of the 'app' flags are determined by the config file
|
||||
* or by the characteristics of the client, browser, etc.
|
||||
*/
|
||||
* The 'app' subtree of this state determines the data and information to be
|
||||
* displayed.
|
||||
* It may refer to those the user interacts with, by selecting,
|
||||
* fitlering and so on, which ultimately operate on the data to be displayed.
|
||||
* Additionally, some of the 'app' flags are determined by the config file
|
||||
* or by the characteristics of the client, browser, etc.
|
||||
*/
|
||||
app: {
|
||||
errors: {
|
||||
source: null,
|
||||
},
|
||||
highlighted: null,
|
||||
selected: [],
|
||||
source: null,
|
||||
narrative: null,
|
||||
filters: {
|
||||
timerange: [
|
||||
d3.timeParse("%Y-%m-%dT%H:%M:%S")("2013-02-23T12:00:00"),
|
||||
d3.timeParse("%Y-%m-%dT%H:%M:%S")("2016-02-23T12:00:00")
|
||||
d3.timeParse("%Y-%m-%dT%H:%M:%S")("2013-02-23T12:00:00"),
|
||||
d3.timeParse("%Y-%m-%dT%H:%M:%S")("2016-02-23T12:00:00")
|
||||
],
|
||||
tags: [],
|
||||
categories: [],
|
||||
@@ -54,36 +56,36 @@ const initial = {
|
||||
duration: 1576800,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '3 meses',
|
||||
duration: 129600,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '3 días',
|
||||
duration: 4320,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '12 horas',
|
||||
duration: 720,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '2 horas',
|
||||
duration: 120,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '30 min',
|
||||
duration: 30,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '10 min',
|
||||
duration: 10,
|
||||
active: false
|
||||
}],
|
||||
{
|
||||
label: '3 meses',
|
||||
duration: 129600,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '3 días',
|
||||
duration: 4320,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '12 horas',
|
||||
duration: 720,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '2 horas',
|
||||
duration: 120,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '30 min',
|
||||
duration: 30,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '10 min',
|
||||
duration: 10,
|
||||
active: false
|
||||
}],
|
||||
features: {
|
||||
USE_TAGS: process.env.features.USE_TAGS,
|
||||
USE_SEARCH: process.env.features.USE_SEARCH
|
||||
@@ -99,10 +101,10 @@ const initial = {
|
||||
},
|
||||
|
||||
/*
|
||||
* The 'ui' subtree of this state refers the state of the cosmetic
|
||||
* elements of the application, such as color palettes of categories
|
||||
* as well as dom elements to attach SVG
|
||||
*/
|
||||
* The 'ui' subtree of this state refers the state of the cosmetic
|
||||
* elements of the application, such as color palettes of categories
|
||||
* as well as dom elements to attach SVG
|
||||
*/
|
||||
ui: {
|
||||
style: {
|
||||
categories: {
|
||||
|
||||
Reference in New Issue
Block a user