Using prettier for linting

This commit is contained in:
Zac Ioannidis
2020-12-08 13:13:50 +00:00
parent fa329066e4
commit 81e00fd917
111 changed files with 3986 additions and 3294 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,29 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>TimeMap - Forensic Architecture</title>
<link rel="stylesheet" href="https://api.mapbox.com/mapbox.js/v3.1.1/mapbox.css">
<link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<style>
@media (hover: none) {
#id { display: none; }
#nodisplay { display: block; }
}
@media (hover: hover) {
#nodisplay {display: none; }
}
</style>
<div class="page">
<div class="page">
<div id="explore-app"></div>
</div>
<div id="nodisplay">
This platform is unsuitable for mobile. Please revisit on a desktop.
</div>
</div>
</body>
</html>

View File

@@ -6,15 +6,12 @@
"private": true,
"scripts": {
"react-scripts:start": "node scripts/start.js",
"react-scripts:build": "node scripts/build.js",
"react-scripts:build": "NODE_ENV=production node scripts/build.js",
"react-scripts:eject": "node scripts/eject.js",
"dev": "webpack-dev-server --content-base static --mode development",
"dev:wsl": "npm run dev -- --host 0.0.0.0",
"build": "NODE_ENV=production webpack --mode production",
"test": "ava --verbose",
"test-watch": "ava --watch",
"lint": "standard \"src/**/*.js\" \"src/**/*.jsx\" \"test/**/*.js\"",
"lint:fix": "npm run lint -- --fix"
"lint:fix": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\""
},
"dependencies": {
"@babel/core": "7.12.3",
@@ -48,6 +45,7 @@
"file-loader": "6.1.1",
"fs-extra": "^9.0.1",
"html-webpack-plugin": "4.5.0",
"husky": "^4.3.5",
"identity-obj-proxy": "3.0.0",
"jest": "26.6.0",
"jest-circus": "26.6.0",
@@ -55,6 +53,7 @@
"jest-watch-typeahead": "0.6.1",
"joi": "^14.0.1",
"leaflet": "^1.0.3",
"lint-staged": "^10.5.3",
"marked": "^0.7.0",
"mini-css-extract-plugin": "0.11.3",
"moment": "^2.26.0",
@@ -66,6 +65,7 @@
"postcss-normalize": "8.0.1",
"postcss-preset-env": "6.7.0",
"postcss-safe-parser": "5.0.2",
"prettier": "^2.2.1",
"prompts": "2.4.0",
"ramda": "^0.26.1",
"react": "^16.13.1",
@@ -104,8 +104,15 @@
"mini-css-extract-plugin": "^0.4.4",
"mocha": "^5.2.0",
"node-sass": "4.13.1",
"redux-devtools": "^3.4.0",
"standard": "^12.0.1"
"redux-devtools": "^3.4.0"
},
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
"prettier --write"
],
"test/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
"prettier --write"
]
},
"ava": {
"files": [

View File

@@ -1,30 +1,31 @@
/* global fetch, alert */
import { urlFromEnv } from '../common/utilities'
import { urlFromEnv } from "../common/utilities";
// TODO: relegate these URLs entirely to environment variables
// const CONFIG_URL = urlFromEnv('CONFIG_EXT')
const EVENT_DATA_URL = urlFromEnv('EVENTS_EXT')
const ASSOCIATIONS_URL = urlFromEnv('ASSOCIATIONS_EXT')
const SOURCES_URL = urlFromEnv('SOURCES_EXT')
const SITES_URL = urlFromEnv('SITES_EXT')
const SHAPES_URL = urlFromEnv('SHAPES_EXT')
const EVENT_DATA_URL = urlFromEnv("EVENTS_EXT");
const ASSOCIATIONS_URL = urlFromEnv("ASSOCIATIONS_EXT");
const SOURCES_URL = urlFromEnv("SOURCES_EXT");
const SITES_URL = urlFromEnv("SITES_EXT");
const SHAPES_URL = urlFromEnv("SHAPES_EXT");
const domainMsg = (domainType) => `Something went wrong fetching ${domainType}. Check the URL or try disabling them in the config file.`
const domainMsg = (domainType) =>
`Something went wrong fetching ${domainType}. Check the URL or try disabling them in the config file.`;
export function fetchDomain () {
let notifications = []
export function fetchDomain() {
const notifications = [];
function handleError (message) {
function handleError(message) {
notifications.push({
message,
type: 'error'
})
return []
type: "error",
});
return [];
}
return (dispatch, getState) => {
const features = getState().features
dispatch(toggleFetchingDomain())
const features = getState().features;
dispatch(toggleFetchingDomain());
// let configPromise = Promise.resolve([])
// if (features.USE_REMOTE_CONFIG) {
@@ -35,46 +36,55 @@ export function fetchDomain () {
// NB: EVENT_DATA_URL is a list, and so results are aggregated
const eventPromise = Promise.all(
EVENT_DATA_URL.map(url => fetch(url)
.then(response => response.json())
.catch(() => handleError('events'))
EVENT_DATA_URL.map((url) =>
fetch(url)
.then((response) => response.json())
.catch(() => handleError("events"))
)
).then(results => results.flatMap(t => t))
).then((results) => results.flatMap((t) => t));
let associationsPromise = Promise.resolve([])
let associationsPromise = Promise.resolve([]);
if (features.USE_ASSOCIATIONS) {
if (!ASSOCIATIONS_URL) {
associationsPromise = Promise.resolve(handleError('USE_ASSOCIATIONS is true, but you have not provided a ASSOCIATIONS_EXT'))
associationsPromise = Promise.resolve(
handleError(
"USE_ASSOCIATIONS is true, but you have not provided a ASSOCIATIONS_EXT"
)
);
} else {
associationsPromise = fetch(ASSOCIATIONS_URL)
.then(response => response.json())
.catch(() => handleError(domainMsg('associations')))
.then((response) => response.json())
.catch(() => handleError(domainMsg("associations")));
}
}
let sourcesPromise = Promise.resolve([])
let sourcesPromise = Promise.resolve([]);
if (features.USE_SOURCES) {
if (!SOURCES_URL) {
sourcesPromise = Promise.resolve(handleError('USE_SOURCES is true, but you have not provided a SOURCES_EXT'))
sourcesPromise = Promise.resolve(
handleError(
"USE_SOURCES is true, but you have not provided a SOURCES_EXT"
)
);
} else {
sourcesPromise = fetch(SOURCES_URL)
.then(response => response.json())
.catch(() => handleError(domainMsg('sources')))
.then((response) => response.json())
.catch(() => handleError(domainMsg("sources")));
}
}
let sitesPromise = Promise.resolve([])
let sitesPromise = Promise.resolve([]);
if (features.USE_SITES) {
sitesPromise = fetch(SITES_URL)
.then(response => response.json())
.catch(() => handleError(domainMsg('sites')))
.then((response) => response.json())
.catch(() => handleError(domainMsg("sites")));
}
let shapesPromise = Promise.resolve([])
let shapesPromise = Promise.resolve([]);
if (features.USE_SHAPES) {
shapesPromise = fetch(SHAPES_URL)
.then(response => response.json())
.catch(() => handleError(domainMsg('shapes')))
.then((response) => response.json())
.catch(() => handleError(domainMsg("shapes")));
}
return Promise.all([
@@ -82,271 +92,277 @@ export function fetchDomain () {
associationsPromise,
sourcesPromise,
sitesPromise,
shapesPromise
shapesPromise,
])
.then(response => {
.then((response) => {
const result = {
events: response[0],
associations: response[1],
sources: response[2],
sites: response[3],
shapes: response[4],
notifications
notifications,
};
if (
Object.values(result).some((resp) => resp.hasOwnProperty("error"))
) {
throw new Error(
"Some URLs returned negative. If you are in development, check the server is running"
);
}
if (Object.values(result).some(resp => resp.hasOwnProperty('error'))) {
throw new Error('Some URLs returned negative. If you are in development, check the server is running')
}
dispatch(toggleFetchingDomain())
dispatch(setInitialCategories(result.associations))
return result
dispatch(toggleFetchingDomain());
dispatch(setInitialCategories(result.associations));
return result;
})
.catch(err => {
dispatch(fetchError(err.message))
dispatch(toggleFetchingDomain())
.catch((err) => {
dispatch(fetchError(err.message));
dispatch(toggleFetchingDomain());
// TODO: handle this appropriately in React hierarchy
alert(err.message)
})
}
alert(err.message);
});
};
}
export const FETCH_ERROR = 'FETCH_ERROR'
export function fetchError (message) {
export const FETCH_ERROR = "FETCH_ERROR";
export function fetchError(message) {
return {
type: FETCH_ERROR,
message
}
message,
};
}
export const UPDATE_DOMAIN = 'UPDATE_DOMAIN'
export function updateDomain (payload) {
export const UPDATE_DOMAIN = "UPDATE_DOMAIN";
export function updateDomain(payload) {
return {
type: UPDATE_DOMAIN,
payload
}
payload,
};
}
export function fetchSource (source) {
return dispatch => {
export function fetchSource(source) {
return (dispatch) => {
if (!SOURCES_URL) {
dispatch(fetchSourceError('No source extension specified.'))
dispatch(fetchSourceError("No source extension specified."));
} else {
dispatch(toggleFetchingSources())
dispatch(toggleFetchingSources());
fetch(`${SOURCES_URL}`)
.then(response => {
.then((response) => {
if (!response.ok) {
throw new Error('No sources are available at the URL specified in the config specified.')
throw new Error(
"No sources are available at the URL specified in the config specified."
);
} else {
return response.json()
return response.json();
}
})
.catch(err => {
dispatch(fetchSourceError(err.message))
dispatch(toggleFetchingSources())
})
.catch((err) => {
dispatch(fetchSourceError(err.message));
dispatch(toggleFetchingSources());
});
}
}
};
}
export const UPDATE_HIGHLIGHTED = 'UPDATE_HIGHLIGHTED'
export function updateHighlighted (highlighted) {
export const UPDATE_HIGHLIGHTED = "UPDATE_HIGHLIGHTED";
export function updateHighlighted(highlighted) {
return {
type: UPDATE_HIGHLIGHTED,
highlighted: highlighted
}
highlighted: highlighted,
};
}
export const UPDATE_SELECTED = 'UPDATE_SELECTED'
export function updateSelected (selected) {
export const UPDATE_SELECTED = "UPDATE_SELECTED";
export function updateSelected(selected) {
return {
type: UPDATE_SELECTED,
selected: selected
}
selected: selected,
};
}
export const UPDATE_DISTRICT = 'UPDATE_DISTRICT'
export function updateDistrict (district) {
export const UPDATE_DISTRICT = "UPDATE_DISTRICT";
export function updateDistrict(district) {
return {
type: UPDATE_DISTRICT,
district
}
district,
};
}
export const CLEAR_FILTER = 'CLEAR_FILTER'
export function clearFilter (filter) {
export const CLEAR_FILTER = "CLEAR_FILTER";
export function clearFilter(filter) {
return {
type: CLEAR_FILTER,
filter
}
filter,
};
}
export const TOGGLE_ASSOCIATIONS = 'TOGGLE_ASSOCIATIONS'
export function toggleAssociations (association, value, shouldColor) {
export const TOGGLE_ASSOCIATIONS = "TOGGLE_ASSOCIATIONS";
export function toggleAssociations(association, value, shouldColor) {
return {
type: TOGGLE_ASSOCIATIONS,
association,
value,
shouldColor
}
shouldColor,
};
}
export const SET_LOADING = 'SET_LOADING'
export function setLoading () {
export const SET_LOADING = "SET_LOADING";
export function setLoading() {
return {
type: SET_LOADING
}
type: SET_LOADING,
};
}
export const SET_NOT_LOADING = 'SET_NOT_LOADING'
export function setNotLoading () {
export const SET_NOT_LOADING = "SET_NOT_LOADING";
export function setNotLoading() {
return {
type: SET_NOT_LOADING
}
type: SET_NOT_LOADING,
};
}
export const SET_INITIAL_CATEGORIES = 'SET_INITIAL_CATEGORIES'
export function setInitialCategories (values) {
export const SET_INITIAL_CATEGORIES = "SET_INITIAL_CATEGORIES";
export function setInitialCategories(values) {
return {
type: SET_INITIAL_CATEGORIES,
values
}
values,
};
}
export const UPDATE_TIMERANGE = 'UPDATE_TIMERANGE'
export function updateTimeRange (timerange) {
export const UPDATE_TIMERANGE = "UPDATE_TIMERANGE";
export function updateTimeRange(timerange) {
return {
type: UPDATE_TIMERANGE,
timerange
}
timerange,
};
}
export const UPDATE_DIMENSIONS = 'UPDATE_DIMENSIONS'
export function updateDimensions (dims) {
export const UPDATE_DIMENSIONS = "UPDATE_DIMENSIONS";
export function updateDimensions(dims) {
return {
type: UPDATE_DIMENSIONS,
dims
}
dims,
};
}
export const UPDATE_NARRATIVE = 'UPDATE_NARRATIVE'
export function updateNarrative (narrative) {
export const UPDATE_NARRATIVE = "UPDATE_NARRATIVE";
export function updateNarrative(narrative) {
return {
type: UPDATE_NARRATIVE,
narrative
}
narrative,
};
}
export const UPDATE_NARRATIVE_STEP_IDX = 'UPDATE_NARRATIVE_STEP_IDX'
export function updateNarrativeStepIdx (idx) {
export const UPDATE_NARRATIVE_STEP_IDX = "UPDATE_NARRATIVE_STEP_IDX";
export function updateNarrativeStepIdx(idx) {
return {
type: UPDATE_NARRATIVE_STEP_IDX,
idx
}
idx,
};
}
export const UPDATE_SOURCE = 'UPDATE_SOURCE'
export function updateSource (source) {
export const UPDATE_SOURCE = "UPDATE_SOURCE";
export function updateSource(source) {
return {
type: UPDATE_SOURCE,
source
}
source,
};
}
export const UPDATE_COLORING_SET = 'UPDATE_COLORING_SET'
export function updateColoringSet (coloringSet) {
export const UPDATE_COLORING_SET = "UPDATE_COLORING_SET";
export function updateColoringSet(coloringSet) {
return {
type: UPDATE_COLORING_SET,
coloringSet
}
coloringSet,
};
}
// UI
export const TOGGLE_SITES = 'TOGGLE_SITES'
export function toggleSites () {
export const TOGGLE_SITES = "TOGGLE_SITES";
export function toggleSites() {
return {
type: TOGGLE_SITES
}
type: TOGGLE_SITES,
};
}
export const TOGGLE_FETCHING_DOMAIN = 'TOGGLE_FETCHING_DOMAIN'
export function toggleFetchingDomain () {
export const TOGGLE_FETCHING_DOMAIN = "TOGGLE_FETCHING_DOMAIN";
export function toggleFetchingDomain() {
return {
type: TOGGLE_FETCHING_DOMAIN
}
type: TOGGLE_FETCHING_DOMAIN,
};
}
export const TOGGLE_FETCHING_SOURCES = 'TOGGLE_FETCHING_SOURCES'
export function toggleFetchingSources () {
export const TOGGLE_FETCHING_SOURCES = "TOGGLE_FETCHING_SOURCES";
export function toggleFetchingSources() {
return {
type: TOGGLE_FETCHING_SOURCES
}
type: TOGGLE_FETCHING_SOURCES,
};
}
export const TOGGLE_LANGUAGE = 'TOGGLE_LANGUAGE'
export function toggleLanguage (language) {
export const TOGGLE_LANGUAGE = "TOGGLE_LANGUAGE";
export function toggleLanguage(language) {
return {
type: TOGGLE_LANGUAGE,
language
}
language,
};
}
export const CLOSE_TOOLBAR = 'CLOSE_TOOLBAR'
export function closeToolbar () {
export const CLOSE_TOOLBAR = "CLOSE_TOOLBAR";
export function closeToolbar() {
return {
type: CLOSE_TOOLBAR
}
type: CLOSE_TOOLBAR,
};
}
export const TOGGLE_INFOPOPUP = 'TOGGLE_INFOPOPUP'
export function toggleInfoPopup () {
export const TOGGLE_INFOPOPUP = "TOGGLE_INFOPOPUP";
export function toggleInfoPopup() {
return {
type: TOGGLE_INFOPOPUP
}
type: TOGGLE_INFOPOPUP,
};
}
export const TOGGLE_INTROPOPUP = 'TOGGLE_INTROPOPUP'
export function toggleIntroPopup () {
export const TOGGLE_INTROPOPUP = "TOGGLE_INTROPOPUP";
export function toggleIntroPopup() {
return {
type: TOGGLE_INTROPOPUP
}
type: TOGGLE_INTROPOPUP,
};
}
export const TOGGLE_NOTIFICATIONS = 'TOGGLE_NOTIFICATIONS'
export function toggleNotifications () {
export const TOGGLE_NOTIFICATIONS = "TOGGLE_NOTIFICATIONS";
export function toggleNotifications() {
return {
type: TOGGLE_NOTIFICATIONS
}
type: TOGGLE_NOTIFICATIONS,
};
}
export const MARK_NOTIFICATIONS_READ = 'MARK_NOTIFICATIONS_READ'
export function markNotificationsRead () {
export const MARK_NOTIFICATIONS_READ = "MARK_NOTIFICATIONS_READ";
export function markNotificationsRead() {
return {
type: MARK_NOTIFICATIONS_READ
}
type: MARK_NOTIFICATIONS_READ,
};
}
export const TOGGLE_COVER = 'TOGGLE_COVER'
export function toggleCover () {
export const TOGGLE_COVER = "TOGGLE_COVER";
export function toggleCover() {
return {
type: TOGGLE_COVER
}
type: TOGGLE_COVER,
};
}
export const UPDATE_SEARCH_QUERY = 'UPDATE_SEARCH_QUERY'
export function updateSearchQuery (searchQuery) {
export const UPDATE_SEARCH_QUERY = "UPDATE_SEARCH_QUERY";
export function updateSearchQuery(searchQuery) {
return {
type: UPDATE_SEARCH_QUERY,
searchQuery
}
searchQuery,
};
}
// ERRORS
export const FETCH_SOURCE_ERROR = 'FETCH_SOURCE_ERROR'
export function fetchSourceError (msg) {
export const FETCH_SOURCE_ERROR = "FETCH_SOURCE_ERROR";
export function fetchSourceError(msg) {
return {
type: FETCH_SOURCE_ERROR,
msg
}
msg,
};
}

View File

@@ -1,7 +1,7 @@
import copy from './data/copy.json'
import { language } from './utilities'
import copy from "./data/copy.json";
import { language } from "./utilities";
const cardStack = copy[language].cardstack
const cardStack = copy[language].cardstack;
// Sensible defaults for generating a basic card layout
// based on the example Timemap datasheet.
@@ -10,28 +10,28 @@ const basic = ({ event }) => {
return [
[
{
kind: 'date',
title: cardStack['date_title'] || 'Incident Dates',
value: event.datetime || event.date || ``
kind: "date",
title: cardStack.date_title || "Incident Dates",
value: event.datetime || event.date || "",
},
{
kind: 'text',
title: cardStack['location_title'] || 'Location',
value: event.location || ``
}
kind: "text",
title: cardStack.location_title || "Location",
value: event.location || "—",
},
],
[{ kind: 'line-break', times: 0.4 }],
[{ kind: "line-break", times: 0.4 }],
[
{
kind: 'text',
title: cardStack['summary_title'] || 'Summary',
value: event.description || ``,
scaleFont: 1.1
}
]
]
}
kind: "text",
title: cardStack.summary_title || "Summary",
value: event.description || "",
scaleFont: 1.1,
},
],
];
};
export default {
basic
}
basic,
};

View File

@@ -1,5 +1,5 @@
export const ASSOCIATION_MODES = {
CATEGORY: 'CATEGORY',
NARRATIVE: 'NARRATIVE',
FILTER: 'FILTER'
}
CATEGORY: "CATEGORY",
NARRATIVE: "NARRATIVE",
FILTER: "FILTER",
};

View File

@@ -3,8 +3,42 @@
"date": "%d/%m/%Y",
"time": "%-I:%M:%S %p",
"periods": ["AM", "PM"],
"days": ["domingo", "lunes", "martes", "miércoles", "jueves", "viernes", "sábado"],
"days": [
"domingo",
"lunes",
"martes",
"miércoles",
"jueves",
"viernes",
"sábado"
],
"shortDays": ["dom", "lun", "mar", "mié", "jue", "vie", "sáb"],
"months": ["enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"],
"shortMonths": ["ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"]
"months": [
"enero",
"febrero",
"marzo",
"abril",
"mayo",
"junio",
"julio",
"agosto",
"septiembre",
"octubre",
"noviembre",
"diciembre"
],
"shortMonths": [
"ene",
"feb",
"mar",
"abr",
"may",
"jun",
"jul",
"ago",
"sep",
"oct",
"nov",
"dic"
]
}

View File

@@ -1,13 +1,13 @@
export const colors = {
fa_red: '#eb443e',
yellow: '#ffd800',
black: '#000',
white: '#fff'
}
fa_red: "#eb443e",
yellow: "#ffd800",
black: "#000",
white: "#fff",
};
export default {
fallbackEventColor: colors.fa_red,
darkBackground: colors.black,
primaryHighlight: colors.fa_red,
secondaryHighlight: colors.white
}
secondaryHighlight: colors.white,
};

View File

@@ -28,8 +28,9 @@ export function getCoordinatesForPercent(radius, percent) {
* Return value:
* ex. {'#fff': 0.5, '#000': 0.5, ...} */
export function zipColorsToPercentages(colors, percentages) {
if (colors.length < percentages.length)
if (colors.length < percentages.length) {
throw new Error("You must declare an appropriate number of filter colors");
}
return percentages.reduce((map, percent, idx) => {
map[colors[idx]] = percent;
@@ -45,7 +46,7 @@ export function zipColorsToPercentages(colors, percentages) {
*/
export function getParameterByName(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[[\]]/g, `\\$&`);
name = name.replace(/[[\]]/g, "\\$&");
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
const results = regex.exec(url);
@@ -101,7 +102,7 @@ export function trimAndEllipse(string, stringNum) {
* Returns the list of parents: ex. ['Chemical', 'Tear Gas', ...]
*/
export function getFilterParents(associations, filter) {
for (let a of associations) {
for (const a of associations) {
const { filter_paths: fp } = a;
if (a.id === filter) {
return fp.slice(0, fp.length - 1);
@@ -340,7 +341,7 @@ export function calculateColorPercentages(set, coloringSet) {
const associationMap = {};
for (const [idx, value] of coloringSet.entries()) {
for (let filter of value) {
for (const filter of value) {
associationMap[filter] = idx;
}
}
@@ -400,11 +401,11 @@ export const dateMax = function () {
* https://stackoverflow.com/questions/22697936/binary-search-in-javascript
* **/
export function binarySearch(ar, el, compareFn) {
var m = 0;
var n = ar.length - 1;
let m = 0;
let n = ar.length - 1;
while (m <= n) {
var k = (n + m) >> 1;
var cmp = compareFn(el, ar[k]);
const k = (n + m) >> 1;
const cmp = compareFn(el, ar[k]);
if (cmp > 0) {
m = k + 1;
} else if (cmp < 0) {

View File

@@ -1,13 +1,11 @@
import '../scss/main.scss'
import React from 'react'
import Layout from './Layout'
import "../scss/main.scss";
import React from "react";
import Layout from "./Layout";
class App extends React.Component {
render () {
return (
<Layout />
)
render() {
return <Layout />;
}
}
export default App
export default App;

View File

@@ -1,82 +1,85 @@
import copy from '../common/data/copy.json'
import React from 'react'
import copy from "../common/data/copy.json";
import React from "react";
import CardCustomField from './presentational/Card/CustomField'
import CardTime from './presentational/Card/Time'
import CardLocation from './presentational/Card/Location'
import CardCaret from './presentational/Card/Caret'
import CardSummary from './presentational/Card/Summary'
import CardSource from './presentational/Card/Source'
import { makeNiceDate } from '../common/utilities'
import CardCustomField from "./presentational/Card/CustomField";
import CardTime from "./presentational/Card/Time";
import CardLocation from "./presentational/Card/Location";
import CardCaret from "./presentational/Card/Caret";
import CardSummary from "./presentational/Card/Summary";
import CardSource from "./presentational/Card/Source";
import { makeNiceDate } from "../common/utilities";
class Card extends React.Component {
constructor (props) {
super(props)
constructor(props) {
super(props);
this.state = {
isOpen: false
}
isOpen: false,
};
}
toggle () {
toggle() {
this.setState({
isOpen: !this.state.isOpen
})
isOpen: !this.state.isOpen,
});
}
makeTimelabel (datetime) {
return makeNiceDate(datetime)
makeTimelabel(datetime) {
return makeNiceDate(datetime);
}
handleCardSelect (e) {
if (!e.target.className.includes('arrow-down')) {
const selectedEventFormat = this.props.idx > 0 ? [this.props.event] : this.props.event
this.props.onSelect(selectedEventFormat, this.props.idx)
handleCardSelect(e) {
if (!e.target.className.includes("arrow-down")) {
const selectedEventFormat =
this.props.idx > 0 ? [this.props.event] : this.props.event;
this.props.onSelect(selectedEventFormat, this.props.idx);
}
}
renderSummary () {
renderSummary() {
return (
<CardSummary
language={this.props.language}
description={this.props.event.description}
isOpen={this.state.isOpen}
/>
)
);
}
renderLocation () {
renderLocation() {
return (
<CardLocation
language={this.props.language}
location={this.props.event.location}
isPrecise={(!this.props.event.type || this.props.event.type === 'Structure')}
isPrecise={
!this.props.event.type || this.props.event.type === "Structure"
}
/>
)
);
}
renderSources () {
renderSources() {
if (this.props.sourceError) {
return <div>ERROR: something went wrong loading sources, TODO:</div>
return <div>ERROR: something went wrong loading sources, TODO:</div>;
}
const sourceLang = copy[this.props.language].cardstack.sources
const sourceLang = copy[this.props.language].cardstack.sources;
return (
<div className='card-col'>
<div className="card-col">
<h4>{sourceLang}: </h4>
{this.props.event.sources.map(source => (
{this.props.event.sources.map((source) => (
<CardSource
isLoading={this.props.isLoading}
source={source}
onClickHandler={source => this.props.onViewSource(source)}
onClickHandler={(source) => this.props.onViewSource(source)}
/>
))}
</div>
)
);
}
// NB: should be internaionalized.
renderTime () {
let timelabel = this.makeTimelabel(this.props.event.datetime)
renderTime() {
const timelabel = this.makeTimelabel(this.props.event.datetime);
// let precision = this.props.event.time_display
// if (precision === '_date_only') {
@@ -97,54 +100,46 @@ class Card extends React.Component {
language={this.props.language}
timelabel={timelabel}
/>
)
);
}
renderCustomFields () {
return this.props.features.CUSTOM_EVENT_FIELDS
.map(field => {
const value = this.props.event[field.key]
return value ? (
<CardCustomField field={field} value={this.props.event[field.key]} />
) : null
})
renderCustomFields() {
return this.props.features.CUSTOM_EVENT_FIELDS.map((field) => {
const value = this.props.event[field.key];
return value ? (
<CardCustomField field={field} value={this.props.event[field.key]} />
) : null;
});
}
renderMain () {
renderMain() {
return (
<div className='card-container'>
<div className='card-row details'>
<div className="card-container">
<div className="card-row details">
{this.renderTime()}
{this.renderLocation()}
</div>
{this.renderSummary()}
{this.renderCustomFields()}
</div>
)
);
}
renderExtra () {
return (
<div className='card-bottomhalf'>
{this.renderSources()}
</div>
)
renderExtra() {
return <div className="card-bottomhalf">{this.renderSources()}</div>;
}
renderCaret () {
renderCaret() {
return this.props.features.USE_SOURCES ? (
<CardCaret
toggle={() => this.toggle()}
isOpen={this.state.isOpen}
/>
) : null
<CardCaret toggle={() => this.toggle()} isOpen={this.state.isOpen} />
) : null;
}
render () {
const { isSelected, idx } = this.props
render() {
const { isSelected, idx } = this.props;
return (
<li
className={`event-card ${isSelected ? 'selected' : ''}`}
className={`event-card ${isSelected ? "selected" : ""}`}
id={`event-card-${idx}`}
ref={this.props.innerRef}
onClick={(e) => this.handleCardSelect(e)}
@@ -153,9 +148,11 @@ class Card extends React.Component {
{this.state.isOpen ? this.renderExtra() : null}
{this.renderCaret()}
</li>
)
);
}
}
// The ref to each card will be used in CardStack for programmatic scrolling
export default React.forwardRef((props, ref) => <Card innerRef={ref} {...props} />)
export default React.forwardRef((props, ref) => (
<Card innerRef={ref} {...props} />
));

View File

@@ -29,8 +29,8 @@ class CardStack extends React.Component {
const cardScroll = this.refs[this.props.narrative.current].current
.offsetTop;
let start = element.scrollTop;
let change = cardScroll - start;
const start = element.scrollTop;
const change = cardScroll - start;
let currentTime = 0;
const increment = 20;

View File

@@ -1,32 +1,57 @@
import React from 'react'
import React from "react";
const Icon = ({ iconType }) => {
if (iconType === 'personas') {
if (iconType === "personas") {
return (
<svg x='0px' y='0px' width='40px' height='40px' viewBox='0 0 40 40' enableBackground='new 0 0 40 40'>
<path d='M15.464,17.713' />
<path d='M5.526,17.713c-1.537,0.595-3,1.472-4.314,2.637l1.114,17.081h16.338' />
<path d='M12.283,15.522c-1.707,0.661-3.332,1.636-4.792,2.93l1.238,18.979h18.153' />
<circle cx='27.432' cy='8.876' r='6.877' />
<path d='M21.297,13.088c-1.896,0.733-3.702,1.817-5.326,3.256l1.375,21.087h20.17l1.376-21.087c-1.624-1.438-3.43-2.522-5.326-3.256' />
<path d='M20.968,6.547c-0.926-0.554-2.006-0.877-3.163-0.877c-3.418,0-6.188,2.771-6.188,6.188c0,2.811,1.875,5.18,4.441,5.935' />
<path d='M12.38,8.881c-0.738-0.361-1.564-0.57-2.441-0.57c-3.076,0-5.57,2.494-5.57,5.57c0,1.983,1.04,3.72,2.601,4.707' />
<svg
x="0px"
y="0px"
width="40px"
height="40px"
viewBox="0 0 40 40"
enableBackground="new 0 0 40 40"
>
<path d="M15.464,17.713" />
<path d="M5.526,17.713c-1.537,0.595-3,1.472-4.314,2.637l1.114,17.081h16.338" />
<path d="M12.283,15.522c-1.707,0.661-3.332,1.636-4.792,2.93l1.238,18.979h18.153" />
<circle cx="27.432" cy="8.876" r="6.877" />
<path d="M21.297,13.088c-1.896,0.733-3.702,1.817-5.326,3.256l1.375,21.087h20.17l1.376-21.087c-1.624-1.438-3.43-2.522-5.326-3.256" />
<path d="M20.968,6.547c-0.926-0.554-2.006-0.877-3.163-0.877c-3.418,0-6.188,2.771-6.188,6.188c0,2.811,1.875,5.18,4.441,5.935" />
<path d="M12.38,8.881c-0.738-0.361-1.564-0.57-2.441-0.57c-3.076,0-5.57,2.494-5.57,5.57c0,1.983,1.04,3.72,2.601,4.707" />
</svg>
)
} else if (iconType === 'tipos') {
);
} else if (iconType === "tipos") {
return (
<svg x='0px' y='0px' width='40px' height='40px' viewBox='0 0 40 40' enableBackground='new 0 0 40 40'>
<path strokeDasharray='3, 4' d='M22.326,5.346
<svg
x="0px"
y="0px"
width="40px"
height="40px"
viewBox="0 0 40 40"
enableBackground="new 0 0 40 40"
>
<path
strokeDasharray="3, 4"
d="M22.326,5.346
c-2.154-2.081-5.082-3.367-8.314-3.367c-6.614,0-11.976,5.361-11.976,11.974c0,6.613,5.361,11.977,11.976,11.977
c0.228,0,0.449-0.021,0.674-0.034' />
<circle cx='23' cy='17.288' r='11.975' />
<circle strokeDasharray='3, 4' cx='25.987' cy='26.926' r='11.976' />
c0.228,0,0.449-0.021,0.674-0.034"
/>
<circle cx="23" cy="17.288" r="11.975" />
<circle strokeDasharray="3, 4" cx="25.987" cy="26.926" r="11.976" />
</svg>
)
} else if (iconType === 'hardware') {
);
} else if (iconType === "hardware") {
return (
<svg x='0px' y='0px' width='40px' height='40px' viewBox='0 0 40 40' enableBackground='new 0 0 40 40'>
<path d='M20,1.695C12.571,1.696,6.286,2.019,5.272,2.452C5.253,2.458,5.233,2.466,5.215,2.474
<svg
x="0px"
y="0px"
width="40px"
height="40px"
viewBox="0 0 40 40"
enableBackground="new 0 0 40 40"
>
<path
d="M20,1.695C12.571,1.696,6.286,2.019,5.272,2.452C5.253,2.458,5.233,2.466,5.215,2.474
c-0.01,0.004-0.019,0.008-0.027,0.012C4.38,2.831,3.803,4.256,3.802,5.907v3.502H2.926H1.175c-0.241,0-0.438,0.196-0.438,0.438
v0.875v5.254c0,0.242,0.196,0.438,0.438,0.438h1.751c0.242,0,0.438-0.195,0.438-0.438V11.16h0.438v15.324h5.691
c0.242,0,0.438,0.195,0.438,0.438v1.751c0,0.241-0.195,0.438-0.438,0.438H3.802v3.063c0,0.626,0.167,1.203,0.438,1.515v3.74
@@ -38,46 +63,83 @@ const Icon = ({ iconType }) => {
V7.22c0,0.242-0.195,0.438-0.438,0.438H4.991c-0.242,0-0.438-0.196-0.438-0.438V5.469C4.553,4.261,4.945,3.28,5.429,3.28z
M5.553,8.534h28.895c0.483,0,0.876,0.392,0.876,0.875v13.134c0,0.484-0.393,0.876-0.876,0.876h-3.466c0,0-0.863,0.613-0.912,0.613
H9.931c-0.113,0-0.225-0.022-0.33-0.065l-0.778-0.548h-3.27c-0.483,0-0.875-0.392-0.875-0.876V9.409
C4.678,8.926,5.069,8.534,5.553,8.534L5.553,8.534z' />
C4.678,8.926,5.069,8.534,5.553,8.534L5.553,8.534z"
/>
</svg>
)
} else if (iconType === 'escenas') {
);
} else if (iconType === "escenas") {
return (
<svg className='scenes' x='0px' y='0px' width='40px' height='40px' viewBox='0 0 40 40' enableBackground='new 0 0 40 40'>
<path d='M36.729,14.743v13.15l-14.225,6.693V21.438L36.729,14.743 M38.732,11.045L20.5,19.625v18.662l18.232-8.58V11.045
L38.732,11.045z' />
<path d='M4.271,14.743l14.225,6.695v13.148L4.271,27.894V14.743 M2.268,11.045v18.662l18.232,8.58V19.625L2.268,11.045L2.268,11.045
z' />
<path d='M20.5,4.844l13.289,6.202L20.5,17.247L7.209,11.046L20.5,4.844 M20.5,2.537L2.268,11.045L20.5,19.554l18.232-8.509
L20.5,2.537L20.5,2.537z' />
<svg
className="scenes"
x="0px"
y="0px"
width="40px"
height="40px"
viewBox="0 0 40 40"
enableBackground="new 0 0 40 40"
>
<path
d="M36.729,14.743v13.15l-14.225,6.693V21.438L36.729,14.743 M38.732,11.045L20.5,19.625v18.662l18.232-8.58V11.045
L38.732,11.045z"
/>
<path
d="M4.271,14.743l14.225,6.695v13.148L4.271,27.894V14.743 M2.268,11.045v18.662l18.232,8.58V19.625L2.268,11.045L2.268,11.045
z"
/>
<path
d="M20.5,4.844l13.289,6.202L20.5,17.247L7.209,11.046L20.5,4.844 M20.5,2.537L2.268,11.045L20.5,19.554l18.232-8.509
L20.5,2.537L20.5,2.537z"
/>
</svg>
)
} else if (iconType === 'docs') {
);
} else if (iconType === "docs") {
return (
<svg x='0px' y='0px' width='40px' height='40px' viewBox='0 0 40 40' enableBackground='new 0 0 40 40'>
<path d='M31.543,5.987V3.158
c0-1.103-0.095-1.197-1.197-1.197H4.791c-1.103,0-1.198,0.095-1.198,1.197V32.84c0,1.103,0.095,1.197,1.198,1.197h2.829' />
<path d='M35.57,36.866
c0,1.103-0.096,1.198-1.198,1.198H8.817c-1.103,0-1.198-0.096-1.198-1.198V7.185c0-1.103,0.095-1.197,1.198-1.197h25.555
c1.103,0,1.198,0.095,1.198,1.197V36.866z' />
<path d='M58.755,29.633' />
<path d='M21.86,40.072' />
<path d='M-22.755,58.555' />
<line x1='11.612' y1='11.977' x2='31.577' y2='11.977' />
<line x1='11.612' y1='17.966' x2='31.577' y2='17.966' />
<line x1='11.612' y1='29.945' x2='31.577' y2='29.945' />
<line x1='11.612' y1='23.955' x2='31.577' y2='23.955' />
<svg
x="0px"
y="0px"
width="40px"
height="40px"
viewBox="0 0 40 40"
enableBackground="new 0 0 40 40"
>
<path
d="M31.543,5.987V3.158
c0-1.103-0.095-1.197-1.197-1.197H4.791c-1.103,0-1.198,0.095-1.198,1.197V32.84c0,1.103,0.095,1.197,1.198,1.197h2.829"
/>
<path
d="M35.57,36.866
c0,1.103-0.096,1.198-1.198,1.198H8.817c-1.103,0-1.198-0.096-1.198-1.198V7.185c0-1.103,0.095-1.197,1.198-1.197h25.555
c1.103,0,1.198,0.095,1.198,1.197V36.866z"
/>
<path d="M58.755,29.633" />
<path d="M21.86,40.072" />
<path d="M-22.755,58.555" />
<line x1="11.612" y1="11.977" x2="31.577" y2="11.977" />
<line x1="11.612" y1="17.966" x2="31.577" y2="17.966" />
<line x1="11.612" y1="29.945" x2="31.577" y2="29.945" />
<line x1="11.612" y1="23.955" x2="31.577" y2="23.955" />
</svg>
)
} else if (iconType === 'search') {
);
} else if (iconType === "search") {
return (
<svg x='0px' y='0px' width='40px' height='40px' viewBox='0 0 40 40' enableBackground='new 0 0 40 40'>
<circle cx='18.306' cy='18.307' r='13.856' />
<path strokeLinecap='round' strokeLinejoin='round' d='M28.24,28.24
l8.346,8.346L28.24,28.24z' />
<svg
x="0px"
y="0px"
width="40px"
height="40px"
viewBox="0 0 40 40"
enableBackground="new 0 0 40 40"
>
<circle cx="18.306" cy="18.307" r="13.856" />
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M28.24,28.24
l8.346,8.346L28.24,28.24z"
/>
</svg>
)
);
}
}
};
export default Icon
export default Icon;

View File

@@ -1,6 +1,6 @@
import React from 'react'
import Popup from './presentational/Popup'
import copy from '../common/data/copy.json'
import React from "react";
import Popup from "./presentational/Popup";
import copy from "../common/data/copy.json";
export default ({ isOpen, onClose, language, styles }) => (
<Popup
@@ -10,4 +10,4 @@ export default ({ isOpen, onClose, language, styles }) => (
isOpen={isOpen}
styles={styles}
/>
)
);

View File

@@ -1,304 +1,321 @@
/* global alert, Event */
import React from 'react'
import React from "react";
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import * as actions from '../actions'
import * as selectors from '../selectors'
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import * as actions from "../actions";
import * as selectors from "../selectors";
import MediaOverlay from './Overlay/Media'
import LoadingOverlay from './Overlay/Loading'
import Map from './Map.jsx'
import Toolbar from './Toolbar/Layout'
import CardStack from './CardStack.jsx'
import MediaOverlay from "./Overlay/Media";
import LoadingOverlay from "./Overlay/Loading";
import Map from "./Map.jsx";
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 Popup from './presentational/Popup'
import Timeline from './Timeline.jsx'
import Notification from './Notification.jsx'
import StateOptions from './StateOptions.jsx'
import StaticPage from './StaticPage'
import TemplateCover from './TemplateCover'
import NarrativeControls from "./presentational/Narrative/Controls.js";
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";
import StaticPage from "./StaticPage";
import TemplateCover from "./TemplateCover";
import colors from '../common/global'
import { binarySearch, insetSourceFrom } from '../common/utilities'
import { isMobileOnly } from 'react-device-detect'
import Search from './Search.jsx'
import colors from "../common/global";
import { binarySearch, insetSourceFrom } from "../common/utilities";
import { isMobileOnly } from "react-device-detect";
import Search from "./Search.jsx";
class Dashboard extends React.Component {
constructor (props) {
super(props)
constructor(props) {
super(props);
this.handleViewSource = this.handleViewSource.bind(this)
this.handleHighlight = this.handleHighlight.bind(this)
this.setNarrative = this.setNarrative.bind(this)
this.setNarrativeFromFilters = this.setNarrativeFromFilters.bind(this)
this.handleSelect = this.handleSelect.bind(this)
this.getCategoryColor = this.getCategoryColor.bind(this)
this.findEventIdx = this.findEventIdx.bind(this)
this.onKeyDown = this.onKeyDown.bind(this)
this.selectNarrativeStep = this.selectNarrativeStep.bind(this)
this.handleViewSource = this.handleViewSource.bind(this);
this.handleHighlight = this.handleHighlight.bind(this);
this.setNarrative = this.setNarrative.bind(this);
this.setNarrativeFromFilters = this.setNarrativeFromFilters.bind(this);
this.handleSelect = this.handleSelect.bind(this);
this.getCategoryColor = this.getCategoryColor.bind(this);
this.findEventIdx = this.findEventIdx.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.selectNarrativeStep = this.selectNarrativeStep.bind(this);
}
componentDidMount () {
componentDidMount() {
if (!this.props.app.isMobile) {
this.props.actions.fetchDomain()
.then(domain =>
this.props.actions.updateDomain({
domain,
features: this.props.features
}))
this.props.actions.fetchDomain().then((domain) =>
this.props.actions.updateDomain({
domain,
features: this.props.features,
})
);
}
// NOTE: hack to get the timeline to always show. Not entirely sure why
// this is necessary.
window.dispatchEvent(new Event('resize'))
window.dispatchEvent(new Event("resize"));
}
handleHighlight (highlighted) {
this.props.actions.updateHighlighted((highlighted) || null)
handleHighlight(highlighted) {
this.props.actions.updateHighlighted(highlighted || null);
}
handleViewSource (source) {
this.props.actions.updateSource(source)
handleViewSource(source) {
this.props.actions.updateSource(source);
}
findEventIdx (theEvent) {
const { events } = this.props.domain
return binarySearch(
events,
theEvent,
(theev, otherev) => {
return theev.datetime - otherev.datetime
}
)
findEventIdx(theEvent) {
const { events } = this.props.domain;
return binarySearch(events, theEvent, (theev, otherev) => {
return theev.datetime - otherev.datetime;
});
}
handleSelect (selected, axis) {
const matchedEvents = []
const TIMELINE_AXIS = 0
handleSelect(selected, axis) {
const matchedEvents = [];
const TIMELINE_AXIS = 0;
if (axis === TIMELINE_AXIS) {
matchedEvents.push(selected)
matchedEvents.push(selected);
// find in events
const { events } = this.props.domain
const idx = this.findEventIdx(selected)
const { events } = this.props.domain;
const idx = this.findEventIdx(selected);
// check events before
let ptr = idx - 1
let ptr = idx - 1;
while (
ptr >= 0 &&
(events[idx].datetime).getTime() === (events[ptr].datetime).getTime()
events[idx].datetime.getTime() === events[ptr].datetime.getTime()
) {
if (events[ptr].id !== selected.id) {
matchedEvents.push(events[ptr])
matchedEvents.push(events[ptr]);
}
ptr -= 1
ptr -= 1;
}
// check events after
ptr = idx + 1
ptr = idx + 1;
while (
ptr < events.length &&
(events[idx].datetime).getTime() === (events[ptr].datetime).getTime()
events[idx].datetime.getTime() === events[ptr].datetime.getTime()
) {
if (events[ptr].id !== selected.id) {
matchedEvents.push(events[ptr])
matchedEvents.push(events[ptr]);
}
ptr += 1
ptr += 1;
}
} else { // Map..
const std = { ...selected }
delete std.sources
Object.values(std).forEach(ev => matchedEvents.push(ev))
}
this.props.actions.updateSelected(matchedEvents)
}
getCategoryColor (category) {
if (!this.props.features.USE_CATEGORIES) { return colors.fallbackEventColor }
const cat = this.props.ui.style.categories[category]
if (cat) {
return cat
} else {
return this.props.ui.style.categories['default']
// Map..
const std = { ...selected };
delete std.sources;
Object.values(std).forEach((ev) => matchedEvents.push(ev));
}
this.props.actions.updateSelected(matchedEvents);
}
getCategoryColor(category) {
if (!this.props.features.USE_CATEGORIES) {
return colors.fallbackEventColor;
}
const cat = this.props.ui.style.categories[category];
if (cat) {
return cat;
} else {
return this.props.ui.style.categories.default;
}
}
setNarrative (narrative) {
setNarrative(narrative) {
// only handleSelect if narrative is not null and has associated events
if (narrative && narrative.steps.length >= 1) {
this.handleSelect([ narrative.steps[0] ])
this.handleSelect([narrative.steps[0]]);
}
this.props.actions.updateNarrative(narrative)
this.props.actions.updateNarrative(narrative);
}
setNarrativeFromFilters (withSteps) {
const { app, domain } = this.props
let activeFilters = app.associations.filters
setNarrativeFromFilters(withSteps) {
const { app, domain } = this.props;
let activeFilters = app.associations.filters;
if (activeFilters.length === 0) {
alert('No filters selected, cant narrativise')
return
alert("No filters selected, cant narrativise");
return;
}
activeFilters = activeFilters.map(f => ({ name: f }))
activeFilters = activeFilters.map((f) => ({ name: f }));
const evs = domain.events.filter(ev => {
let hasOne = false
const evs = domain.events.filter((ev) => {
let hasOne = false;
// add event if it has at least one matching filter
for (let i = 0; i < activeFilters.length; i++) {
if (ev.associations.includes(activeFilters[i].name)) {
hasOne = true
break
hasOne = true;
break;
}
}
if (hasOne) return true
return false
})
if (hasOne) return true;
return false;
});
if (evs.length === 0) {
alert('No associated events, cant narrativise')
return
alert("No associated events, cant narrativise");
return;
}
const name = activeFilters.map(f => f.name).join('-')
const desc = activeFilters.map(f => f.description).join('\n\n')
const name = activeFilters.map((f) => f.name).join("-");
const desc = activeFilters.map((f) => f.description).join("\n\n");
this.setNarrative({
id: name,
label: name,
description: desc,
withLines: withSteps,
steps: evs.map(insetSourceFrom(domain.sources))
})
steps: evs.map(insetSourceFrom(domain.sources)),
});
}
selectNarrativeStep (idx) {
selectNarrativeStep(idx) {
// Try to find idx if event passed rather than number
if (typeof idx !== 'number') {
let e = idx[0] || idx
if (typeof idx !== "number") {
const e = idx[0] || idx;
if (this.props.app.associations.narrative) {
const { steps } = this.props.app.associations.narrative
const { steps } = this.props.app.associations.narrative;
// choose the first event at a given location
const locationEventId = e.id
const narrativeIdxObj = steps.find(s => s.id === locationEventId)
let narrativeIdx = steps.indexOf(narrativeIdxObj)
const locationEventId = e.id;
const narrativeIdxObj = steps.find((s) => s.id === locationEventId);
const narrativeIdx = steps.indexOf(narrativeIdxObj);
if (narrativeIdx > -1) {
idx = narrativeIdx
idx = narrativeIdx;
}
}
}
const { narrative } = this.props.app.associations
if (narrative === null) return
const { narrative } = this.props.app.associations;
if (narrative === null) return;
if (idx < narrative.steps.length && idx >= 0) {
const step = narrative.steps[idx]
const step = narrative.steps[idx];
this.handleSelect([step])
this.props.actions.updateNarrativeStepIdx(idx)
this.handleSelect([step]);
this.props.actions.updateNarrativeStepIdx(idx);
}
}
onKeyDown (e) {
const { narrative, selected } = this.props.app
const { events } = this.props.domain
onKeyDown(e) {
const { narrative, selected } = this.props.app;
const { events } = this.props.domain;
const prev = idx => {
const prev = (idx) => {
if (narrative === null) {
this.handleSelect(events[idx - 1], 0)
this.handleSelect(events[idx - 1], 0);
} else {
this.selectNarrativeStep(this.props.narrativeIdx - 1)
this.selectNarrativeStep(this.props.narrativeIdx - 1);
}
}
const next = idx => {
};
const next = (idx) => {
if (narrative === null) {
this.handleSelect(events[idx + 1], 0)
this.handleSelect(events[idx + 1], 0);
} else {
this.selectNarrativeStep(this.props.narrativeIdx + 1)
this.selectNarrativeStep(this.props.narrativeIdx + 1);
}
}
};
if (selected.length > 0) {
const ev = selected[selected.length - 1]
const idx = this.findEventIdx(ev)
const ev = selected[selected.length - 1];
const idx = this.findEventIdx(ev);
switch (e.keyCode) {
case 37: // left arrow
case 38: // up arrow
if (idx <= 0) return
prev(idx)
break
if (idx <= 0) return;
prev(idx);
break;
case 39: // right arrow
case 40: // down arrow
if (idx < 0 || idx >= this.props.domain.length - 1) return
next(idx)
break
if (idx < 0 || idx >= this.props.domain.length - 1) return;
next(idx);
break;
default:
}
}
}
renderIntroPopup (isMobile, styles) {
const { app, actions } = this.props
renderIntroPopup(isMobile, styles) {
const { app, actions } = this.props;
const extraContent = isMobile ? <div style={{ position: 'relative', bottom: 0 }}>
<h3 style={{ color: 'var(--error-red)' }}>This platform is not suitable for mobile.<br /><br />Please re-visit the site on a device with a larger screen.</h3>
</div> : null
const extraContent = isMobile ? (
<div style={{ position: "relative", bottom: 0 }}>
<h3 style={{ color: "var(--error-red)" }}>
This platform is not suitable for mobile.
<br />
<br />
Please re-visit the site on a device with a larger screen.
</h3>
</div>
) : null;
return <Popup
title='Introduction to the platform'
theme='dark'
isOpen={app.flags.isIntropopup}
onClose={actions.toggleIntroPopup}
content={app.intro}
styles={styles}
isMobile={isMobile}
>
{extraContent}
</Popup>
return (
<Popup
title="Introduction to the platform"
theme="dark"
isOpen={app.flags.isIntropopup}
onClose={actions.toggleIntroPopup}
content={app.intro}
styles={styles}
isMobile={isMobile}
>
{extraContent}
</Popup>
);
}
render () {
const { actions, app, domain, features } = this.props
const dateHeight = 80
const padding = 2
const checkMobile = (isMobileOnly || window.innerWidth < 600)
render() {
const { actions, app, domain, features } = this.props;
const dateHeight = 80;
const padding = 2;
const checkMobile = isMobileOnly || window.innerWidth < 600;
const popupStyles = {
height: checkMobile ? '100vh' : 'fit-content',
display: checkMobile ? 'block' : 'table',
width: checkMobile ? '100vw' : window.innerWidth > 768 ? '60vw' : `calc(100vw - var(--toolbar-width))`,
maxWidth: checkMobile ? '100vw' : 600,
maxHeight: checkMobile ? '100vh' : window.innerHeight > 768 ? `calc(100vh - ${app.timeline.dimensions.height}px - ${dateHeight}px)` : `100vh`,
left: checkMobile ? padding : 'var(--toolbar-width)',
height: checkMobile ? "100vh" : "fit-content",
display: checkMobile ? "block" : "table",
width: checkMobile
? "100vw"
: window.innerWidth > 768
? "60vw"
: "calc(100vw - var(--toolbar-width))",
maxWidth: checkMobile ? "100vw" : 600,
maxHeight: checkMobile
? "100vh"
: window.innerHeight > 768
? `calc(100vh - ${app.timeline.dimensions.height}px - ${dateHeight}px)`
: "100vh",
left: checkMobile ? padding : "var(--toolbar-width)",
top: 0,
overflowY: 'scroll'
}
overflowY: "scroll",
};
if (checkMobile) {
const msg = 'This platform is not suitable for mobile. Please re-visit the site on a device with a larger screen.'
const msg =
"This platform is not suitable for mobile. Please re-visit the site on a device with a larger screen.";
return (
<div>
{(features.USE_COVER && !app.intro) && (
{features.USE_COVER && !app.intro && (
<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 */}
<TemplateCover showAppHandler={() => {
/* eslint-disable no-undef */
alert(msg)
/* eslint-enable no-undef */
}} />
<TemplateCover
showAppHandler={() => {
/* eslint-disable no-undef */
alert(msg);
/* eslint-enable no-undef */
}}
/>
</StaticPage>
)}
{app.intro && <>
{this.renderIntroPopup(true, popupStyles)}
</>}
{app.intro && <>{this.renderIntroPopup(true, popupStyles)}</>}
{!app.intro && !features.USE_COVER && (
<div className='fixedTooSmallMessage'>{msg}</div>
<div className="fixedTooSmallMessage">{msg}</div>
)}
</div>
)
);
}
return (
@@ -307,9 +324,11 @@ class Dashboard extends React.Component {
isNarrative={!!app.associations.narrative}
methods={{
onTitle: actions.toggleCover,
onSelectFilter: filters => actions.toggleAssociations('filters', filters),
onCategoryFilter: categories => actions.toggleAssociations('categories', categories),
onSelectNarrative: this.setNarrative
onSelectFilter: (filters) =>
actions.toggleAssociations("filters", filters),
onCategoryFilter: (categories) =>
actions.toggleAssociations("categories", categories),
onSelectNarrative: this.setNarrative,
}}
/>
<Map
@@ -317,39 +336,54 @@ class Dashboard extends React.Component {
methods={{
onSelectNarrative: this.setNarrative,
getCategoryColor: this.getCategoryColor,
onSelect: app.associations.narrative ? this.selectNarrativeStep : ev => this.handleSelect(ev, 1)
onSelect: app.associations.narrative
? this.selectNarrativeStep
: (ev) => this.handleSelect(ev, 1),
}}
/>
<Timeline
onKeyDown={this.onKeyDown}
methods={{
onSelect: app.associations.narrative ? this.selectNarrativeStep : ev => this.handleSelect(ev, 0),
onSelect: app.associations.narrative
? this.selectNarrativeStep
: (ev) => this.handleSelect(ev, 0),
onUpdateTimerange: actions.updateTimeRange,
getCategoryColor: this.getCategoryColor
getCategoryColor: this.getCategoryColor,
}}
/>
<CardStack
timelineDims={app.timeline.dimensions}
onViewSource={this.handleViewSource}
onSelect={app.associations.narrative ? this.selectNarrativeStep : () => null}
onSelect={
app.associations.narrative ? this.selectNarrativeStep : () => null
}
onHighlight={this.handleHighlight}
onToggleCardstack={() => actions.updateSelected([])}
getCategoryColor={this.getCategoryColor}
/>
<StateOptions
showing={this.props.narratives && this.props.narratives.length !== 0 && !app.associations.narrative && app.associations.filters.length > 0}
showing={
this.props.narratives &&
this.props.narratives.length !== 0 &&
!app.associations.narrative &&
app.associations.filters.length > 0
}
timelineDims={app.timeline.dimensions}
onClickHandler={this.setNarrativeFromFilters}
/>
<NarrativeControls
narrative={app.associations.narrative ? {
...app.associations.narrative,
current: this.props.narrativeIdx
} : null}
narrative={
app.associations.narrative
? {
...app.associations.narrative,
current: this.props.narrativeIdx,
}
: null
}
methods={{
onNext: () => this.selectNarrativeStep(this.props.narrativeIdx + 1),
onPrev: () => this.selectNarrativeStep(this.props.narrativeIdx - 1),
onSelectNarrative: this.setNarrative
onSelectNarrative: this.setNarrative,
}}
/>
<InfoPopup
@@ -359,24 +393,27 @@ class Dashboard extends React.Component {
onClose={actions.toggleInfoPopup}
/>
{this.renderIntroPopup(false, popupStyles)}
{app.debug ? <Notification
isNotification={app.flags.isNotification}
notifications={domain.notifications}
onToggle={actions.markNotificationsRead}
/> : null}
{features.USE_SEARCH && (<Search
narrative={app.narrative}
queryString={app.searchQuery}
events={domain.events}
onSearchRowClick={this.handleSelect}
/>)}
{app.debug ? (
<Notification
isNotification={app.flags.isNotification}
notifications={domain.notifications}
onToggle={actions.markNotificationsRead}
/>
) : null}
{features.USE_SEARCH && (
<Search
narrative={app.narrative}
queryString={app.searchQuery}
events={domain.events}
onSearchRowClick={this.handleSelect}
/>
)}
{app.source ? (
<MediaOverlay
source={app.source}
onCancel={() => {
actions.updateSource(null)
}
}
actions.updateSource(null);
}}
/>
) : null}
<LoadingOverlay
@@ -388,26 +425,29 @@ class Dashboard extends React.Component {
<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 */}
<TemplateCover showing={app.flags.isCover} showAppHandler={actions.toggleCover} />
<TemplateCover
showing={app.flags.isCover}
showAppHandler={actions.toggleCover}
/>
</StaticPage>
)}
</div>
)
);
}
}
function mapDispatchToProps (dispatch) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch)
}
actions: bindActionCreators(actions, dispatch),
};
}
export default connect(
state => ({
(state) => ({
...state,
narrativeIdx: selectors.selectNarrativeIdx(state),
narratives: selectors.selectNarratives(state),
selected: selectors.selectSelected(state)
selected: selectors.selectSelected(state),
}),
mapDispatchToProps
)(Dashboard)
)(Dashboard);

View File

@@ -1,255 +1,302 @@
/* global L, Event */
import React from 'react'
import { Portal } from 'react-portal'
import Supercluster from 'supercluster'
import React from "react";
import { Portal } from "react-portal";
import Supercluster from "supercluster";
import { connect } from 'react-redux'
import * as selectors from '../selectors'
import { connect } from "react-redux";
import * as selectors from "../selectors";
import 'leaflet'
import "leaflet";
import Sites from './presentational/Map/Sites.jsx'
import Shapes from './presentational/Map/Shapes.jsx'
import Events from './presentational/Map/Events.jsx'
import Clusters from './presentational/Map/Clusters.jsx'
import SelectedEvents from './presentational/Map/SelectedEvents.jsx'
import Narratives from './presentational/Map/Narratives'
import DefsMarkers from './presentational/Map/DefsMarkers.jsx'
import LoadingOverlay from '../components/Overlay/Loading'
import Sites from "./presentational/Map/Sites.jsx";
import Shapes from "./presentational/Map/Shapes.jsx";
import Events from "./presentational/Map/Events.jsx";
import Clusters from "./presentational/Map/Clusters.jsx";
import SelectedEvents from "./presentational/Map/SelectedEvents.jsx";
import Narratives from "./presentational/Map/Narratives";
import DefsMarkers from "./presentational/Map/DefsMarkers.jsx";
import LoadingOverlay from "../components/Overlay/Loading";
import { mapClustersToLocations, isIdentical, isLatitude, isLongitude, calculateTotalClusterPoints, calcClusterSize } from '../common/utilities'
import {
mapClustersToLocations,
isIdentical,
isLatitude,
isLongitude,
calculateTotalClusterPoints,
calcClusterSize,
} from "../common/utilities";
// NB: important constants for map, TODO: make statics
const supportedMapboxMap = ['streets', 'satellite']
const defaultToken = 'your_token'
const supportedMapboxMap = ["streets", "satellite"];
const defaultToken = "your_token";
class Map extends React.Component {
constructor () {
super()
this.projectPoint = this.projectPoint.bind(this)
this.onClusterSelect = this.onClusterSelect.bind(this)
this.loadClusterData = this.loadClusterData.bind(this)
this.getClusterChildren = this.getClusterChildren.bind(this)
this.svgRef = React.createRef()
this.map = null
this.superclusterIndex = null
constructor() {
super();
this.projectPoint = this.projectPoint.bind(this);
this.onClusterSelect = this.onClusterSelect.bind(this);
this.loadClusterData = this.loadClusterData.bind(this);
this.getClusterChildren = this.getClusterChildren.bind(this);
this.svgRef = React.createRef();
this.map = null;
this.superclusterIndex = null;
this.state = {
mapTransformX: 0,
mapTransformY: 0,
indexLoaded: false,
clusters: []
}
this.styleLocation = this.styleLocation.bind(this)
clusters: [],
};
this.styleLocation = this.styleLocation.bind(this);
}
componentDidMount () {
componentDidMount() {
if (this.map === null) {
this.initializeMap()
this.initializeMap();
}
window.dispatchEvent(new Event('resize'))
window.dispatchEvent(new Event("resize"));
}
componentWillReceiveProps (nextProps) {
componentWillReceiveProps(nextProps) {
if (!isIdentical(nextProps.domain.locations, this.props.domain.locations)) {
this.loadClusterData(nextProps.domain.locations)
this.loadClusterData(nextProps.domain.locations);
}
// Set appropriate zoom for narrative
const { bounds } = nextProps.app.map
if (!isIdentical(bounds, this.props.app.map.bounds) &&
bounds !== null) {
this.map.fitBounds(bounds)
const { bounds } = nextProps.app.map;
if (!isIdentical(bounds, this.props.app.map.bounds) && bounds !== null) {
this.map.fitBounds(bounds);
} else {
if (!isIdentical(nextProps.app.selected, this.props.app.selected)) {
// Fly to first of events selected
const eventPoint = (nextProps.app.selected.length > 0) ? nextProps.app.selected[0] : null
const eventPoint =
nextProps.app.selected.length > 0 ? nextProps.app.selected[0] : null;
if (eventPoint !== null && eventPoint.latitude && eventPoint.longitude) {
if (
eventPoint !== null &&
eventPoint.latitude &&
eventPoint.longitude
) {
// this.map.setView([eventPoint.latitude, eventPoint.longitude])
this.map.setView([eventPoint.latitude, eventPoint.longitude], this.map.getZoom(), {
'animate': true,
'pan': {
'duration': 0.7
this.map.setView(
[eventPoint.latitude, eventPoint.longitude],
this.map.getZoom(),
{
animate: true,
pan: {
duration: 0.7,
},
}
})
);
}
}
}
}
initializeMap () {
initializeMap() {
/**
* Creates a Leaflet map and a tilelayer for the map background
*/
const { map: mapConfig, cluster: clusterConfig } = this.props.app
const { map: mapConfig, cluster: clusterConfig } = this.props.app;
const map =
L.map(this.props.ui.dom.map)
.setView(mapConfig.anchor, mapConfig.startZoom)
.setMinZoom(mapConfig.minZoom)
.setMaxZoom(mapConfig.maxZoom)
.setMaxBounds(mapConfig.maxBounds)
const map = L.map(this.props.ui.dom.map)
.setView(mapConfig.anchor, mapConfig.startZoom)
.setMinZoom(mapConfig.minZoom)
.setMaxZoom(mapConfig.maxZoom)
.setMaxBounds(mapConfig.maxBounds);
// Initialize supercluster index
this.superclusterIndex = new Supercluster(clusterConfig)
this.superclusterIndex = new Supercluster(clusterConfig);
let firstLayer
let firstLayer;
if ((supportedMapboxMap.indexOf(this.props.ui.tiles) !== -1) && process.env.MAPBOX_TOKEN && process.env.MAPBOX_TOKEN !== defaultToken) {
if (
supportedMapboxMap.indexOf(this.props.ui.tiles) !== -1 &&
process.env.MAPBOX_TOKEN &&
process.env.MAPBOX_TOKEN !== defaultToken
) {
firstLayer = L.tileLayer(
`http://a.tiles.mapbox.com/v4/mapbox.${this.props.ui.tiles}/{z}/{x}/{y}@2x.png?access_token=${process.env.MAPBOX_TOKEN}`
)
} else if (process.env.MAPBOX_TOKEN && process.env.MAPBOX_TOKEN !== defaultToken) {
);
} else if (
process.env.MAPBOX_TOKEN &&
process.env.MAPBOX_TOKEN !== defaultToken
) {
firstLayer = L.tileLayer(
`http://a.tiles.mapbox.com/styles/v1/${this.props.ui.tiles}/tiles/{z}/{x}/{y}?access_token=${process.env.MAPBOX_TOKEN}`
)
);
} else {
firstLayer = L.tileLayer(
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
)
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
);
}
firstLayer.addTo(map)
firstLayer.addTo(map);
map.keyboard.disable()
map.zoomControl.remove()
map.keyboard.disable();
map.zoomControl.remove();
map.on('moveend', () => {
this.updateClusters()
this.alignLayers()
})
map.on("moveend", () => {
this.updateClusters();
this.alignLayers();
});
map.on('move zoomend viewreset', () => this.alignLayers())
map.on('zoomstart', () => { if (this.svgRef.current !== null) this.svgRef.current.classList.add('hide') })
map.on('zoomend', () => { if (this.svgRef.current !== null) this.svgRef.current.classList.remove('hide') })
window.addEventListener('resize', () => { this.alignLayers() })
map.on("move zoomend viewreset", () => this.alignLayers());
map.on("zoomstart", () => {
if (this.svgRef.current !== null)
this.svgRef.current.classList.add("hide");
});
map.on("zoomend", () => {
if (this.svgRef.current !== null)
this.svgRef.current.classList.remove("hide");
});
window.addEventListener("resize", () => {
this.alignLayers();
});
this.map = map
this.map = map;
}
getMapDetails () {
const bounds = this.map.getBounds()
const bbox = [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()]
const zoom = this.map.getZoom()
return [bbox, zoom]
getMapDetails() {
const bounds = this.map.getBounds();
const bbox = [
bounds.getWest(),
bounds.getSouth(),
bounds.getEast(),
bounds.getNorth(),
];
const zoom = this.map.getZoom();
return [bbox, zoom];
}
updateClusters () {
const [bbox, zoom] = this.getMapDetails()
updateClusters() {
const [bbox, zoom] = this.getMapDetails();
if (this.superclusterIndex && this.state.indexLoaded) {
this.setState({
clusters: this.superclusterIndex.getClusters(bbox, zoom)
})
clusters: this.superclusterIndex.getClusters(bbox, zoom),
});
}
}
loadClusterData (locations) {
loadClusterData(locations) {
if (locations && locations.length > 0 && this.superclusterIndex) {
const convertedLocations = locations.reduce((acc, loc) => {
const { longitude, latitude } = loc
const validCoordinates = isLatitude(latitude) && isLongitude(longitude)
const { longitude, latitude } = loc;
const validCoordinates = isLatitude(latitude) && isLongitude(longitude);
if (validCoordinates) {
const feature = {
type: 'Feature',
type: "Feature",
properties: {
cluster: false,
id: loc.label
id: loc.label,
},
geometry: {
type: 'Point',
coordinates: [longitude, latitude]
}
}
acc.push(feature)
type: "Point",
coordinates: [longitude, latitude],
},
};
acc.push(feature);
}
return acc
}, [])
this.superclusterIndex.load(convertedLocations)
return acc;
}, []);
this.superclusterIndex.load(convertedLocations);
this.setState({ indexLoaded: true }, () => {
this.updateClusters()
})
this.updateClusters();
});
} else {
this.setState({ clusters: [] })
this.setState({ clusters: [] });
}
}
getClusterChildren (clusterId) {
getClusterChildren(clusterId) {
if (this.superclusterIndex) {
try {
const children = this.superclusterIndex.getLeaves(clusterId, Infinity, 0)
return mapClustersToLocations(children, this.props.domain.locations)
const children = this.superclusterIndex.getLeaves(
clusterId,
Infinity,
0
);
return mapClustersToLocations(children, this.props.domain.locations);
} catch (err) {
return []
return [];
}
}
return []
return [];
}
getSelectedClusters () {
const { selected } = this.props.app
const selectedIds = selected.map(sl => sl.id)
getSelectedClusters() {
const { selected } = this.props.app;
const selectedIds = selected.map((sl) => sl.id);
if (this.state.clusters && this.state.clusters.length > 0) {
return this.state.clusters.reduce((acc, cl) => {
if (cl.properties.cluster) {
const children = this.getClusterChildren(cl.properties.cluster_id)
const children = this.getClusterChildren(cl.properties.cluster_id);
if (children && children.length > 0) {
children.forEach(child => {
const clusterPresent = acc.findIndex(item => item.id === cl.id) >= 0
children.forEach((child) => {
const clusterPresent =
acc.findIndex((item) => item.id === cl.id) >= 0;
if (selectedIds.includes(child.id) && !clusterPresent) {
acc.push(cl)
acc.push(cl);
}
})
});
}
}
return acc
}, [])
return acc;
}, []);
}
return []
return [];
}
alignLayers () {
const mapNode = document.querySelector('.leaflet-map-pane')
if (mapNode === null) return { transformX: 0, transformY: 0 }
alignLayers() {
const mapNode = document.querySelector(".leaflet-map-pane");
if (mapNode === null) return { transformX: 0, transformY: 0 };
// We'll get the transform of the leaflet container,
// which will let us offset the SVG by the same quantity
const transform = window
.getComputedStyle(mapNode)
.getPropertyValue('transform')
.getPropertyValue("transform");
// Offset with leaflet map transform boundaries
this.setState({
mapTransformX: +transform.split(',')[4],
mapTransformY: +transform.split(',')[5].split(')')[0]
})
mapTransformX: +transform.split(",")[4],
mapTransformY: +transform.split(",")[5].split(")")[0],
});
}
projectPoint (location) {
const latLng = new L.LatLng(location[0], location[1])
projectPoint(location) {
const latLng = new L.LatLng(location[0], location[1]);
return {
x: this.map.latLngToLayerPoint(latLng).x + this.state.mapTransformX,
y: this.map.latLngToLayerPoint(latLng).y + this.state.mapTransformY
}
y: this.map.latLngToLayerPoint(latLng).y + this.state.mapTransformY,
};
}
onClusterSelect ({ id, latitude, longitude }) {
const expansionZoom = Math.max(this.superclusterIndex.getClusterExpansionZoom(parseInt(id)), this.superclusterIndex.options.minZoom)
const zoomLevelsToSkip = 2
const zoomToFly = Math.max(expansionZoom + zoomLevelsToSkip, this.props.app.cluster.maxZoom)
this.map.flyTo(new L.LatLng(latitude, longitude), zoomToFly)
onClusterSelect({ id, latitude, longitude }) {
const expansionZoom = Math.max(
this.superclusterIndex.getClusterExpansionZoom(parseInt(id)),
this.superclusterIndex.options.minZoom
);
const zoomLevelsToSkip = 2;
const zoomToFly = Math.max(
expansionZoom + zoomLevelsToSkip,
this.props.app.cluster.maxZoom
);
this.map.flyTo(new L.LatLng(latitude, longitude), zoomToFly);
}
getClientDims () {
const boundingClient = document.querySelector(`#${this.props.ui.dom.map}`).getBoundingClientRect()
getClientDims() {
const boundingClient = document
.querySelector(`#${this.props.ui.dom.map}`)
.getBoundingClientRect();
return {
width: boundingClient.width,
height: boundingClient.height
}
height: boundingClient.height,
};
}
renderTiles () {
const pane = this.map.getPanes().overlayPane
const { width, height } = this.getClientDims()
renderTiles() {
const pane = this.map.getPanes().overlayPane;
const { width, height } = this.getClientDims();
return this.map ? (
<Portal node={pane}>
@@ -257,24 +304,27 @@ class Map extends React.Component {
ref={this.svgRef}
width={width}
height={height}
style={{ transform: `translate3d(${-this.state.mapTransformX}px, ${-this.state.mapTransformY}px, 0)` }}
className='leaflet-svg'
style={{
transform: `translate3d(${-this.state.mapTransformX}px, ${-this
.state.mapTransformY}px, 0)`,
}}
className="leaflet-svg"
/>
</Portal>
) : null
) : null;
}
renderSites () {
renderSites() {
return (
<Sites
sites={this.props.domain.sites}
projectPoint={this.projectPoint}
isEnabled={this.props.app.views.sites}
/>
)
);
}
renderShapes () {
renderShapes() {
return (
<Shapes
svg={this.svgRef.current}
@@ -282,22 +332,26 @@ class Map extends React.Component {
projectPoint={this.projectPoint}
styles={this.props.ui.shapes}
/>
)
);
}
renderNarratives () {
const hasNarratives = this.props.domain.narratives.length > 0
renderNarratives() {
const hasNarratives = this.props.domain.narratives.length > 0;
return (
<Narratives
svg={this.svgRef.current}
narratives={hasNarratives ? this.props.domain.narratives : [this.props.app.narrative]}
narratives={
hasNarratives
? this.props.domain.narratives
: [this.props.app.narrative]
}
projectPoint={this.projectPoint}
narrative={this.props.app.narrative}
styles={this.props.ui.narratives}
onSelectNarrative={this.props.methods.onSelectNarrative}
features={this.props.features}
/>
)
);
}
/**
@@ -309,22 +363,27 @@ class Map extends React.Component {
* at the second index is an optional additional component that renders in
* the <g/> div.
*/
styleLocation (location) {
return [null, null]
styleLocation(location) {
return [null, null];
}
styleCluster (cluster) {
return [null, null]
styleCluster(cluster) {
return [null, null];
}
renderEvents () {
renderEvents() {
/*
Uncomment below to filter out the locations already present in a cluster.
Leaving these lines commented out renders all the locations on the map, regardless of whether or not they are clustered
*/
const individualClusters = this.state.clusters.filter(cl => !cl.properties.cluster)
const filteredLocations = mapClustersToLocations(individualClusters, this.props.domain.locations)
const individualClusters = this.state.clusters.filter(
(cl) => !cl.properties.cluster
);
const filteredLocations = mapClustersToLocations(
individualClusters,
this.props.domain.locations
);
return (
<Events
svg={this.svgRef.current}
@@ -343,11 +402,13 @@ class Map extends React.Component {
filterColors={this.props.ui.filterColors}
features={this.props.features}
/>
)
);
}
renderClusters () {
const allClusters = this.state.clusters.filter(cl => cl.properties.cluster)
renderClusters() {
const allClusters = this.state.clusters.filter(
(cl) => cl.properties.cluster
);
return (
<Clusters
svg={this.svgRef.current}
@@ -360,34 +421,37 @@ class Map extends React.Component {
getClusterChildren={this.getClusterChildren}
filterColors={this.props.ui.filterColors}
/>
)
);
}
renderSelected () {
const selectedClusters = this.getSelectedClusters()
const totalMarkers = []
renderSelected() {
const selectedClusters = this.getSelectedClusters();
const totalMarkers = [];
this.props.app.selected.forEach(s => {
const { latitude, longitude } = s
this.props.app.selected.forEach((s) => {
const { latitude, longitude } = s;
totalMarkers.push({
latitude,
longitude,
radius: this.props.ui.eventRadius
})
})
radius: this.props.ui.eventRadius,
});
});
const totalClusterPoints = calculateTotalClusterPoints(this.state.clusters)
const totalClusterPoints = calculateTotalClusterPoints(this.state.clusters);
selectedClusters.forEach(cl => {
selectedClusters.forEach((cl) => {
if (cl.properties.cluster) {
const { coordinates } = cl.geometry
const { coordinates } = cl.geometry;
totalMarkers.push({
latitude: String(coordinates[1]),
longitude: String(coordinates[0]),
radius: calcClusterSize(cl.properties.point_count, totalClusterPoints)
})
radius: calcClusterSize(
cl.properties.point_count,
totalClusterPoints
),
});
}
})
});
return (
<SelectedEvents
@@ -396,22 +460,24 @@ class Map extends React.Component {
projectPoint={this.projectPoint}
styles={this.props.ui.mapSelectedEvents}
/>
)
);
}
renderMarkers () {
renderMarkers() {
return (
<Portal node={this.svgRef.current}>
<DefsMarkers />
</Portal>
)
);
}
render () {
const { isShowingSites, isFetchingDomain } = this.props.app.flags
const classes = this.props.app.narrative ? 'map-wrapper narrative-mode' : 'map-wrapper'
render() {
const { isShowingSites, isFetchingDomain } = this.props.app.flags;
const classes = this.props.app.narrative
? "map-wrapper narrative-mode"
: "map-wrapper";
const innerMap = this.map ? (
<React.Fragment>
<>
{this.renderTiles()}
{this.renderMarkers()}
{isShowingSites ? this.renderSites() : null}
@@ -420,14 +486,11 @@ class Map extends React.Component {
{this.renderEvents()}
{this.renderClusters()}
{this.renderSelected()}
</React.Fragment>
) : null
</>
) : null;
return (
<div className={classes}
onKeyDown={this.props.onKeyDown}
tabIndex='0'
>
<div className={classes} onKeyDown={this.props.onKeyDown} tabIndex="0">
<div id={this.props.ui.dom.map} />
<LoadingOverlay
isLoading={this.props.app.loading || isFetchingDomain}
@@ -436,18 +499,18 @@ class Map extends React.Component {
/>
{innerMap}
</div>
)
);
}
}
function mapStateToProps (state) {
function mapStateToProps(state) {
return {
domain: {
locations: selectors.selectLocations(state),
narratives: selectors.selectNarratives(state),
categories: selectors.getCategories(state),
sites: selectors.selectSites(state),
shapes: selectors.selectShapes(state)
shapes: selectors.selectShapes(state),
},
app: {
views: state.app.associations.views,
@@ -461,8 +524,8 @@ function mapStateToProps (state) {
coloringSet: state.app.associations.coloringSet,
flags: {
isShowingSites: state.app.flags.isShowingSites,
isFetchingDomain: state.app.flags.isFetchingDomain
}
isFetchingDomain: state.app.flags.isFetchingDomain,
},
},
ui: {
tiles: state.ui.tiles,
@@ -472,10 +535,10 @@ function mapStateToProps (state) {
shapes: state.ui.style.shapes,
eventRadius: state.ui.eventRadius,
radial: state.ui.style.clusters.radial,
filterColors: state.ui.coloring.colors
filterColors: state.ui.coloring.colors,
},
features: selectors.getFeatures(state)
}
features: selectors.getFeatures(state),
};
}
export default connect(mapStateToProps)(Map)
export default connect(mapStateToProps)(Map);

View File

@@ -1,69 +1,71 @@
import React from 'react'
import React from "react";
export default class Notification extends React.Component {
constructor (props) {
super()
constructor(props) {
super();
this.state = {
isExtended: false
}
isExtended: false,
};
}
toggleDetails () {
this.setState({ isExtended: !this.state.isExtended })
toggleDetails() {
this.setState({ isExtended: !this.state.isExtended });
}
renderItems (items) {
if (!items) return ''
renderItems(items) {
if (!items) return "";
return (
<div>
{items.map((item) => {
if (item.error) {
return (<p>{item.error.message}</p>)
return <p>{item.error.message}</p>;
}
return ''
return "";
})}
</div>
)
);
}
renderNotificationContent (notification) {
let { type, message, items } = notification
renderNotificationContent(notification) {
const { type, message, items } = notification;
return (
<div>
<div className={`message ${type}`}>
{message}
</div>
<div className={`message ${type}`}>{message}</div>
<div className={`details ${this.state.isExtended}`}>
{(items !== null) ? this.renderItems(items) : ''}
{items !== null ? this.renderItems(items) : ""}
</div>
</div>
)
);
}
render () {
if (!this.props.notifications) return null
const notificationsToRender = this.props.notifications.filter(n => !('isRead' in n && n.isRead))
render() {
if (!this.props.notifications) return null;
const notificationsToRender = this.props.notifications.filter(
(n) => !("isRead" in n && n.isRead)
);
if (notificationsToRender.length > 0) {
return (
<div className={`notification-wrapper`}>
<div className="notification-wrapper">
{this.props.notifications.map((notification) => {
return (
<div className='notification' onClick={() => this.toggleDetails()}>
<div
className="notification"
onClick={() => this.toggleDetails()}
>
<button
onClick={this.props.onToggle}
className='side-menu-burg over-white is-active'
className="side-menu-burg over-white is-active"
>
<span />
</button>
{this.renderNotificationContent(notification)}
</div>
)
})
}
);
})}
</div>
)
);
}
return (<div />)
return <div />;
}
}

View File

@@ -1,76 +1,94 @@
import React from 'react'
import { Player } from 'video-react'
import Img from 'react-image'
import Md from './Md'
import Spinner from '../presentational/Spinner'
import NoSource from '../presentational/NoSource'
import React from "react";
import { Player } from "video-react";
import Img from "react-image";
import Md from "./Md";
import Spinner from "../presentational/Spinner";
import NoSource from "../presentational/NoSource";
export default ({ media, viewIdx, translations, switchLanguage, langIdx }) => {
const el = document.querySelector(`.source-media-gallery`)
const shiftW = el ? el.getBoundingClientRect().width : 0
const el = document.querySelector(".source-media-gallery");
const shiftW = el ? el.getBoundingClientRect().width : 0;
function renderMedia (media) {
let { path, type, poster } = media
function renderMedia(media) {
const { path, type, poster } = media;
switch (type) {
case 'Image':
case "Image":
return (
<div className='source-image-container'>
<div className="source-image-container">
<Img
className='source-image'
className="source-image"
src={path}
loader={<div className='source-image-loader'><Spinner /></div>}
unloader={<NoSource failedUrls={[ path ]} />}
onClick={() => window.open(path, '_blank')}
loader={
<div className="source-image-loader">
<Spinner />
</div>
}
unloader={<NoSource failedUrls={[path]} />}
onClick={() => window.open(path, "_blank")}
/>
</div>
)
case 'Video':
);
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 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'
className="source-video"
playsInline
src={path}
/>
</div>
)
case 'Text':
);
case "Text":
return (
<div className='source-text-container'>
<div className="source-text-container">
<Md
path={path}
loader={<Spinner />}
unloader={() => this.renderError()}
/>
</div>
)
case 'Document':
return (
<iframe className='source-document' src={path} />
)
);
case "Document":
return <iframe className="source-document" src={path} />;
default:
return (
<NoSource failedUrls={[`Application does not support extension: ${path.split('.')[1]}`]} />
)
<NoSource
failedUrls={[
`Application does not support extension: ${path.split(".")[1]}`,
]}
/>
);
}
}
return (
<div
className='source-media-gallery'
className="source-media-gallery"
style={{ transform: `translate(${viewIdx * -shiftW}px)` }}
>
{media.map((m) => renderMedia(m))}
</div>
)
}
);
};

View File

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

View File

@@ -1,21 +1,23 @@
import React from 'react'
import copy from '../../common/data/copy.json'
import React from "react";
import copy from "../../common/data/copy.json";
const LoadingOverlay = ({ isLoading, language }) => {
let classes = 'loading-overlay'
classes += (!isLoading) ? ' hidden' : ''
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 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
export default LoadingOverlay;

View File

@@ -1,36 +1,41 @@
/* global fetch */
import React from 'react'
import PropTypes from 'prop-types'
import marked from 'marked'
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 }
constructor(props) {
super(props);
this.state = { md: null, error: null };
}
componentDidMount () {
componentDidMount() {
fetch(this.props.path)
.then(resp => resp.text())
.then(text => {
if (text.length <= 0) { throw new Error() }
.then((resp) => resp.text())
.then((text) => {
if (text.length <= 0) {
throw new Error();
}
this.setState({ md: marked(text) })
this.setState({ md: marked(text) });
})
.catch(() => {
this.setState({ error: true })
})
this.setState({ error: true });
});
}
render () {
render() {
if (this.state.md && !this.state.error) {
return (
<div className='md-container' dangerouslySetInnerHTML={{ __html: this.state.md }} />
)
<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>
return this.props.unloader || <div>Error: couldn't load source</div>;
} else {
return this.props.loader
return this.props.loader;
}
}
}
@@ -38,7 +43,7 @@ class Md extends React.Component {
Md.propTypes = {
loader: PropTypes.func,
unloader: PropTypes.func.isRequired,
path: PropTypes.string.isRequired
}
path: PropTypes.string.isRequired,
};
export default Md
export default Md;

View File

@@ -1,8 +1,8 @@
import React from 'react'
import marked from 'marked'
import Content from './Content'
import Controls from './Controls'
import { selectTypeFromPathWithPoster } from '../../common/utilities'
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
@@ -10,95 +10,124 @@ import { selectTypeFromPathWithPoster } from '../../common/utilities'
* state.
*/
class SourceOverlay extends React.Component {
constructor () {
super()
this.state = { mediaIdx: 0, langIdx: 0 }
this.onShiftGallery = this.onShiftGallery.bind(this)
constructor() {
super();
this.state = { mediaIdx: 0, langIdx: 0 };
this.onShiftGallery = this.onShiftGallery.bind(this);
}
getTypeCounts (media) {
getTypeCounts(media) {
return media.reduce(
(acc, vl) => {
acc[vl.type] += 1
return acc
acc[vl.type] += 1;
return acc;
},
{ Image: 0, Video: 0, Text: 0 }
)
);
}
onShiftGallery (shift) {
onShiftGallery(shift) {
// no more left
if (this.state.mediaIdx === 0 && shift === -1) return
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 })
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 })
switchLanguage(idx) {
this.setState({ langIdx: idx });
}
renderContent (source) {
const { url, title, paths, date, type, poster, description } = source
const shortenedTitle = title.substring(0, 100)
renderContent(source) {
const { url, title, paths, date, type, poster, description } = source;
const shortenedTitle = title.substring(0, 100);
return (
<React.Fragment>
<div className='mo-banner'>
<div className='mo-banner-close' onClick={this.props.onCancel}>
<i className='material-icons'>close</i>
<>
<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>
<h3 className="mo-banner-content">{shortenedTitle}</h3>
</div>
<div className='mo-container' onClick={e => e.stopPropagation()}>
<div className='mo-media-container'>
<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))}
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-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}
<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'>
{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}
{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}
{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'>Link to original URL</a></span> : null}
{url ? (
<span>
<i className="material-icons left">link</i>
<a href={url} target="_blank">
Link to original URL
</a>
</span>
) : null}
</div>
</div>
) : null}
</div>
</div>
</React.Fragment>
)
</>
);
}
renderIntlContent () {
const { langIdx } = this.state
const { translations, source } = this.props
let translated = null
renderIntlContent() {
const { langIdx } = this.state;
const { translations, source } = this.props;
let translated = null;
if (translations && translations.length && langIdx > 0) {
translated = translations[langIdx - 1]
translated = translations[langIdx - 1];
}
if (translated) {
translated = {
@@ -106,24 +135,24 @@ class SourceOverlay extends React.Component {
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
}
paths: translated.file ? [translated.file] : translated.paths,
};
}
return this.renderContent(langIdx === 0 ? source : translated)
return this.renderContent(langIdx === 0 ? source : translated);
}
render () {
if (typeof (this.props.source) !== 'object') {
return this.renderError()
render() {
if (typeof this.props.source !== "object") {
return this.renderError();
}
return (
<div className={`mo-overlay ${this.props.opaque ? 'opaque' : ''}`}>
<div className={`mo-overlay ${this.props.opaque ? "opaque" : ""}`}>
{this.renderIntlContent()}
</div>
)
);
}
}
export default SourceOverlay
export default SourceOverlay;

View File

@@ -1,76 +1,100 @@
import React from 'react'
import React from "react";
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import * as actions from '../actions'
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import * as actions from "../actions";
import '../scss/search.scss'
import "../scss/search.scss";
import SearchRow from './SearchRow.jsx'
import SearchRow from "./SearchRow.jsx";
class Search extends React.Component {
constructor (props) {
super(props)
constructor(props) {
super(props);
this.state = {
isFolded: true
}
this.onButtonClick = this.onButtonClick.bind(this)
this.updateSearchQuery = this.updateSearchQuery.bind(this)
isFolded: true,
};
this.onButtonClick = this.onButtonClick.bind(this);
this.updateSearchQuery = this.updateSearchQuery.bind(this);
}
onButtonClick () {
this.setState(prevState => {
return { isFolded: !prevState.isFolded }
})
onButtonClick() {
this.setState((prevState) => {
return { isFolded: !prevState.isFolded };
});
}
updateSearchQuery (e) {
let queryString = e.target.value
this.props.actions.updateSearchQuery(queryString)
updateSearchQuery(e) {
const queryString = e.target.value;
this.props.actions.updateSearchQuery(queryString);
}
render () {
let searchResults
render() {
let searchResults;
const searchAttributes = ['description', 'location', 'category', 'date']
const searchAttributes = ["description", "location", "category", "date"];
if (!this.props.queryString) {
searchResults = []
searchResults = [];
} else {
searchResults = this.props.events.filter(event =>
searchAttributes.some(attribute => event[attribute].toLowerCase().includes(this.props.queryString.toLowerCase()))
)
searchResults = this.props.events.filter((event) =>
searchAttributes.some((attribute) =>
event[attribute]
.toLowerCase()
.includes(this.props.queryString.toLowerCase())
)
);
}
return (
<div class={'search-outer-container' + (this.props.narrative ? ' narrative-mode ' : '')}>
<div id='search-bar-icon-container' onClick={this.onButtonClick}>
<i className='material-icons'>search</i>
<div
class={
"search-outer-container" +
(this.props.narrative ? " narrative-mode " : "")
}
>
<div id="search-bar-icon-container" onClick={this.onButtonClick}>
<i className="material-icons">search</i>
</div>
<div class={'search-bar-overlay' + (this.state.isFolded ? ' folded' : '')}>
<div class='search-input-container'>
<input class='search-bar-input' onChange={this.updateSearchQuery} type='text' />
<i id='close-search-overlay' className='material-icons' onClick={this.onButtonClick} >close</i>
<div
class={"search-bar-overlay" + (this.state.isFolded ? " folded" : "")}
>
<div class="search-input-container">
<input
class="search-bar-input"
onChange={this.updateSearchQuery}
type="text"
/>
<i
id="close-search-overlay"
className="material-icons"
onClick={this.onButtonClick}
>
close
</i>
</div>
<div class='search-results'>
{searchResults.map(result => {
return <SearchRow onSearchRowClick={this.props.onSearchRowClick} eventObj={result} query={this.props.queryString} />
<div class="search-results">
{searchResults.map((result) => {
return (
<SearchRow
onSearchRowClick={this.props.onSearchRowClick}
eventObj={result}
query={this.props.queryString}
/>
);
})}
</div>
</div>
</div>
)
);
}
}
function mapDispatchToProps (dispatch) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch)
}
actions: bindActionCreators(actions, dispatch),
};
}
export default connect(
state => state,
mapDispatchToProps
)(Search)
export default connect((state) => state, mapDispatchToProps)(Search);

View File

@@ -1,40 +1,62 @@
import React from 'react'
import React from "react";
const SearchRow = ({ query, eventObj, onSearchRowClick }) => {
const { description, location, date } = eventObj
function getHighlightedText (text, highlight) {
const { description, location, date } = eventObj;
function getHighlightedText(text, highlight) {
// Split text on highlight term, include term itself into parts, ignore case
const parts = text.split(new RegExp(`(${highlight})`, 'gi'))
return <span>{ parts.map(part => part.toLowerCase() === highlight.toLowerCase() ? <span style={{ backgroundColor: 'yellow', color: 'black' }}>{part}</span> : part) }</span>
const parts = text.split(new RegExp(`(${highlight})`, "gi"));
return (
<span>
{parts.map((part) =>
part.toLowerCase() === highlight.toLowerCase() ? (
<span style={{ backgroundColor: "yellow", color: "black" }}>
{part}
</span>
) : (
part
)
)}
</span>
);
}
function getShortDescription (text, searchQuery) {
var regexp = new RegExp(`(([^ ]* ){0,6}[a-zA-Z]*${searchQuery.toLowerCase()}[a-zA-Z]*( [^ ]*){0,5})`, 'gm')
let parts = text.toLowerCase().match(regexp)
for (var x = 0; x < (parts ? parts.length : 0); x++) {
parts[x] = '...' + parts[x]
function getShortDescription(text, searchQuery) {
const regexp = new RegExp(
`(([^ ]* ){0,6}[a-zA-Z]*${searchQuery.toLowerCase()}[a-zA-Z]*( [^ ]*){0,5})`,
"gm"
);
const parts = text.toLowerCase().match(regexp);
for (let x = 0; x < (parts ? parts.length : 0); x++) {
parts[x] = "..." + parts[x];
}
const firstLine = [text.match('(([^ ]* ){0,10})', 'm')[0]]
return parts || firstLine
const firstLine = [text.match("(([^ ]* ){0,10})", "m")[0]];
return parts || firstLine;
}
return (
<div className='search-row' onClick={() => onSearchRowClick([eventObj])}>
<div className='location-date-container'>
<div className='date-container'>
<i className='material-icons'>event</i>
<div className="search-row" onClick={() => onSearchRowClick([eventObj])}>
<div className="location-date-container">
<div className="date-container">
<i className="material-icons">event</i>
<p>{getHighlightedText(date, query)}</p>
</div>
<div className='location-container'>
<i className='material-icons'>location_on</i>
<div className="location-container">
<i className="material-icons">location_on</i>
<p>{getHighlightedText(location, query)}</p>
</div>
</div>
<p>{getShortDescription(description, query).map(match => {
return <span>{getHighlightedText(match, query)}...<br /></span>
})}</p>
<p>
{getShortDescription(description, query).map((match) => {
return (
<span>
{getHighlightedText(match, query)}...
<br />
</span>
);
})}
</p>
</div>
)
}
);
};
export default SearchRow
export default SearchRow;

View File

@@ -1,19 +1,28 @@
import React, { useState } from 'react'
import React, { useState } from "react";
export default ({ showing, onClickHandler, timelineDims }) => {
if (!showing) {
return null
return null;
}
const [checked, setChecked] = useState(false)
const handleCheck = () => setChecked(!checked)
const onNarrativise = () => onClickHandler(checked)
const [checked, setChecked] = useState(false);
const handleCheck = () => setChecked(!checked);
const onNarrativise = () => onClickHandler(checked);
return <div className='stateoptions-panel' style={{ bottom: timelineDims.height }}>
<div>
<div className='button' onClick={onNarrativise}>Narrativise</div>
<label for='withlines'>Connect by lines</label>
<input name='withlines' onClick={handleCheck} checked={checked} type='checkbox' />
return (
<div className="stateoptions-panel" style={{ bottom: timelineDims.height }}>
<div>
<div className="button" onClick={onNarrativise}>
Narrativise
</div>
<label for="withlines">Connect by lines</label>
<input
name="withlines"
onClick={handleCheck}
checked={checked}
type="checkbox"
/>
</div>
</div>
</div>
}
);
};

View File

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

View File

@@ -1,11 +1,11 @@
import React from 'react'
import { connect } from 'react-redux'
import { Player } from 'video-react'
import marked from 'marked'
import MediaOverlay from './Overlay/Media'
import falogo from '../assets/fa-logo.png'
import bcatlogo from '../assets/bellingcat-logo.png'
const MEDIA_HIDDEN = -2
import React from "react";
import { connect } from "react-redux";
import { Player } from "video-react";
import marked from "marked";
import MediaOverlay from "./Overlay/Media";
import falogo from "../assets/fa-logo.png";
import bcatlogo from "../assets/bellingcat-logo.png";
const MEDIA_HIDDEN = -2;
/**
* Manages the presentation of props that come in from the store's app.cover.
@@ -14,211 +14,260 @@ const MEDIA_HIDDEN = -2
* a couple of weird offset calculations... but it works for the time being.
*/
class TemplateCover extends React.Component {
constructor (props) {
super(props)
constructor(props) {
super(props);
this.state = {
video: MEDIA_HIDDEN,
featureLang: 0
}
featureLang: 0,
};
}
getVideo (index, headerEndIndex) {
getVideo(index, headerEndIndex) {
if (index < headerEndIndex) {
return this.props.cover.headerVideos[index]
return this.props.cover.headerVideos[index];
} else if (index >= 0) {
return this.props.cover.videos[index - headerEndIndex]
return this.props.cover.videos[index - headerEndIndex];
} else {
return null
return null;
}
}
onVideoClickHandler (index) {
const buffer = this.props.cover.headerVideos ? this.props.cover.headerVideos.length : 0
onVideoClickHandler(index) {
const buffer = this.props.cover.headerVideos
? this.props.cover.headerVideos.length
: 0;
return () => {
this.setState({
video: index + buffer
})
}
video: index + buffer,
});
};
}
renderFeature () {
const { featureVideo } = this.props.cover
const { featureLang } = this.state
const { translations } = featureVideo
const source = featureLang === 0
? featureVideo
: {
...translations[featureLang - 1],
poster: featureVideo.poster
}
renderFeature() {
const { featureVideo } = this.props.cover;
const { featureLang } = this.state;
const { translations } = featureVideo;
const source =
featureLang === 0
? featureVideo
: {
...translations[featureLang - 1],
poster: featureVideo.poster,
};
return (
<div>
<div className='banner-trans right-overlay'>
{translations && translations.map((trans, idx) => {
const langIdx = idx + 1 // default lang idx is 0
if (featureLang !== langIdx) {
return <div onClick={() => this.setState({ featureLang: langIdx })} className='trans-button'>{trans.code}</div>
} else {
return <div onClick={() => this.setState({ featureLang: 0 })} className='trans-button'>ENG</div>
}
})}
<div className="banner-trans right-overlay">
{translations &&
translations.map((trans, idx) => {
const langIdx = idx + 1; // default lang idx is 0
if (featureLang !== langIdx) {
return (
<div
onClick={() => this.setState({ featureLang: langIdx })}
className="trans-button"
>
{trans.code}
</div>
);
} else {
return (
<div
onClick={() => this.setState({ featureLang: 0 })}
className="trans-button"
>
ENG
</div>
);
}
})}
</div>
<Player
className='source-video'
className="source-video"
poster={source.poster}
playsInline
src={source.file}
/>
</div>
)
);
}
renderHeaderVideos () {
const { headerVideos } = this.props.cover
renderHeaderVideos() {
const { headerVideos } = this.props.cover;
return (
<div className='row'>
{ headerVideos.slice(0, 2).map((media, index) => (
<div className='cell plain' onClick={() => this.setState({ video: index })}>
<div className="row">
{headerVideos.slice(0, 2).map((media, index) => (
<div
className="cell plain"
onClick={() => this.setState({ video: index })}
>
{media.buttonTitle}
</div>
)) }
))}
</div>
)
);
}
renderButton (button, yellow) {
renderButton(button, yellow) {
return (
<div className='row'>
<a className={`cell ${yellow ? 'yellow' : 'plain'}`} href={button.href}>
<div className="row">
<a className={`cell ${yellow ? "yellow" : "plain"}`} href={button.href}>
{button.title}
</a>
</div>
)
);
}
renderMediaOverlay () {
const video = this.getVideo(this.state.video, this.props.cover.headerVideos ? this.props.cover.headerVideos.length : 0)
renderMediaOverlay() {
const video = this.getVideo(
this.state.video,
this.props.cover.headerVideos ? this.props.cover.headerVideos.length : 0
);
return (
<MediaOverlay
opaque
source={
{
title: video.title,
desc: video.desc,
paths: [video.file],
poster: video.poster
}}
source={{
title: video.title,
desc: video.desc,
paths: [video.file],
poster: video.poster,
}}
translations={video.translations}
onCancel={() => this.setState({ video: MEDIA_HIDDEN })}
/>
)
);
}
render () {
render() {
if (!this.props.cover) {
return (
<div className='default-cover-container'>
You haven't specified any cover props. Put them in the values that overwrite the store in <code>app.cover</code>
<div className="default-cover-container">
You haven't specified any cover props. Put them in the values that
overwrite the store in <code>app.cover</code>
</div>
)
);
}
const { videos, footerButton } = this.props.cover
const { showing } = this.props
const { videos, footerButton } = this.props.cover;
const { showing } = this.props;
return (
<div className='default-cover-container'>
<div className={showing ? 'cover-header' : 'cover-header minimized'}>
<a className='cover-logo-container' href='https://forensic-architecture.org'>
<img className='cover-logo' src={falogo} />
<div className="default-cover-container">
<div className={showing ? "cover-header" : "cover-header minimized"}>
<a
className="cover-logo-container"
href="https://forensic-architecture.org"
>
<img className="cover-logo" src={falogo} />
</a>
<a className='cover-logo-container' href='https://bellingcat.com'>
<img className='cover-logo' src={bcatlogo} />
<a className="cover-logo-container" href="https://bellingcat.com">
<img className="cover-logo" src={bcatlogo} />
</a>
</div>
<div className='cover-content'>
{
this.props.cover.bgVideo ? (
<div className={`fullscreen-bg ${!this.props.showing ? 'hidden' : ''}`}>
<video
loop
muted
autoPlay
preload='auto'
className='fullscreen-bg__video'
>
<source src={this.props.cover.bgVideo} type='video/mp4' />
</video>
</div>
) : null
}
<h2 style={{ margin: 0 }} dangerouslySetInnerHTML={{ __html: marked(this.props.cover.title) }} />
{
this.props.cover.subtitle ? (
<h3 style={{ marginTop: 0 }}>{this.props.cover.subtitle}</h3>
) : null
}
{
this.props.cover.subsubtitle ? (
<h5>{this.props.cover.subsubtitle}</h5>
) : null
}
<div className="cover-content">
{this.props.cover.bgVideo ? (
<div
className={`fullscreen-bg ${!this.props.showing ? "hidden" : ""}`}
>
<video
loop
muted
autoPlay
preload="auto"
className="fullscreen-bg__video"
>
<source src={this.props.cover.bgVideo} type="video/mp4" />
</video>
</div>
) : null}
<h2
style={{ margin: 0 }}
dangerouslySetInnerHTML={{ __html: marked(this.props.cover.title) }}
/>
{this.props.cover.subtitle ? (
<h3 style={{ marginTop: 0 }}>{this.props.cover.subtitle}</h3>
) : null}
{this.props.cover.subsubtitle ? (
<h5>{this.props.cover.subsubtitle}</h5>
) : null}
{this.props.cover.featureVideo ? this.renderFeature() : null}
<div className='hero thin'>
<div className="hero thin">
{this.props.cover.headerVideos ? this.renderHeaderVideos() : null}
{this.props.cover.headerButton ? this.renderButton(this.props.cover.headerButton) : null}
<div className='row'>
<div className='cell yellow' onClick={this.props.showAppHandler}>
{this.props.cover.headerButton
? this.renderButton(this.props.cover.headerButton)
: null}
<div className="row">
<div className="cell yellow" onClick={this.props.showAppHandler}>
{this.props.cover.exploreButton}
</div>
</div>
</div>
{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) }} />}
{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'>
<div className='row'>
<div className="hero">
<div className="row">
{/* NOTE: only take first four videos, drop any others for style reasons */}
{ videos && videos.slice(0, 2).map((media, index) => (
<div className='cell small' onClick={this.onVideoClickHandler(index)} >
{media.buttonTitle}<br />{media.buttonSubtitle}
</div>
)) }
{videos &&
videos.slice(0, 2).map((media, index) => (
<div
className="cell small"
onClick={this.onVideoClickHandler(index)}
>
{media.buttonTitle}
<br />
{media.buttonSubtitle}
</div>
))}
</div>
<div className='row'>
{ videos.length > 2 && this.props.cover.videos.slice(2, 4).map((media, index) => (
<div className='cell small' onClick={this.onVideoClickHandler(index + 2)} >
{media.buttonTitle}<br />{media.buttonSubtitle}
</div>
)) }
<div className="row">
{videos.length > 2 &&
this.props.cover.videos.slice(2, 4).map((media, index) => (
<div
className="cell small"
onClick={this.onVideoClickHandler(index + 2)}
>
{media.buttonTitle}
<br />
{media.buttonSubtitle}
</div>
))}
</div>
</div>
) : null}
{footerButton ? (
<div className='hero'>
<div className='row'>
{this.renderButton(footerButton)}
</div>
<div className="hero">
<div className="row">{this.renderButton(footerButton)}</div>
</div>
) : null}
</div>
{
this.state.video !== MEDIA_HIDDEN ? this.renderMediaOverlay() : null }
{this.state.video !== MEDIA_HIDDEN ? this.renderMediaOverlay() : null}
</div>
)
);
}
}
function mapStateToProps (state) {
function mapStateToProps(state) {
return {
cover: state.app.cover
}
cover: state.app.cover,
};
}
export default connect(mapStateToProps)(TemplateCover)
export default connect(mapStateToProps)(TemplateCover);

View File

@@ -1,29 +1,29 @@
import React from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import * as d3 from 'd3'
import * as selectors from '../selectors'
import { setLoading, setNotLoading } from '../actions'
import hash from 'object-hash'
import React from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import * as d3 from "d3";
import * as selectors from "../selectors";
import { setLoading, setNotLoading } from "../actions";
import hash from "object-hash";
import copy from '../common/data/copy.json'
import Header from './presentational/Timeline/Header'
import Axis from './TimelineAxis.jsx'
import Clip from './presentational/Timeline/Clip'
import Handles from './presentational/Timeline/Handles.js'
import ZoomControls from './presentational/Timeline/ZoomControls.js'
import Markers from './presentational/Timeline/Markers.js'
import Events from './presentational/Timeline/Events.js'
import Categories from './TimelineCategories.jsx'
import copy from "../common/data/copy.json";
import Header from "./presentational/Timeline/Header";
import Axis from "./TimelineAxis.jsx";
import Clip from "./presentational/Timeline/Clip";
import Handles from "./presentational/Timeline/Handles.js";
import ZoomControls from "./presentational/Timeline/ZoomControls.js";
import Markers from "./presentational/Timeline/Markers.js";
import Events from "./presentational/Timeline/Events.js";
import Categories from "./TimelineCategories.jsx";
class Timeline extends React.Component {
constructor (props) {
super(props)
this.styleDatetime = this.styleDatetime.bind(this)
this.getDatetimeX = this.getDatetimeX.bind(this)
this.getY = this.getY.bind(this)
this.onApplyZoom = this.onApplyZoom.bind(this)
this.svgRef = React.createRef()
constructor(props) {
super(props);
this.styleDatetime = this.styleDatetime.bind(this);
this.getDatetimeX = this.getDatetimeX.bind(this);
this.getY = this.getY.bind(this);
this.onApplyZoom = this.onApplyZoom.bind(this);
this.svgRef = React.createRef();
this.state = {
isFolded: false,
dims: props.dimensions,
@@ -31,103 +31,128 @@ class Timeline extends React.Component {
scaleY: null,
timerange: [null, null], // two datetimes
dragPos0: null,
transitionDuration: 300
}
transitionDuration: 300,
};
}
componentDidMount () {
this.addEventListeners()
componentDidMount() {
this.addEventListeners();
}
componentWillReceiveProps (nextProps) {
componentWillReceiveProps(nextProps) {
if (hash(nextProps) !== hash(this.props)) {
this.setState({
timerange: nextProps.app.timeline.range,
scaleX: this.makeScaleX()
})
scaleX: this.makeScaleX(),
});
}
if ((hash(nextProps.domain.categories) !== hash(this.props.domain.categories)) || hash(nextProps.dimensions) !== hash(this.props.dimensions)) {
const { trackHeight, marginTop } = nextProps.dimensions
if (
hash(nextProps.domain.categories) !==
hash(this.props.domain.categories) ||
hash(nextProps.dimensions) !== hash(this.props.dimensions)
) {
const { trackHeight, marginTop } = nextProps.dimensions;
this.setState({
scaleY: this.makeScaleY(nextProps.domain.categories, trackHeight, marginTop)
})
scaleY: this.makeScaleY(
nextProps.domain.categories,
trackHeight,
marginTop
),
});
}
if (nextProps.dimensions.trackHeight !== this.props.dimensions.trackHeight) {
this.computeDims()
if (
nextProps.dimensions.trackHeight !== this.props.dimensions.trackHeight
) {
this.computeDims();
}
}
addEventListeners () {
window.addEventListener('resize', () => { this.computeDims() })
let element = document.querySelector('.timeline-wrapper')
addEventListeners() {
window.addEventListener("resize", () => {
this.computeDims();
});
const element = document.querySelector(".timeline-wrapper");
if (element !== null) {
element.addEventListener('transitionend', (event) => {
this.computeDims()
})
element.addEventListener("transitionend", (event) => {
this.computeDims();
});
}
}
makeScaleX () {
return d3.scaleTime()
makeScaleX() {
return d3
.scaleTime()
.domain(this.state.timerange)
.range([this.state.dims.marginLeft, this.state.dims.width - this.state.dims.width_controls])
.range([
this.state.dims.marginLeft,
this.state.dims.width - this.state.dims.width_controls,
]);
}
makeScaleY (categories, trackHeight, marginTop) {
const { features } = this.props
makeScaleY(categories, trackHeight, marginTop) {
const { features } = this.props;
if (features.GRAPH_NONLOCATED && features.GRAPH_NONLOCATED.categories) {
categories = categories.filter(cat => !features.GRAPH_NONLOCATED.categories.includes(cat.id))
categories = categories.filter(
(cat) => !features.GRAPH_NONLOCATED.categories.includes(cat.id)
);
}
const extraPadding = 0
const catHeight = categories.length > 2 ? trackHeight / categories.length : trackHeight / (categories.length + 1)
const extraPadding = 0;
const catHeight =
categories.length > 2
? trackHeight / categories.length
: trackHeight / (categories.length + 1);
const catsYpos = categories.map((g, i) => {
return ((i + 1) * catHeight) + marginTop + (extraPadding / 2)
})
const catMap = categories.map(c => c.id)
return (i + 1) * catHeight + marginTop + extraPadding / 2;
});
const catMap = categories.map((c) => c.id);
return (cat) => {
const idx = catMap.indexOf(cat)
return catsYpos[idx]
}
const idx = catMap.indexOf(cat);
return catsYpos[idx];
};
}
componentDidUpdate (prevProps, prevState) {
componentDidUpdate(prevProps, prevState) {
if (prevState.timerange !== this.state.timerange) {
this.setState({ scaleX: this.makeScaleX() })
this.setState({ scaleX: this.makeScaleX() });
}
}
/**
* Returns the time scale (x) extent in minutes
*/
getTimeScaleExtent () {
if (!this.state.scaleX) return 0
const timeDomain = this.state.scaleX.domain()
return (timeDomain[1].getTime() - timeDomain[0].getTime()) / 60000
getTimeScaleExtent() {
if (!this.state.scaleX) return 0;
const timeDomain = this.state.scaleX.domain();
return (timeDomain[1].getTime() - timeDomain[0].getTime()) / 60000;
}
onClickArrow () {
onClickArrow() {
this.setState((prevState, props) => {
return { isFolded: !prevState.isFolded }
})
return { isFolded: !prevState.isFolded };
});
}
computeDims () {
const dom = this.props.ui.dom.timeline
computeDims() {
const dom = this.props.ui.dom.timeline;
if (document.querySelector(`#${dom}`) !== null) {
const boundingClient = document.querySelector(`#${dom}`).getBoundingClientRect()
const boundingClient = document
.querySelector(`#${dom}`)
.getBoundingClientRect();
this.setState({
dims: {
...this.props.dimensions,
width: boundingClient.width
this.setState(
{
dims: {
...this.props.dimensions,
width: boundingClient.width,
},
},
() => {
this.setState({ scaleX: this.makeScaleX() });
}
},
() => {
this.setState({ scaleX: this.makeScaleX() })
})
);
}
}
@@ -135,34 +160,37 @@ class Timeline extends React.Component {
* Shift time range by moving forward or backwards
* @param {String} direction: 'forward' / 'backwards'
*/
onMoveTime (direction) {
const extent = this.getTimeScaleExtent()
const newCentralTime = d3.timeMinute.offset(this.state.scaleX.domain()[0], extent / 2)
onMoveTime(direction) {
const extent = this.getTimeScaleExtent();
const newCentralTime = d3.timeMinute.offset(
this.state.scaleX.domain()[0],
extent / 2
);
// if forward
let domain0 = newCentralTime
let domainF = d3.timeMinute.offset(newCentralTime, extent)
let domain0 = newCentralTime;
let domainF = d3.timeMinute.offset(newCentralTime, extent);
// if backwards
if (direction === 'backwards') {
domain0 = d3.timeMinute.offset(newCentralTime, -extent)
domainF = newCentralTime
if (direction === "backwards") {
domain0 = d3.timeMinute.offset(newCentralTime, -extent);
domainF = newCentralTime;
}
this.setState({ timerange: [domain0, domainF] }, () => {
this.props.methods.onUpdateTimerange(this.state.timerange)
})
this.props.methods.onUpdateTimerange(this.state.timerange);
});
}
onCenterTime (newCentralTime) {
const extent = this.getTimeScaleExtent()
onCenterTime(newCentralTime) {
const extent = this.getTimeScaleExtent();
const domain0 = d3.timeMinute.offset(newCentralTime, -extent / 2)
const domainF = d3.timeMinute.offset(newCentralTime, +extent / 2)
const domain0 = d3.timeMinute.offset(newCentralTime, -extent / 2);
const domainF = d3.timeMinute.offset(newCentralTime, +extent / 2);
this.setState({ timerange: [domain0, domainF] }, () => {
this.props.methods.onUpdateTimerange(this.state.timerange)
})
this.props.methods.onUpdateTimerange(this.state.timerange);
});
}
/**
@@ -170,119 +198,132 @@ class Timeline extends React.Component {
* WITHOUT updating the store, or data shown.
* Used for updates in the middle of a transition, for performance purposes
*/
onSoftTimeRangeUpdate (timerange) {
this.setState({ timerange })
onSoftTimeRangeUpdate(timerange) {
this.setState({ timerange });
}
/**
* Apply zoom level to timeline
* @param {object} zoom: zoom level from zoomLevels
*/
onApplyZoom (zoom) {
const extent = this.getTimeScaleExtent()
const newCentralTime = d3.timeMinute.offset(this.state.scaleX.domain()[0], extent / 2)
const { rangeLimits } = this.props.app.timeline
onApplyZoom(zoom) {
const extent = this.getTimeScaleExtent();
const newCentralTime = d3.timeMinute.offset(
this.state.scaleX.domain()[0],
extent / 2
);
const { rangeLimits } = this.props.app.timeline;
let newDomain0 = d3.timeMinute.offset(newCentralTime, -zoom.duration / 2)
let newDomainF = d3.timeMinute.offset(newCentralTime, zoom.duration / 2)
let newDomain0 = d3.timeMinute.offset(newCentralTime, -zoom.duration / 2);
let newDomainF = d3.timeMinute.offset(newCentralTime, zoom.duration / 2);
if (rangeLimits) {
// If the store contains absolute time limits,
// make sure the zoom doesn't go over them
const minDate = rangeLimits[0]
const maxDate = rangeLimits[1]
const minDate = rangeLimits[0];
const maxDate = rangeLimits[1];
if (newDomain0 < minDate) {
newDomain0 = minDate
newDomainF = d3.timeMinute.offset(newDomain0, zoom.duration)
newDomain0 = minDate;
newDomainF = d3.timeMinute.offset(newDomain0, zoom.duration);
}
if (newDomainF > maxDate) {
newDomainF = maxDate
newDomain0 = d3.timeMinute.offset(newDomainF, -zoom.duration)
newDomainF = maxDate;
newDomain0 = d3.timeMinute.offset(newDomainF, -zoom.duration);
}
}
this.setState({ timerange: [
newDomain0,
newDomainF
] }, () => {
this.props.methods.onUpdateTimerange(this.state.timerange)
})
this.setState(
{
timerange: [newDomain0, newDomainF],
},
() => {
this.props.methods.onUpdateTimerange(this.state.timerange);
}
);
}
toggleTransition (isTransition) {
this.setState({ transitionDuration: (isTransition) ? 300 : 0 })
toggleTransition(isTransition) {
this.setState({ transitionDuration: isTransition ? 300 : 0 });
}
/*
* Setup drag behavior
*/
onDragStart () {
d3.event.sourceEvent.stopPropagation()
this.setState({
dragPos0: d3.event.x
}, () => {
this.toggleTransition(false)
})
onDragStart() {
d3.event.sourceEvent.stopPropagation();
this.setState(
{
dragPos0: d3.event.x,
},
() => {
this.toggleTransition(false);
}
);
}
/*
* Drag and update
*/
onDrag () {
const drag0 = this.state.scaleX.invert(this.state.dragPos0).getTime()
const dragNow = this.state.scaleX.invert(d3.event.x).getTime()
const timeShift = (drag0 - dragNow) / 1000
onDrag() {
const drag0 = this.state.scaleX.invert(this.state.dragPos0).getTime();
const dragNow = this.state.scaleX.invert(d3.event.x).getTime();
const timeShift = (drag0 - dragNow) / 1000;
const { range, rangeLimits } = this.props.app.timeline
let newDomain0 = d3.timeSecond.offset(range[0], timeShift)
let newDomainF = d3.timeSecond.offset(range[1], timeShift)
const { range, rangeLimits } = this.props.app.timeline;
let newDomain0 = d3.timeSecond.offset(range[0], timeShift);
let newDomainF = d3.timeSecond.offset(range[1], timeShift);
if (rangeLimits) {
// If the store contains absolute time limits,
// make sure the zoom doesn't go over them
const minDate = rangeLimits[0]
const maxDate = rangeLimits[1]
const minDate = rangeLimits[0];
const maxDate = rangeLimits[1];
newDomain0 = (newDomain0 < minDate) ? minDate : newDomain0
newDomainF = (newDomainF > maxDate) ? maxDate : newDomainF
newDomain0 = newDomain0 < minDate ? minDate : newDomain0;
newDomainF = newDomainF > maxDate ? maxDate : newDomainF;
}
// Updates components without updating timerange
this.onSoftTimeRangeUpdate([newDomain0, newDomainF])
this.onSoftTimeRangeUpdate([newDomain0, newDomainF]);
}
/**
* Stop dragging and update data
*/
onDragEnd () {
this.toggleTransition(true)
this.props.methods.onUpdateTimerange(this.state.timerange)
onDragEnd() {
this.toggleTransition(true);
this.props.methods.onUpdateTimerange(this.state.timerange);
}
getDatetimeX (datetime) {
return this.state.scaleX(datetime)
getDatetimeX(datetime) {
return this.state.scaleX(datetime);
}
getY (event) {
const { features, domain } = this.props
const { USE_CATEGORIES, GRAPH_NONLOCATED } = features
const { categories } = domain
const categoriesExist = USE_CATEGORIES && categories && categories.length > 0
getY(event) {
const { features, domain } = this.props;
const { USE_CATEGORIES, GRAPH_NONLOCATED } = features;
const { categories } = domain;
const categoriesExist =
USE_CATEGORIES && categories && categories.length > 0;
if (!categoriesExist) {
return this.state.dims.trackHeight / 2
return this.state.dims.trackHeight / 2;
}
const { category } = event
const { category } = event;
if (GRAPH_NONLOCATED && GRAPH_NONLOCATED.categories.includes(category)) {
const { project } = event
return this.state.dims.marginTop + domain.projects[project].offset + this.props.ui.eventRadius
const { project } = event;
return (
this.state.dims.marginTop +
domain.projects[project].offset +
this.props.ui.eventRadius
);
}
if (!this.state.scaleY) return 0
if (!this.state.scaleY) return 0;
return this.state.scaleY(category)
return this.state.scaleY(category);
}
/**
@@ -294,39 +335,44 @@ class Timeline extends React.Component {
* at the second index is an optional additional component that renders in
* the <g/> div.
*/
styleDatetime (timestamp, category) {
return [null, null]
styleDatetime(timestamp, category) {
return [null, null];
}
render () {
const { isNarrative, app } = this.props
let classes = `timeline-wrapper ${(this.state.isFolded) ? ' folded' : ''}`
classes += (app.narrative !== null) ? ' narrative-mode' : ''
const { dims } = this.state
const foldedStyle = { bottom: this.state.isFolded ? -dims.height : 0 }
const heightStyle = { height: dims.height }
const extraStyle = { ...heightStyle, ...foldedStyle }
const contentHeight = { height: dims.contentHeight }
const { categories } = this.props.domain
render() {
const { isNarrative, app } = this.props;
let classes = `timeline-wrapper ${this.state.isFolded ? " folded" : ""}`;
classes += app.narrative !== null ? " narrative-mode" : "";
const { dims } = this.state;
const foldedStyle = { bottom: this.state.isFolded ? -dims.height : 0 };
const heightStyle = { height: dims.height };
const extraStyle = { ...heightStyle, ...foldedStyle };
const contentHeight = { height: dims.contentHeight };
const { categories } = this.props.domain;
return (
<div className={classes} style={extraStyle} onKeyDown={this.props.onKeyDown} tabIndex='1'>
<div
className={classes}
style={extraStyle}
onKeyDown={this.props.onKeyDown}
tabIndex="1"
>
<Header
title={copy[this.props.app.language].timeline.info}
from={this.state.timerange[0]}
to={this.state.timerange[1]}
onClick={() => { this.onClickArrow() }}
onClick={() => {
this.onClickArrow();
}}
hideInfo={isNarrative}
/>
<div className='timeline-content' style={heightStyle}>
<div id={this.props.ui.dom.timeline} className='timeline' style={contentHeight} >
<svg
ref={this.svgRef}
width={dims.width}
style={contentHeight}
>
<Clip
dims={dims}
/>
<div className="timeline-content" style={heightStyle}>
<div
id={this.props.ui.dom.timeline}
className="timeline"
style={contentHeight}
>
<svg ref={this.svgRef} width={dims.width} style={contentHeight}>
<Clip dims={dims} />
<Axis
dims={dims}
extent={this.getTimeScaleExtent()}
@@ -335,17 +381,30 @@ class Timeline extends React.Component {
/>
<Categories
dims={dims}
getCategoryY={category => this.getY({ category, project: null })}
onDragStart={() => { this.onDragStart() }}
onDrag={() => { this.onDrag() }}
onDragEnd={() => { this.onDragEnd() }}
categories={categories.map(c => c.id)}
getCategoryY={(category) =>
this.getY({ category, project: null })
}
onDragStart={() => {
this.onDragStart();
}}
onDrag={() => {
this.onDrag();
}}
onDragEnd={() => {
this.onDragEnd();
}}
categories={categories.map((c) => c.id)}
features={this.props.features}
fallbackLabel={copy[this.props.app.language].timeline.default_categories_label}
fallbackLabel={
copy[this.props.app.language].timeline
.default_categories_label
}
/>
<Handles
dims={dims}
onMoveTime={(dir) => { this.onMoveTime(dir) }}
onMoveTime={(dir) => {
this.onMoveTime(dir);
}}
/>
<ZoomControls
extent={this.getTimeScaleExtent()}
@@ -356,7 +415,7 @@ class Timeline extends React.Component {
<Markers
dims={dims}
selected={this.props.app.selected}
getEventX={ev => this.getDatetimeX(ev.datetime)}
getEventX={(ev) => this.getDatetimeX(ev.datetime)}
getEventY={this.getY}
categories={categories}
transitionDuration={this.state.transitionDuration}
@@ -372,11 +431,11 @@ class Timeline extends React.Component {
narrative={this.props.app.narrative}
getDatetimeX={this.getDatetimeX}
getY={this.getY}
getHighlights={group => {
if (group === 'None') {
return []
getHighlights={(group) => {
if (group === "None") {
return [];
}
return categories.map(c => c.group === group)
return categories.map((c) => c.group === group);
}}
getCategoryColor={this.props.methods.getCategoryColor}
transitionDuration={this.state.transitionDuration}
@@ -393,48 +452,45 @@ class Timeline extends React.Component {
</div>
</div>
</div>
)
);
}
}
function mapStateToProps (state) {
function mapStateToProps(state) {
return {
dimensions: selectors.selectDimensions(state),
isNarrative: !!state.app.associations.narrative,
domain: {
events: selectors.selectStackedEvents(state),
projects: selectors.selectProjects(state),
categories: (state => {
const allcats = selectors.getCategories(state)
const active = selectors.getActiveCategories(state)
return allcats.filter(c => active.includes(c.id))
categories: ((state) => {
const allcats = selectors.getCategories(state);
const active = selectors.getActiveCategories(state);
return allcats.filter((c) => active.includes(c.id));
})(state),
narratives: state.domain.narratives
narratives: state.domain.narratives,
},
app: {
selected: state.app.selected,
language: state.app.language,
timeline: state.app.timeline,
narrative: state.app.associations.narrative,
coloringSet: state.app.associations.coloringSet
coloringSet: state.app.associations.coloringSet,
},
ui: {
dom: state.ui.dom,
styles: state.ui.style.selectedEvents,
eventRadius: state.ui.eventRadius,
filterColors: state.ui.coloring.colors
filterColors: state.ui.coloring.colors,
},
features: selectors.getFeatures(state)
}
features: selectors.getFeatures(state),
};
}
function mapDispatchToProps (dispatch) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({ setLoading, setNotLoading }, dispatch)
}
actions: bindActionCreators({ setLoading, setNotLoading }, dispatch),
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Timeline)
export default connect(mapStateToProps, mapDispatchToProps)(Timeline);

View File

@@ -1,85 +1,85 @@
import React from 'react'
import * as d3 from 'd3'
import { setD3Locale } from '../common/utilities'
import React from "react";
import * as d3 from "d3";
import { setD3Locale } from "../common/utilities";
const TEXT_HEIGHT = 15
setD3Locale(d3)
const TEXT_HEIGHT = 15;
setD3Locale(d3);
class TimelineAxis extends React.Component {
constructor () {
super()
this.xAxis0Ref = React.createRef()
this.xAxis1Ref = React.createRef()
constructor() {
super();
this.xAxis0Ref = React.createRef();
this.xAxis1Ref = React.createRef();
this.state = {
isInitialized: false
}
isInitialized: false,
};
}
componentDidUpdate () {
let fstFmt, sndFmt
componentDidUpdate() {
let fstFmt, sndFmt;
// 10yrs
if (this.props.extent > 5256000) {
fstFmt = '%Y'
sndFmt = ''
// 1yr
fstFmt = "%Y";
sndFmt = "";
// 1yr
} else if (this.props.extent > 43200) {
sndFmt = '%d %b'
fstFmt = ''
sndFmt = "%d %b";
fstFmt = "";
} else {
sndFmt = '%d %b'
fstFmt = '%H:%M'
sndFmt = "%d %b";
fstFmt = "%H:%M";
}
let { marginTop, contentHeight } = this.props.dims
const { marginTop, contentHeight } = this.props.dims;
if (this.props.scaleX) {
this.x0 =
d3.axisBottom(this.props.scaleX)
.ticks(10)
.tickPadding(0)
.tickSize(contentHeight - TEXT_HEIGHT - marginTop)
.tickFormat(d3.timeFormat(fstFmt))
this.x0 = d3
.axisBottom(this.props.scaleX)
.ticks(10)
.tickPadding(0)
.tickSize(contentHeight - TEXT_HEIGHT - marginTop)
.tickFormat(d3.timeFormat(fstFmt));
this.x1 =
d3.axisBottom(this.props.scaleX)
.ticks(10)
.tickPadding(marginTop)
.tickSize(0)
.tickFormat(d3.timeFormat(sndFmt))
this.x1 = d3
.axisBottom(this.props.scaleX)
.ticks(10)
.tickPadding(marginTop)
.tickSize(0)
.tickFormat(d3.timeFormat(sndFmt));
if (!this.state.isInitialized) this.setState({ isInitialized: true })
if (!this.state.isInitialized) this.setState({ isInitialized: true });
}
if (this.state.isInitialized) {
d3.select(this.xAxis0Ref.current)
.transition()
.duration(this.props.transitionDuration)
.call(this.x0)
.call(this.x0);
d3.select(this.xAxis1Ref.current)
.transition()
.duration(this.props.transitionDuration)
.call(this.x1)
.call(this.x1);
}
}
render () {
render() {
return (
<React.Fragment>
<>
<g
ref={this.xAxis0Ref}
transform={`translate(0, ${this.props.dims.marginTop})`}
clipPath={`url(#clip)`}
className={`axis xAxis`}
clipPath="url(#clip)"
className="axis xAxis"
/>
<g
ref={this.xAxis1Ref}
transform={`translate(0, ${this.props.dims.marginTop})`}
clipPath={`url(#clip)`}
className={`axis xAxis`}
clipPath="url(#clip)"
className="axis xAxis"
/>
</React.Fragment>
)
</>
);
}
}
export default TimelineAxis
export default TimelineAxis;

View File

@@ -1,76 +1,84 @@
import React from 'react'
import * as d3 from 'd3'
import React from "react";
import * as d3 from "d3";
class TimelineCategories extends React.Component {
constructor (props) {
super(props)
this.grabRef = React.createRef()
constructor(props) {
super(props);
this.grabRef = React.createRef();
this.state = {
isInitialized: false
}
isInitialized: false,
};
}
componentDidUpdate () {
componentDidUpdate() {
if (!this.state.isInitialized) {
const drag = d3.drag()
.on('start', this.props.onDragStart)
.on('drag', this.props.onDrag)
.on('end', this.props.onDragEnd)
const drag = d3
.drag()
.on("start", this.props.onDragStart)
.on("drag", this.props.onDrag)
.on("end", this.props.onDragEnd);
d3.select(this.grabRef.current)
.call(drag)
d3.select(this.grabRef.current).call(drag);
this.setState({ isInitialized: true })
this.setState({ isInitialized: true });
}
}
renderCategory (cat, idx) {
const { features, dims } = this.props
const strokeWidth = 1 // dims.trackHeight / (this.props.categories.length + 1)
if (features.GRAPH_NONLOCATED &&
renderCategory(cat, idx) {
const { features, dims } = this.props;
const strokeWidth = 1; // dims.trackHeight / (this.props.categories.length + 1)
if (
features.GRAPH_NONLOCATED &&
features.GRAPH_NONLOCATED.categories &&
features.GRAPH_NONLOCATED.categories.includes(cat)) {
return null
features.GRAPH_NONLOCATED.categories.includes(cat)
) {
return null;
}
return (
<React.Fragment>
<>
<g
class='tick'
class="tick"
style={{ strokeWidth }}
opacity='0.5'
opacity="0.5"
transform={`translate(0,${this.props.getCategoryY(cat)})`}
>
<line x1={dims.marginLeft} x2={dims.width - dims.width_controls} />
</g>
<g class='tick' opacity='1' transform={`translate(0,${this.props.getCategoryY(cat)})`}>
<text x={dims.marginLeft - 5} dy='0.32em'>{cat}</text>
<g
class="tick"
opacity="1"
transform={`translate(0,${this.props.getCategoryY(cat)})`}
>
<text x={dims.marginLeft - 5} dy="0.32em">
{cat}
</text>
</g>
</React.Fragment>
)
</>
);
}
render () {
const { dims, categories, fallbackLabel } = this.props
const categoriesExist = categories && categories.length > 0
render() {
const { dims, categories, fallbackLabel } = this.props;
const categoriesExist = categories && categories.length > 0;
const renderedCategories = categoriesExist
? this.props.categories.map((cat, idx) => this.renderCategory(cat, idx))
: this.renderCategory(fallbackLabel, 0)
: this.renderCategory(fallbackLabel, 0);
return (
<g class='yAxis'>
<g class="yAxis">
{renderedCategories}
<rect
ref={this.grabRef}
class='drag-grabber'
class="drag-grabber"
x={dims.marginLeft}
y={dims.marginTop}
width={dims.width - dims.marginLeft - dims.width_controls}
height={dims.contentHeight}
/>
</g>
)
);
}
}
export default TimelineCategories
export default TimelineCategories;

View File

@@ -1,37 +1,35 @@
import React from 'react'
import React from "react";
import SitesIcon from '../presentational/Icons/Sites'
import CoverIcon from '../presentational/Icons/Cover'
import InfoIcon from '../presentational/Icons/Info'
import SitesIcon from "../presentational/Icons/Sites";
import CoverIcon from "../presentational/Icons/Cover";
import InfoIcon from "../presentational/Icons/Info";
function BottomActions (props) {
function renderToggles () {
function BottomActions(props) {
function renderToggles() {
return [
<div className='bottom-action-block'>
{props.features.USE_SITES ? <SitesIcon
isActive={props.sites.enabled}
onClickHandler={props.sites.toggle}
/> : null}
<div className="bottom-action-block">
{props.features.USE_SITES ? (
<SitesIcon
isActive={props.sites.enabled}
onClickHandler={props.sites.toggle}
/>
) : null}
</div>,
<div className='botttom-action-block'>
<div className="botttom-action-block">
<InfoIcon
isActive={props.info.enabled}
onClickHandler={props.info.toggle}
/>
</div>,
<div className='botttom-action-block'>
{props.features.USE_COVER ? <CoverIcon
onClickHandler={props.cover.toggle}
/> : null}
</div>
]
<div className="botttom-action-block">
{props.features.USE_COVER ? (
<CoverIcon onClickHandler={props.cover.toggle} />
) : null}
</div>,
];
}
return (
<div className='bottom-actions'>
{renderToggles()}
</div>
)
return <div className="bottom-actions">{renderToggles()}</div>;
}
export default BottomActions
export default BottomActions;

View File

@@ -1,39 +1,47 @@
import React from 'react'
import marked from 'marked'
import Checkbox from '../presentational/Checkbox'
import copy from '../../common/data/copy.json'
import React from "react";
import marked from "marked";
import Checkbox from "../presentational/Checkbox";
import copy from "../../common/data/copy.json";
export default ({
categories,
activeCategories,
onCategoryFilter,
language
language,
}) => {
function renderCategoryTree () {
function renderCategoryTree() {
return (
<div>
{categories.map(cat => {
return (<li
key={cat.id.replace(/ /g, '_')}
className={'filter-filter active'}
style={{ marginLeft: '20px' }}
>
<Checkbox
label={cat.id}
isActive={activeCategories.includes(cat.id)}
onClickCheckbox={() => onCategoryFilter(cat.id)}
/>
</li>)
{categories.map((cat) => {
return (
<li
key={cat.id.replace(/ /g, "_")}
className="filter-filter active"
style={{ marginLeft: "20px" }}
>
<Checkbox
label={cat.id}
isActive={activeCategories.includes(cat.id)}
onClickCheckbox={() => onCategoryFilter(cat.id)}
/>
</li>
);
})}
</div>
)
);
}
return (
<div className='react-innertabpanel'>
<div className="react-innertabpanel">
<h2>{copy[language].toolbar.categories}</h2>
<p dangerouslySetInnerHTML={{ __html: marked(copy[language].toolbar.explore_by_category__description) }} />
<p
dangerouslySetInnerHTML={{
__html: marked(
copy[language].toolbar.explore_by_category__description
),
}}
/>
{renderCategoryTree()}
</div>
)
}
);
};

View File

@@ -1,63 +1,69 @@
import React from 'react'
import Checkbox from '../presentational/Checkbox'
import marked from 'marked'
import copy from '../../common/data/copy.json'
import { getFilterIdxFromColorSet } from '../../common/utilities'
import React from "react";
import Checkbox from "../presentational/Checkbox";
import marked from "marked";
import copy from "../../common/data/copy.json";
import { getFilterIdxFromColorSet } from "../../common/utilities";
/** recursively get an array of node keys to toggle */
function getFiltersToToggle (filter, activeFilters) {
const [key, children] = filter
function getFiltersToToggle(filter, activeFilters) {
const [key, children] = filter;
// base case: no children to recurse through
if (children === {}) return [key]
if (children === {}) return [key];
const turningOff = activeFilters.includes(key)
let childKeys = Object.entries(children)
.flatMap(filter => getFiltersToToggle(filter, activeFilters))
.filter(child => activeFilters.includes(child) === turningOff)
const turningOff = activeFilters.includes(key);
const childKeys = Object.entries(children)
.flatMap((filter) => getFiltersToToggle(filter, activeFilters))
.filter((child) => activeFilters.includes(child) === turningOff);
childKeys.push(key)
return childKeys
childKeys.push(key);
return childKeys;
}
function aggregatePaths (filters) {
function insertPath (children = {}, [headOfPath, ...remainder]) {
let childKey = Object.keys(children).find(key => key === headOfPath)
if (!childKey) children[headOfPath] = {}
if (remainder.length > 0) insertPath(children[headOfPath], remainder)
return children
function aggregatePaths(filters) {
function insertPath(children = {}, [headOfPath, ...remainder]) {
const childKey = Object.keys(children).find((key) => key === headOfPath);
if (!childKey) children[headOfPath] = {};
if (remainder.length > 0) insertPath(children[headOfPath], remainder);
return children;
}
const allPaths = []
filters.forEach(filterItem => allPaths.push(filterItem.filter_paths))
const allPaths = [];
filters.forEach((filterItem) => allPaths.push(filterItem.filter_paths));
let aggregatedPaths = allPaths.reduce((children, path) => insertPath(children, path), {})
return aggregatedPaths
const aggregatedPaths = allPaths.reduce(
(children, path) => insertPath(children, path),
{}
);
return aggregatedPaths;
}
function FilterListPanel ({
function FilterListPanel({
filters,
activeFilters,
onSelectFilter,
language,
coloringSet,
filterColors
filterColors,
}) {
function createNodeComponent (filter, depth) {
const [key, children] = filter
const matchingKeys = getFiltersToToggle(filter, activeFilters)
const idxFromColorSet = getFilterIdxFromColorSet(key, coloringSet)
const assignedColor = idxFromColorSet !== -1 && activeFilters.includes(key) ? filterColors[idxFromColorSet] : ''
function createNodeComponent(filter, depth) {
const [key, children] = filter;
const matchingKeys = getFiltersToToggle(filter, activeFilters);
const idxFromColorSet = getFilterIdxFromColorSet(key, coloringSet);
const assignedColor =
idxFromColorSet !== -1 && activeFilters.includes(key)
? filterColors[idxFromColorSet]
: "";
const styles = ({
const styles = {
color: assignedColor,
marginLeft: `${depth * 20}px`
})
marginLeft: `${depth * 20}px`,
};
return (
<li
key={key.replace(/ /g, '_')}
className={'filter-filter'}
key={key.replace(/ /g, "_")}
className="filter-filter"
style={{ ...styles }}
>
<Checkbox
@@ -67,29 +73,37 @@ function FilterListPanel ({
color={assignedColor}
/>
{Object.keys(children).length > 0
? Object.entries(children).map(filter => createNodeComponent(filter, depth + 1))
? Object.entries(children).map((filter) =>
createNodeComponent(filter, depth + 1)
)
: null}
</li>
)
);
}
function renderTree (filters) {
const aggregatedFilterPaths = aggregatePaths(filters)
function renderTree(filters) {
const aggregatedFilterPaths = aggregatePaths(filters);
return (
<div>
{Object.entries(aggregatedFilterPaths).map(filter => createNodeComponent(filter, 1))}
{Object.entries(aggregatedFilterPaths).map((filter) =>
createNodeComponent(filter, 1)
)}
</div>
)
);
}
return (
<div className='react-innertabpanel'>
<div className="react-innertabpanel">
<h2>{copy[language].toolbar.filters}</h2>
<p dangerouslySetInnerHTML={{ __html: marked(copy[language].toolbar.explore_by_filter__description) }} />
<p
dangerouslySetInnerHTML={{
__html: marked(copy[language].toolbar.explore_by_filter__description),
}}
/>
{renderTree(filters)}
</div>
)
);
}
export default FilterListPanel
export default FilterListPanel;

View File

@@ -1,105 +1,122 @@
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as actions from '../../actions'
import * as selectors from '../../selectors'
import React from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import * as actions from "../../actions";
import * as selectors from "../../selectors";
import { Tabs, TabPanel } from 'react-tabs'
import FilterListPanel from './FilterListPanel'
import CategoriesListPanel from './CategoriesListPanel'
import BottomActions from './BottomActions'
import copy from '../../common/data/copy.json'
import { trimAndEllipse, getImmediateFilterParent, getFilterSiblings, getFilterParents } from '../../common/utilities.js'
import { Tabs, TabPanel } from "react-tabs";
import FilterListPanel from "./FilterListPanel";
import CategoriesListPanel from "./CategoriesListPanel";
import BottomActions from "./BottomActions";
import copy from "../../common/data/copy.json";
import {
trimAndEllipse,
getImmediateFilterParent,
getFilterSiblings,
getFilterParents,
} from "../../common/utilities.js";
class Toolbar extends React.Component {
constructor (props) {
super(props)
this.onSelectFilter = this.onSelectFilter.bind(this)
this.state = { _selected: -1 }
constructor(props) {
super(props);
this.onSelectFilter = this.onSelectFilter.bind(this);
this.state = { _selected: -1 };
}
selectTab (selected) {
const _selected = (this.state._selected === selected) ? -1 : selected
this.setState({ _selected })
selectTab(selected) {
const _selected = this.state._selected === selected ? -1 : selected;
this.setState({ _selected });
}
onSelectFilter (key, matchingKeys) {
const { filters, activeFilters, coloringSet, maxNumOfColors } = this.props
onSelectFilter(key, matchingKeys) {
const { filters, activeFilters, coloringSet, maxNumOfColors } = this.props;
const parent = getImmediateFilterParent(filters, key)
const isTurningOff = activeFilters.includes(key)
const parent = getImmediateFilterParent(filters, key);
const isTurningOff = activeFilters.includes(key);
if (!isTurningOff) {
const flattenedColoringSet = coloringSet.flatMap(f => f)
const newColoringSet = matchingKeys.filter(k => flattenedColoringSet.indexOf(k) === -1)
const flattenedColoringSet = coloringSet.flatMap((f) => f);
const newColoringSet = matchingKeys.filter(
(k) => flattenedColoringSet.indexOf(k) === -1
);
const updatedColoringSet = [...coloringSet, newColoringSet]
const updatedColoringSet = [...coloringSet, newColoringSet];
if (updatedColoringSet.length <= maxNumOfColors) {
this.props.actions.updateColoringSet(updatedColoringSet)
this.props.actions.updateColoringSet(updatedColoringSet);
}
} else {
const newColoringSets = coloringSet.map(set => (
set.filter(s => {
return !matchingKeys.includes(s)
const newColoringSets = coloringSet.map((set) =>
set.filter((s) => {
return !matchingKeys.includes(s);
})
))
this.props.actions.updateColoringSet(newColoringSets.filter(item => item.length !== 0))
);
this.props.actions.updateColoringSet(
newColoringSets.filter((item) => item.length !== 0)
);
}
if (isTurningOff) {
if (parent && activeFilters.includes(parent)) {
const siblings = getFilterSiblings(filters, parent, key)
let siblingsOff = true
for (let sibling of siblings) {
const siblings = getFilterSiblings(filters, parent, key);
let siblingsOff = true;
for (const sibling of siblings) {
if (activeFilters.includes(sibling)) {
siblingsOff = false
break
siblingsOff = false;
break;
}
}
if (siblingsOff) {
const grandparentsOn = getFilterParents(filters, key).filter(filt => activeFilters.includes(filt))
matchingKeys = matchingKeys.concat(grandparentsOn)
const grandparentsOn = getFilterParents(filters, key).filter((filt) =>
activeFilters.includes(filt)
);
matchingKeys = matchingKeys.concat(grandparentsOn);
}
}
}
this.props.methods.onSelectFilter(matchingKeys)
this.props.methods.onSelectFilter(matchingKeys);
}
renderClosePanel () {
renderClosePanel() {
return (
<div className='panel-header' onClick={() => this.selectTab(-1)}>
<div className='caret' />
<div className="panel-header" onClick={() => this.selectTab(-1)}>
<div className="caret" />
</div>
)
);
}
goToNarrative (narrative) {
this.selectTab(-1) // set all unselected within this component
this.props.methods.onSelectNarrative(narrative)
goToNarrative(narrative) {
this.selectTab(-1); // set all unselected within this component
this.props.methods.onSelectNarrative(narrative);
}
renderToolbarNarrativePanel () {
renderToolbarNarrativePanel() {
return (
<TabPanel>
<h2>{copy[this.props.language].toolbar.narrative_panel_title}</h2>
<p>{copy[this.props.language].toolbar.narrative_summary}</p>
{this.props.narratives.map((narr) => {
return (
<div className='panel-action action'>
<button onClick={() => { this.goToNarrative(narr) }}>
<div className="panel-action action">
<button
onClick={() => {
this.goToNarrative(narr);
}}
>
<p>{narr.id}</p>
<p><small>{trimAndEllipse(narr.desc, 120)}</small></p>
<p>
<small>{trimAndEllipse(narr.desc, 120)}</small>
</p>
</button>
</div>
)
);
})}
</TabPanel>
)
);
}
renderToolbarCategoriesPanel () {
renderToolbarCategoriesPanel() {
if (this.props.features.CATEGORIES_AS_FILTERS) {
return (
<TabPanel>
@@ -110,11 +127,11 @@ class Toolbar extends React.Component {
language={this.props.language}
/>
</TabPanel>
)
);
}
}
renderToolbarFilterPanel () {
renderToolbarFilterPanel() {
return (
<TabPanel>
<FilterListPanel
@@ -126,106 +143,135 @@ class Toolbar extends React.Component {
filterColors={this.props.filterColors}
/>
</TabPanel>
)
);
}
renderToolbarTab (_selected, label, iconKey) {
const isActive = (this.state._selected === _selected)
let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab'
renderToolbarTab(_selected, label, iconKey) {
const isActive = this.state._selected === _selected;
const classes = isActive ? "toolbar-tab active" : "toolbar-tab";
return (
<div className={classes} onClick={() => { this.selectTab(_selected) }}>
<i className='material-icons'>{iconKey}</i>
<div className='tab-caption'>{label}</div>
<div
className={classes}
onClick={() => {
this.selectTab(_selected);
}}
>
<i className="material-icons">{iconKey}</i>
<div className="tab-caption">{label}</div>
</div>
)
);
}
renderToolbarPanels () {
const { features, narratives } = this.props
let classes = (this.state._selected >= 0) ? 'toolbar-panels' : 'toolbar-panels folded'
renderToolbarPanels() {
const { features, narratives } = this.props;
const classes =
this.state._selected >= 0 ? "toolbar-panels" : "toolbar-panels folded";
return (
<div className={classes}>
{this.renderClosePanel()}
<Tabs selectedIndex={this.state._selected}>
{narratives && narratives.length !== 0 ? this.renderToolbarNarrativePanel() : null}
{features.CATEGORIES_AS_FILTERS ? this.renderToolbarCategoriesPanel() : null}
{narratives && narratives.length !== 0
? this.renderToolbarNarrativePanel()
: null}
{features.CATEGORIES_AS_FILTERS
? this.renderToolbarCategoriesPanel()
: null}
{features.USE_ASSOCIATIONS ? this.renderToolbarFilterPanel() : null}
</Tabs>
</div>
)
);
}
renderToolbarNavs () {
renderToolbarNavs() {
if (this.props.narratives) {
return this.props.narratives.map((nar, idx) => {
const isActive = (idx === this.state._selected)
const isActive = idx === this.state._selected;
let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab'
const classes = isActive ? "toolbar-tab active" : "toolbar-tab";
return (
<div className={classes} onClick={() => { this.selectTab(idx) }}>
<div className='tab-caption'>{nar.label}</div>
<div
className={classes}
onClick={() => {
this.selectTab(idx);
}}
>
<div className="tab-caption">{nar.label}</div>
</div>
)
})
);
});
}
return null
return null;
}
renderToolbarTabs () {
const { features, narratives } = this.props
const narrativesExist = narratives && narratives.length !== 0
let title = copy[this.props.language].toolbar.title
if (process.env.display_title) title = process.env.display_title
const narrativesLabel = copy[this.props.language].toolbar.narratives_label
const filtersLabel = copy[this.props.language].toolbar.filters_label
const categoriesLabel = 'Categories' // TODO:
renderToolbarTabs() {
const { features, narratives } = this.props;
const narrativesExist = narratives && narratives.length !== 0;
let title = copy[this.props.language].toolbar.title;
if (process.env.display_title) title = process.env.display_title;
const narrativesLabel = copy[this.props.language].toolbar.narratives_label;
const filtersLabel = copy[this.props.language].toolbar.filters_label;
const categoriesLabel = "Categories"; // TODO:
const narrativesIdx = 0
const categoriesIdx = narrativesExist ? 1 : 0
const filtersIdx = (narrativesExist && features.CATEGORIES_AS_FILTERS) ? 2 : (
narrativesExist || features.CATEGORIES_AS_FILTERS ? 1 : 0
)
const narrativesIdx = 0;
const categoriesIdx = narrativesExist ? 1 : 0;
const filtersIdx =
narrativesExist && features.CATEGORIES_AS_FILTERS
? 2
: narrativesExist || features.CATEGORIES_AS_FILTERS
? 1
: 0;
return (
<div className='toolbar'>
<div className='toolbar-header'onClick={this.props.methods.onTitle}><p>{title}</p></div>
<div className='toolbar-tabs'>
{narrativesExist ? this.renderToolbarTab(narrativesIdx, narrativesLabel, 'timeline') : null}
{features.CATEGORIES_AS_FILTERS ? this.renderToolbarTab(categoriesIdx, categoriesLabel, 'widgets') : null}
{features.USE_ASSOCIATIONS ? this.renderToolbarTab(filtersIdx, filtersLabel, 'filter_list') : null}
<div className="toolbar">
<div className="toolbar-header" onClick={this.props.methods.onTitle}>
<p>{title}</p>
</div>
<div className="toolbar-tabs">
{narrativesExist
? this.renderToolbarTab(narrativesIdx, narrativesLabel, "timeline")
: null}
{features.CATEGORIES_AS_FILTERS
? this.renderToolbarTab(categoriesIdx, categoriesLabel, "widgets")
: null}
{features.USE_ASSOCIATIONS
? this.renderToolbarTab(filtersIdx, filtersLabel, "filter_list")
: null}
</div>
<BottomActions
info={{
enabled: this.props.infoShowing,
toggle: this.props.actions.toggleInfoPopup
toggle: this.props.actions.toggleInfoPopup,
}}
sites={{
enabled: this.props.sitesShowing,
toggle: this.props.actions.toggleSites
toggle: this.props.actions.toggleSites,
}}
cover={{
toggle: this.props.actions.toggleCover
toggle: this.props.actions.toggleCover,
}}
features={this.props.features}
/>
</div>
)
);
}
render () {
const { isNarrative } = this.props
render() {
const { isNarrative } = this.props;
return (
<div id='toolbar-wrapper' className={`toolbar-wrapper ${(isNarrative) ? 'narrative-mode' : ''}`}>
<div
id="toolbar-wrapper"
className={`toolbar-wrapper ${isNarrative ? "narrative-mode" : ""}`}
>
{this.renderToolbarTabs()}
{this.renderToolbarPanels()}
</div>
)
);
}
}
function mapStateToProps (state) {
function mapStateToProps(state) {
return {
filters: selectors.getFilters(state),
categories: selectors.getCategories(state),
@@ -240,14 +286,14 @@ function mapStateToProps (state) {
coloringSet: state.app.associations.coloringSet,
maxNumOfColors: state.ui.coloring.maxNumOfColors,
filterColors: state.ui.coloring.colors,
features: selectors.getFeatures(state)
}
features: selectors.getFeatures(state),
};
}
function mapDispatchToProps (dispatch) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch)
}
actions: bindActionCreators(actions, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Toolbar)
export default connect(mapStateToProps, mapDispatchToProps)(Toolbar);

View File

@@ -1,82 +1,80 @@
import React from 'react'
import Checkbox from '../presentational/Checkbox'
import React from "react";
import Checkbox from "../presentational/Checkbox";
function SelectFilter (props) {
function isActive () {
function SelectFilter(props) {
function isActive() {
if (props.isCategory) {
return props.categoryFilters.includes(props.filter.id)
return props.categoryFilters.includes(props.filter.id);
}
return props.filterFilters.includes(props.filter.id)
return props.filterFilters.includes(props.filter.id);
}
function onClickFilter () {
function onClickFilter() {
if (isActive()) {
props.filter({
filters: props.filterFilters.filter(element => element !== props.filter.id)
})
filters: props.filterFilters.filter(
(element) => element !== props.filter.id
),
});
} else {
props.filter({
filters: props.filterFilters.concat(props.filter.id)
})
filters: props.filterFilters.concat(props.filter.id),
});
}
}
function onClickCategory () {
function onClickCategory() {
if (isActive()) {
props.filter({
categories: props.categoryFilters.filter(element => element !== props.filter.id)
})
categories: props.categoryFilters.filter(
(element) => element !== props.filter.id
),
});
} else {
props.filter({
categories: props.categoryFilters.concat(props.filter.id)
})
categories: props.categoryFilters.concat(props.filter.id),
});
}
}
function renderFilter () {
const filter = props.filter
let classes = (isActive()) ? 'filter-filter active' : 'filter-filter'
let label = `${filter.name} ( ${filter.mentions} )`
function renderFilter() {
const filter = props.filter;
const classes = isActive() ? "filter-filter active" : "filter-filter";
let label = `${filter.name} ( ${filter.mentions} )`;
if (props.isShowTree) {
label = `${filter.group} > ${filter.subgroup} > ${filter.name} ( ${filter.mentions} )`
label = `${filter.group} > ${filter.subgroup} > ${filter.name} ( ${filter.mentions} )`;
}
return (
<li
key={props.filter.id}
className={classes}
>
<li key={props.filter.id} className={classes}>
<Checkbox
isActive={isActive()}
label={label}
onClickCheckbox={() => onClickFilter()}
/>
</li>
)
);
}
function renderCategory () {
const category = props.categories[props.filter.id]
let classes = (isActive()) ? 'filter-filter active' : 'filter-filter'
function renderCategory() {
const category = props.categories[props.filter.id];
const classes = isActive() ? "filter-filter active" : "filter-filter";
if (category) {
return (
<li
key={props.filter.id}
className={classes}
>
<li key={props.filter.id} className={classes}>
<Checkbox
isActive={isActive()}
label={`${category.name} ( ${category.counts} )`}
onClickCheckbox={onClickCategory}
/>
</li>
)
);
}
return (<div />)
return <div />;
}
if (props.isCategory) return (renderCategory())
return (renderFilter())
if (props.isCategory) return renderCategory();
return renderFilter();
}
export default SelectFilter
export default SelectFilter;

View File

@@ -1,17 +1,15 @@
import React from 'react'
import React from "react";
const CardCaret = ({ isOpen, toggle }) => {
let classes = (isOpen)
? 'arrow-down'
: 'arrow-down folded'
const classes = isOpen ? "arrow-down" : "arrow-down folded";
return (
<div className='card-toggle' onClick={toggle}>
<div className="card-toggle" onClick={toggle}>
<p>
<i className={classes} />
</p>
</div>
)
}
);
};
export default CardCaret
export default CardCaret;

View File

@@ -1,15 +1,15 @@
import React from 'react'
import React from "react";
import { capitalize } from '../../../common/utilities.js'
import { capitalize } from "../../../common/utilities.js";
const CardCategory = ({ categoryTitle, categoryLabel, color }) => (
<div className='card-row card-cell category'>
<div className="card-row card-cell category">
<h4>{categoryTitle}</h4>
<p>
{capitalize(categoryLabel)}
<span className='color-category' style={{ background: color }} />
<span className="color-category" style={{ background: color }} />
</p>
</div>
)
);
export default CardCategory
export default CardCategory;

View File

@@ -1,14 +1,14 @@
import React from 'react'
import marked from 'marked'
import React from "react";
import marked from "marked";
const CardCustomField = ({ field, value }) => (
<div className='card-cell'>
<div className="card-cell">
<p>
<i className='material-icons left'>{field.icon}</i>
<b>{field.title ? `${field.title}: ` : '- '}</b>
{field.kind === 'text' ? value : marked(`[${value}](${field.value})`)}
<i className="material-icons left">{field.icon}</i>
<b>{field.title ? `${field.title}: ` : "- "}</b>
{field.kind === "text" ? value : marked(`[${value}](${field.value})`)}
</p>
</div>
)
);
export default CardCustomField
export default CardCustomField;

View File

@@ -1,28 +1,28 @@
import React from 'react'
import React from "react";
import copy from '../../../common/data/copy.json'
import copy from "../../../common/data/copy.json";
const CardLocation = ({ language, location, isPrecise }) => {
if (location !== '') {
if (location !== "") {
return (
<div className='card-cell location'>
<div className="card-cell location">
<p>
<i className='material-icons left'>location_on</i>
{`${location}${(isPrecise) ? '' : ' (Approximated)'}`}
<i className="material-icons left">location_on</i>
{`${location}${isPrecise ? "" : " (Approximated)"}`}
</p>
</div>
)
);
} else {
const unknown = copy[language].cardstack.unknown_location
const unknown = copy[language].cardstack.unknown_location;
return (
<div className='card-cell location'>
<div className="card-cell location">
<p>
<i className='material-icons left'>location_on</i>
<i className="material-icons left">location_on</i>
{unknown}
</p>
</div>
)
);
}
}
};
export default CardLocation
export default CardLocation;

View File

@@ -1,79 +1,79 @@
import React from 'react'
import Img from 'react-image'
import Spinner from '../Spinner'
import { typeForPath } from '../../../common/utilities'
import React from "react";
import Img from "react-image";
import Spinner from "../Spinner";
import { typeForPath } from "../../../common/utilities";
const CardSource = ({ source, isLoading, onClickHandler }) => {
function renderIconText (type) {
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'
case 'Document':
return 'picture_as_pdf'
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";
case "Document":
return "picture_as_pdf";
default:
return 'help'
return "help";
}
}
if (!source) {
return (
<div className='card-source'>
<div className="card-source">
<div>Error: this source was not found</div>
</div>
)
);
}
const isImgUrl = /\.(jpg|png)$/
let thumbnail = source.thumbnail
const isImgUrl = /\.(jpg|png)$/;
let thumbnail = source.thumbnail;
if (!thumbnail || thumbnail === '' || !thumbnail.match(isImgUrl)) {
if (!thumbnail || thumbnail === "" || !thumbnail.match(isImgUrl)) {
// default to first image in paths, null if no images
const imgs = source.paths.filter(p => p.match(isImgUrl))
thumbnail = imgs.length > 0 ? imgs[0] : null
const imgs = source.paths.filter((p) => p.match(isImgUrl));
thumbnail = imgs.length > 0 ? imgs[0] : null;
}
if (source.type === '' && source.paths.length >= 1) {
source.type = typeForPath(source.paths[0])
if (source.type === "" && source.paths.length >= 1) {
source.type = typeForPath(source.paths[0]);
}
const fallbackIcon = (
<i className='material-icons source-icon'>
{renderIconText(source.type)}
</i>
)
<i className="material-icons source-icon">{renderIconText(source.type)}</i>
);
return (
<div className='card-source'>
{isLoading
? <Spinner />
: (
<div className='source-row' onClick={() => onClickHandler(source)}>
{thumbnail ? (
<Img
className='source-icon'
src={thumbnail}
loader={<Spinner small />}
unloader={fallbackIcon}
width={30}
height={30}
/>
) : fallbackIcon}
<p>{source.title ? source.title : source.id}</p>
</div>
)}
<div className="card-source">
{isLoading ? (
<Spinner />
) : (
<div className="source-row" onClick={() => onClickHandler(source)}>
{thumbnail ? (
<Img
className="source-icon"
src={thumbnail}
loader={<Spinner small />}
unloader={fallbackIcon}
width={30}
height={30}
/>
) : (
fallbackIcon
)}
<p>{source.title ? source.title : source.id}</p>
</div>
)}
</div>
)
}
);
};
export default CardSource
export default CardSource;

View File

@@ -1,18 +1,18 @@
import React from 'react'
import React from "react";
import copy from '../../../common/data/copy.json'
import copy from "../../../common/data/copy.json";
const CardSummary = ({ language, description, isHighlighted }) => {
const summary = copy[language].cardstack.description
const summary = copy[language].cardstack.description;
return (
<div className='card-row summary'>
<div className='card-cell'>
<div className="card-row summary">
<div className="card-cell">
<h4>{summary}</h4>
<p>{description}</p>
</div>
</div>
)
}
);
};
export default CardSummary
export default CardSummary;

View File

@@ -1,32 +1,33 @@
import React from 'react'
import React from "react";
import copy from '../../../common/data/copy.json'
import { isNotNullNorUndefined } from '../../../common/utilities'
import copy from "../../../common/data/copy.json";
import { isNotNullNorUndefined } from "../../../common/utilities";
const CardTime = ({ timelabel, language, precision }) => {
// const daytimeLang = copy[language].cardstack.timestamp
// const estimatedLang = copy[language].cardstack.estimated
const unknownLang = copy[language].cardstack.unknown_time
const unknownLang = copy[language].cardstack.unknown_time;
if (isNotNullNorUndefined(timelabel)) {
return (
<div className='card-cell timestamp'>
<div className="card-cell timestamp">
<p>
<i className='material-icons left'>today</i>
{timelabel}{(precision && precision !== '') ? ` - ${precision}` : ''}
<i className="material-icons left">today</i>
{timelabel}
{precision && precision !== "" ? ` - ${precision}` : ""}
</p>
</div>
)
);
} else {
return (
<div className='card-cell timestamp'>
<div className="card-cell timestamp">
<p>
<i className='material-icons left'>today</i>
<i className="material-icons left">today</i>
{unknownLang}
</p>
</div>
)
);
}
}
};
export default CardTime
export default CardTime;

View File

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

View File

@@ -1,20 +1,43 @@
import React from 'react'
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' />
<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
export default CoeventIcon;

View File

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

View File

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

View File

@@ -1,11 +1,23 @@
import React from 'react'
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
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

@@ -1,16 +1,21 @@
import React from 'react'
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 ' />
<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
export default RouteIcon;

View File

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

View File

@@ -63,7 +63,7 @@ function Cluster({
return (
<g
className={"cluster-event"}
className="cluster-event"
transform={`translate(${x}, ${y})`}
onClick={(e) => onClick({ id: clusterId, latitude, longitude })}
onMouseEnter={() => setHovered(true)}
@@ -75,7 +75,7 @@ function Cluster({
styles={{
...styles,
}}
className={"cluster-event-marker"}
className="cluster-event-marker"
/>
{hovered ? renderHover(cluster) : null}
</g>

View File

@@ -1,35 +1,41 @@
import React from 'react'
import { getCoordinatesForPercent } from '../../../common/utilities'
import React from "react";
import { getCoordinatesForPercent } from "../../../common/utilities";
function ColoredMarkers ({ radius, colorPercentMap, styles, className }) {
let cumulativeAngleSweep = 0
const colors = Object.keys(colorPercentMap)
function ColoredMarkers({ radius, colorPercentMap, styles, className }) {
let cumulativeAngleSweep = 0;
const colors = Object.keys(colorPercentMap);
return (
<React.Fragment>
<>
{colors.map((color, idx) => {
const colorPercent = colorPercentMap[color]
const colorPercent = colorPercentMap[color];
const [startX, startY] = getCoordinatesForPercent(radius, cumulativeAngleSweep)
const [startX, startY] = getCoordinatesForPercent(
radius,
cumulativeAngleSweep
);
cumulativeAngleSweep += colorPercent
cumulativeAngleSweep += colorPercent;
const [endX, endY] = getCoordinatesForPercent(radius, cumulativeAngleSweep)
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
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(' ')
"L 0 0 ", // Line
`L ${startX} ${startY} Z`, // Line
].join(" ");
const extraStyles = ({
const extraStyles = {
...styles,
fill: color
})
fill: color,
};
return (
<path
@@ -38,10 +44,10 @@ function ColoredMarkers ({ radius, colorPercentMap, styles, className }) {
d={arc}
style={extraStyles}
/>
)
);
})}
</React.Fragment>
)
</>
);
}
export default ColoredMarkers
export default ColoredMarkers;

View File

@@ -1,14 +1,30 @@
import React from 'react'
import React from "react";
const MapDefsMarkers = () => (
<defs>
<marker id='arrow' viewBox='0 0 6 6' refX='3' refY='3' markerWidth='6' markerHeight='6' orient='auto'>
<path d='M0,3v-3l6,3l-6,3z' style={{ fill: 'red' }} />
<marker
id="arrow"
viewBox="0 0 6 6"
refX="3"
refY="3"
markerWidth="6"
markerHeight="6"
orient="auto"
>
<path d="M0,3v-3l6,3l-6,3z" style={{ fill: "red" }} />
</marker>
<marker id='arrow-off' viewBox='0 0 6 6' refX='3' refY='3' markerWidth='6' markerHeight='6' orient='auto'>
<path d='M0,3v-3l6,3l-6,3z' style={{ fill: 'black', fillOpacity: 0.2 }} />
<marker
id="arrow-off"
viewBox="0 0 6 6"
refX="3"
refY="3"
markerWidth="6"
markerHeight="6"
orient="auto"
>
<path d="M0,3v-3l6,3l-6,3z" style={{ fill: "black", fillOpacity: 0.2 }} />
</marker>
</defs>
)
);
export default MapDefsMarkers
export default MapDefsMarkers;

View File

@@ -1,10 +1,15 @@
import React from 'react'
import { Portal } from 'react-portal'
import colors from '../../../common/global.js'
import ColoredMarkers from './ColoredMarkers.jsx'
import { calcOpacity, getCoordinatesForPercent, calculateColorPercentages, zipColorsToPercentages } from '../../../common/utilities'
import React from "react";
import { Portal } from "react-portal";
import colors from "../../../common/global.js";
import ColoredMarkers from "./ColoredMarkers.jsx";
import {
calcOpacity,
getCoordinatesForPercent,
calculateColorPercentages,
zipColorsToPercentages,
} from "../../../common/utilities";
function MapEvents ({
function MapEvents({
getCategoryColor,
categories,
projectPoint,
@@ -17,110 +22,120 @@ function MapEvents ({
eventRadius,
coloringSet,
filterColors,
features
features,
}) {
function handleEventSelect (e, location) {
const events = e.shiftKey ? selected.concat(location.events) : location.events
onSelect(events)
function handleEventSelect(e, location) {
const events = e.shiftKey
? selected.concat(location.events)
: location.events;
onSelect(events);
}
function renderBorder () {
function renderBorder() {
return (
<React.Fragment>
{<circle
class='event-hover'
cx='0'
cy='0'
r='10'
<>
<circle
class="event-hover"
cx="0"
cy="0"
r="10"
stroke={colors.primaryHighlight}
fill-opacity='0.0'
/>}
</React.Fragment>
)
fill-opacity="0.0"
/>
</>
);
}
function renderLocationSlicesByAssociation (location) {
const colorPercentages = calculateColorPercentages([location], coloringSet)
function renderLocationSlicesByAssociation(location) {
const colorPercentages = calculateColorPercentages([location], coloringSet);
let styles = ({
const styles = {
stroke: colors.darkBackground,
strokeWidth: 0,
fillOpacity: narrative ? 1 : calcOpacity(location.events.length)
})
fillOpacity: narrative ? 1 : calcOpacity(location.events.length),
};
return (
<ColoredMarkers
radius={eventRadius}
colorPercentMap={zipColorsToPercentages(filterColors, colorPercentages)}
styles={{
...styles
...styles,
}}
className={'location-event-marker'}
className="location-event-marker"
/>
)
);
}
function renderLocationSlicesByCategory (location) {
const locCategory = location.events.length > 0 ? location.events[0].category : 'default'
const customStyles = styleLocation ? styleLocation(location) : null
const extraStyles = customStyles[0]
function renderLocationSlicesByCategory(location) {
const locCategory =
location.events.length > 0 ? location.events[0].category : "default";
const customStyles = styleLocation ? styleLocation(location) : null;
const extraStyles = customStyles[0];
let styles = ({
const styles = {
fill: getCategoryColor(locCategory),
stroke: colors.darkBackground,
strokeWidth: 0,
fillOpacity: narrative ? 1 : calcOpacity(location.events.length),
...extraStyles
})
...extraStyles,
};
const colorSlices = location.events.map(e => e.colour ? e.colour : getCategoryColor(e.category))
const colorSlices = location.events.map((e) =>
e.colour ? e.colour : getCategoryColor(e.category)
);
let cumulativeAngleSweep = 0
let cumulativeAngleSweep = 0;
return (
<React.Fragment>
<>
{colorSlices.map((color, idx) => {
const r = eventRadius
const r = eventRadius;
// Based on the number of events in each location,
// create a slice per event filled with its category color
const [startX, startY] = getCoordinatesForPercent(r, cumulativeAngleSweep)
const [startX, startY] = getCoordinatesForPercent(
r,
cumulativeAngleSweep
);
cumulativeAngleSweep = (idx + 1) / colorSlices.length
cumulativeAngleSweep = (idx + 1) / colorSlices.length;
const [endX, endY] = getCoordinatesForPercent(r, cumulativeAngleSweep)
const [endX, endY] = getCoordinatesForPercent(
r,
cumulativeAngleSweep
);
// if the slices are less than 2, take the long arc
const largeArcFlag = (colorSlices.length === 1) ? 1 : 0
const largeArcFlag = colorSlices.length === 1 ? 1 : 0;
// create an array and join it just for code readability
const arc = [
`M ${startX} ${startY}`, // Move
`A ${r} ${r} 0 ${largeArcFlag} 1 ${endX} ${endY}`, // Arc
`L 0 0 `, // Line
`L ${startX} ${startY} Z` // Line
].join(' ')
"L 0 0 ", // Line
`L ${startX} ${startY} Z`, // Line
].join(" ");
const extraStyles = ({
const extraStyles = {
...styles,
fill: color
})
fill: color,
};
return (
<path
class='location-event-marker'
class="location-event-marker"
id={`arc_${idx}`}
d={arc}
style={extraStyles}
/>
)
);
})}
</React.Fragment>
)
</>
);
}
function renderLocation (location) {
function renderLocation(location) {
/**
{
events: [...],
@@ -129,53 +144,55 @@ function MapEvents ({
longitude: '32.2'
}
*/
if (!location.latitude || !location.longitude) return null
const { x, y } = projectPoint([location.latitude, location.longitude])
if (!location.latitude || !location.longitude) return null;
const { x, y } = projectPoint([location.latitude, location.longitude]);
// in narrative mode, only render events in narrative
// TODO: move this to a selector
if (narrative) {
const { steps } = narrative
const onlyIfInNarrative = e => steps.map(s => s.id).includes(e.id)
const eventsInNarrative = location.events.filter(onlyIfInNarrative)
const { steps } = narrative;
const onlyIfInNarrative = (e) => steps.map((s) => s.id).includes(e.id);
const eventsInNarrative = location.events.filter(onlyIfInNarrative);
if (eventsInNarrative.length <= 0) {
return null
return null;
}
}
const customStyles = styleLocation ? styleLocation(location) : null
const extraRender = () => (
<React.Fragment>
{customStyles[1]}
</React.Fragment>
)
const customStyles = styleLocation ? styleLocation(location) : null;
const extraRender = () => <>{customStyles[1]}</>;
const isSelected = selected.reduce((acc, event) => {
return acc || (event.latitude === location.latitude && event.longitude === location.longitude)
}, false)
return (
acc ||
(event.latitude === location.latitude &&
event.longitude === location.longitude)
);
}, false);
return (
<g
className={`location-event ${narrative ? 'no-hover' : ''}`}
className={`location-event ${narrative ? "no-hover" : ""}`}
transform={`translate(${x}, ${y})`}
onClick={(e) => handleEventSelect(e, location)}
>
{features.COLOR_BY_ASSOCIATION ? renderLocationSlicesByAssociation(location) : null}
{features.COLOR_BY_CATEGORY ? renderLocationSlicesByCategory(location) : null}
{features.COLOR_BY_ASSOCIATION
? renderLocationSlicesByAssociation(location)
: null}
{features.COLOR_BY_CATEGORY
? renderLocationSlicesByCategory(location)
: null}
{extraRender ? extraRender() : null}
{isSelected ? null : renderBorder()}
</g>
)
);
}
return (
<Portal node={svg}>
<g className='event-locations'>
{locations.map(renderLocation)}
</g>
<g className="event-locations">{locations.map(renderLocation)}</g>
</Portal>
)
);
}
export default MapEvents
export default MapEvents;

View File

@@ -1,206 +1,208 @@
import React from 'react'
import { Portal } from 'react-portal'
import React from "react";
import { Portal } from "react-portal";
// import { concatStatic } from 'rxjs/operator/concat'
// import { single } from 'rxjs/operator/single'
const defaultStyles = {
strokeOpacity: 1,
strokeWidth: 0,
strokeDasharray: 'none',
stroke: 'none'
}
strokeDasharray: "none",
stroke: "none",
};
function MapNarratives ({
function MapNarratives({
styles,
onSelectNarrative,
svg,
narrative,
narratives,
projectPoint,
features
features,
}) {
function getNarrativeStyle (narrativeId) {
const styleName = (narrativeId && narrativeId in styles)
? narrativeId
: 'default'
return styles[styleName]
function getNarrativeStyle(narrativeId) {
const styleName =
narrativeId && narrativeId in styles ? narrativeId : "default";
return styles[styleName];
}
const narrativesExist = narratives && narratives.length !== 0
const narrativesExist = narratives && narratives.length !== 0;
function hasNoLocation (step) {
return (step.latitude === '' || step.longitude === '')
function hasNoLocation(step) {
return step.latitude === "" || step.longitude === "";
}
function _renderNarrativeStepArrow (p1, p2, styles) {
const distance = Math.sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y))
const theta = Math.atan2(p2.y - p1.y, p2.x - p1.x) // Angle of narrative step line
const alpha = Math.atan2(1, 2) // Angle of arrow overture
const edge = 10 // Arrow edge length
const offset = (distance < 24) ? distance / 2 : 24
function _renderNarrativeStepArrow(p1, p2, styles) {
const distance = Math.sqrt(
(p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y)
);
const theta = Math.atan2(p2.y - p1.y, p2.x - p1.x); // Angle of narrative step line
const alpha = Math.atan2(1, 2); // Angle of arrow overture
const edge = 10; // Arrow edge length
const offset = distance < 24 ? distance / 2 : 24;
// Arrow corners
const coord0 = {
x: p2.x - offset * Math.cos(theta),
y: p2.y - offset * Math.sin(theta)
}
y: p2.y - offset * Math.sin(theta),
};
const coord1 = {
x: coord0.x - edge * Math.cos(-theta - alpha),
y: coord0.y + edge * Math.sin(-theta - alpha)
}
y: coord0.y + edge * Math.sin(-theta - alpha),
};
const coord2 = {
x: coord0.x - edge * Math.cos(-theta + alpha),
y: coord0.y + edge * Math.sin(-theta + alpha)
}
y: coord0.y + edge * Math.sin(-theta + alpha),
};
return (<path
className='narrative-step-arrow'
d={`
return (
<path
className="narrative-step-arrow"
d={`
M ${coord0.x} ${coord0.y}
L ${coord1.x} ${coord1.y}
L ${coord2.x} ${coord2.y} Z
`}
style={{
...styles,
fillOpacity: styles.strokeOpacity,
fill: styles.stroke
}}
/>)
style={{
...styles,
fillOpacity: styles.strokeOpacity,
fill: styles.stroke,
}}
/>
);
}
function _renderNarrativeStep (p1, p2, styles) {
const { stroke, strokeWidth, strokeDasharray, strokeOpacity } = styles
function _renderNarrativeStep(p1, p2, styles) {
const { stroke, strokeWidth, strokeDasharray, strokeOpacity } = styles;
return (
<g>
<line
className='narrative-step'
className="narrative-step"
x1={p1.x}
x2={p2.x}
y1={p1.y}
y2={p2.y}
markerStart='none'
onClick={n => onSelectNarrative(n)}
markerStart="none"
onClick={(n) => onSelectNarrative(n)}
style={{
strokeWidth,
strokeDasharray,
strokeOpacity,
stroke
stroke,
}}
/>
{(stroke !== 'none')
? _renderNarrativeStepArrow(p1, p2, styles)
: ''
}
{stroke !== "none" ? _renderNarrativeStepArrow(p1, p2, styles) : ""}
</g>
)
);
}
function renderBetweenSteps (step1, step2, extraStyles) {
function renderBetweenSteps(step1, step2, extraStyles) {
// don't draw if one of the steps has no location, or not in narrative
if (hasNoLocation(step1) || hasNoLocation(step2)) {
return null
return null;
}
// don't draw if something else is up
if (!step1 || !step2) {
return null
return null;
}
const p1 = projectPoint([step1.latitude, step1.longitude])
const p2 = projectPoint([step2.latitude, step2.longitude])
const p1 = projectPoint([step1.latitude, step1.longitude]);
const p2 = projectPoint([step2.latitude, step2.longitude]);
return _renderNarrativeStep(p1, p2, {
...defaultStyles,
...(extraStyles || {})
})
...(extraStyles || {}),
});
}
function renderFullNarrative (n) {
function renderFullNarrative(n) {
if (n === null || n.id !== narrative.id) {
return null
return null;
}
const arrows = []
const arrows = [];
for (let idx = 0; idx < n.steps.length - 1; idx += 1) {
const step1 = n.steps[idx]
const step2 = n.steps[idx + 1]
arrows.push(renderBetweenSteps(step1, step2, getNarrativeStyle(n.id)))
const step1 = n.steps[idx];
const step2 = n.steps[idx + 1];
arrows.push(renderBetweenSteps(step1, step2, getNarrativeStyle(n.id)));
}
return arrows
return arrows;
}
function renderBetweenMarked (n) {
function renderBetweenMarked(n) {
// this function should only be called if features.NARRATIVE_STEP_STYLES
// is true, and thus there is a 'stepStyles' attributes in events
if (n === null || n.id !== narrative.id) {
return null
return null;
}
const arrows = []
const arrows = [];
let lastMarked = null
let lastMarked = null;
if (narrativesExist) {
for (let idx = 0; idx < n.steps.length; idx += 1) {
const step = n.steps[idx]
const step = n.steps[idx];
if (lastMarked) {
arrows.push(renderBetweenSteps(
lastMarked,
step,
n.withLines ? { strokeWidth: '1px', stroke: step.colour } : {})
)
arrows.push(
renderBetweenSteps(
lastMarked,
step,
n.withLines ? { strokeWidth: "1px", stroke: step.colour } : {}
)
);
}
lastMarked = step
lastMarked = step;
}
} else {
for (let idx = 0; idx < n.steps.length; idx += 1) {
const step = n.steps[idx]
const _idx = step.narratives.indexOf(n.id)
const stepStyle = step.narrative___stepStyles[_idx]
const step = n.steps[idx];
const _idx = step.narratives.indexOf(n.id);
const stepStyle = step.narrative___stepStyles[_idx];
if (stepStyle !== 'None') {
if (stepStyle !== "None") {
if (lastMarked) {
arrows.push(renderBetweenSteps(lastMarked, step, styles.stepStyles[stepStyle]))
arrows.push(
renderBetweenSteps(lastMarked, step, styles.stepStyles[stepStyle])
);
}
lastMarked = step
lastMarked = step;
}
}
}
return arrows
return arrows;
}
function renderNarrative (n) {
const narrativeId = `narrative-${n.id.replace(/ /g, '_')}`
function renderNarrative(n) {
const narrativeId = `narrative-${n.id.replace(/ /g, "_")}`;
const body = narrativesExist
? renderBetweenMarked(n)
: (features.NARRATIVE_STEP_STYLES
? renderBetweenMarked(n)
: renderFullNarrative(n))
: features.NARRATIVE_STEP_STYLES
? renderBetweenMarked(n)
: renderFullNarrative(n);
return (
<g id={narrativeId} className='narrative'>
<g id={narrativeId} className="narrative">
{body}
</g>
)
);
}
// don't render in explore mode
if (narrative === null) {
return null
return null;
}
return (
<Portal node={svg}>
<g className='narratives'>
{narratives.map(renderNarrative)}
</g>
<g className="narratives">{narratives.map(renderNarrative)}</g>
</Portal>
)
);
}
export default MapNarratives
export default MapNarratives;

View File

@@ -1,38 +1,38 @@
import React from 'react'
import { Portal } from 'react-portal'
import colors from '../../../common/global.js'
import React from "react";
import { Portal } from "react-portal";
import colors from "../../../common/global.js";
class MapSelectedEvents extends React.Component {
renderMarker (marker) {
const { x, y } = this.props.projectPoint([marker.latitude, marker.longitude])
const styles = this.props.styles
const r = marker.radius ? marker.radius + 5 : 24
renderMarker(marker) {
const { x, y } = this.props.projectPoint([
marker.latitude,
marker.longitude,
]);
const styles = this.props.styles;
const r = marker.radius ? marker.radius + 5 : 24;
return (
<g
className='location-marker'
transform={`translate(${x - r}, ${y})`}
>
<g className="location-marker" transform={`translate(${x - r}, ${y})`}>
<path
className='leaflet-interactive'
className="leaflet-interactive"
stroke={styles ? styles.stroke : colors.primaryHighlight}
stroke-opacity='1'
stroke-width={styles ? styles['stroke-width'] : 2}
stroke-linecap=''
stroke-linejoin='round'
stroke-dasharray={styles ? styles['stroke-dasharray'] : '2,2'}
fill='none'
stroke-opacity="1"
stroke-width={styles ? styles["stroke-width"] : 2}
stroke-linecap=""
stroke-linejoin="round"
stroke-dasharray={styles ? styles["stroke-dasharray"] : "2,2"}
fill="none"
d={`M0,0a${r},${r} 0 1,0 ${r * 2},0 a${r},${r} 0 1,0 -${r * 2},0 `}
/>
</g>
)
);
}
render () {
render() {
return (
<Portal node={this.props.svg}>
{this.props.selected.map(s => this.renderMarker(s))}
{this.props.selected.map((s) => this.renderMarker(s))}
</Portal>
)
);
}
}
export default MapSelectedEvents
export default MapSelectedEvents;

View File

@@ -1,49 +1,47 @@
import React from 'react'
import { Portal } from 'react-portal'
import React from "react";
import { Portal } from "react-portal";
function MapShapes ({ svg, shapes, projectPoint, styles }) {
function renderShape (shape) {
const lineCoords = []
const points = shape.points
.map(projectPoint)
function MapShapes({ svg, shapes, projectPoint, styles }) {
function renderShape(shape) {
const lineCoords = [];
const points = shape.points.map(projectPoint);
points.forEach((p1, idx) => {
if (idx < shape.points.length - 1) {
const p2 = points[idx + 1]
const p2 = points[idx + 1];
lineCoords.push({
x1: p1.x,
y1: p1.y,
x2: p2.x,
y2: p2.y
})
y2: p2.y,
});
}
})
});
return lineCoords.map(coords => {
const shapeStyles = (shape.name in styles)
? styles[shape.name]
: styles.default
return lineCoords.map((coords) => {
const shapeStyles =
shape.name in styles ? styles[shape.name] : styles.default;
return (
<line
id={`${shape.name}_style`}
markerStart='none'
markerStart="none"
{...coords}
style={shapeStyles}
/>
)
})
);
});
}
if (!shapes || !shapes.length) return null
if (!shapes || !shapes.length) return null;
return (
<Portal node={svg}>
<g id={`shapes-layer`} className='narrative'>
<g id="shapes-layer" className="narrative">
{shapes.map(renderShape)}
</g>
</Portal>
)
);
}
export default MapShapes
export default MapShapes;

View File

@@ -1,24 +1,25 @@
import React from 'react'
import React from "react";
function MapSites ({ sites, projectPoint }) {
function renderSite (site) {
const { x, y } = projectPoint([site.latitude, site.longitude])
function MapSites({ sites, projectPoint }) {
function renderSite(site) {
const { x, y } = projectPoint([site.latitude, site.longitude]);
return (<div
className='leaflet-tooltip site-label leaflet-zoom-animated leaflet-tooltip-top'
style={{ opacity: 1, transform: `translate3d(calc(${x}px - 50%), ${y - 25}px, 0px)` }}>
{site.site}
</div>
)
return (
<div
className="leaflet-tooltip site-label leaflet-zoom-animated leaflet-tooltip-top"
style={{
opacity: 1,
transform: `translate3d(calc(${x}px - 50%), ${y - 25}px, 0px)`,
}}
>
{site.site}
</div>
);
}
if (!sites || !sites.length) return null
if (!sites || !sites.length) return null;
return (
<div className='sites-layer'>
{sites.map(renderSite)}
</div>
)
return <div className="sites-layer">{sites.map(renderSite)}</div>;
}
export default MapSites
export default MapSites;

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React from "react";
export default ({ isDisabled, direction, onClickHandler }) => {
return (
@@ -6,11 +6,9 @@ export default ({ isDisabled, direction, onClickHandler }) => {
className={`narrative-adjust ${direction}`}
onClick={!isDisabled ? onClickHandler : null}
>
<i
className={`material-icons ${isDisabled ? 'disabled' : ''}`}
>
<i className={`material-icons ${isDisabled ? "disabled" : ""}`}>
{`chevron_${direction}`}
</i>
</div>
)
}
);
};

View File

@@ -1,17 +1,17 @@
import React from 'react'
import { connect } from 'react-redux'
import { selectActiveNarrative } from '../../../selectors'
import React from "react";
import { connect } from "react-redux";
import { selectActiveNarrative } from "../../../selectors";
function NarrativeCard ({ narrative }) {
function NarrativeCard({ narrative }) {
// no display if no narrative
const { steps, current } = narrative
const { steps, current } = narrative;
if (steps[current]) {
return (
<div className='narrative-info'>
<div className='narrative-info-header'>
<div className='count-container'>
<div className='count'>
<div className="narrative-info">
<div className="narrative-info-header">
<div className="count-container">
<div className="count">
{current + 1}/{steps.length}
</div>
</div>
@@ -22,19 +22,19 @@ function NarrativeCard ({ narrative }) {
{/* <i className='material-icons left'>location_on</i> */}
{/* {_renderActions(current, steps)} */}
<div className='narrative-info-desc'>
<div className="narrative-info-desc">
<p>{narrative.description}</p>
</div>
</div>
)
);
} else {
return null
return null;
}
}
function mapStateToProps (state) {
function mapStateToProps(state) {
return {
narrative: selectActiveNarrative(state)
}
narrative: selectActiveNarrative(state),
};
}
export default connect(mapStateToProps)(NarrativeCard)
export default connect(mapStateToProps)(NarrativeCard);

View File

@@ -1,17 +1,12 @@
import React from 'react'
import React from "react";
export default ({ onClickHandler, closeMsg }) => {
return (
<div
className='narrative-close'
onClick={onClickHandler}
>
<button
className='side-menu-burg is-active'
>
<div className="narrative-close" onClick={onClickHandler}>
<button className="side-menu-burg is-active">
<span />
</button>
<div className='close-text'>{closeMsg}</div>
<div className="close-text">{closeMsg}</div>
</div>
)
}
);
};

View File

@@ -1,32 +1,32 @@
import React from 'react'
import Card from './Card'
import Adjust from './Adjust'
import Close from './Close'
import React from "react";
import Card from "./Card";
import Adjust from "./Adjust";
import Close from "./Close";
export default ({ narrative, methods }) => {
if (!narrative) return null
if (!narrative) return null;
const { current, steps } = narrative
const prevExists = current !== 0
const nextExists = current < steps.length - 1
const { current, steps } = narrative;
const prevExists = current !== 0;
const nextExists = current < steps.length - 1;
return (
<React.Fragment>
<>
<Card narrative={narrative} />
<Adjust
isDisabled={!prevExists}
direction='left'
direction="left"
onClickHandler={methods.onPrev}
/>
<Adjust
isDisabled={!nextExists}
direction='right'
direction="right"
onClickHandler={methods.onNext}
/>
<Close
onClickHandler={() => methods.onSelectNarrative(null)}
closeMsg='-- exit from narrative --'
closeMsg="-- exit from narrative --"
/>
</React.Fragment>
)
}
</>
);
};

View File

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

View File

@@ -1,7 +1,7 @@
import React from 'react'
import marked from 'marked'
import React from "react";
import marked from "marked";
const fontSize = window.innerWidth > 1000 ? 14 : 18
const fontSize = window.innerWidth > 1000 ? 14 : 18;
export default ({
content = [],
@@ -9,18 +9,30 @@ export default ({
isOpen = true,
onClose,
title,
theme = 'light',
theme = "light",
isMobile = false,
children
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>
<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 => <div dangerouslySetInnerHTML={{ __html: marked(t) }} />)}
{content.map((t) => (
<div dangerouslySetInnerHTML={{ __html: marked(t) }} />
))}
{children}
</div>
</div>
)
);

View File

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

View File

@@ -1,14 +1,14 @@
import React from 'react'
import React from "react";
const TimelineClip = ({ dims }) => (
<clipPath id='clip'>
<clipPath id="clip">
<rect
x={dims.marginLeft}
y='0'
y="0"
width={dims.width - dims.marginLeft - dims.width_controls}
height={dims.contentHeight}
/>
</clipPath>
)
);
export default TimelineClip
export default TimelineClip;

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React from "react";
export default ({
highlights,
@@ -9,35 +9,35 @@ export default ({
height,
onSelect,
styleProps,
extraRender
extraRender,
}) => {
if (highlights.length === 0) {
return (
<rect
onClick={onSelect}
className='event'
className="event"
x={x}
y={y}
style={styleProps}
width={width}
height={height}
/>
)
);
}
const sectionHeight = height / highlights.length
const sectionHeight = height / highlights.length;
return (
<React.Fragment>
<>
{highlights.map((h, idx) => (
<rect
onClick={onSelect}
className='event'
className="event"
x={x}
y={y - sectionHeight + (idx * sectionHeight) + (sectionHeight / 2)}
y={y - sectionHeight + idx * sectionHeight + sectionHeight / 2}
style={{ ...styleProps, opacity: h ? 0.3 : 0.1 }}
width={width}
height={sectionHeight}
/>
))}
</React.Fragment>
)
}
</>
);
};

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React from "react";
export default ({
category,
@@ -8,17 +8,17 @@ export default ({
r,
onSelect,
styleProps,
extraRender
extraRender,
}) => {
if (!y) return null
if (!y) return null;
return (
<circle
onClick={onSelect}
className='event'
className="event"
cx={x}
cy={y}
style={styleProps}
r={r}
/>
)
}
);
};

View File

@@ -1,18 +1,10 @@
import React from 'react'
import React from "react";
export default ({
x,
y,
r,
transform,
onSelect,
styleProps,
extraRender
}) => {
export default ({ x, y, r, transform, onSelect, styleProps, extraRender }) => {
return (
<rect
onClick={onSelect}
className='event'
className="event"
x={x}
y={y - r}
style={styleProps}
@@ -20,5 +12,5 @@ export default ({
height={r}
transform={`rotate(45, ${x}, ${y})`}
/>
)
}
);
};

View File

@@ -1,23 +1,17 @@
import React from 'react'
import React from "react";
export default ({
x,
y,
r,
transform,
onSelect,
styleProps,
extraRender
}) => {
const s = r * 2 / 3
export default ({ x, y, r, transform, onSelect, styleProps, extraRender }) => {
const s = (r * 2) / 3;
return (
<polygon
onClick={onSelect}
className='event'
className="event"
x={x}
y={y - r}
style={styleProps}
points={`${x},${y + s} ${x - s},${y - s} ${x + s},${y} ${x - s},${y} ${x + s},${y - s}`}
points={`${x},${y + s} ${x - s},${y - s} ${x + s},${y} ${x - s},${y} ${
x + s
},${y - s}`}
/>
)
}
);
};

View File

@@ -1,74 +1,89 @@
import React from 'react'
import DatetimeBar from './DatetimeBar'
import DatetimeSquare from './DatetimeSquare'
import DatetimeStar from './DatetimeStar'
import Project from './Project'
import ColoredMarkers from '../Map/ColoredMarkers.jsx'
import React from "react";
import DatetimeBar from "./DatetimeBar";
import DatetimeSquare from "./DatetimeSquare";
import DatetimeStar from "./DatetimeStar";
import Project from "./Project";
import ColoredMarkers from "../Map/ColoredMarkers.jsx";
import {
calcOpacity,
getEventCategories,
zipColorsToPercentages,
calculateColorPercentages,
isLatitude,
isLongitude } from '../../../common/utilities'
isLongitude,
} from "../../../common/utilities";
function renderDot (event, styles, props) {
const colorPercentages = calculateColorPercentages([event], props.coloringSet)
function renderDot(event, styles, props) {
const colorPercentages = calculateColorPercentages(
[event],
props.coloringSet
);
return (
<g
className={'timeline-event'}
className="timeline-event"
onClick={props.onSelect}
transform={`translate(${props.x}, ${props.y})`}
>
<ColoredMarkers
radius={props.eventRadius}
colorPercentMap={zipColorsToPercentages(props.filterColors, colorPercentages)}
colorPercentMap={zipColorsToPercentages(
props.filterColors,
colorPercentages
)}
styles={{
...styles
...styles,
}}
className={'event'}
className="event"
/>
</g>
)
);
}
function renderBar (event, styles, props) {
function renderBar(event, styles, props) {
const fillOpacity = props.features.GRAPH_NONLOCATED
? event.projectOffset >= 0 ? styles.opacity : 0.5
: calcOpacity(1)
? event.projectOffset >= 0
? styles.opacity
: 0.5
: calcOpacity(1);
return <DatetimeBar
onSelect={props.onSelect}
category={event.category}
events={[event]}
x={props.x}
y={props.dims.marginTop}
width={props.eventRadius / 4}
height={props.dims.trackHeight}
styleProps={{ ...styles, fillOpacity }}
highlights={props.highlights}
/>
return (
<DatetimeBar
onSelect={props.onSelect}
category={event.category}
events={[event]}
x={props.x}
y={props.dims.marginTop}
width={props.eventRadius / 4}
height={props.dims.trackHeight}
styleProps={{ ...styles, fillOpacity }}
highlights={props.highlights}
/>
);
}
function renderDiamond (event, styles, props) {
return <DatetimeSquare
onSelect={props.onSelect}
x={props.x}
y={props.y}
r={1.8 * props.eventRadius}
styleProps={styles}
/>
function renderDiamond(event, styles, props) {
return (
<DatetimeSquare
onSelect={props.onSelect}
x={props.x}
y={props.y}
r={1.8 * props.eventRadius}
styleProps={styles}
/>
);
}
function renderStar (event, styles, props) {
return <DatetimeStar
onSelect={props.onSelect}
x={props.x}
y={props.y}
r={1.8 * props.eventRadius}
styleProps={{ ...styles, fillRule: 'nonzero' }}
transform='rotate(90)'
/>
function renderStar(event, styles, props) {
return (
<DatetimeStar
onSelect={props.onSelect}
x={props.x}
y={props.y}
r={1.8 * props.eventRadius}
styleProps={{ ...styles, fillRule: "nonzero" }}
transform="rotate(90)"
/>
);
}
const TimelineEvents = ({
@@ -88,95 +103,105 @@ const TimelineEvents = ({
setNotLoading,
eventRadius,
filterColors,
coloringSet
coloringSet,
}) => {
const narIds = narrative ? narrative.steps.map(s => s.id) : []
const narIds = narrative ? narrative.steps.map((s) => s.id) : [];
function renderEvent (acc, event) {
function renderEvent(acc, event) {
if (narrative) {
if (!(narIds.includes(event.id))) {
return null
if (!narIds.includes(event.id)) {
return null;
}
}
const isDot = (isLatitude(event.latitude) && isLongitude(event.longitude)) || (features.GRAPH_NONLOCATED && event.projectOffset !== -1)
const isDot =
(isLatitude(event.latitude) && isLongitude(event.longitude)) ||
(features.GRAPH_NONLOCATED && event.projectOffset !== -1);
let renderShape = isDot ? renderDot : renderBar
let renderShape = isDot ? renderDot : renderBar;
if (event.shape) {
if (event.shape === 'bar') {
renderShape = renderBar
} else if (event.shape === 'diamond') {
renderShape = renderDiamond
} else if (event.shape === 'star') {
renderShape = renderStar
if (event.shape === "bar") {
renderShape = renderBar;
} else if (event.shape === "diamond") {
renderShape = renderDiamond;
} else if (event.shape === "star") {
renderShape = renderStar;
} else {
renderShape = renderDot
renderShape = renderDot;
}
}
// if an event has multiple categories, it should be rendered on each of
// those timelines: so we create as many event 'shadows' as there are
// categories
const evShadows = getEventCategories(event, categories).map(cat => {
const y = getY({ ...event, category: cat.id })
const evShadows = getEventCategories(event, categories).map((cat) => {
const y = getY({ ...event, category: cat.id });
let colour = event.colour ? event.colour : getCategoryColor(cat.id)
const colour = event.colour ? event.colour : getCategoryColor(cat.id);
const styles = {
fill: colour,
fillOpacity: y > 0 ? calcOpacity(1) : 0,
transition: `transform ${transitionDuration / 1000}s ease`
}
transition: `transform ${transitionDuration / 1000}s ease`,
};
return { y, styles }
})
return { y, styles };
});
function getRender (y, styles) {
function getRender(y, styles) {
return renderShape(event, styles, {
x: getDatetimeX(event.datetime),
y,
eventRadius,
onSelect: () => onSelect(event),
dims,
highlights: features.HIGHLIGHT_GROUPS ? getHighlights(event.filters[features.HIGHLIGHT_GROUPS.filterIndexIndicatingGroup]) : [],
highlights: features.HIGHLIGHT_GROUPS
? getHighlights(
event.filters[
features.HIGHLIGHT_GROUPS.filterIndexIndicatingGroup
]
)
: [],
features,
filterColors,
coloringSet
})
coloringSet,
});
}
if (evShadows.length === 0) {
acc.push(getRender(getY(event), { fill: getCategoryColor(null) }))
acc.push(getRender(getY(event), { fill: getCategoryColor(null) }));
} else {
evShadows.forEach(evShadow => {
acc.push(getRender(evShadow.y, evShadow.styles))
})
evShadows.forEach((evShadow) => {
acc.push(getRender(evShadow.y, evShadow.styles));
});
}
return acc
return acc;
}
let renderProjects = () => null
let renderProjects = () => null;
if (features.GRAPH_NONLOCATED) {
renderProjects = function () {
return <React.Fragment>
{Object.values(projects).map(project => <Project
{...project}
eventRadius={eventRadius}
onClick={() => console.log(project)}
getX={getDatetimeX}
dims={dims}
colour={getCategoryColor(project.category)}
/>)}
</React.Fragment>
}
return (
<>
{Object.values(projects).map((project) => (
<Project
{...project}
eventRadius={eventRadius}
onClick={() => console.log(project)}
getX={getDatetimeX}
dims={dims}
colour={getCategoryColor(project.category)}
/>
))}
</>
);
};
}
return (
<g
clipPath={'url(#clip)'}
>
<g clipPath="url(#clip)">
{renderProjects()}
{events.reduce(renderEvent, [])}
</g>
)
}
);
};
export default TimelineEvents
export default TimelineEvents;

View File

@@ -1,26 +1,36 @@
import React from 'react'
import React from "react";
const TimelineHandles = ({ dims, onMoveTime }) => {
const transform = 'scale(1.5,1.5)'
const size = 45
const transform = "scale(1.5,1.5)";
const size = 45;
return (
<g className='time-controls-inline'>
<g className="time-controls-inline">
<g
transform={`translate(${dims.marginLeft - 20}, ${dims.contentHeight - 10})`}
onClick={() => onMoveTime('backwards')}
transform={`translate(${dims.marginLeft - 20}, ${
dims.contentHeight - 10
})`}
onClick={() => onMoveTime("backwards")}
>
<circle r={size} />
<path d='M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z' transform={`rotate(270) ${transform}`} />
<path
d="M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z"
transform={`rotate(270) ${transform}`}
/>
</g>
<g
transform={`translate(${dims.width - dims.width_controls + 20}, ${dims.contentHeight - 10})`}
onClick={() => onMoveTime('forward')}
transform={`translate(${dims.width - dims.width_controls + 20}, ${
dims.contentHeight - 10
})`}
onClick={() => onMoveTime("forward")}
>
<circle r={size} />
<path d='M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z' transform={`rotate(90) ${transform}`} />
<path
d="M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z"
transform={`rotate(90) ${transform}`}
/>
</g>
</g>
)
}
);
};
export default TimelineHandles
export default TimelineHandles;

View File

@@ -1,20 +1,24 @@
import React from 'react'
import { makeNiceDate } from '../../../common/utilities'
import React from "react";
import { makeNiceDate } from "../../../common/utilities";
const TimelineHeader = ({ title, from, to, onClick, hideInfo }) => {
const d0 = from && makeNiceDate(from)
const d1 = to && makeNiceDate(to)
const d0 = from && makeNiceDate(from);
const d1 = to && makeNiceDate(to);
return (
<div className='timeline-header'>
<div className='timeline-toggle' onClick={() => onClick()}>
<p><i className='arrow-down' /></p>
<div className="timeline-header">
<div className="timeline-toggle" onClick={() => onClick()}>
<p>
<i className="arrow-down" />
</p>
</div>
<div className={`timeline-info ${hideInfo ? 'hidden' : ''}`}>
<div className={`timeline-info ${hideInfo ? "hidden" : ""}`}>
<p>{title}</p>
<p>{d0} - {d1}</p>
<p>
{d0} - {d1}
</p>
</div>
</div>
)
}
);
};
export default TimelineHeader
export default TimelineHeader;

View File

@@ -1,39 +1,35 @@
import React from 'react'
import React from "react";
const TimelineLabels = ({ dims, timelabels }) => {
return (
<g>
<line
class='axisBoundaries'
class="axisBoundaries"
x1={dims.marginLeft}
x2={dims.marginLeft}
y1='10'
y2='20'
y1="10"
y2="20"
/>
<line
class='axisBoundaries'
class="axisBoundaries"
x1={dims.width - dims.width_controls}
x2={dims.width - dims.width_controls}
y1='10'
y2='20'
y1="10"
y2="20"
/>
<text
class='timeLabel0 timeLabel'
x='5'
y='15'
>
<text class="timeLabel0 timeLabel" x="5" y="15">
{timelabels[0]}
</text>
<text
class='timelabelF timeLabel'
class="timelabelF timeLabel"
x={dims.width - dims.width_controls - 5}
y='135'
style={{ textAnchor: 'end' }}
y="135"
style={{ textAnchor: "end" }}
>
{timelabels[1]}
</text>
</g>
)
}
);
};
export default TimelineLabels
export default TimelineLabels;

View File

@@ -1,6 +1,10 @@
import React from 'react'
import colors from '../../../common/global'
import { getEventCategories, isLatitude, isLongitude } from '../../../common/utilities'
import React from "react";
import colors from "../../../common/global";
import {
getEventCategories,
isLatitude,
isLongitude,
} from "../../../common/utilities";
const TimelineMarkers = ({
styles,
@@ -11,79 +15,83 @@ const TimelineMarkers = ({
transitionDuration,
selected,
dims,
features
features,
}) => {
function renderMarker (acc, event) {
function renderCircle (y) {
return <circle
className='timeline-marker'
cx={0}
cy={0}
stroke={styles ? styles.stroke : colors.primaryHighlight}
stroke-opacity='1'
stroke-width={styles ? styles['stroke-width'] : 1}
stroke-linejoin='round'
stroke-dasharray={styles ? styles['stroke-dasharray'] : '2,2'}
style={{
'transform': `translate(${getEventX(event)}px, ${y}px)`,
'-webkit-transition': `transform ${transitionDuration / 1000}s ease`,
'-moz-transition': 'none',
'opacity': 1
}}
r={eventRadius * 2}
/>
function renderMarker(acc, event) {
function renderCircle(y) {
return (
<circle
className="timeline-marker"
cx={0}
cy={0}
stroke={styles ? styles.stroke : colors.primaryHighlight}
stroke-opacity="1"
stroke-width={styles ? styles["stroke-width"] : 1}
stroke-linejoin="round"
stroke-dasharray={styles ? styles["stroke-dasharray"] : "2,2"}
style={{
transform: `translate(${getEventX(event)}px, ${y}px)`,
"-webkit-transition": `transform ${
transitionDuration / 1000
}s ease`,
"-moz-transition": "none",
opacity: 1,
}}
r={eventRadius * 2}
/>
);
}
function renderBar () {
return <rect
className='timeline-marker'
x={0}
y={dims.marginTop}
width={eventRadius / 1.5}
height={dims.contentHeight - 55}
stroke={styles ? styles.stroke : colors.primaryHighlight}
stroke-opacity='1'
stroke-width={styles ? styles['stroke-width'] : 1}
stroke-dasharray={styles ? styles['stroke-dasharray'] : '2,2'}
style={{
'transform': `translate(${getEventX(event)}px)`,
'opacity': 0.7
}}
/>
function renderBar() {
return (
<rect
className="timeline-marker"
x={0}
y={dims.marginTop}
width={eventRadius / 1.5}
height={dims.contentHeight - 55}
stroke={styles ? styles.stroke : colors.primaryHighlight}
stroke-opacity="1"
stroke-width={styles ? styles["stroke-width"] : 1}
stroke-dasharray={styles ? styles["stroke-dasharray"] : "2,2"}
style={{
transform: `translate(${getEventX(event)}px)`,
opacity: 0.7,
}}
/>
);
}
const isDot = (isLatitude(event.latitude) && isLongitude(event.longitude)) || (features.GRAPH_NONLOCATED && event.projectOffset !== -1)
const evShadows = getEventCategories(event, categories).map(cat => getEventY({ ...event, category: cat.id }))
const isDot =
(isLatitude(event.latitude) && isLongitude(event.longitude)) ||
(features.GRAPH_NONLOCATED && event.projectOffset !== -1);
const evShadows = getEventCategories(event, categories).map((cat) =>
getEventY({ ...event, category: cat.id })
);
function renderMarkerForEvent (y) {
function renderMarkerForEvent(y) {
switch (event.shape) {
case 'circle':
case 'diamond':
case 'star':
acc.push(renderCircle(y))
break
case 'bar':
acc.push(renderBar(y))
break
case "circle":
case "diamond":
case "star":
acc.push(renderCircle(y));
break;
case "bar":
acc.push(renderBar(y));
break;
default:
return isDot ? acc.push(renderCircle(y)) : acc.push(renderBar(y))
return isDot ? acc.push(renderCircle(y)) : acc.push(renderBar(y));
}
}
if (evShadows.length > 0) {
evShadows.forEach(renderMarkerForEvent)
evShadows.forEach(renderMarkerForEvent);
} else {
renderMarkerForEvent(getEventY(event))
renderMarkerForEvent(getEventY(event));
}
return acc
return acc;
}
return (
<g
clipPath={'url(#clip)'}
>
{selected.reduce(renderMarker, [])}
</g>
)
}
return <g clipPath="url(#clip)">{selected.reduce(renderMarker, [])}</g>;
};
export default TimelineMarkers
export default TimelineMarkers;

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React from "react";
export default ({
offset,
@@ -10,17 +10,19 @@ export default ({
dims,
colour,
eventRadius,
onClick
onClick,
}) => {
const length = getX(end) - getX(start)
if (offset === undefined) return null
return <rect
onClick={onClick}
className='project'
x={getX(start)}
y={dims.marginTop + offset}
width={length}
style={{ fill: colour, fillOpacity: 0.2 }}
height={2 * eventRadius}
/>
}
const length = getX(end) - getX(start);
if (offset === undefined) return null;
return (
<rect
onClick={onClick}
className="project"
x={getX(start)}
y={dims.marginTop + offset}
width={length}
style={{ fill: colour, fillOpacity: 0.2 }}
height={2 * eventRadius}
/>
);
};

View File

@@ -1,45 +1,47 @@
import React from 'react'
import React from "react";
const DEFAULT_ZOOM_LEVELS = [
{ label: '20 years', duration: 10512000 },
{ label: '2 years', duration: 1051200 },
{ label: '3 months', duration: 129600 },
{ label: '3 days', duration: 4320 },
{ label: '12 hours', duration: 720 },
{ label: '1 hour', duration: 60 }
]
{ label: "20 years", duration: 10512000 },
{ label: "2 years", duration: 1051200 },
{ label: "3 months", duration: 129600 },
{ label: "3 days", duration: 4320 },
{ label: "12 hours", duration: 720 },
{ label: "1 hour", duration: 60 },
];
function zoomIsActive (duration, extent, max) {
function zoomIsActive(duration, extent, max) {
if (duration >= max && extent >= max) {
return true
return true;
}
return duration === extent
return duration === extent;
}
const TimelineZoomControls = ({ extent, zoomLevels, dims, onApplyZoom }) => {
function renderZoom (zoom, idx) {
const max = zoomLevels.reduce((acc, vl) => acc.duration < vl.duration ? vl : acc)
const isActive = zoomIsActive(zoom.duration, extent, max.duration)
function renderZoom(zoom, idx) {
const max = zoomLevels.reduce((acc, vl) =>
acc.duration < vl.duration ? vl : acc
);
const isActive = zoomIsActive(zoom.duration, extent, max.duration);
return (
<text
className={`zoom-level-button ${isActive ? 'active' : ''}`}
x='60'
y={(idx * 15) + 20}
className={`zoom-level-button ${isActive ? "active" : ""}`}
x="60"
y={idx * 15 + 20}
onClick={() => onApplyZoom(zoom)}
>
{zoom.label}
</text>
)
);
}
if (zoomLevels.length === 0) {
zoomLevels = DEFAULT_ZOOM_LEVELS
zoomLevels = DEFAULT_ZOOM_LEVELS;
}
return (
<g transform={`translate(${dims.width - dims.width_controls}, 0)`}>
{zoomLevels.map((z, idx) => renderZoom(z, idx))}
</g>
)
}
);
};
export default TimelineZoomControls
export default TimelineZoomControls;

View File

@@ -4,8 +4,6 @@ import { Provider } from "react-redux";
import store from "./store/index.js";
import App from "./components/App.jsx";
console.log(process.env);
ReactDOM.render(
<Provider store={store}>
<App />

View File

@@ -1,6 +1,6 @@
import initial from '../store/initial.js'
import { ASSOCIATION_MODES } from '../common/constants'
import { toggleFlagAC } from '../common/utilities'
import initial from "../store/initial.js";
import { ASSOCIATION_MODES } from "../common/constants";
import { toggleFlagAC } from "../common/utilities";
import {
UPDATE_HIGHLIGHTED,
@@ -26,291 +26,298 @@ import {
SET_LOADING,
SET_NOT_LOADING,
SET_INITIAL_CATEGORIES,
UPDATE_SEARCH_QUERY
} from '../actions'
UPDATE_SEARCH_QUERY,
} from "../actions";
function updateHighlighted (appState, action) {
function updateHighlighted(appState, action) {
return Object.assign({}, appState, {
highlighted: action.highlighted
})
highlighted: action.highlighted,
});
}
function updateSelected (appState, action) {
function updateSelected(appState, action) {
return Object.assign({}, appState, {
selected: action.selected
})
selected: action.selected,
});
}
function updateColoringSet (appState, action) {
function updateColoringSet(appState, action) {
return {
...appState,
associations: {
...appState.associations,
coloringSet: action.coloringSet
}
}
coloringSet: action.coloringSet,
},
};
}
function updateNarrative (appState, action) {
let minTime = appState.timeline.range[0]
let maxTime = appState.timeline.range[1]
function updateNarrative(appState, action) {
let minTime = appState.timeline.range[0];
let maxTime = appState.timeline.range[1];
let cornerBound0 = [180, 180]
let cornerBound1 = [-180, -180]
const cornerBound0 = [180, 180];
const cornerBound1 = [-180, -180];
// Compute narrative time range and map bounds
if (action.narrative) {
// Forced to comment out min and max time changes, not sure why?
minTime = appState.timeline.rangeLimits[0]
maxTime = appState.timeline.rangeLimits[1]
minTime = appState.timeline.rangeLimits[0];
maxTime = appState.timeline.rangeLimits[1];
// Find max and mins coordinates of narrative events
action.narrative.steps.forEach(step => {
const stepTime = step.datetime
if (stepTime < minTime) minTime = stepTime
if (stepTime > maxTime) maxTime = stepTime
action.narrative.steps.forEach((step) => {
const stepTime = step.datetime;
if (stepTime < minTime) minTime = stepTime;
if (stepTime > maxTime) maxTime = stepTime;
if (!!step.longitude && !!step.latitude) {
if (+step.longitude < cornerBound0[1]) cornerBound0[1] = +step.longitude
if (+step.longitude > cornerBound1[1]) cornerBound1[1] = +step.longitude
if (+step.latitude < cornerBound0[0]) cornerBound0[0] = +step.latitude
if (+step.latitude > cornerBound1[0]) cornerBound1[0] = +step.latitude
if (+step.longitude < cornerBound0[1])
cornerBound0[1] = +step.longitude;
if (+step.longitude > cornerBound1[1])
cornerBound1[1] = +step.longitude;
if (+step.latitude < cornerBound0[0]) cornerBound0[0] = +step.latitude;
if (+step.latitude > cornerBound1[0]) cornerBound1[0] = +step.latitude;
}
})
});
// Adjust bounds to center around first event, while keeping visible all others
// Takes first event, finds max ditance with first attempt bounds, and use this max distance
// on the other side, both in latitude and longitude
const first = action.narrative.steps[0]
const first = action.narrative.steps[0];
if (!!first.longitude && !!first.latitude) {
const firstToLong0 = Math.abs(+first.longitude - cornerBound0[1])
const firstToLong1 = Math.abs(+first.longitude - cornerBound1[1])
const firstToLat0 = Math.abs(+first.latitude - cornerBound0[0])
const firstToLat1 = Math.abs(+first.latitude - cornerBound1[0])
const firstToLong0 = Math.abs(+first.longitude - cornerBound0[1]);
const firstToLong1 = Math.abs(+first.longitude - cornerBound1[1]);
const firstToLat0 = Math.abs(+first.latitude - cornerBound0[0]);
const firstToLat1 = Math.abs(+first.latitude - cornerBound1[0]);
if (firstToLong0 > firstToLong1) cornerBound1[1] = +first.longitude + firstToLong0
if (firstToLong0 < firstToLong1) cornerBound0[1] = +first.longitude - firstToLong1
if (firstToLat0 > firstToLat1) cornerBound1[0] = +first.latitude + firstToLat0
if (firstToLat0 < firstToLat1) cornerBound0[0] = +first.latitude - firstToLat1
if (firstToLong0 > firstToLong1)
cornerBound1[1] = +first.longitude + firstToLong0;
if (firstToLong0 < firstToLong1)
cornerBound0[1] = +first.longitude - firstToLong1;
if (firstToLat0 > firstToLat1)
cornerBound1[0] = +first.latitude + firstToLat0;
if (firstToLat0 < firstToLat1)
cornerBound0[0] = +first.latitude - firstToLat1;
}
// Add some buffer on both sides of the time extent
minTime = minTime - Math.abs((maxTime - minTime) / 10)
maxTime = maxTime + Math.abs((maxTime - minTime) / 10)
minTime = minTime - Math.abs((maxTime - minTime) / 10);
maxTime = maxTime + Math.abs((maxTime - minTime) / 10);
}
return {
...appState,
associations: {
...appState.associations,
narrative: action.narrative
narrative: action.narrative,
},
map: {
...appState.map,
bounds: (action.narrative) ? [cornerBound0, cornerBound1] : null
bounds: action.narrative ? [cornerBound0, cornerBound1] : null,
},
timeline: {
...appState.timeline,
range: [minTime, maxTime]
}
}
range: [minTime, maxTime],
},
};
}
function updateNarrativeStepIdx (appState, action) {
function updateNarrativeStepIdx(appState, action) {
return {
...appState,
narrativeState: {
current: action.idx
}
}
current: action.idx,
},
};
}
function toggleAssociations (appState, action) {
function toggleAssociations(appState, action) {
if (!(action.value instanceof Array)) {
action.value = [action.value]
action.value = [action.value];
}
const { association: associationType } = action
const { association: associationType } = action;
let newAssociations = appState.associations[associationType].slice(0)
action.value.forEach(vl => {
let newAssociations = appState.associations[associationType].slice(0);
action.value.forEach((vl) => {
if (newAssociations.includes(vl)) {
newAssociations = newAssociations.filter(s => s !== vl)
newAssociations = newAssociations.filter((s) => s !== vl);
} else {
newAssociations.push(vl)
newAssociations.push(vl);
}
})
});
return {
...appState,
associations: {
...appState.associations,
[associationType]: newAssociations
}
}
[associationType]: newAssociations,
},
};
}
function clearFilter (appState, action) {
function clearFilter(appState, action) {
return {
...appState,
filters: {
...appState.filters,
[action.filter]: []
}
}
[action.filter]: [],
},
};
}
function updateTimeRange (appState, action) { // XXX
function updateTimeRange(appState, action) {
// XXX
return {
...appState,
timeline: {
...appState.timeline,
range: action.timerange
}
}
range: action.timerange,
},
};
}
function updateDimensions (appState, action) {
function updateDimensions(appState, action) {
return {
...appState,
timeline: {
...appState.timeline,
dimensions: {
...appState.timeline.dimensions,
...action.dims
}
}
}
...action.dims,
},
},
};
}
function toggleLanguage (appState, action) {
let otherLanguage = (appState.language === 'es-MX') ? 'en-US' : 'es-MX'
function toggleLanguage(appState, action) {
const otherLanguage = appState.language === "es-MX" ? "en-US" : "es-MX";
return Object.assign({}, appState, {
language: action.language || otherLanguage
})
language: action.language || otherLanguage,
});
}
function updateSource (appState, action) {
function updateSource(appState, action) {
return {
...appState,
source: action.source
}
source: action.source,
};
}
function fetchError (state, action) {
function fetchError(state, action) {
return {
...state,
error: action.message,
notifications: [{ type: 'error', message: action.message }]
}
notifications: [{ type: "error", message: action.message }],
};
}
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')
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");
function fetchSourceError (appState, action) {
function fetchSourceError(appState, action) {
return {
...appState,
errors: {
...appState.errors,
source: action.msg
}
}
source: action.msg,
},
};
}
function setLoading (appState) {
function setLoading(appState) {
return {
...appState,
loading: true
}
loading: true,
};
}
function setNotLoading (appState) {
function setNotLoading(appState) {
return {
...appState,
loading: false
}
loading: false,
};
}
function setInitialCategories (appState, action) {
function setInitialCategories(appState, action) {
const categories = action.values.reduce((acc, val) => {
if (val.mode === ASSOCIATION_MODES.CATEGORY) acc.push(val.id)
return acc
}, [])
if (val.mode === ASSOCIATION_MODES.CATEGORY) acc.push(val.id);
return acc;
}, []);
return {
...appState,
associations: {
...appState.associations,
categories: categories
}
}
categories: categories,
},
};
}
function updateSearchQuery (appState, action) {
function updateSearchQuery(appState, action) {
return {
...appState,
searchQuery: action.searchQuery
}
searchQuery: action.searchQuery,
};
}
function app (appState = initial.app, action) {
function app(appState = initial.app, action) {
switch (action.type) {
case UPDATE_HIGHLIGHTED:
return updateHighlighted(appState, action)
return updateHighlighted(appState, action);
case UPDATE_SELECTED:
return updateSelected(appState, action)
return updateSelected(appState, action);
case UPDATE_COLORING_SET:
return updateColoringSet(appState, action)
return updateColoringSet(appState, action);
case CLEAR_FILTER:
return clearFilter(appState, action)
return clearFilter(appState, action);
case TOGGLE_ASSOCIATIONS:
return toggleAssociations(appState, action)
return toggleAssociations(appState, action);
case UPDATE_TIMERANGE:
return updateTimeRange(appState, action)
return updateTimeRange(appState, action);
case UPDATE_DIMENSIONS:
return updateDimensions(appState, action)
return updateDimensions(appState, action);
case UPDATE_NARRATIVE:
return updateNarrative(appState, action)
return updateNarrative(appState, action);
case UPDATE_NARRATIVE_STEP_IDX:
return updateNarrativeStepIdx(appState, action)
return updateNarrativeStepIdx(appState, action);
case UPDATE_SOURCE:
return updateSource(appState, action)
return updateSource(appState, action);
/* toggles */
case TOGGLE_LANGUAGE:
return toggleLanguage(appState, action)
return toggleLanguage(appState, action);
case TOGGLE_SITES:
return toggleSites(appState)
return toggleSites(appState);
case TOGGLE_FETCHING_DOMAIN:
return toggleFetchingDomain(appState)
return toggleFetchingDomain(appState);
case TOGGLE_FETCHING_SOURCES:
return toggleFetchingSources(appState)
return toggleFetchingSources(appState);
case TOGGLE_INFOPOPUP:
return toggleInfoPopup(appState)
return toggleInfoPopup(appState);
case TOGGLE_INTROPOPUP:
return toggleIntroPopup(appState)
return toggleIntroPopup(appState);
case TOGGLE_NOTIFICATIONS:
return toggleNotifications(appState)
return toggleNotifications(appState);
case TOGGLE_COVER:
return toggleCover(appState)
return toggleCover(appState);
/* errors */
case FETCH_ERROR:
return fetchError(appState, action)
return fetchError(appState, action);
case FETCH_SOURCE_ERROR:
return fetchSourceError(appState, action)
return fetchSourceError(appState, action);
case SET_LOADING:
return setLoading(appState)
return setLoading(appState);
case SET_NOT_LOADING:
return setNotLoading(appState)
return setNotLoading(appState);
case SET_INITIAL_CATEGORIES:
return setInitialCategories(appState, action)
return setInitialCategories(appState, action);
case UPDATE_SEARCH_QUERY:
return updateSearchQuery(appState, action)
return updateSearchQuery(appState, action);
default:
return appState
return appState;
}
}
export default app
export default app;

View File

@@ -1,31 +1,34 @@
import initial from '../store/initial.js'
import initial from "../store/initial.js";
import { UPDATE_DOMAIN, MARK_NOTIFICATIONS_READ } from '../actions'
import { validateDomain } from './validate/validators.js'
import { UPDATE_DOMAIN, MARK_NOTIFICATIONS_READ } from "../actions";
import { validateDomain } from "./validate/validators.js";
function updateDomain (domainState, action) {
function updateDomain(domainState, action) {
return {
...domainState,
...validateDomain(action.payload.domain, action.payload.features)
}
...validateDomain(action.payload.domain, action.payload.features),
};
}
function markNotificationsRead (domainState, action) {
function markNotificationsRead(domainState, action) {
return {
...domainState,
notifications: domainState.notifications.map(n => ({ ...n, isRead: true }))
}
notifications: domainState.notifications.map((n) => ({
...n,
isRead: true,
})),
};
}
function domain (domainState = initial.domain, action) {
function domain(domainState = initial.domain, action) {
switch (action.type) {
case UPDATE_DOMAIN:
return updateDomain(domainState, action)
return updateDomain(domainState, action);
case MARK_NOTIFICATIONS_READ:
return markNotificationsRead(domainState, action)
return markNotificationsRead(domainState, action);
default:
return domainState
return domainState;
}
}
export default domain
export default domain;

View File

@@ -1,7 +1,7 @@
import initial from '../store/initial.js'
import initial from "../store/initial.js";
function features (featureState = initial.features, action) {
return featureState
function features(featureState = initial.features, action) {
return featureState;
}
export default features
export default features;

View File

@@ -1,13 +1,13 @@
import { combineReducers } from 'redux'
import { combineReducers } from "redux";
import domain from './domain.js'
import app from './app.js'
import ui from './ui.js'
import features from './features.js'
import domain from "./domain.js";
import app from "./app.js";
import ui from "./ui.js";
import features from "./features.js";
export default combineReducers({
app,
domain,
ui,
features
})
features,
});

View File

@@ -1,9 +1,9 @@
import initial from '../store/initial.js'
import initial from "../store/initial.js";
import {} from '../actions'
import {} from "../actions";
function ui (uiState = initial.ui, action) {
return uiState
function ui(uiState = initial.ui, action) {
return uiState;
}
export default ui
export default ui;

View File

@@ -1,10 +1,10 @@
import Joi from 'joi'
import Joi from "joi";
const associationsSchema = Joi.object().keys({
id: Joi.string().allow('').required(),
desc: Joi.string().allow(''),
mode: Joi.string().allow('').required(),
filter_paths: Joi.array()
})
id: Joi.string().allow("").required(),
desc: Joi.string().allow(""),
mode: Joi.string().allow("").required(),
filter_paths: Joi.array(),
});
export default associationsSchema
export default associationsSchema;

View File

@@ -1,43 +1,44 @@
import Joi from 'joi'
import Joi from "joi";
function joiFromCustom (custom) {
const output = {}
custom.forEach(field => {
if (field.kind === 'text' || field.kind === 'link') {
output[field.key] = Joi.string().allow('')
function joiFromCustom(custom) {
const output = {};
custom.forEach((field) => {
if (field.kind === "text" || field.kind === "link") {
output[field.key] = Joi.string().allow("");
}
if (field.kind === 'list') {
output[field.key] = Joi.array().allow('')
if (field.kind === "list") {
output[field.key] = Joi.array().allow("");
}
})
return output
});
return output;
}
function createEventSchema (custom) {
return Joi.object().keys({
id: Joi.string().allow(''),
description: Joi.string().allow('').required(),
date: Joi.string().allow(''),
time: Joi.string().allow(''),
time_precision: Joi.string().allow(''),
location: Joi.string().allow(''),
latitude: Joi.string().allow(''),
longitude: Joi.string().allow(''),
type: Joi.string().allow(''),
category: Joi.string().allow(''),
category_full: Joi.string().allow(''),
associations: Joi.array().required().default([]),
sources: Joi.array(),
comments: Joi.string().allow(''),
time_display: Joi.string().allow(''),
// nested
narrative___stepStyles: Joi.array(),
shape: Joi.string().allow(''),
colour: Joi.string().allow(''),
...joiFromCustom(custom)
})
.and('latitude', 'longitude')
.or('date', 'latitude')
function createEventSchema(custom) {
return Joi.object()
.keys({
id: Joi.string().allow(""),
description: Joi.string().allow("").required(),
date: Joi.string().allow(""),
time: Joi.string().allow(""),
time_precision: Joi.string().allow(""),
location: Joi.string().allow(""),
latitude: Joi.string().allow(""),
longitude: Joi.string().allow(""),
type: Joi.string().allow(""),
category: Joi.string().allow(""),
category_full: Joi.string().allow(""),
associations: Joi.array().required().default([]),
sources: Joi.array(),
comments: Joi.string().allow(""),
time_display: Joi.string().allow(""),
// nested
narrative___stepStyles: Joi.array(),
shape: Joi.string().allow(""),
colour: Joi.string().allow(""),
...joiFromCustom(custom),
})
.and("latitude", "longitude")
.or("date", "latitude");
}
export default createEventSchema
export default createEventSchema;

View File

@@ -1,8 +1,8 @@
import Joi from 'joi'
import Joi from "joi";
const shapeSchema = Joi.object().keys({
name: Joi.string().required(),
items: Joi.array().required()
})
items: Joi.array().required(),
});
export default shapeSchema
export default shapeSchema;

View File

@@ -1,12 +1,12 @@
import Joi from 'joi'
import Joi from "joi";
const siteSchema = Joi.object().keys({
id: Joi.string().required(),
description: Joi.string().allow('').required(),
description: Joi.string().allow("").required(),
site: Joi.string().required(),
latitude: Joi.string().required(),
longitude: Joi.string().required(),
enabled: Joi.string().allow('')
})
enabled: Joi.string().allow(""),
});
export default siteSchema
export default siteSchema;

View File

@@ -1,18 +1,18 @@
import Joi from 'joi'
import Joi from "joi";
const sourceSchema = Joi.object().keys({
id: Joi.string().required(),
title: Joi.string().allow(''),
thumbnail: Joi.string().allow(''),
title: Joi.string().allow(""),
thumbnail: Joi.string().allow(""),
paths: Joi.array().required(),
type: Joi.string().allow(''),
affil_s: Joi.array().allow(''),
url: Joi.string().allow(''),
description: Joi.string().allow(''),
parent: Joi.string().allow(''),
author: Joi.string().allow(''),
date: Joi.string().allow(''),
notes: Joi.string().allow('')
})
type: Joi.string().allow(""),
affil_s: Joi.array().allow(""),
url: Joi.string().allow(""),
description: Joi.string().allow(""),
parent: Joi.string().allow(""),
author: Joi.string().allow(""),
date: Joi.string().allow(""),
notes: Joi.string().allow(""),
});
export default sourceSchema
export default sourceSchema;

View File

@@ -1,64 +1,64 @@
import Joi from 'joi'
import Joi from "joi";
import createEventSchema from './eventSchema'
import siteSchema from './siteSchema'
import associationsSchema from './associationsSchema'
import sourceSchema from './sourceSchema'
import shapeSchema from './shapeSchema'
import createEventSchema from "./eventSchema";
import siteSchema from "./siteSchema";
import associationsSchema from "./associationsSchema";
import sourceSchema from "./sourceSchema";
import shapeSchema from "./shapeSchema";
import { calcDatetime, capitalize } from '../../common/utilities'
import { calcDatetime, capitalize } from "../../common/utilities";
/*
* Create an error notification object
* Types: ['error', 'warning', 'good', 'neural']
*/
function makeError (type, id, message) {
function makeError(type, id, message) {
return {
type: 'error',
type: "error",
id,
message: `${type} ${id}: ${message}`
}
message: `${type} ${id}: ${message}`,
};
}
function isValidDate (d) {
return d instanceof Date && !isNaN(d)
function isValidDate(d) {
return d instanceof Date && !isNaN(d);
}
function findDuplicateAssociations (associations) {
const seenSet = new Set([])
const duplicates = []
function findDuplicateAssociations(associations) {
const seenSet = new Set([]);
const duplicates = [];
associations.forEach((item) => {
if (seenSet.has(item.id)) {
duplicates.push({
id: item.id,
error: makeError(
'Association',
"Association",
item.id,
'association was found more than once. Ignoring duplicate.'
)
})
"association was found more than once. Ignoring duplicate."
),
});
} else {
seenSet.add(item.id)
seenSet.add(item.id);
}
})
return duplicates
});
return duplicates;
}
/*
* Validate domain schema
*/
export function validateDomain (domain, features) {
export function validateDomain(domain, features) {
const sanitizedDomain = {
events: [],
sites: [],
associations: [],
sources: {},
shapes: [],
notifications: domain ? domain.notifications : null
}
notifications: domain ? domain.notifications : null,
};
if (domain === undefined) {
return sanitizedDomain
return sanitizedDomain;
}
const discardedDomain = {
@@ -66,104 +66,105 @@ export function validateDomain (domain, features) {
sites: [],
associations: [],
sources: [],
shapes: []
}
shapes: [],
};
function validateArrayItem (item, domainKey, schema) {
const result = Joi.validate(item, schema)
function validateArrayItem(item, domainKey, schema) {
const result = Joi.validate(item, schema);
if (result.error !== null) {
const id = item.id || '-'
const domainStr = capitalize(domainKey)
const error = makeError(domainStr, id, result.error.message)
const id = item.id || "-";
const domainStr = capitalize(domainKey);
const error = makeError(domainStr, id, result.error.message);
discardedDomain[domainKey].push(Object.assign(item, { error }))
discardedDomain[domainKey].push(Object.assign(item, { error }));
} else {
sanitizedDomain[domainKey].push(item)
sanitizedDomain[domainKey].push(item);
}
}
function validateArray (items, domainKey, schema) {
function validateArray(items, domainKey, schema) {
items.forEach((item) => {
validateArrayItem(item, domainKey, schema)
})
validateArrayItem(item, domainKey, schema);
});
}
function validateObject (obj, domainKey, itemSchema) {
function validateObject(obj, domainKey, itemSchema) {
Object.keys(obj).forEach((key) => {
const vl = obj[key]
const result = Joi.validate(vl, itemSchema)
const vl = obj[key];
const result = Joi.validate(vl, itemSchema);
if (result.error !== null) {
const id = vl.id || '-'
const domainStr = capitalize(domainKey)
const id = vl.id || "-";
const domainStr = capitalize(domainKey);
discardedDomain[domainKey].push({
...vl,
error: makeError(domainStr, id, result.error.message)
})
error: makeError(domainStr, id, result.error.message),
});
} else {
sanitizedDomain[domainKey][key] = vl
sanitizedDomain[domainKey][key] = vl;
}
})
});
}
if (!Array.isArray(features.CUSTOM_EVENT_FIELDS)) {
features.CUSTOM_EVENT_FIELDS = []
features.CUSTOM_EVENT_FIELDS = [];
}
const eventSchema = createEventSchema(features.CUSTOM_EVENT_FIELDS)
validateArray(domain.events, 'events', eventSchema)
validateArray(domain.sites, 'sites', siteSchema)
validateArray(domain.associations, 'associations', associationsSchema)
validateObject(domain.sources, 'sources', sourceSchema)
validateObject(domain.shapes, 'shapes', shapeSchema)
const eventSchema = createEventSchema(features.CUSTOM_EVENT_FIELDS);
validateArray(domain.events, "events", eventSchema);
validateArray(domain.sites, "sites", siteSchema);
validateArray(domain.associations, "associations", associationsSchema);
validateObject(domain.sources, "sources", sourceSchema);
validateObject(domain.shapes, "shapes", shapeSchema);
// NB: [lat, lon] array is best format for projecting into map
sanitizedDomain.shapes = sanitizedDomain.shapes.map((shape) => ({
name: shape.name,
points: shape.items.map((coords) => coords.replace(/\s/g, '').split(','))
}))
points: shape.items.map((coords) => coords.replace(/\s/g, "").split(",")),
}));
const duplicateAssociations = findDuplicateAssociations(domain.associations)
const duplicateAssociations = findDuplicateAssociations(domain.associations);
// Duplicated associations
if (duplicateAssociations.length > 0) {
sanitizedDomain.notifications.push({
message: `Associations are required to be unique. Ignoring duplicates for now.`,
message:
"Associations are required to be unique. Ignoring duplicates for now.",
items: duplicateAssociations,
type: 'error'
})
type: "error",
});
}
sanitizedDomain.associations = domain.associations
sanitizedDomain.associations = domain.associations;
// append events with datetime and sort
sanitizedDomain.events = sanitizedDomain.events.filter((event, idx) => {
event.id = idx
event.datetime = calcDatetime(event.date, event.time)
event.id = idx;
event.datetime = calcDatetime(event.date, event.time);
if (!isValidDate(event.datetime)) {
discardedDomain['events'].push({
discardedDomain.events.push({
...event,
error: makeError(
'events',
"events",
event.id,
`Invalid date. It's been dropped, as otherwise timemap won't work as expected.`
)
})
return false
"Invalid date. It's been dropped, as otherwise timemap won't work as expected."
),
});
return false;
}
return true
})
return true;
});
sanitizedDomain.events.sort((a, b) => a.datetime - b.datetime)
sanitizedDomain.events.sort((a, b) => a.datetime - b.datetime);
// Message the number of failed items in domain
Object.keys(discardedDomain).forEach((disc) => {
const len = discardedDomain[disc].length
const len = discardedDomain[disc].length;
if (len) {
sanitizedDomain.notifications.push({
message: `${len} invalid ${disc} not displayed.`,
items: discardedDomain[disc],
type: 'error'
})
type: "error",
});
}
})
});
return sanitizedDomain
return sanitizedDomain;
}

View File

@@ -1,4 +1,3 @@
// Burger transition
.side-menu-burg {
position: absolute;
@@ -64,7 +63,7 @@
}
}
&.is-active {
&.is-active {
span {
background: $midwhite;
transform: rotate(45deg);

View File

@@ -1,12 +1,12 @@
@font-face {
font-family: 'GT-Zirkon';
font-family: "GT-Zirkon";
src: url(../assets/fonts/timemapfont.woff); // a Lato woff by default
}
$event_default: red;
$offwhite: #efefef;
$offwhite-transparent: rgba(239,239,239, 0.9);
$offwhite-transparent: rgba(239, 239, 239, 0.9);
$lightwhite: #dfdfdf;
$midwhite: #a0a0a0;
$darkwhite: darken($midwhite, 15%);
@@ -16,28 +16,36 @@ $green: rgb(61, 241, 79);
$midgrey: rgb(44, 44, 44);
$darkgrey: #232323;
$black: #000000;
$black-transparent: rgba(0,0,0,0.7);
$black-transparent: rgba(0, 0, 0, 0.7);
// Category colors
$default: red;
$alpha: #00ff00;
$beta: #ff00ff;
$other: yellow;
$default: red;
$alpha: #00ff00;
$beta: #ff00ff;
$other: yellow;
.default { background: $default; }
.other { background: $other; }
.alpha { background: $alpha; }
.beta { background: $beta; }
.default {
background: $default;
}
.other {
background: $other;
}
.alpha {
background: $alpha;
}
.beta {
background: $beta;
}
$mainfont: 'GT-Zirkon', 'Lato', Helvetica, sans-serif;
$mainfont: "GT-Zirkon", "Lato", Helvetica, sans-serif;
// Font sizes
$xsmall: 10px;//0.7em;
$small: 11px;//0.9em;
$normal: 12px;//1em;
$large: 14px;//1.1em;
$xlarge: 16px;//1.2em;
$xxlarge: 20px;//1.4em;
$xsmall: 10px; //0.7em;
$small: 11px; //0.9em;
$normal: 12px; //1em;
$large: 14px; //1.1em;
$xlarge: 16px; //1.2em;
$xxlarge: 20px; //1.4em;
$xxxlarge: 32px;
// z-index levels
@@ -51,7 +59,6 @@ $scene: 1;
$hidden: -1;
$timeline: 3;
// platform-specific sizes
$infopopup-width: 400px;
$infopopup-left: 122px;

View File

@@ -16,7 +16,7 @@
height: 100%;
-webkit-filter: contrast(70%) brightness(70%) grayscale(30%); /* Webkit */
filter: gray; /* IE6-9 */
filter: contrast(70%) brightness(70%) grayscale(30%) /* W3C */
filter: contrast(70%) brightness(70%) grayscale(30%); /* W3C */
}
@media (min-aspect-ratio: 16/9) {
@@ -35,7 +35,8 @@
@media (max-width: 767px) {
.fullscreen-bg {
background: url('/static/archive/img/city.jpg') center center / cover no-repeat;
background: url("/static/archive/img/city.jpg") center center / cover
no-repeat;
}
.fullscreen-bg__video {

View File

@@ -42,7 +42,8 @@
margin-right: 5px;
}
.card-row, .card-col {
.card-row,
.card-col {
display: flex;
flex-direction: row;
margin: 5px 0 10px 0;
@@ -50,7 +51,7 @@
.card-cell {
flex: 1;
}
}
h4 {
min-width: 80px;
@@ -115,7 +116,6 @@
}
}
.card-bottomhalf {
transition: 0.4s ease;
height: auto;
@@ -125,7 +125,6 @@
height: 0;
overflow: hidden;
}
}
.card-toggle p {

View File

@@ -1,4 +1,4 @@
@import 'variables';
@import "variables";
body {
margin: 0;
@@ -133,5 +133,5 @@ Scrollbar
}
.hidden {
visibility: hidden;
visibility: hidden;
}

View File

@@ -33,7 +33,6 @@
padding: 20px 0 0 20px;
display: flex;
&.minimized {
}
.cover-logo {
transition: all 1s;
@@ -59,9 +58,6 @@
}
}
.fullscreen-bg {
&.hidden {
top: -100%;
@@ -73,7 +69,8 @@
left: 0;
// overflow: hidden;
z-index: -100;
background: #000000; }
background: #000000;
}
.fullscreen-bg__video {
position: relative;
@@ -101,11 +98,11 @@
flex-direction: column;
max-height: 100%;
hr, br {
hr,
br {
width: 100%;
}
.sidebar {
display: flex;
flex-direction: column;
@@ -163,7 +160,7 @@
}
justify-content: space-around;
&.vertical {
flex-direction: column;
flex-direction: column;
}
.cell {
border: 1px solid white;
@@ -211,10 +208,20 @@
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 10em;
h1, h2, h3, h4, h5 { text-align: center; }
h1 { margin-bottom: -15px; margin-top: 30px; }
h5 { margin-top: -15px; }
h1,
h2,
h3,
h4,
h5 {
text-align: center;
}
h1 {
margin-bottom: -15px;
margin-top: 30px;
}
h5 {
margin-top: -15px;
}
.md-container {
width: 100%;
@@ -260,16 +267,17 @@
.il-cover-verification {
.il-video {
border-radius: 1em;
background-color: rgba(240,240,240,0.5);
background-color: rgba(240, 240, 240, 0.5);
}
}
}
}
_::-webkit-full-page-media, _:future, :root .cover-content {
_::-webkit-full-page-media,
_:future,
:root .cover-content {
max-width: auto;
}
}
.cover-footer {

View File

@@ -1,8 +1,9 @@
@import 'burger';
@import "burger";
.infopopup {
width: $infopopup-width;
box-shadow: 10px -10px 38px rgba(0, 0, 0, 0.3), 10px 15px 12px rgba(0, 0, 0, 0.22);
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-transparent;
@@ -36,7 +37,7 @@
&.dark {
// background: $black-transparent;
background: rgba(0,0,0,0.8);
background: rgba(0, 0, 0, 0.8);
color: white;
}
@@ -60,7 +61,6 @@
}
}
.legend {
display: flex;
flex-direction: column;

View File

@@ -3,7 +3,7 @@
width: 100%;
height: 100%;
position: absolute;
background: rgba(0,0,0,0.9);
background: rgba(0, 0, 0, 0.9);
transition: 0.4s ease;
z-index: $loading-overlay;
opacity: 1;
@@ -26,7 +26,7 @@
}
&.hidden {
transition: opacity 0.4s ease, z-index .1s 0.4s;
transition: opacity 0.4s ease, z-index 0.1s 0.4s;
opacity: 0;
z-index: $hidden;
}
@@ -49,7 +49,8 @@ https://github.com/tobiasahlin/SpinKit/blob/master/LICENSE
}
}
.double-bounce, .double-bounce-overlay {
.double-bounce,
.double-bounce-overlay {
width: 100%;
height: 100%;
border-radius: 50%;
@@ -59,28 +60,35 @@ https://github.com/tobiasahlin/SpinKit/blob/master/LICENSE
top: 0;
left: 0;
-webkit-animation: sk-bounce 3.0s infinite ease-in-out;
animation: sk-bounce 3.0s infinite ease-in-out;
-webkit-animation: sk-bounce 3s infinite ease-in-out;
animation: sk-bounce 3s infinite ease-in-out;
}
.double-bounce-overlay {
-webkit-animation-delay: -1.0s;
animation-delay: -1.0s;
-webkit-animation-delay: -1s;
animation-delay: -1s;
background-color: black;
}
@-webkit-keyframes sk-bounce {
0%, 100% { -webkit-transform: scale(0.3) }
50% { -webkit-transform: scale(1.0) }
0%,
100% {
-webkit-transform: scale(0.3);
}
50% {
-webkit-transform: scale(1);
}
}
@keyframes sk-bounce {
0%, 100% {
0%,
100% {
transform: scale(0.3);
-webkit-transform: scale(0.3);
} 50% {
transform: scale(1.0);
-webkit-transform: scale(1.0);
}
50% {
transform: scale(1);
-webkit-transform: scale(1);
}
}

View File

@@ -1,17 +1,15 @@
@import 'variables';
@import 'common';
@import 'loading';
@import 'header';
@import 'cardstack';
@import 'narrativecard';
@import 'overlay';
@import 'map';
@import 'timeline';
@import 'toolbar';
@import 'infopopup';
@import 'notification';
@import 'mediaplayer';
@import 'cover';
@import 'stateoptions';
@import "variables";
@import "common";
@import "loading";
@import "header";
@import "cardstack";
@import "narrativecard";
@import "overlay";
@import "map";
@import "timeline";
@import "toolbar";
@import "infopopup";
@import "notification";
@import "mediaplayer";
@import "cover";
@import "stateoptions";

View File

@@ -1,9 +1,15 @@
@import 'popup';
@import "popup";
@-webkit-keyframes pulsate {
0% { opacity: 0.1; }
50% { opacity: 0.25; }
100% { opacity: 0.1; }
0% {
opacity: 0.1;
}
50% {
opacity: 0.25;
}
100% {
opacity: 0.1;
}
}
.map-wrapper {
@@ -41,27 +47,29 @@
}
.site-label {
background: rgba($black,0.6);
background: rgba($black, 0.6);
color: #fff;
padding: 5px;
font-weight: 500;
font-size: 11px;
border: rgba($black,0.6);
border: rgba($black, 0.6);
letter-spacing: 0.05em;
&::before {
border-top-color: rgba($black,0.6);
border-top-color: rgba($black, 0.6);
}
}
.sites-layer, .shapes-layer {
.sites-layer,
.shapes-layer {
position: fixed;
top: 0px;
left: 110px;
}
&.narrative-mode {
.sites-layer, .shapes-layer {
.sites-layer,
.shapes-layer {
position: fixed;
top: 0px;
left: 0px;
@@ -143,7 +151,6 @@
pointer-events: auto;
}
.eventLocationMarker {
fill: none;
stroke: $yellow;
@@ -211,7 +218,10 @@
// no hover styles for events when in narrative mode
.narrative-mode {
.event-hover:hover { opacity: 0; }
.no-hover { cursor: inherit; }
.event-hover:hover {
opacity: 0;
}
.no-hover {
cursor: inherit;
}
}

View File

@@ -1 +1 @@
@import '~video-react/styles/scss/video-react';
@import "~video-react/styles/scss/video-react";

View File

@@ -43,7 +43,8 @@ NARRATIVE INFO
padding: 0 15px 15px 15px;
}
h3, h6 {
h3,
h6 {
text-align: center;
}
@@ -100,7 +101,7 @@ NARRATIVE INFO
position: fixed;
bottom: $timeline-height;
right: auto;
background-color: rgba(0,0,0,0.8);
background-color: rgba(0, 0, 0, 0.8);
z-index: $header;
&.left {
@@ -135,7 +136,6 @@ NARRATIVE INFO
right: $card-right;
top: 5px;
width: $card-width - 12px; // subtracting the extra width added by padding
;
// width: 15px;
background-color: black;
height: 20px;
@@ -158,12 +158,13 @@ NARRATIVE INFO
// disable whitening of crosshair on hover
button {
span, span:before, span:after {
span,
span:before,
span:after {
background: $midwhite !important;
}
}
&:hover {
cursor: pointer;
background-color: $offwhite;

Some files were not shown because too many files have changed in this diff Show More