diff --git a/package.json b/package.json index b4a1c53..c5d556e 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "es6-promise": "^4.1.1", "joi": "^14.0.1", "leaflet": "^1.0.3", + "marked": "^0.6.0", "normalizr": "^3.2.3", "object-hash": "^1.3.0", "react": "^16.6.3", diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx index d734de0..066cffd 100644 --- a/src/components/Dashboard.jsx +++ b/src/components/Dashboard.jsx @@ -154,6 +154,16 @@ function mapDispatchToProps(dispatch) { }; } +function injectSource(id) { + return state => ({ + ...state, + app: { + ...state.app, + source: state.domain.sources[id] + } + }) +} + export default connect( state => state, mapDispatchToProps, diff --git a/src/components/Md.jsx b/src/components/Md.jsx new file mode 100644 index 0000000..2510359 --- /dev/null +++ b/src/components/Md.jsx @@ -0,0 +1,41 @@ +import React from 'react' +import PropTypes from 'prop-types' +import marked from 'marked' + +class Md extends React.Component { + constructor(props) { + super(props) + this.state = { md: null, error: null } + } + + componentDidMount() { + fetch(this.props.path) + .then(resp => resp.text()) + .then(text => { + this.setState({ md: marked(text) }) + }) + .catch(err => { + this.setState({ error: true }) + }) + } + + render() { + if (this.state.md && !this.state.error) { + return ( +
+ ) + } else if (this.state.error) { + return this.props.unloader ||
Error: couldn't load source
+ } else { + return this.props.loader + } + } +} + +Md.propTypes = { + loader: PropTypes.func, + unloader: PropTypes.func, + path: PropTypes.string.isRequired +} + +export default Md diff --git a/src/components/SourceOverlay.jsx b/src/components/SourceOverlay.jsx index 8f6da4e..452eebf 100644 --- a/src/components/SourceOverlay.jsx +++ b/src/components/SourceOverlay.jsx @@ -1,122 +1,159 @@ import React from 'react' import Img from 'react-image' import { Player } from 'video-react' +import Md from './Md.jsx' import Spinner from './presentational/Spinner' import NoSource from './presentational/NoSource' +// TODO: move render functions into presentational components -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) +function SourceOverlay ({ source, onCancel }) { + function renderImage(path) { + return ( +
+ } + unloader={} + /> +
+ ) } - renderVideo() { + function renderVideo(path) { // NB: assume only one video return (
) } - renderPhoto() { + function renderText(path) { return ( -
- + } - unloader={} + unloader={renderError()} />
) } - renderPhotobook() { - return ( -
- {this.props.source.paths.map((url, idx) => ( - } - unloader={} - /> - - ))} -
- ) - } - - renderError() { + function renderError() { return ( ) } - renderTestimony() { + function renderNoSupport(ext) { + return ( + + ) + } + + function toMedia(path) { + let type; + switch (true) { + case /\.(png|jpg)$/.test(path): + type = 'Image'; break + case /\.(mp4)$/.test(path): + type = 'Video'; break + case /\.(md)$/.test(path): + type = 'Text'; break + default: + type = 'Unknown'; break + } + return { type, path } + } + + function getTypeCounts(media) { + let counts = { Image: 0, Video: 0, Text: 0 } + media.forEach(m => { + counts[m.type] += 1 + }) + return counts + } + + function _renderPath(media) { + const { path, type } = media + switch (type) { + case 'Image': + return renderImage(path) + case 'Video': + return renderVideo(path) + case 'Text': + return renderText(path) + default: + return renderNoSupport(path.split('.')[1]) + } + } + + function _renderCounts(counts) { + const strFor = type => + counts[type] > 0 ? + `${counts[type]} ${type.toLowerCase()}${counts[type] > 1 ? 's': ''}` + : '' + const img = strFor('Image') + const vid = strFor('Video') + const txt = strFor('Text') + return (
- Download Testimony + {img ? img : ''} + {vid ? `, ${vid}`: ''} + {txt ? `, ${txt}`: ''}
) } - _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() - } + function _renderContent(media) { + return ( + + {media.map(_renderPath)} + + ) } - render() { - if (typeof(this.props.source) !== 'object') { - return this.renderError() - } - const {id, url, title, date, type, affil_1, affil_2} = this.props.source - return ( -
-
-
-
- -
-
{this.props.source.id}
+ if (typeof(source) !== 'object') { + return renderError() + } + const {id, url, title, paths, date, type, desc} = source + const media = paths.map(toMedia) + const counts = getTypeCounts(media) + + + return ( +
+
+
+
+
-
- {this._renderSwitch()} -
-
-
- {id ?
{id}
: null} - {title?
{title}
: null} -
- {type ?
Type: {type}
: null} - {date ?
Date:{date}
: null} -
- {url ? : null} -
+
{source.title}
+
+
+ {_renderContent(media)} +
+
+
+ {title?
{title}
: null} +
{_renderCounts(counts)}
+ {type ?
{type}
: null} + {date ?
Date:{date}
: null} + {url ? : null} +
+ {desc ?
{desc}
: null}
- ) - } +
+ ) } export default SourceOverlay diff --git a/src/components/presentational/CardSource.js b/src/components/presentational/CardSource.js index 9eab0af..9b3da5b 100644 --- a/src/components/presentational/CardSource.js +++ b/src/components/presentational/CardSource.js @@ -35,15 +35,20 @@ const CardSource = ({ source, isLoading, onClickHandler }) => { ) } + const isImgUrl = /\.(jpg|png)$/ let thumbnail = source.thumbnail - if (!thumbnail || thumbnail === '') { + + if (!thumbnail || thumbnail === '' || !thumbnail.match(isImgUrl)) { // default to first image in paths, null if no images - const imgs = source.paths.filter(p => p.match(/\.(jpg|png)$/)) + const imgs = source.paths.filter(p => p.match(isImgUrl)) thumbnail = imgs.length > 0 ? imgs[0] : null } - console.log(!!thumbnail) - console.log(thumbnail) + const fallbackIcon = ( + + {renderIconText(source.type)} + + ) return (
@@ -52,12 +57,15 @@ const CardSource = ({ source, isLoading, onClickHandler }) => { : (
onClickHandler(source)}> {!!thumbnail ? ( - - ) : ( - - {renderIconText(source.type)} - - )} + } + unloader={fallbackIcon} + width={30} + height={30} + /> + ) : fallbackIcon}

{source.id}

)} diff --git a/src/components/presentational/Spinner.js b/src/components/presentational/Spinner.js index 062bd83..f3c483c 100644 --- a/src/components/presentational/Spinner.js +++ b/src/components/presentational/Spinner.js @@ -1,8 +1,8 @@ import React from 'react'; -const Spinner = () => { +const Spinner = ({ small }) => { return ( -
+
diff --git a/src/js/utilities.js b/src/js/utilities.js index e9692e2..4ba3755 100644 --- a/src/js/utilities.js +++ b/src/js/utilities.js @@ -72,3 +72,18 @@ export function formatterWithYear(datetime) { export function formatter(datetime) { return d3.timeFormat("%d %b, %H:%M")(datetime); } + +/** + * Debugging function: put in place of a mapStateToProps function to + * view that source modal by default + */ +function injectSource(id) { + return state => ({ + ...state, + app: { + ...state.app, + source: state.domain.sources[id] + } + }) +} + diff --git a/src/reducers/schema/sourceSchema.js b/src/reducers/schema/sourceSchema.js index c465ba5..1b61ac0 100644 --- a/src/reducers/schema/sourceSchema.js +++ b/src/reducers/schema/sourceSchema.js @@ -2,11 +2,12 @@ import Joi from 'joi'; const sourceSchema = Joi.object().keys({ id: Joi.string().required(), + title: Joi.string().allow(''), thumbnail: Joi.string().allow(''), paths: Joi.array().required(), type: Joi.string().allow(''), - affil_1: Joi.string().allow(''), - affil_2: Joi.string().allow(''), + // affil_1: Joi.string().allow(''), + // affil_2: Joi.string().allow(''), url: Joi.string().allow(''), desc: Joi.string().allow(''), parent: Joi.string().allow(''), diff --git a/src/scss/loading.scss b/src/scss/loading.scss index 1fdcff6..063c622 100644 --- a/src/scss/loading.scss +++ b/src/scss/loading.scss @@ -42,6 +42,12 @@ https://github.com/tobiasahlin/SpinKit/blob/master/LICENSE position: relative; margin: 10px auto; + + &.small { + width: 15px; + height: 15px; + margin: 5px 20px 5px 10px; + } } .double-bounce, .double-bounce-overlay { diff --git a/src/scss/mediaoverlay.scss b/src/scss/mediaoverlay.scss index 9046ba7..ae72493 100644 --- a/src/scss/mediaoverlay.scss +++ b/src/scss/mediaoverlay.scss @@ -48,34 +48,46 @@ $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; + // 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; + height: 80vh; + max-width: 90vw; + box-shadow: 0 19px 19px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22); .mo-media-container { flex: 1; display: flex; + flex-direction: column; 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; + max-height: 60vh; + overflow-y: auto; .media-player { width: 100%; max-width: $vimeo-width; } + + .media-content { + display: flex; + flex-direction: column; + } + + // NB: topcushion seems to be necessary with certain overflows.. + &.topcushion { + padding-top: 150px; + } } .mo-meta-container { @@ -144,16 +156,17 @@ $header-inset: 10px; } } -.source-image-container { - padding: 0 25px; - overflow-y: scroll; - height: 100%; +.source-image-container, .source-text-container { + padding: 0 10em; + display: flex; + justify-content: center; + align-items: center; } .source-image, .source-video { max-width: calc(100% - 20px); max-height: 350px !important; - height: 100%; + // height: 100%; padding: 10px; } diff --git a/yarn.lock b/yarn.lock index a455221..9f5e536 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3962,7 +3962,6 @@ lodash.tail@^4.1.1: lodash.throttle@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" - integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.3, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@~4.17.10: version "4.17.11" @@ -4036,6 +4035,10 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +marked@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.6.0.tgz#a18d01cfdcf8d15c3c455b71c8329e5e0f01faa1" + matcher@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/matcher/-/matcher-1.1.1.tgz#51d8301e138f840982b338b116bb0c09af62c1c2" @@ -5244,7 +5247,6 @@ redux@^3.6.0: redux@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.1.tgz#436cae6cc40fbe4727689d7c8fae44808f1bfef5" - integrity sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg== dependencies: loose-envify "^1.4.0" symbol-observable "^1.2.0" @@ -6403,7 +6405,6 @@ verror@1.10.0: video-react@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/video-react/-/video-react-0.13.1.tgz#5d0dc68748f9b12e118beea1998d6ae5f6cbd6ba" - integrity sha512-AeGSpddfHv0UxeJztWUALYEjCdzXM1QdtQ5GD1VUd3vxcgwgIfB7EzFKcewRevSHHK8TDmjNksbvbWRobF/QeA== dependencies: classnames "^2.2.3" lodash.throttle "^4.1.1"