Cleaning technical debt (#192)

* abstract Space component to switch out Map

* basic viewing possible

* restructure components dir

* all jsx --> js

* App.jsx --> App.js

* comment out 3d for now
This commit is contained in:
Lachlan Kermode
2021-01-19 22:22:12 +01:00
committed by GitHub
parent 745953a435
commit e99398ceab
75 changed files with 121 additions and 745 deletions

View File

@@ -0,0 +1,19 @@
import React from "react";
const Checkbox = ({ label, isActive, onClickCheckbox, color }) => {
const styles = {
background: isActive ? color : "none",
border: `1px solid ${color}`,
};
return (
<div className={isActive ? "item active" : "item"}>
<span style={{ color: color }}>{label}</span>
<button onClick={onClickCheckbox}>
<div className="checkbox" style={styles} />
</button>
</div>
);
};
export default Checkbox;

View File

@@ -0,0 +1,43 @@
import React from "react";
const CoeventIcon = ({ isEnabled, toggleMapViews }) => {
return (
<button onClick={() => toggleMapViews("coevents")}>
<svg
className="coevents"
x="0px"
y="0px"
width="30px"
height="20px"
viewBox="0 0 30 20"
enableBackground="new 0 0 30 20"
>
<polygon
stroke-linejoin="round"
stroke-miterlimit="10"
points="19.178,20 10.823,20 10.473,14.081
10,13.396 10,6.084 20,6.084 20,13.396 19.445,14.021 "
/>
<rect
className="no-fill"
x="11.4"
y="7.867"
width="7.2"
height="3.35"
/>
<line
stroke-linejoin="round"
stroke-miterlimit="10"
x1="12.125"
y1="1"
x2="12.125"
y2="5.35"
/>
<rect x="11.4" y="4.271" width="1.496" height="1.079" />
<rect x="17.104" y="4.271" width="1.496" height="1.079" />
</svg>
</button>
);
};
export default CoeventIcon;

View File

@@ -0,0 +1,53 @@
import React from "react";
import { getCoordinatesForPercent } from "../../common/utilities";
function ColoredMarkers({ radius, colorPercentMap, styles, className }) {
let cumulativeAngleSweep = 0;
const colors = Object.keys(colorPercentMap);
return (
<>
{colors.map((color, idx) => {
const colorPercent = colorPercentMap[color];
const [startX, startY] = getCoordinatesForPercent(
radius,
cumulativeAngleSweep
);
cumulativeAngleSweep += colorPercent;
const [endX, endY] = getCoordinatesForPercent(
radius,
cumulativeAngleSweep
);
// if the slices are less than 2, take the long arc
const largeArcFlag = colors.length === 1 || colorPercent > 0.5 ? 1 : 0;
// create an array and join it just for code readability
const arc = [
`M ${startX} ${startY}`, // Move
`A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endX} ${endY}`, // Arc
"L 0 0 ", // Line
`L ${startX} ${startY} Z`, // Line
].join(" ");
const extraStyles = {
...styles,
fill: color,
};
return (
<path
class={className}
id={`arc_${idx}`}
d={arc}
style={extraStyles}
/>
);
})}
</>
);
}
export default ColoredMarkers;

View File

@@ -0,0 +1,96 @@
import React from "react";
import { Player } from "video-react";
import Img from "react-image";
import Md from "./Md";
import Spinner from "../atoms/Spinner";
import NoSource from "../atoms/NoSource";
const Content = ({ media, viewIdx, translations, switchLanguage, langIdx }) => {
const el = document.querySelector(".source-media-gallery");
const shiftW = el ? el.getBoundingClientRect().width : 0;
function renderMedia(media) {
const { path, type, poster } = media;
switch (type) {
case "Image":
return (
<div className="source-image-container">
<Img
className="source-image"
src={path}
loader={
<div className="source-image-loader">
<Spinner />
</div>
}
unloader={<NoSource failedUrls={[path]} />}
onClick={() => window.open(path, "_blank")}
/>
</div>
);
case "Video":
return (
<div className="media-player">
<div className="banner-trans right-overlay">
{translations
? translations.map((trans, idx) =>
langIdx !== idx + 1 ? (
<div
className="trans-button"
onClick={() => switchLanguage(idx + 1)}
>
{trans.code}
</div>
) : (
<div
className="trans-button"
onClick={() => switchLanguage(0)}
>
EN
</div>
)
)
: null}
</div>
<Player
poster={poster}
className="source-video"
playsInline
src={path}
/>
</div>
);
case "Text":
return (
<div className="source-text-container">
<Md
path={path}
loader={<Spinner />}
unloader={() => this.renderError()}
/>
</div>
);
case "Document":
return <iframe title={path} className="source-document" src={path} />;
default:
return (
<NoSource
failedUrls={[
`Application does not support extension: ${path.split(".")[1]}`,
]}
/>
);
}
}
return (
<div
className="source-media-gallery"
style={{ transform: `translate(${viewIdx * -shiftW}px)` }}
>
{media.map((m) => renderMedia(m))}
</div>
);
};
export default Content;

View File

@@ -0,0 +1,32 @@
import React from "react";
const OverlayControls = ({ viewIdx, paths, onShiftHandler }) => {
const backArrow =
viewIdx !== 0 ? (
<div className="back" onClick={() => onShiftHandler(-1)}>
<div className="centerer">
<i className="material-icons">arrow_left</i>
</div>
</div>
) : null;
const forwardArrow =
viewIdx < paths.length - 1 ? (
<div className="next" onClick={() => onShiftHandler(1)}>
<div className="centerer">
<i className="material-icons">arrow_right</i>
</div>
</div>
) : null;
if (paths.length > 1) {
return (
<div className="media-gallery-controls">
{backArrow}
{forwardArrow}
</div>
);
}
return <div className="media-gallery-controls" />;
};
export default OverlayControls;

View File

@@ -0,0 +1,16 @@
import React from "react";
const CoverIcon = ({ isActive, isDisabled, onClickHandler }) => {
let classes = isActive ? "action-button enabled" : "action-button";
if (isDisabled) {
classes = "action-button disabled";
}
return (
<button className={classes} onClick={onClickHandler}>
<i class="material-icons">home</i>
</button>
);
};
export default CoverIcon;

View File

@@ -0,0 +1,16 @@
import React from "react";
const CoverIcon = ({ isActive, isDisabled, onClickHandler }) => {
let classes = isActive ? "action-button enabled" : "action-button";
if (isDisabled) {
classes = "action-button disabled";
}
return (
<button className={classes} onClick={onClickHandler}>
<i className="material-icons">info</i>
</button>
);
};
export default CoverIcon;

View File

@@ -0,0 +1,23 @@
import React from "react";
import copy from "../../common/data/copy.json";
const LoadingOverlay = ({ isLoading, language }) => {
let classes = "loading-overlay";
classes += !isLoading ? " hidden" : "";
return (
<div id="loading-overlay" className={classes}>
<div className="loading-wrapper">
<span id="loading-text" className="text">
{copy[language].loading}
</span>
<div className="spinner">
<div className="double-bounce1" />
<div className="double-bounce2" />
</div>
</div>
</div>
);
};
export default LoadingOverlay;

View File

@@ -0,0 +1,48 @@
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) => {
if (text.length <= 0) {
throw new Error();
}
this.setState({ md: marked(text) });
})
.catch(() => {
this.setState({ error: true });
});
}
render() {
if (this.state.md && !this.state.error) {
return (
<div
className="md-container"
dangerouslySetInnerHTML={{ __html: this.state.md }}
/>
);
} else if (this.state.error) {
return this.props.unloader || <div>Error: couldn't load source</div>;
} else {
return this.props.loader;
}
}
}
Md.propTypes = {
loader: PropTypes.func,
unloader: PropTypes.func.isRequired,
path: PropTypes.string.isRequired,
};
export default Md;

View File

@@ -0,0 +1,158 @@
import React from "react";
import marked from "marked";
import Content from "./Content";
import Controls from "./Controls";
import { selectTypeFromPathWithPoster } from "../../common/utilities";
/*
* Inside the SourceOverlay, both the currently displaying media and language
* can be changed by the user. These are both managed in this component's React
* state.
*/
class SourceOverlay extends React.Component {
constructor() {
super();
this.state = { mediaIdx: 0, langIdx: 0 };
this.onShiftGallery = this.onShiftGallery.bind(this);
}
getTypeCounts(media) {
return media.reduce(
(acc, vl) => {
acc[vl.type] += 1;
return acc;
},
{ Image: 0, Video: 0, Text: 0 }
);
}
onShiftGallery(shift) {
// no more left
if (this.state.mediaIdx === 0 && shift === -1) return;
// no more right
if (
this.state.mediaIdx === this.props.source.paths.length - 1 &&
shift === 1
)
return;
this.setState({ mediaIdx: this.state.mediaIdx + shift });
}
switchLanguage(idx) {
this.setState({ langIdx: idx });
}
renderContent(source) {
const { url, title, paths, date, type, poster, description } = source;
const shortenedTitle = title.substring(0, 100);
return (
<>
<div className="mo-banner">
<div className="mo-banner-close" onClick={this.props.onCancel}>
<i className="material-icons">close</i>
</div>
<h3 className="mo-banner-content">{shortenedTitle}</h3>
</div>
<div className="mo-container" onClick={(e) => e.stopPropagation()}>
<div className="mo-media-container">
<Content
switchLanguage={(lang) => this.switchLanguage(lang)}
translations={this.props.translations}
langIdx={this.state.langIdx}
media={paths.map((p) => selectTypeFromPathWithPoster(p, poster))}
viewIdx={this.state.mediaIdx}
/>
</div>
</div>
<div className="mo-footer">
<Controls
paths={paths}
viewIdx={this.state.mediaIdx}
onShiftHandler={this.onShiftGallery}
/>
<div className="mo-meta-container">
{description ? (
<div className="mo-box-desc">
<div
className="md-container"
dangerouslySetInnerHTML={{ __html: marked(description) }}
/>
</div>
) : null}
{type || date || url ? (
<div className="mo-box">
<div>
{type ? <h4>Evidence type</h4> : null}
{type ? (
<p>
<i className="material-icons left">perm_media</i>
{type}
</p>
) : null}
</div>
<div>
{date ? <h4>Date Published</h4> : null}
{date ? (
<p>
<i className="material-icons left">today</i>
{date}
</p>
) : null}
</div>
<div>
{url ? <h4>Link</h4> : null}
{url ? (
<span>
<i className="material-icons left">link</i>
<a href={url} target="_blank" rel="noreferrer">
Link to original URL
</a>
</span>
) : null}
</div>
</div>
) : null}
</div>
</div>
</>
);
}
renderIntlContent() {
const { langIdx } = this.state;
const { translations, source } = this.props;
let translated = null;
if (translations && translations.length && langIdx > 0) {
translated = translations[langIdx - 1];
}
if (translated) {
translated = {
...translated,
poster: source.poster,
// NOTE: this is to allow a slightly nicer syntax when using the Media
// overlay in cover videos.
paths: translated.file ? [translated.file] : translated.paths,
};
}
return this.renderContent(langIdx === 0 ? source : translated);
}
render() {
if (typeof this.props.source !== "object") {
return this.renderError();
}
return (
<div className={`mo-overlay ${this.props.opaque ? "opaque" : ""}`}>
{this.renderIntlContent()}
</div>
);
}
}
export default SourceOverlay;

View File

@@ -0,0 +1,19 @@
import React from "react";
const NoSource = ({ failedUrls }) => {
return (
<div className="no-source-container">
<div className="no-source-row">
<p>
<i className="material-icons no-source-icon">error</i>
</p>
<p>
No media found, as the original media has not yet been uploaded to the
platform.
</p>
</div>
</div>
);
};
export default NoSource;

View File

@@ -0,0 +1,40 @@
import React from "react";
import marked from "marked";
const fontSize = window.innerWidth > 1000 ? 14 : 18;
const Popup = ({
content = [],
styles = {},
isOpen = true,
onClose,
title,
theme = "light",
isMobile = false,
children,
}) => (
<div>
<div
className={`infopopup ${isOpen ? "" : "hidden"} ${
theme === "dark" ? "dark" : "light"
} ${isMobile ? "mobile" : ""}`}
style={{ ...styles, fontSize }}
>
<div className="legend-header">
<button
onClick={onClose}
className="side-menu-burg over-white is-active"
>
<span />
</button>
<h2>{title}</h2>
</div>
{content.map((t, idx) => (
<div key={idx} dangerouslySetInnerHTML={{ __html: marked(t) }} />
))}
{children}
</div>
</div>
);
export default Popup;

View File

@@ -0,0 +1,23 @@
import React from "react";
export default ({ isActive, isDisabled, onClickHandler }) => {
return (
<svg
className="reset"
x="0px"
y="0px"
width="25px"
height="25px"
viewBox="7.5 7.5 25 25"
enableBackground="new 7.5 7.5 25 25"
>
<path
stroke-width="2"
stroke-miterlimit="10"
d="M28.822,16.386c1.354,3.219,0.898,7.064-1.5,9.924
c-3.419,4.073-9.49,4.604-13.562,1.186c-4.073-3.417-4.604-9.49-1.187-13.562c1.987-2.368,4.874-3.54,7.74-3.433"
/>
<polygon points="26.137,12.748 27.621,19.464 28.9,16.741 31.898,16.503" />
</svg>
);
};

View File

@@ -0,0 +1,21 @@
import React from "react";
const RouteIcon = ({ isEnabled, toggleMapViews }) => {
return (
<button onClick={() => toggleMapViews("routes")}>
<svg
x="0px"
y="0px"
width="30px"
height="20px"
viewBox="0 0 30 20"
enableBackground="new 0 0 30 20"
>
<path d="M0.806,13.646h7.619c2.762,0,3-0.238,3-3v-0.414c0-2.762,0.301-3,3.246-3h14.523" />
<polyline points="16.671,9.228 19.103,7.233 16.671,5.237 " />
</svg>
</button>
);
};
export default RouteIcon;

View File

@@ -0,0 +1,16 @@
import React from "react";
const SitesIcon = ({ isActive, isDisabled, onClickHandler }) => {
let classes = isActive ? "action-button enabled" : "action-button";
if (isDisabled) {
classes = "action-button disabled";
}
return (
<button className={classes} onClick={onClickHandler}>
<i class="material-icons">location_on</i>
</button>
);
};
export default SitesIcon;

View File

@@ -0,0 +1,12 @@
import React from "react";
const Spinner = ({ small }) => {
return (
<div className={`spinner ${small ? "small" : ""}`}>
<div className="double-bounce-overlay" />
<div className="double-bounce" />
</div>
);
};
export default Spinner;

View File

@@ -0,0 +1,9 @@
import React from "react";
const StaticPage = ({ showing, children }) => (
<div className={`cover-container ${showing ? "showing" : ""}`}>
{children}
</div>
);
export default StaticPage;