mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-12 21:38:35 +03:00
Using prettier for linting
This commit is contained in:
File diff suppressed because one or more lines are too long
29
index.html
29
index.html
@@ -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>
|
||||
21
package.json
21
package.json
@@ -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": [
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const ASSOCIATION_MODES = {
|
||||
CATEGORY: 'CATEGORY',
|
||||
NARRATIVE: 'NARRATIVE',
|
||||
FILTER: 'FILTER'
|
||||
}
|
||||
CATEGORY: "CATEGORY",
|
||||
NARRATIVE: "NARRATIVE",
|
||||
FILTER: "FILTER",
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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} />
|
||||
));
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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" />;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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})`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import 'variables';
|
||||
@import "variables";
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
@@ -133,5 +133,5 @@ Scrollbar
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
@import '~video-react/styles/scss/video-react';
|
||||
@import "~video-react/styles/scss/video-react";
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user