mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-12 21:38:35 +03:00
Merge pull request #188 from forensic-architecture/feature/build-system-cra-ejected
New build system with ejected Create React App
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,12 +1,19 @@
|
||||
.idea/
|
||||
build/
|
||||
node_modules/
|
||||
|
||||
*config.js
|
||||
dev.config.js
|
||||
!config/webpack*.config.js
|
||||
!config/getHttpsConfig.js
|
||||
|
||||
|
||||
tags
|
||||
tags.lock
|
||||
tags.temp
|
||||
|
||||
.eslintcache
|
||||
|
||||
src/\.DS_Store
|
||||
src/assets/fonts
|
||||
|
||||
|
||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
||||
# Ignore Create React App-related scaffolding
|
||||
config/
|
||||
scripts/
|
||||
test/server_process.js
|
||||
125
config/env.js
Normal file
125
config/env.js
Normal file
@@ -0,0 +1,125 @@
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const paths = require("./paths");
|
||||
|
||||
// Make sure that including paths.js after env.js will read .env variables.
|
||||
delete require.cache[require.resolve("./paths")];
|
||||
|
||||
// START - injecting env variables from config.js
|
||||
// This section is responsible for require-ing the provided
|
||||
// command-line argument configuration file and serializing it.
|
||||
// It will later be available inside our app under process.env,
|
||||
// through webpack.definePlugin
|
||||
const CONFIG = process.env.CONFIG || "config.js";
|
||||
const envConfig = require("../" + CONFIG);
|
||||
|
||||
const userConfig = {};
|
||||
for (const k in envConfig) {
|
||||
userConfig[k] = JSON.stringify(envConfig[k]);
|
||||
}
|
||||
// END - injecting env variables from config.js
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV;
|
||||
if (!NODE_ENV) {
|
||||
throw new Error(
|
||||
"The NODE_ENV environment variable is required but was not specified."
|
||||
);
|
||||
}
|
||||
|
||||
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
|
||||
const dotenvFiles = [
|
||||
`${paths.dotenv}.${NODE_ENV}.local`,
|
||||
// Don't include `.env.local` for `test` environment
|
||||
// since normally you expect tests to produce the same
|
||||
// results for everyone
|
||||
NODE_ENV !== "test" && `${paths.dotenv}.local`,
|
||||
`${paths.dotenv}.${NODE_ENV}`,
|
||||
paths.dotenv,
|
||||
].filter(Boolean);
|
||||
|
||||
// Load environment variables from .env* files. Suppress warnings using silent
|
||||
// if this file is missing. dotenv will never modify any environment variables
|
||||
// that have already been set. Variable expansion is supported in .env files.
|
||||
// https://github.com/motdotla/dotenv
|
||||
// https://github.com/motdotla/dotenv-expand
|
||||
dotenvFiles.forEach((dotenvFile) => {
|
||||
if (fs.existsSync(dotenvFile)) {
|
||||
require("dotenv-expand")(
|
||||
require("dotenv").config({
|
||||
path: dotenvFile,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// We support resolving modules according to `NODE_PATH`.
|
||||
// This lets you use absolute paths in imports inside large monorepos:
|
||||
// https://github.com/facebook/create-react-app/issues/253.
|
||||
// It works similar to `NODE_PATH` in Node itself:
|
||||
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
|
||||
// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
|
||||
// Otherwise, we risk importing Node.js core modules into an app instead of webpack shims.
|
||||
// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421
|
||||
// We also resolve them to make sure all tools using them work consistently.
|
||||
const appDirectory = fs.realpathSync(process.cwd());
|
||||
process.env.NODE_PATH = (process.env.NODE_PATH || "")
|
||||
.split(path.delimiter)
|
||||
.filter((folder) => folder && !path.isAbsolute(folder))
|
||||
.map((folder) => path.resolve(appDirectory, folder))
|
||||
.join(path.delimiter);
|
||||
|
||||
// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
|
||||
// injected into the application via DefinePlugin in webpack configuration.
|
||||
const REACT_APP = /^REACT_APP_/i;
|
||||
|
||||
function getClientEnvironment(publicUrl) {
|
||||
const raw = Object.keys(process.env)
|
||||
.filter((key) => REACT_APP.test(key))
|
||||
.reduce(
|
||||
(env, key) => {
|
||||
env[key] = process.env[key];
|
||||
return env;
|
||||
},
|
||||
{
|
||||
// Useful for determining whether we’re running in production mode.
|
||||
// Most importantly, it switches React into the correct mode.
|
||||
NODE_ENV: process.env.NODE_ENV || "development",
|
||||
// Useful for resolving the correct path to static assets in `public`.
|
||||
// For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
|
||||
// This should only be used as an escape hatch. Normally you would put
|
||||
// images into the `src` and `import` them in code to get their paths.
|
||||
PUBLIC_URL: publicUrl,
|
||||
// We support configuring the sockjs pathname during development.
|
||||
// These settings let a developer run multiple simultaneous projects.
|
||||
// They are used as the connection `hostname`, `pathname` and `port`
|
||||
// in webpackHotDevClient. They are used as the `sockHost`, `sockPath`
|
||||
// and `sockPort` options in webpack-dev-server.
|
||||
WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST,
|
||||
WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH,
|
||||
WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT,
|
||||
// Whether or not react-refresh is enabled.
|
||||
// react-refresh is not 100% stable at this time,
|
||||
// which is why it's disabled by default.
|
||||
// It is defined here so it is available in the webpackHotDevClient.
|
||||
FAST_REFRESH: process.env.FAST_REFRESH !== "false",
|
||||
}
|
||||
);
|
||||
// Stringify all values so we can feed into webpack DefinePlugin
|
||||
const stringified = {
|
||||
"process.env": Object.keys(raw).reduce(
|
||||
(env, key) => {
|
||||
env[key] = JSON.stringify(raw[key]);
|
||||
return env;
|
||||
},
|
||||
{
|
||||
...userConfig,
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
return { raw, stringified };
|
||||
}
|
||||
|
||||
module.exports = getClientEnvironment;
|
||||
66
config/getHttpsConfig.js
Normal file
66
config/getHttpsConfig.js
Normal file
@@ -0,0 +1,66 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const chalk = require('react-dev-utils/chalk');
|
||||
const paths = require('./paths');
|
||||
|
||||
// Ensure the certificate and key provided are valid and if not
|
||||
// throw an easy to debug error
|
||||
function validateKeyAndCerts({ cert, key, keyFile, crtFile }) {
|
||||
let encrypted;
|
||||
try {
|
||||
// publicEncrypt will throw an error with an invalid cert
|
||||
encrypted = crypto.publicEncrypt(cert, Buffer.from('test'));
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// privateDecrypt will throw an error with an invalid key
|
||||
crypto.privateDecrypt(key, encrypted);
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${
|
||||
err.message
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Read file and throw an error if it doesn't exist
|
||||
function readEnvFile(file, type) {
|
||||
if (!fs.existsSync(file)) {
|
||||
throw new Error(
|
||||
`You specified ${chalk.cyan(
|
||||
type
|
||||
)} in your env, but the file "${chalk.yellow(file)}" can't be found.`
|
||||
);
|
||||
}
|
||||
return fs.readFileSync(file);
|
||||
}
|
||||
|
||||
// Get the https config
|
||||
// Return cert files if provided in env, otherwise just true or false
|
||||
function getHttpsConfig() {
|
||||
const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env;
|
||||
const isHttps = HTTPS === 'true';
|
||||
|
||||
if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) {
|
||||
const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE);
|
||||
const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE);
|
||||
const config = {
|
||||
cert: readEnvFile(crtFile, 'SSL_CRT_FILE'),
|
||||
key: readEnvFile(keyFile, 'SSL_KEY_FILE'),
|
||||
};
|
||||
|
||||
validateKeyAndCerts({ ...config, keyFile, crtFile });
|
||||
return config;
|
||||
}
|
||||
return isHttps;
|
||||
}
|
||||
|
||||
module.exports = getHttpsConfig;
|
||||
14
config/jest/cssTransform.js
Normal file
14
config/jest/cssTransform.js
Normal file
@@ -0,0 +1,14 @@
|
||||
"use strict";
|
||||
|
||||
// This is a custom Jest transformer turning style imports into empty objects.
|
||||
// http://facebook.github.io/jest/docs/en/webpack.html
|
||||
|
||||
module.exports = {
|
||||
process() {
|
||||
return "module.exports = {};";
|
||||
},
|
||||
getCacheKey() {
|
||||
// The output is always the same.
|
||||
return "cssTransform";
|
||||
},
|
||||
};
|
||||
40
config/jest/fileTransform.js
Normal file
40
config/jest/fileTransform.js
Normal file
@@ -0,0 +1,40 @@
|
||||
"use strict";
|
||||
|
||||
const path = require("path");
|
||||
const camelcase = require("camelcase");
|
||||
|
||||
// This is a custom Jest transformer turning file imports into filenames.
|
||||
// http://facebook.github.io/jest/docs/en/webpack.html
|
||||
|
||||
module.exports = {
|
||||
process(src, filename) {
|
||||
const assetFilename = JSON.stringify(path.basename(filename));
|
||||
|
||||
if (filename.match(/\.svg$/)) {
|
||||
// Based on how SVGR generates a component name:
|
||||
// https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
|
||||
const pascalCaseFilename = camelcase(path.parse(filename).name, {
|
||||
pascalCase: true,
|
||||
});
|
||||
const componentName = `Svg${pascalCaseFilename}`;
|
||||
return `const React = require('react');
|
||||
module.exports = {
|
||||
__esModule: true,
|
||||
default: ${assetFilename},
|
||||
ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
|
||||
return {
|
||||
$$typeof: Symbol.for('react.element'),
|
||||
type: 'svg',
|
||||
ref: ref,
|
||||
key: null,
|
||||
props: Object.assign({}, props, {
|
||||
children: ${assetFilename}
|
||||
})
|
||||
};
|
||||
}),
|
||||
};`;
|
||||
}
|
||||
|
||||
return `module.exports = ${assetFilename};`;
|
||||
},
|
||||
};
|
||||
2
config/jest/setEnvVars.js
Normal file
2
config/jest/setEnvVars.js
Normal file
@@ -0,0 +1,2 @@
|
||||
const envConfig = require("../../" + (process.env.CONFIG || 'config.js'));
|
||||
process.env = { ...process.env, ...envConfig };
|
||||
134
config/modules.js
Normal file
134
config/modules.js
Normal file
@@ -0,0 +1,134 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const paths = require('./paths');
|
||||
const chalk = require('react-dev-utils/chalk');
|
||||
const resolve = require('resolve');
|
||||
|
||||
/**
|
||||
* Get additional module paths based on the baseUrl of a compilerOptions object.
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
function getAdditionalModulePaths(options = {}) {
|
||||
const baseUrl = options.baseUrl;
|
||||
|
||||
if (!baseUrl) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
|
||||
|
||||
// We don't need to do anything if `baseUrl` is set to `node_modules`. This is
|
||||
// the default behavior.
|
||||
if (path.relative(paths.appNodeModules, baseUrlResolved) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Allow the user set the `baseUrl` to `appSrc`.
|
||||
if (path.relative(paths.appSrc, baseUrlResolved) === '') {
|
||||
return [paths.appSrc];
|
||||
}
|
||||
|
||||
// If the path is equal to the root directory we ignore it here.
|
||||
// We don't want to allow importing from the root directly as source files are
|
||||
// not transpiled outside of `src`. We do allow importing them with the
|
||||
// absolute path (e.g. `src/Components/Button.js`) but we set that up with
|
||||
// an alias.
|
||||
if (path.relative(paths.appPath, baseUrlResolved) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Otherwise, throw an error.
|
||||
throw new Error(
|
||||
chalk.red.bold(
|
||||
"Your project's `baseUrl` can only be set to `src` or `node_modules`." +
|
||||
' Create React App does not support other values at this time.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get webpack aliases based on the baseUrl of a compilerOptions object.
|
||||
*
|
||||
* @param {*} options
|
||||
*/
|
||||
function getWebpackAliases(options = {}) {
|
||||
const baseUrl = options.baseUrl;
|
||||
|
||||
if (!baseUrl) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
|
||||
|
||||
if (path.relative(paths.appPath, baseUrlResolved) === '') {
|
||||
return {
|
||||
src: paths.appSrc,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get jest aliases based on the baseUrl of a compilerOptions object.
|
||||
*
|
||||
* @param {*} options
|
||||
*/
|
||||
function getJestAliases(options = {}) {
|
||||
const baseUrl = options.baseUrl;
|
||||
|
||||
if (!baseUrl) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
|
||||
|
||||
if (path.relative(paths.appPath, baseUrlResolved) === '') {
|
||||
return {
|
||||
'^src/(.*)$': '<rootDir>/src/$1',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getModules() {
|
||||
// Check if TypeScript is setup
|
||||
const hasTsConfig = fs.existsSync(paths.appTsConfig);
|
||||
const hasJsConfig = fs.existsSync(paths.appJsConfig);
|
||||
|
||||
if (hasTsConfig && hasJsConfig) {
|
||||
throw new Error(
|
||||
'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.'
|
||||
);
|
||||
}
|
||||
|
||||
let config;
|
||||
|
||||
// If there's a tsconfig.json we assume it's a
|
||||
// TypeScript project and set up the config
|
||||
// based on tsconfig.json
|
||||
if (hasTsConfig) {
|
||||
const ts = require(resolve.sync('typescript', {
|
||||
basedir: paths.appNodeModules,
|
||||
}));
|
||||
config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
|
||||
// Otherwise we'll check if there is jsconfig.json
|
||||
// for non TS projects.
|
||||
} else if (hasJsConfig) {
|
||||
config = require(paths.appJsConfig);
|
||||
}
|
||||
|
||||
config = config || {};
|
||||
const options = config.compilerOptions || {};
|
||||
|
||||
const additionalModulePaths = getAdditionalModulePaths(options);
|
||||
|
||||
return {
|
||||
additionalModulePaths: additionalModulePaths,
|
||||
webpackAliases: getWebpackAliases(options),
|
||||
jestAliases: getJestAliases(options),
|
||||
hasTsConfig,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = getModules();
|
||||
73
config/paths.js
Normal file
73
config/paths.js
Normal file
@@ -0,0 +1,73 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath');
|
||||
|
||||
// Make sure any symlinks in the project folder are resolved:
|
||||
// https://github.com/facebook/create-react-app/issues/637
|
||||
const appDirectory = fs.realpathSync(process.cwd());
|
||||
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
|
||||
|
||||
// We use `PUBLIC_URL` environment variable or "homepage" field to infer
|
||||
// "public path" at which the app is served.
|
||||
// webpack needs to know it to put the right <script> hrefs into HTML even in
|
||||
// single-page apps that may serve index.html for nested URLs like /todos/42.
|
||||
// We can't use a relative path in HTML because we don't want to load something
|
||||
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
|
||||
const publicUrlOrPath = getPublicUrlOrPath(
|
||||
process.env.NODE_ENV === 'development',
|
||||
require(resolveApp('package.json')).homepage,
|
||||
process.env.PUBLIC_URL
|
||||
);
|
||||
|
||||
const moduleFileExtensions = [
|
||||
'web.mjs',
|
||||
'mjs',
|
||||
'web.js',
|
||||
'js',
|
||||
'web.ts',
|
||||
'ts',
|
||||
'web.tsx',
|
||||
'tsx',
|
||||
'json',
|
||||
'web.jsx',
|
||||
'jsx',
|
||||
];
|
||||
|
||||
// Resolve file paths in the same order as webpack
|
||||
const resolveModule = (resolveFn, filePath) => {
|
||||
const extension = moduleFileExtensions.find(extension =>
|
||||
fs.existsSync(resolveFn(`${filePath}.${extension}`))
|
||||
);
|
||||
|
||||
if (extension) {
|
||||
return resolveFn(`${filePath}.${extension}`);
|
||||
}
|
||||
|
||||
return resolveFn(`${filePath}.js`);
|
||||
};
|
||||
|
||||
// config after eject: we're in ./config/
|
||||
module.exports = {
|
||||
dotenv: resolveApp('.env'),
|
||||
appPath: resolveApp('.'),
|
||||
appBuild: resolveApp('build'),
|
||||
appPublic: resolveApp('public'),
|
||||
appHtml: resolveApp('public/index.html'),
|
||||
appIndexJs: resolveModule(resolveApp, 'src/index'),
|
||||
appPackageJson: resolveApp('package.json'),
|
||||
appSrc: resolveApp('src'),
|
||||
appTsConfig: resolveApp('tsconfig.json'),
|
||||
appJsConfig: resolveApp('jsconfig.json'),
|
||||
yarnLockFile: resolveApp('yarn.lock'),
|
||||
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
|
||||
proxySetup: resolveApp('src/setupProxy.js'),
|
||||
appNodeModules: resolveApp('node_modules'),
|
||||
swSrc: resolveModule(resolveApp, 'src/service-worker'),
|
||||
publicUrlOrPath,
|
||||
};
|
||||
|
||||
|
||||
|
||||
module.exports.moduleFileExtensions = moduleFileExtensions;
|
||||
35
config/pnpTs.js
Normal file
35
config/pnpTs.js
Normal file
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
const { resolveModuleName } = require('ts-pnp');
|
||||
|
||||
exports.resolveModuleName = (
|
||||
typescript,
|
||||
moduleName,
|
||||
containingFile,
|
||||
compilerOptions,
|
||||
resolutionHost
|
||||
) => {
|
||||
return resolveModuleName(
|
||||
moduleName,
|
||||
containingFile,
|
||||
compilerOptions,
|
||||
resolutionHost,
|
||||
typescript.resolveModuleName
|
||||
);
|
||||
};
|
||||
|
||||
exports.resolveTypeReferenceDirective = (
|
||||
typescript,
|
||||
moduleName,
|
||||
containingFile,
|
||||
compilerOptions,
|
||||
resolutionHost
|
||||
) => {
|
||||
return resolveModuleName(
|
||||
moduleName,
|
||||
containingFile,
|
||||
compilerOptions,
|
||||
resolutionHost,
|
||||
typescript.resolveTypeReferenceDirective
|
||||
);
|
||||
};
|
||||
748
config/webpack.config.js
Normal file
748
config/webpack.config.js
Normal file
@@ -0,0 +1,748 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const resolve = require('resolve');
|
||||
const PnpWebpackPlugin = require('pnp-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
|
||||
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
const safePostCssParser = require('postcss-safe-parser');
|
||||
const ManifestPlugin = require('webpack-manifest-plugin');
|
||||
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
|
||||
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
|
||||
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
|
||||
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
|
||||
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
|
||||
const ESLintPlugin = require('eslint-webpack-plugin');
|
||||
const paths = require('./paths');
|
||||
const modules = require('./modules');
|
||||
const getClientEnvironment = require('./env');
|
||||
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
|
||||
const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
|
||||
const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
|
||||
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
||||
|
||||
const postcssNormalize = require('postcss-normalize');
|
||||
|
||||
const appPackageJson = require(paths.appPackageJson);
|
||||
|
||||
// Source maps are resource heavy and can cause out of memory issue for large source files.
|
||||
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
|
||||
|
||||
const webpackDevClientEntry = require.resolve(
|
||||
'react-dev-utils/webpackHotDevClient'
|
||||
);
|
||||
const reactRefreshOverlayEntry = require.resolve(
|
||||
'react-dev-utils/refreshOverlayInterop'
|
||||
);
|
||||
|
||||
// Some apps do not need the benefits of saving a web request, so not inlining the chunk
|
||||
// makes for a smoother build process.
|
||||
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
|
||||
|
||||
const imageInlineSizeLimit = parseInt(
|
||||
process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
|
||||
);
|
||||
|
||||
// Check if TypeScript is setup
|
||||
const useTypeScript = fs.existsSync(paths.appTsConfig);
|
||||
|
||||
// Get the path to the uncompiled service worker (if it exists).
|
||||
const swSrc = paths.swSrc;
|
||||
|
||||
// style files regexes
|
||||
const cssRegex = /\.css$/;
|
||||
const cssModuleRegex = /\.module\.css$/;
|
||||
const sassRegex = /\.(scss|sass)$/;
|
||||
const sassModuleRegex = /\.module\.(scss|sass)$/;
|
||||
|
||||
const hasJsxRuntime = (() => {
|
||||
if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
require.resolve('react/jsx-runtime');
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
// This is the production and development configuration.
|
||||
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
|
||||
module.exports = function (webpackEnv) {
|
||||
const isEnvDevelopment = webpackEnv === 'development';
|
||||
const isEnvProduction = webpackEnv === 'production';
|
||||
|
||||
// Variable used for enabling profiling in Production
|
||||
// passed into alias object. Uses a flag if passed into the build command
|
||||
const isEnvProductionProfile =
|
||||
isEnvProduction && process.argv.includes('--profile');
|
||||
|
||||
// We will provide `paths.publicUrlOrPath` to our app
|
||||
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
|
||||
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
|
||||
// Get environment variables to inject into our app.
|
||||
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
|
||||
|
||||
const shouldUseReactRefresh = env.raw.FAST_REFRESH;
|
||||
|
||||
// common function to get style loaders
|
||||
const getStyleLoaders = (cssOptions, preProcessor) => {
|
||||
const loaders = [
|
||||
isEnvDevelopment && require.resolve('style-loader'),
|
||||
isEnvProduction && {
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
// css is located in `static/css`, use '../../' to locate index.html folder
|
||||
// in production `paths.publicUrlOrPath` can be a relative path
|
||||
options: paths.publicUrlOrPath.startsWith('.')
|
||||
? { publicPath: '../../' }
|
||||
: {},
|
||||
},
|
||||
{
|
||||
loader: require.resolve('css-loader'),
|
||||
options: cssOptions,
|
||||
},
|
||||
{
|
||||
// Options for PostCSS as we reference these options twice
|
||||
// Adds vendor prefixing based on your specified browser support in
|
||||
// package.json
|
||||
loader: require.resolve('postcss-loader'),
|
||||
options: {
|
||||
// Necessary for external CSS imports to work
|
||||
// https://github.com/facebook/create-react-app/issues/2677
|
||||
ident: 'postcss',
|
||||
plugins: () => [
|
||||
require('postcss-flexbugs-fixes'),
|
||||
require('postcss-preset-env')({
|
||||
autoprefixer: {
|
||||
flexbox: 'no-2009',
|
||||
},
|
||||
stage: 3,
|
||||
}),
|
||||
// Adds PostCSS Normalize as the reset css with default options,
|
||||
// so that it honors browserslist config in package.json
|
||||
// which in turn let's users customize the target behavior as per their needs.
|
||||
postcssNormalize(),
|
||||
],
|
||||
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||
},
|
||||
},
|
||||
].filter(Boolean);
|
||||
if (preProcessor) {
|
||||
loaders.push(
|
||||
{
|
||||
loader: require.resolve('resolve-url-loader'),
|
||||
options: {
|
||||
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||
root: paths.appSrc,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve(preProcessor),
|
||||
options: {
|
||||
sourceMap: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
return loaders;
|
||||
};
|
||||
|
||||
return {
|
||||
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
|
||||
// Stop compilation early in production
|
||||
bail: isEnvProduction,
|
||||
devtool: isEnvProduction
|
||||
? shouldUseSourceMap
|
||||
? 'source-map'
|
||||
: false
|
||||
: isEnvDevelopment && 'cheap-module-source-map',
|
||||
// These are the "entry points" to our application.
|
||||
// This means they will be the "root" imports that are included in JS bundle.
|
||||
entry:
|
||||
isEnvDevelopment && !shouldUseReactRefresh
|
||||
? [
|
||||
// Include an alternative client for WebpackDevServer. A client's job is to
|
||||
// connect to WebpackDevServer by a socket and get notified about changes.
|
||||
// When you save a file, the client will either apply hot updates (in case
|
||||
// of CSS changes), or refresh the page (in case of JS changes). When you
|
||||
// make a syntax error, this client will display a syntax error overlay.
|
||||
// Note: instead of the default WebpackDevServer client, we use a custom one
|
||||
// to bring better experience for Create React App users. You can replace
|
||||
// the line below with these two lines if you prefer the stock client:
|
||||
//
|
||||
// require.resolve('webpack-dev-server/client') + '?/',
|
||||
// require.resolve('webpack/hot/dev-server'),
|
||||
//
|
||||
// When using the experimental react-refresh integration,
|
||||
// the webpack plugin takes care of injecting the dev client for us.
|
||||
webpackDevClientEntry,
|
||||
// Finally, this is your app's code:
|
||||
paths.appIndexJs,
|
||||
// We include the app code last so that if there is a runtime error during
|
||||
// initialization, it doesn't blow up the WebpackDevServer client, and
|
||||
// changing JS code would still trigger a refresh.
|
||||
]
|
||||
: paths.appIndexJs,
|
||||
output: {
|
||||
// The build folder.
|
||||
path: isEnvProduction ? paths.appBuild : undefined,
|
||||
// Add /* filename */ comments to generated require()s in the output.
|
||||
pathinfo: isEnvDevelopment,
|
||||
// There will be one main bundle, and one file per asynchronous chunk.
|
||||
// In development, it does not produce real files.
|
||||
filename: isEnvProduction
|
||||
? 'static/js/[name].[contenthash:8].js'
|
||||
: isEnvDevelopment && 'static/js/bundle.js',
|
||||
// TODO: remove this when upgrading to webpack 5
|
||||
futureEmitAssets: true,
|
||||
// There are also additional JS chunk files if you use code splitting.
|
||||
chunkFilename: isEnvProduction
|
||||
? 'static/js/[name].[contenthash:8].chunk.js'
|
||||
: isEnvDevelopment && 'static/js/[name].chunk.js',
|
||||
// webpack uses `publicPath` to determine where the app is being served from.
|
||||
// It requires a trailing slash, or the file assets will get an incorrect path.
|
||||
// We inferred the "public path" (such as / or /my-project) from homepage.
|
||||
publicPath: paths.publicUrlOrPath,
|
||||
// Point sourcemap entries to original disk location (format as URL on Windows)
|
||||
devtoolModuleFilenameTemplate: isEnvProduction
|
||||
? info =>
|
||||
path
|
||||
.relative(paths.appSrc, info.absoluteResourcePath)
|
||||
.replace(/\\/g, '/')
|
||||
: isEnvDevelopment &&
|
||||
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
|
||||
// Prevents conflicts when multiple webpack runtimes (from different apps)
|
||||
// are used on the same page.
|
||||
jsonpFunction: `webpackJsonp${appPackageJson.name}`,
|
||||
// this defaults to 'window', but by setting it to 'this' then
|
||||
// module chunks which are built will work in web workers as well.
|
||||
globalObject: 'this',
|
||||
},
|
||||
optimization: {
|
||||
minimize: isEnvProduction,
|
||||
minimizer: [
|
||||
// This is only used in production mode
|
||||
new TerserPlugin({
|
||||
terserOptions: {
|
||||
parse: {
|
||||
// We want terser to parse ecma 8 code. However, we don't want it
|
||||
// to apply any minification steps that turns valid ecma 5 code
|
||||
// into invalid ecma 5 code. This is why the 'compress' and 'output'
|
||||
// sections only apply transformations that are ecma 5 safe
|
||||
// https://github.com/facebook/create-react-app/pull/4234
|
||||
ecma: 8,
|
||||
},
|
||||
compress: {
|
||||
ecma: 5,
|
||||
warnings: false,
|
||||
// Disabled because of an issue with Uglify breaking seemingly valid code:
|
||||
// https://github.com/facebook/create-react-app/issues/2376
|
||||
// Pending further investigation:
|
||||
// https://github.com/mishoo/UglifyJS2/issues/2011
|
||||
comparisons: false,
|
||||
// Disabled because of an issue with Terser breaking valid code:
|
||||
// https://github.com/facebook/create-react-app/issues/5250
|
||||
// Pending further investigation:
|
||||
// https://github.com/terser-js/terser/issues/120
|
||||
inline: 2,
|
||||
},
|
||||
mangle: {
|
||||
safari10: true,
|
||||
},
|
||||
// Added for profiling in devtools
|
||||
keep_classnames: isEnvProductionProfile,
|
||||
keep_fnames: isEnvProductionProfile,
|
||||
output: {
|
||||
ecma: 5,
|
||||
comments: false,
|
||||
// Turned on because emoji and regex is not minified properly using default
|
||||
// https://github.com/facebook/create-react-app/issues/2488
|
||||
ascii_only: true,
|
||||
},
|
||||
},
|
||||
sourceMap: shouldUseSourceMap,
|
||||
}),
|
||||
// This is only used in production mode
|
||||
new OptimizeCSSAssetsPlugin({
|
||||
cssProcessorOptions: {
|
||||
parser: safePostCssParser,
|
||||
map: shouldUseSourceMap
|
||||
? {
|
||||
// `inline: false` forces the sourcemap to be output into a
|
||||
// separate file
|
||||
inline: false,
|
||||
// `annotation: true` appends the sourceMappingURL to the end of
|
||||
// the css file, helping the browser find the sourcemap
|
||||
annotation: true,
|
||||
}
|
||||
: false,
|
||||
},
|
||||
cssProcessorPluginOptions: {
|
||||
preset: ['default', { minifyFontValues: { removeQuotes: false } }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
// Automatically split vendor and commons
|
||||
// https://twitter.com/wSokra/status/969633336732905474
|
||||
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
name: false,
|
||||
},
|
||||
// Keep the runtime chunk separated to enable long term caching
|
||||
// https://twitter.com/wSokra/status/969679223278505985
|
||||
// https://github.com/facebook/create-react-app/issues/5358
|
||||
runtimeChunk: {
|
||||
name: entrypoint => `runtime-${entrypoint.name}`,
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
// This allows you to set a fallback for where webpack should look for modules.
|
||||
// We placed these paths second because we want `node_modules` to "win"
|
||||
// if there are any conflicts. This matches Node resolution mechanism.
|
||||
// https://github.com/facebook/create-react-app/issues/253
|
||||
modules: ['node_modules', paths.appNodeModules].concat(
|
||||
modules.additionalModulePaths || []
|
||||
),
|
||||
// These are the reasonable defaults supported by the Node ecosystem.
|
||||
// We also include JSX as a common component filename extension to support
|
||||
// some tools, although we do not recommend using it, see:
|
||||
// https://github.com/facebook/create-react-app/issues/290
|
||||
// `web` extension prefixes have been added for better support
|
||||
// for React Native Web.
|
||||
extensions: paths.moduleFileExtensions
|
||||
.map(ext => `.${ext}`)
|
||||
.filter(ext => useTypeScript || !ext.includes('ts')),
|
||||
alias: {
|
||||
// Support React Native Web
|
||||
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
|
||||
'react-native': 'react-native-web',
|
||||
// Allows for better profiling with ReactDevTools
|
||||
...(isEnvProductionProfile && {
|
||||
'react-dom$': 'react-dom/profiling',
|
||||
'scheduler/tracing': 'scheduler/tracing-profiling',
|
||||
}),
|
||||
...(modules.webpackAliases || {}),
|
||||
},
|
||||
plugins: [
|
||||
// Adds support for installing with Plug'n'Play, leading to faster installs and adding
|
||||
// guards against forgotten dependencies and such.
|
||||
PnpWebpackPlugin,
|
||||
// Prevents users from importing files from outside of src/ (or node_modules/).
|
||||
// This often causes confusion because we only process files within src/ with babel.
|
||||
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
|
||||
// please link the files into your node_modules/ and let module-resolution kick in.
|
||||
// Make sure your source files are compiled, as they will not be processed in any way.
|
||||
new ModuleScopePlugin(paths.appSrc, [
|
||||
paths.appPackageJson,
|
||||
reactRefreshOverlayEntry,
|
||||
]),
|
||||
],
|
||||
},
|
||||
resolveLoader: {
|
||||
plugins: [
|
||||
// Also related to Plug'n'Play, but this time it tells webpack to load its loaders
|
||||
// from the current package.
|
||||
PnpWebpackPlugin.moduleLoader(module),
|
||||
],
|
||||
},
|
||||
module: {
|
||||
strictExportPresence: true,
|
||||
rules: [
|
||||
// Disable require.ensure as it's not a standard language feature.
|
||||
{ parser: { requireEnsure: false } },
|
||||
{
|
||||
// "oneOf" will traverse all following loaders until one will
|
||||
// match the requirements. When no loader matches it will fall
|
||||
// back to the "file" loader at the end of the loader list.
|
||||
oneOf: [
|
||||
// TODO: Merge this config once `image/avif` is in the mime-db
|
||||
// https://github.com/jshttp/mime-db
|
||||
{
|
||||
test: [/\.avif$/],
|
||||
loader: require.resolve('url-loader'),
|
||||
options: {
|
||||
limit: imageInlineSizeLimit,
|
||||
mimetype: 'image/avif',
|
||||
name: 'static/media/[name].[hash:8].[ext]',
|
||||
},
|
||||
},
|
||||
// "url" loader works like "file" loader except that it embeds assets
|
||||
// smaller than specified limit in bytes as data URLs to avoid requests.
|
||||
// A missing `test` is equivalent to a match.
|
||||
{
|
||||
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
|
||||
loader: require.resolve('url-loader'),
|
||||
options: {
|
||||
limit: imageInlineSizeLimit,
|
||||
name: 'static/media/[name].[hash:8].[ext]',
|
||||
},
|
||||
},
|
||||
// Process application JS with Babel.
|
||||
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
|
||||
{
|
||||
test: /\.(js|mjs|jsx|ts|tsx)$/,
|
||||
include: paths.appSrc,
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
customize: require.resolve(
|
||||
'babel-preset-react-app/webpack-overrides'
|
||||
),
|
||||
presets: [
|
||||
[
|
||||
require.resolve('babel-preset-react-app'),
|
||||
{
|
||||
runtime: hasJsxRuntime ? 'automatic' : 'classic',
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
plugins: [
|
||||
[
|
||||
require.resolve('babel-plugin-named-asset-import'),
|
||||
{
|
||||
loaderMap: {
|
||||
svg: {
|
||||
ReactComponent:
|
||||
'@svgr/webpack?-svgo,+titleProp,+ref![path]',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
isEnvDevelopment &&
|
||||
shouldUseReactRefresh &&
|
||||
require.resolve('react-refresh/babel'),
|
||||
].filter(Boolean),
|
||||
// This is a feature of `babel-loader` for webpack (not Babel itself).
|
||||
// It enables caching results in ./node_modules/.cache/babel-loader/
|
||||
// directory for faster rebuilds.
|
||||
cacheDirectory: true,
|
||||
// See #6846 for context on why cacheCompression is disabled
|
||||
cacheCompression: false,
|
||||
compact: isEnvProduction,
|
||||
},
|
||||
},
|
||||
// Process any JS outside of the app with Babel.
|
||||
// Unlike the application JS, we only compile the standard ES features.
|
||||
{
|
||||
test: /\.(js|mjs)$/,
|
||||
exclude: /@babel(?:\/|\\{1,2})runtime/,
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
babelrc: false,
|
||||
configFile: false,
|
||||
compact: false,
|
||||
presets: [
|
||||
[
|
||||
require.resolve('babel-preset-react-app/dependencies'),
|
||||
{ helpers: true },
|
||||
],
|
||||
],
|
||||
cacheDirectory: true,
|
||||
// See #6846 for context on why cacheCompression is disabled
|
||||
cacheCompression: false,
|
||||
|
||||
// Babel sourcemaps are needed for debugging into node_modules
|
||||
// code. Without the options below, debuggers like VSCode
|
||||
// show incorrect code and set breakpoints on the wrong lines.
|
||||
sourceMaps: shouldUseSourceMap,
|
||||
inputSourceMap: shouldUseSourceMap,
|
||||
},
|
||||
},
|
||||
// "postcss" loader applies autoprefixer to our CSS.
|
||||
// "css" loader resolves paths in CSS and adds assets as dependencies.
|
||||
// "style" loader turns CSS into JS modules that inject <style> tags.
|
||||
// In production, we use MiniCSSExtractPlugin to extract that CSS
|
||||
// to a file, but in development "style" loader enables hot editing
|
||||
// of CSS.
|
||||
// By default we support CSS Modules with the extension .module.css
|
||||
{
|
||||
test: cssRegex,
|
||||
exclude: cssModuleRegex,
|
||||
use: getStyleLoaders({
|
||||
importLoaders: 1,
|
||||
sourceMap: isEnvProduction
|
||||
? shouldUseSourceMap
|
||||
: isEnvDevelopment,
|
||||
}),
|
||||
// Don't consider CSS imports dead code even if the
|
||||
// containing package claims to have no side effects.
|
||||
// Remove this when webpack adds a warning or an error for this.
|
||||
// See https://github.com/webpack/webpack/issues/6571
|
||||
sideEffects: true,
|
||||
},
|
||||
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
|
||||
// using the extension .module.css
|
||||
{
|
||||
test: cssModuleRegex,
|
||||
use: getStyleLoaders({
|
||||
importLoaders: 1,
|
||||
sourceMap: isEnvProduction
|
||||
? shouldUseSourceMap
|
||||
: isEnvDevelopment,
|
||||
modules: {
|
||||
getLocalIdent: getCSSModuleLocalIdent,
|
||||
},
|
||||
}),
|
||||
},
|
||||
// Opt-in support for SASS (using .scss or .sass extensions).
|
||||
// By default we support SASS Modules with the
|
||||
// extensions .module.scss or .module.sass
|
||||
{
|
||||
test: sassRegex,
|
||||
exclude: sassModuleRegex,
|
||||
use: getStyleLoaders(
|
||||
{
|
||||
importLoaders: 3,
|
||||
sourceMap: isEnvProduction
|
||||
? shouldUseSourceMap
|
||||
: isEnvDevelopment,
|
||||
},
|
||||
'sass-loader'
|
||||
),
|
||||
// Don't consider CSS imports dead code even if the
|
||||
// containing package claims to have no side effects.
|
||||
// Remove this when webpack adds a warning or an error for this.
|
||||
// See https://github.com/webpack/webpack/issues/6571
|
||||
sideEffects: true,
|
||||
},
|
||||
// Adds support for CSS Modules, but using SASS
|
||||
// using the extension .module.scss or .module.sass
|
||||
{
|
||||
test: sassModuleRegex,
|
||||
use: getStyleLoaders(
|
||||
{
|
||||
importLoaders: 3,
|
||||
sourceMap: isEnvProduction
|
||||
? shouldUseSourceMap
|
||||
: isEnvDevelopment,
|
||||
modules: {
|
||||
getLocalIdent: getCSSModuleLocalIdent,
|
||||
},
|
||||
},
|
||||
'sass-loader'
|
||||
),
|
||||
},
|
||||
// "file" loader makes sure those assets get served by WebpackDevServer.
|
||||
// When you `import` an asset, you get its (virtual) filename.
|
||||
// In production, they would get copied to the `build` folder.
|
||||
// This loader doesn't use a "test" so it will catch all modules
|
||||
// that fall through the other loaders.
|
||||
{
|
||||
loader: require.resolve('file-loader'),
|
||||
// Exclude `js` files to keep "css" loader working as it injects
|
||||
// its runtime that would otherwise be processed through "file" loader.
|
||||
// Also exclude `html` and `json` extensions so they get processed
|
||||
// by webpacks internal loaders.
|
||||
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
|
||||
options: {
|
||||
name: 'static/media/[name].[hash:8].[ext]',
|
||||
},
|
||||
},
|
||||
// ** STOP ** Are you adding a new loader?
|
||||
// Make sure to add the new loader(s) before the "file" loader.
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
// Generates an `index.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin(
|
||||
Object.assign(
|
||||
{},
|
||||
{
|
||||
inject: true,
|
||||
template: paths.appHtml,
|
||||
},
|
||||
isEnvProduction
|
||||
? {
|
||||
minify: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
removeRedundantAttributes: true,
|
||||
useShortDoctype: true,
|
||||
removeEmptyAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
keepClosingSlash: true,
|
||||
minifyJS: true,
|
||||
minifyCSS: true,
|
||||
minifyURLs: true,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
),
|
||||
// Inlines the webpack runtime script. This script is too small to warrant
|
||||
// a network request.
|
||||
// https://github.com/facebook/create-react-app/issues/5358
|
||||
isEnvProduction &&
|
||||
shouldInlineRuntimeChunk &&
|
||||
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
|
||||
// Makes some environment variables available in index.html.
|
||||
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
|
||||
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// It will be an empty string unless you specify "homepage"
|
||||
// in `package.json`, in which case it will be the pathname of that URL.
|
||||
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
|
||||
// This gives some necessary context to module not found errors, such as
|
||||
// the requesting resource.
|
||||
new ModuleNotFoundPlugin(paths.appPath),
|
||||
// Makes some environment variables available to the JS code, for example:
|
||||
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
|
||||
// It is absolutely essential that NODE_ENV is set to production
|
||||
// during a production build.
|
||||
// Otherwise React will be compiled in the very slow development mode.
|
||||
new webpack.DefinePlugin(env.stringified),
|
||||
// This is necessary to emit hot updates (CSS and Fast Refresh):
|
||||
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
|
||||
// Experimental hot reloading for React .
|
||||
// https://github.com/facebook/react/tree/master/packages/react-refresh
|
||||
isEnvDevelopment &&
|
||||
shouldUseReactRefresh &&
|
||||
new ReactRefreshWebpackPlugin({
|
||||
overlay: {
|
||||
entry: webpackDevClientEntry,
|
||||
// The expected exports are slightly different from what the overlay exports,
|
||||
// so an interop is included here to enable feedback on module-level errors.
|
||||
module: reactRefreshOverlayEntry,
|
||||
// Since we ship a custom dev client and overlay integration,
|
||||
// the bundled socket handling logic can be eliminated.
|
||||
sockIntegration: false,
|
||||
},
|
||||
}),
|
||||
// Watcher doesn't work well if you mistype casing in a path so we use
|
||||
// a plugin that prints an error when you attempt to do this.
|
||||
// See https://github.com/facebook/create-react-app/issues/240
|
||||
isEnvDevelopment && new CaseSensitivePathsPlugin(),
|
||||
// If you require a missing module and then `npm install` it, you still have
|
||||
// to restart the development server for webpack to discover it. This plugin
|
||||
// makes the discovery automatic so you don't have to restart.
|
||||
// See https://github.com/facebook/create-react-app/issues/186
|
||||
isEnvDevelopment &&
|
||||
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
|
||||
isEnvProduction &&
|
||||
new MiniCssExtractPlugin({
|
||||
// Options similar to the same options in webpackOptions.output
|
||||
// both options are optional
|
||||
filename: 'static/css/[name].[contenthash:8].css',
|
||||
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
|
||||
}),
|
||||
// Generate an asset manifest file with the following content:
|
||||
// - "files" key: Mapping of all asset filenames to their corresponding
|
||||
// output file so that tools can pick it up without having to parse
|
||||
// `index.html`
|
||||
// - "entrypoints" key: Array of files which are included in `index.html`,
|
||||
// can be used to reconstruct the HTML if necessary
|
||||
new ManifestPlugin({
|
||||
fileName: 'asset-manifest.json',
|
||||
publicPath: paths.publicUrlOrPath,
|
||||
generate: (seed, files, entrypoints) => {
|
||||
const manifestFiles = files.reduce((manifest, file) => {
|
||||
manifest[file.name] = file.path;
|
||||
return manifest;
|
||||
}, seed);
|
||||
const entrypointFiles = entrypoints.main.filter(
|
||||
fileName => !fileName.endsWith('.map')
|
||||
);
|
||||
|
||||
return {
|
||||
files: manifestFiles,
|
||||
entrypoints: entrypointFiles,
|
||||
};
|
||||
},
|
||||
}),
|
||||
// Moment.js is an extremely popular library that bundles large locale files
|
||||
// by default due to how webpack interprets its code. This is a practical
|
||||
// solution that requires the user to opt into importing specific locales.
|
||||
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
|
||||
// You can remove this if you don't use Moment.js:
|
||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||
// Generate a service worker script that will precache, and keep up to date,
|
||||
// the HTML & assets that are part of the webpack build.
|
||||
isEnvProduction &&
|
||||
fs.existsSync(swSrc) &&
|
||||
new WorkboxWebpackPlugin.InjectManifest({
|
||||
swSrc,
|
||||
dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
|
||||
exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/],
|
||||
// Bump up the default maximum size (2mb) that's precached,
|
||||
// to make lazy-loading failure scenarios less likely.
|
||||
// See https://github.com/cra-template/pwa/issues/13#issuecomment-722667270
|
||||
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
||||
}),
|
||||
// TypeScript type checking
|
||||
useTypeScript &&
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
typescript: resolve.sync('typescript', {
|
||||
basedir: paths.appNodeModules,
|
||||
}),
|
||||
async: isEnvDevelopment,
|
||||
checkSyntacticErrors: true,
|
||||
resolveModuleNameModule: process.versions.pnp
|
||||
? `${__dirname}/pnpTs.js`
|
||||
: undefined,
|
||||
resolveTypeReferenceDirectiveModule: process.versions.pnp
|
||||
? `${__dirname}/pnpTs.js`
|
||||
: undefined,
|
||||
tsconfig: paths.appTsConfig,
|
||||
reportFiles: [
|
||||
// This one is specifically to match during CI tests,
|
||||
// as micromatch doesn't match
|
||||
// '../cra-template-typescript/template/src/App.tsx'
|
||||
// otherwise.
|
||||
'../**/src/**/*.{ts,tsx}',
|
||||
'**/src/**/*.{ts,tsx}',
|
||||
'!**/src/**/__tests__/**',
|
||||
'!**/src/**/?(*.)(spec|test).*',
|
||||
'!**/src/setupProxy.*',
|
||||
'!**/src/setupTests.*',
|
||||
],
|
||||
silent: true,
|
||||
// The formatter is invoked directly in WebpackDevServerUtils during development
|
||||
formatter: isEnvProduction ? typescriptFormatter : undefined,
|
||||
}),
|
||||
new ESLintPlugin({
|
||||
// Plugin options
|
||||
extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'],
|
||||
formatter: require.resolve('react-dev-utils/eslintFormatter'),
|
||||
eslintPath: require.resolve('eslint'),
|
||||
context: paths.appSrc,
|
||||
cache: true,
|
||||
// ESLint class options
|
||||
cwd: paths.appPath,
|
||||
resolvePluginsRelativeTo: __dirname,
|
||||
baseConfig: {
|
||||
extends: [require.resolve('eslint-config-react-app/base')],
|
||||
rules: {
|
||||
...(!hasJsxRuntime && {
|
||||
'react/react-in-jsx-scope': 'error',
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
].filter(Boolean),
|
||||
// Some libraries import Node modules but don't use them in the browser.
|
||||
// Tell webpack to provide empty mocks for them so importing them works.
|
||||
node: {
|
||||
module: 'empty',
|
||||
dgram: 'empty',
|
||||
dns: 'mock',
|
||||
fs: 'empty',
|
||||
http2: 'empty',
|
||||
net: 'empty',
|
||||
tls: 'empty',
|
||||
child_process: 'empty',
|
||||
},
|
||||
// Turn off performance processing because we utilize
|
||||
// our own hints via the FileSizeReporter
|
||||
performance: false,
|
||||
};
|
||||
};
|
||||
130
config/webpackDevServer.config.js
Normal file
130
config/webpackDevServer.config.js
Normal file
@@ -0,0 +1,130 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware');
|
||||
const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware');
|
||||
const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware');
|
||||
const ignoredFiles = require('react-dev-utils/ignoredFiles');
|
||||
const redirectServedPath = require('react-dev-utils/redirectServedPathMiddleware');
|
||||
const paths = require('./paths');
|
||||
const getHttpsConfig = require('./getHttpsConfig');
|
||||
|
||||
const host = process.env.HOST || '0.0.0.0';
|
||||
const sockHost = process.env.WDS_SOCKET_HOST;
|
||||
const sockPath = process.env.WDS_SOCKET_PATH; // default: '/sockjs-node'
|
||||
const sockPort = process.env.WDS_SOCKET_PORT;
|
||||
|
||||
module.exports = function (proxy, allowedHost) {
|
||||
return {
|
||||
// WebpackDevServer 2.4.3 introduced a security fix that prevents remote
|
||||
// websites from potentially accessing local content through DNS rebinding:
|
||||
// https://github.com/webpack/webpack-dev-server/issues/887
|
||||
// https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
|
||||
// However, it made several existing use cases such as development in cloud
|
||||
// environment or subdomains in development significantly more complicated:
|
||||
// https://github.com/facebook/create-react-app/issues/2271
|
||||
// https://github.com/facebook/create-react-app/issues/2233
|
||||
// While we're investigating better solutions, for now we will take a
|
||||
// compromise. Since our WDS configuration only serves files in the `public`
|
||||
// folder we won't consider accessing them a vulnerability. However, if you
|
||||
// use the `proxy` feature, it gets more dangerous because it can expose
|
||||
// remote code execution vulnerabilities in backends like Django and Rails.
|
||||
// So we will disable the host check normally, but enable it if you have
|
||||
// specified the `proxy` setting. Finally, we let you override it if you
|
||||
// really know what you're doing with a special environment variable.
|
||||
disableHostCheck:
|
||||
!proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true',
|
||||
// Enable gzip compression of generated files.
|
||||
compress: true,
|
||||
// Silence WebpackDevServer's own logs since they're generally not useful.
|
||||
// It will still show compile warnings and errors with this setting.
|
||||
clientLogLevel: 'none',
|
||||
// By default WebpackDevServer serves physical files from current directory
|
||||
// in addition to all the virtual build products that it serves from memory.
|
||||
// This is confusing because those files won’t automatically be available in
|
||||
// production build folder unless we copy them. However, copying the whole
|
||||
// project directory is dangerous because we may expose sensitive files.
|
||||
// Instead, we establish a convention that only files in `public` directory
|
||||
// get served. Our build script will copy `public` into the `build` folder.
|
||||
// In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
|
||||
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
|
||||
// Note that we only recommend to use `public` folder as an escape hatch
|
||||
// for files like `favicon.ico`, `manifest.json`, and libraries that are
|
||||
// for some reason broken when imported through webpack. If you just want to
|
||||
// use an image, put it in `src` and `import` it from JavaScript instead.
|
||||
contentBase: paths.appPublic,
|
||||
contentBasePublicPath: paths.publicUrlOrPath,
|
||||
// By default files from `contentBase` will not trigger a page reload.
|
||||
watchContentBase: true,
|
||||
// Enable hot reloading server. It will provide WDS_SOCKET_PATH endpoint
|
||||
// for the WebpackDevServer client so it can learn when the files were
|
||||
// updated. The WebpackDevServer client is included as an entry point
|
||||
// in the webpack development configuration. Note that only changes
|
||||
// to CSS are currently hot reloaded. JS changes will refresh the browser.
|
||||
hot: true,
|
||||
// Use 'ws' instead of 'sockjs-node' on server since we're using native
|
||||
// websockets in `webpackHotDevClient`.
|
||||
transportMode: 'ws',
|
||||
// Prevent a WS client from getting injected as we're already including
|
||||
// `webpackHotDevClient`.
|
||||
injectClient: false,
|
||||
// Enable custom sockjs pathname for websocket connection to hot reloading server.
|
||||
// Enable custom sockjs hostname, pathname and port for websocket connection
|
||||
// to hot reloading server.
|
||||
sockHost,
|
||||
sockPath,
|
||||
sockPort,
|
||||
// It is important to tell WebpackDevServer to use the same "publicPath" path as
|
||||
// we specified in the webpack config. When homepage is '.', default to serving
|
||||
// from the root.
|
||||
// remove last slash so user can land on `/test` instead of `/test/`
|
||||
publicPath: paths.publicUrlOrPath.slice(0, -1),
|
||||
// WebpackDevServer is noisy by default so we emit custom message instead
|
||||
// by listening to the compiler events with `compiler.hooks[...].tap` calls above.
|
||||
quiet: true,
|
||||
// Reportedly, this avoids CPU overload on some systems.
|
||||
// https://github.com/facebook/create-react-app/issues/293
|
||||
// src/node_modules is not ignored to support absolute imports
|
||||
// https://github.com/facebook/create-react-app/issues/1065
|
||||
watchOptions: {
|
||||
ignored: ignoredFiles(paths.appSrc),
|
||||
},
|
||||
https: getHttpsConfig(),
|
||||
host,
|
||||
overlay: false,
|
||||
historyApiFallback: {
|
||||
// Paths with dots should still use the history fallback.
|
||||
// See https://github.com/facebook/create-react-app/issues/387.
|
||||
disableDotRule: true,
|
||||
index: paths.publicUrlOrPath,
|
||||
},
|
||||
public: allowedHost,
|
||||
// `proxy` is run between `before` and `after` `webpack-dev-server` hooks
|
||||
proxy,
|
||||
before(app, server) {
|
||||
// Keep `evalSourceMapMiddleware` and `errorOverlayMiddleware`
|
||||
// middlewares before `redirectServedPath` otherwise will not have any effect
|
||||
// This lets us fetch source contents from webpack for the error overlay
|
||||
app.use(evalSourceMapMiddleware(server));
|
||||
// This lets us open files from the runtime error overlay.
|
||||
app.use(errorOverlayMiddleware());
|
||||
|
||||
if (fs.existsSync(paths.proxySetup)) {
|
||||
// This registers user provided middleware for proxy reasons
|
||||
require(paths.proxySetup)(app);
|
||||
}
|
||||
},
|
||||
after(app) {
|
||||
// Redirect to `PUBLIC_URL` or `homepage` from `package.json` if url not match
|
||||
app.use(redirectServedPath(paths.publicUrlOrPath));
|
||||
|
||||
// This service worker file is effectively a 'no-op' that will reset any
|
||||
// previous service worker registered for the same host:port combination.
|
||||
// We do this in development to avoid hitting the production cache if
|
||||
// it used the same host and port.
|
||||
// https://github.com/facebook/create-react-app/issues/2272#issuecomment-302832432
|
||||
app.use(noopServiceWorkerMiddleware(paths.publicUrlOrPath));
|
||||
},
|
||||
};
|
||||
};
|
||||
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>
|
||||
182
package.json
182
package.json
@@ -5,59 +5,195 @@
|
||||
"homepage": "",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"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",
|
||||
"dev": "node scripts/start.js",
|
||||
"build": "NODE_ENV=production node scripts/build.js",
|
||||
"dev:wsl": "HOST=0.0.0.0 npm run dev",
|
||||
"test": "node scripts/test.js --silent",
|
||||
"test:ava": "NODE_ENV=production ava --verbose",
|
||||
"test-watch": "ava --watch",
|
||||
"lint": "standard \"src/**/*.js\" \"src/**/*.jsx\" \"test/**/*.js\"",
|
||||
"lint:fix": "npm run lint -- --fix"
|
||||
"lint": "echo 'This is a placeholder for Travis'",
|
||||
"lint:fix": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@forensic-architecture/design-system": "0.6.1",
|
||||
"@babel/core": "7.12.3",
|
||||
"@forensic-architecture/design-system": "0.6.2",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "0.4.2",
|
||||
"@svgr/webpack": "5.4.0",
|
||||
"@testing-library/jest-dom": "^5.11.6",
|
||||
"@testing-library/react": "^11.2.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.5.0",
|
||||
"@typescript-eslint/parser": "^4.5.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^26.6.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"babel-plugin-named-asset-import": "^0.3.7",
|
||||
"babel-preset-react-app": "^10.0.0",
|
||||
"bfj": "^7.0.2",
|
||||
"camelcase": "^6.1.0",
|
||||
"case-sensitive-paths-webpack-plugin": "2.3.0",
|
||||
"css-loader": "4.3.0",
|
||||
"d3": "^5.7.0",
|
||||
"dotenv": "8.2.0",
|
||||
"dotenv-expand": "5.1.0",
|
||||
"eslint": "^7.11.0",
|
||||
"eslint-config-react-app": "^6.0.0",
|
||||
"eslint-plugin-flowtype": "^5.2.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jest": "^24.1.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.3.1",
|
||||
"eslint-plugin-react": "^7.21.5",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"eslint-plugin-testing-library": "^3.9.2",
|
||||
"eslint-webpack-plugin": "^2.1.0",
|
||||
"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",
|
||||
"jest-resolve": "26.6.0",
|
||||
"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",
|
||||
"object-hash": "^1.3.0",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.4",
|
||||
"pnp-webpack-plugin": "1.6.4",
|
||||
"postcss-flexbugs-fixes": "4.2.1",
|
||||
"postcss-loader": "3.0.0",
|
||||
"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",
|
||||
"react-app-polyfill": "^2.0.0",
|
||||
"react-dev-utils": "^11.0.1",
|
||||
"react-device-detect": "^1.6.2",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-image": "^1.5.1",
|
||||
"react-portal": "^4.2.0",
|
||||
"react-redux": "^5.0.4",
|
||||
"react-refresh": "^0.8.3",
|
||||
"react-tabs": "3.0.0",
|
||||
"redux": "^3.6.0",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"reselect": "^3.0.1",
|
||||
"resolve": "1.18.1",
|
||||
"resolve-url-loader": "^3.1.2",
|
||||
"sass-loader": "8.0.2",
|
||||
"semver": "7.3.2",
|
||||
"style-loader": "1.3.0",
|
||||
"supercluster": "^7.1.0",
|
||||
"video-react": "^0.13.1"
|
||||
"terser-webpack-plugin": "4.2.3",
|
||||
"ts-pnp": "1.2.0",
|
||||
"url-loader": "4.1.1",
|
||||
"video-react": "^0.13.1",
|
||||
"webpack": "4.44.2",
|
||||
"webpack-dev-server": "3.11.0",
|
||||
"webpack-manifest-plugin": "2.2.0",
|
||||
"workbox-webpack-plugin": "5.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.1.2",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"ava": "1.0.0-beta.8",
|
||||
"babel-loader": "^8.0.4",
|
||||
"css-loader": "^1.0.0",
|
||||
"file-loader": "^2.0.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"mini-css-extract-plugin": "^0.4.4",
|
||||
"mocha": "^5.2.0",
|
||||
"node-sass": "4.13.1",
|
||||
"redux-devtools": "^3.4.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"standard": "^12.0.1",
|
||||
"style-loader": "^0.23.1",
|
||||
"webpack": "^4.20.2",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-dev-server": "^3.1.11"
|
||||
"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"
|
||||
]
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"ava": {
|
||||
"files": [
|
||||
"test/**/*.js"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"roots": [
|
||||
"<rootDir>/src"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.{js,jsx,ts,tsx}",
|
||||
"!src/**/*.d.ts"
|
||||
],
|
||||
"setupFiles": [
|
||||
"react-app-polyfill/jsdom"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/src/setupTests.js",
|
||||
"<rootDir>/config/jest/setEnvVars.js"
|
||||
],
|
||||
"testMatch": [
|
||||
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
|
||||
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
|
||||
],
|
||||
"testEnvironment": "jsdom",
|
||||
"testRunner": "<rootDir>/node_modules/jest-circus/runner.js",
|
||||
"transform": {
|
||||
"^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "<rootDir>/node_modules/babel-jest",
|
||||
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
|
||||
"^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$",
|
||||
"^.+\\.module\\.(css|sass|scss)$"
|
||||
],
|
||||
"modulePaths": [],
|
||||
"moduleNameMapper": {
|
||||
"^react-native$": "react-native-web",
|
||||
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
|
||||
},
|
||||
"moduleFileExtensions": [
|
||||
"web.js",
|
||||
"js",
|
||||
"web.ts",
|
||||
"ts",
|
||||
"web.tsx",
|
||||
"tsx",
|
||||
"json",
|
||||
"web.jsx",
|
||||
"jsx",
|
||||
"node"
|
||||
],
|
||||
"watchPlugins": [
|
||||
"jest-watch-typeahead/filename",
|
||||
"jest-watch-typeahead/testname"
|
||||
],
|
||||
"resetMocks": true
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"react-app"
|
||||
]
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
}
|
||||
}
|
||||
|
||||
44
public/index.html
Normal file
44
public/index.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<!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>
|
||||
212
scripts/build.js
Normal file
212
scripts/build.js
Normal file
@@ -0,0 +1,212 @@
|
||||
'use strict';
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = 'production';
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require('../config/env');
|
||||
|
||||
|
||||
const path = require('path');
|
||||
const chalk = require('react-dev-utils/chalk');
|
||||
const fs = require('fs-extra');
|
||||
const bfj = require('bfj');
|
||||
const webpack = require('webpack');
|
||||
const configFactory = require('../config/webpack.config');
|
||||
const paths = require('../config/paths');
|
||||
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
|
||||
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
|
||||
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
|
||||
const printBuildError = require('react-dev-utils/printBuildError');
|
||||
|
||||
const measureFileSizesBeforeBuild =
|
||||
FileSizeReporter.measureFileSizesBeforeBuild;
|
||||
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
|
||||
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||
|
||||
// These sizes are pretty large. We'll warn for bundles exceeding them.
|
||||
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
|
||||
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
|
||||
|
||||
const isInteractive = process.stdout.isTTY;
|
||||
|
||||
// Warn and crash if required files are missing
|
||||
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const writeStatsJson = argv.indexOf('--stats') !== -1;
|
||||
|
||||
// Generate configuration
|
||||
const config = configFactory('production');
|
||||
|
||||
// We require that you explicitly set browsers and do not fall back to
|
||||
// browserslist defaults.
|
||||
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
||||
checkBrowsers(paths.appPath, isInteractive)
|
||||
.then(() => {
|
||||
// First, read the current file sizes in build directory.
|
||||
// This lets us display how much they changed later.
|
||||
return measureFileSizesBeforeBuild(paths.appBuild);
|
||||
})
|
||||
.then(previousFileSizes => {
|
||||
// Remove all content but keep the directory so that
|
||||
// if you're in it, you don't end up in Trash
|
||||
fs.emptyDirSync(paths.appBuild);
|
||||
// Merge with the public folder
|
||||
copyPublicFolder();
|
||||
// Start the webpack build
|
||||
return build(previousFileSizes);
|
||||
})
|
||||
.then(
|
||||
({ stats, previousFileSizes, warnings }) => {
|
||||
if (warnings.length) {
|
||||
console.log(chalk.yellow('Compiled with warnings.\n'));
|
||||
console.log(warnings.join('\n\n'));
|
||||
console.log(
|
||||
'\nSearch for the ' +
|
||||
chalk.underline(chalk.yellow('keywords')) +
|
||||
' to learn more about each warning.'
|
||||
);
|
||||
console.log(
|
||||
'To ignore, add ' +
|
||||
chalk.cyan('// eslint-disable-next-line') +
|
||||
' to the line before.\n'
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.green('Compiled successfully.\n'));
|
||||
}
|
||||
|
||||
console.log('File sizes after gzip:\n');
|
||||
printFileSizesAfterBuild(
|
||||
stats,
|
||||
previousFileSizes,
|
||||
paths.appBuild,
|
||||
WARN_AFTER_BUNDLE_GZIP_SIZE,
|
||||
WARN_AFTER_CHUNK_GZIP_SIZE
|
||||
);
|
||||
console.log();
|
||||
|
||||
const appPackage = require(paths.appPackageJson);
|
||||
const publicUrl = paths.publicUrlOrPath;
|
||||
const publicPath = config.output.publicPath;
|
||||
const buildFolder = path.relative(process.cwd(), paths.appBuild);
|
||||
printHostingInstructions(
|
||||
appPackage,
|
||||
publicUrl,
|
||||
publicPath,
|
||||
buildFolder,
|
||||
useYarn
|
||||
);
|
||||
},
|
||||
err => {
|
||||
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
|
||||
if (tscCompileOnError) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Compiled with the following type errors (you may want to check these before deploying your app):\n'
|
||||
)
|
||||
);
|
||||
printBuildError(err);
|
||||
} else {
|
||||
console.log(chalk.red('Failed to compile.\n'));
|
||||
printBuildError(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch(err => {
|
||||
if (err && err.message) {
|
||||
console.log(err.message);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Create the production build and print the deployment instructions.
|
||||
function build(previousFileSizes) {
|
||||
console.log('Creating an optimized production build...');
|
||||
|
||||
const compiler = webpack(config);
|
||||
return new Promise((resolve, reject) => {
|
||||
compiler.run((err, stats) => {
|
||||
let messages;
|
||||
if (err) {
|
||||
if (!err.message) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
let errMessage = err.message;
|
||||
|
||||
// Add additional information for postcss errors
|
||||
if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) {
|
||||
errMessage +=
|
||||
'\nCompileError: Begins at CSS selector ' +
|
||||
err['postcssNode'].selector;
|
||||
}
|
||||
|
||||
messages = formatWebpackMessages({
|
||||
errors: [errMessage],
|
||||
warnings: [],
|
||||
});
|
||||
} else {
|
||||
messages = formatWebpackMessages(
|
||||
stats.toJson({ all: false, warnings: true, errors: true })
|
||||
);
|
||||
}
|
||||
if (messages.errors.length) {
|
||||
// Only keep the first error. Others are often indicative
|
||||
// of the same problem, but confuse the reader with noise.
|
||||
if (messages.errors.length > 1) {
|
||||
messages.errors.length = 1;
|
||||
}
|
||||
return reject(new Error(messages.errors.join('\n\n')));
|
||||
}
|
||||
if (
|
||||
process.env.CI &&
|
||||
(typeof process.env.CI !== 'string' ||
|
||||
process.env.CI.toLowerCase() !== 'false') &&
|
||||
messages.warnings.length
|
||||
) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'\nTreating warnings as errors because process.env.CI = true.\n' +
|
||||
'Most CI servers set it automatically.\n'
|
||||
)
|
||||
);
|
||||
return reject(new Error(messages.warnings.join('\n\n')));
|
||||
}
|
||||
|
||||
const resolveArgs = {
|
||||
stats,
|
||||
previousFileSizes,
|
||||
warnings: messages.warnings,
|
||||
};
|
||||
|
||||
if (writeStatsJson) {
|
||||
return bfj
|
||||
.write(paths.appBuild + '/bundle-stats.json', stats.toJson())
|
||||
.then(() => resolve(resolveArgs))
|
||||
.catch(error => reject(new Error(error)));
|
||||
}
|
||||
|
||||
return resolve(resolveArgs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function copyPublicFolder() {
|
||||
fs.copySync(paths.appPublic, paths.appBuild, {
|
||||
dereference: true,
|
||||
filter: file => file !== paths.appHtml,
|
||||
});
|
||||
}
|
||||
166
scripts/start.js
Normal file
166
scripts/start.js
Normal file
@@ -0,0 +1,166 @@
|
||||
'use strict';
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = 'development';
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require('../config/env');
|
||||
|
||||
|
||||
const fs = require('fs');
|
||||
const chalk = require('react-dev-utils/chalk');
|
||||
const webpack = require('webpack');
|
||||
const WebpackDevServer = require('webpack-dev-server');
|
||||
const clearConsole = require('react-dev-utils/clearConsole');
|
||||
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||
const {
|
||||
choosePort,
|
||||
createCompiler,
|
||||
prepareProxy,
|
||||
prepareUrls,
|
||||
} = require('react-dev-utils/WebpackDevServerUtils');
|
||||
const openBrowser = require('react-dev-utils/openBrowser');
|
||||
const semver = require('semver');
|
||||
const paths = require('../config/paths');
|
||||
const configFactory = require('../config/webpack.config');
|
||||
const createDevServerConfig = require('../config/webpackDevServer.config');
|
||||
const getClientEnvironment = require('../config/env');
|
||||
const react = require(require.resolve('react', { paths: [paths.appPath] }));
|
||||
|
||||
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
|
||||
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||
const isInteractive = process.stdout.isTTY;
|
||||
|
||||
// Warn and crash if required files are missing
|
||||
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Tools like Cloud9 rely on this.
|
||||
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 8080;
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
|
||||
if (process.env.HOST) {
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Attempting to bind to HOST environment variable: ${chalk.yellow(
|
||||
chalk.bold(process.env.HOST)
|
||||
)}`
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
`If this was unintentional, check that you haven't mistakenly set it in your shell.`
|
||||
);
|
||||
console.log(
|
||||
`Learn more here: ${chalk.yellow('https://cra.link/advanced-config')}`
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// We require that you explicitly set browsers and do not fall back to
|
||||
// browserslist defaults.
|
||||
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
||||
checkBrowsers(paths.appPath, isInteractive)
|
||||
.then(() => {
|
||||
// We attempt to use the default port but if it is busy, we offer the user to
|
||||
// run on a different port. `choosePort()` Promise resolves to the next free port.
|
||||
return choosePort(HOST, DEFAULT_PORT);
|
||||
})
|
||||
.then(port => {
|
||||
if (port == null) {
|
||||
// We have not found a port.
|
||||
return;
|
||||
}
|
||||
|
||||
const config = configFactory('development');
|
||||
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
|
||||
const appName = require(paths.appPackageJson).name;
|
||||
|
||||
const useTypeScript = fs.existsSync(paths.appTsConfig);
|
||||
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
|
||||
const urls = prepareUrls(
|
||||
protocol,
|
||||
HOST,
|
||||
port,
|
||||
paths.publicUrlOrPath.slice(0, -1)
|
||||
);
|
||||
const devSocket = {
|
||||
warnings: warnings =>
|
||||
devServer.sockWrite(devServer.sockets, 'warnings', warnings),
|
||||
errors: errors =>
|
||||
devServer.sockWrite(devServer.sockets, 'errors', errors),
|
||||
};
|
||||
// Create a webpack compiler that is configured with custom messages.
|
||||
const compiler = createCompiler({
|
||||
appName,
|
||||
config,
|
||||
devSocket,
|
||||
urls,
|
||||
useYarn,
|
||||
useTypeScript,
|
||||
tscCompileOnError,
|
||||
webpack,
|
||||
});
|
||||
// Load proxy config
|
||||
const proxySetting = require(paths.appPackageJson).proxy;
|
||||
const proxyConfig = prepareProxy(
|
||||
proxySetting,
|
||||
paths.appPublic,
|
||||
paths.publicUrlOrPath
|
||||
);
|
||||
// Serve webpack assets generated by the compiler over a web server.
|
||||
const serverConfig = createDevServerConfig(
|
||||
proxyConfig,
|
||||
urls.lanUrlForConfig
|
||||
);
|
||||
const devServer = new WebpackDevServer(compiler, serverConfig);
|
||||
// Launch WebpackDevServer.
|
||||
devServer.listen(port, HOST, err => {
|
||||
if (err) {
|
||||
return console.log(err);
|
||||
}
|
||||
if (isInteractive) {
|
||||
clearConsole();
|
||||
}
|
||||
|
||||
if (env.raw.FAST_REFRESH && semver.lt(react.version, '16.10.0')) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('Starting the development server...\n'));
|
||||
openBrowser(urls.localUrlForBrowser);
|
||||
});
|
||||
|
||||
['SIGINT', 'SIGTERM'].forEach(function (sig) {
|
||||
process.on(sig, function () {
|
||||
devServer.close();
|
||||
process.exit();
|
||||
});
|
||||
});
|
||||
|
||||
if (process.env.CI !== 'true') {
|
||||
// Gracefully exit when stdin ends
|
||||
process.stdin.on('end', function () {
|
||||
devServer.close();
|
||||
process.exit();
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err && err.message) {
|
||||
console.log(err.message);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
53
scripts/test.js
Normal file
53
scripts/test.js
Normal file
@@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = 'test';
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.PUBLIC_URL = '';
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require('../config/env');
|
||||
|
||||
|
||||
const jest = require('jest');
|
||||
const execSync = require('child_process').execSync;
|
||||
let argv = process.argv.slice(2);
|
||||
|
||||
function isInGitRepository() {
|
||||
try {
|
||||
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isInMercurialRepository() {
|
||||
try {
|
||||
execSync('hg --cwd . root', { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Watch unless on CI or explicitly running all tests
|
||||
if (
|
||||
!process.env.CI &&
|
||||
argv.indexOf('--watchAll') === -1 &&
|
||||
argv.indexOf('--watchAll=false') === -1
|
||||
) {
|
||||
// https://github.com/facebook/create-react-app/issues/5210
|
||||
const hasSourceControl = isInGitRepository() || isInMercurialRepository();
|
||||
argv.push(hasSourceControl ? '--watch' : '--watchAll');
|
||||
}
|
||||
|
||||
|
||||
jest.run(argv);
|
||||
@@ -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,37 +0,0 @@
|
||||
import copy from './data/copy.json'
|
||||
import { language } from './utilities'
|
||||
|
||||
const cardStack = copy[language].cardstack
|
||||
|
||||
// Sensible defaults for generating a basic card layout
|
||||
// based on the example Timemap datasheet.
|
||||
|
||||
const basic = ({ event }) => {
|
||||
return [
|
||||
[
|
||||
{
|
||||
kind: 'date',
|
||||
title: cardStack['date_title'] || 'Incident Dates',
|
||||
value: event.datetime || event.date || ``
|
||||
},
|
||||
{
|
||||
kind: 'text',
|
||||
title: cardStack['location_title'] || 'Location',
|
||||
value: event.location || `—`
|
||||
}
|
||||
],
|
||||
[{ kind: 'line-break', times: 0.4 }],
|
||||
[
|
||||
{
|
||||
kind: 'text',
|
||||
title: cardStack['summary_title'] || 'Summary',
|
||||
value: event.description || ``,
|
||||
scaleFont: 1.1
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
export default {
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import moment from 'moment'
|
||||
import hash from 'object-hash'
|
||||
import moment from "moment";
|
||||
import hash from "object-hash";
|
||||
|
||||
let { DATE_FMT, TIME_FMT } = process.env
|
||||
if (!DATE_FMT) DATE_FMT = 'MM/DD/YYYY'
|
||||
if (!TIME_FMT) TIME_FMT = 'HH:mm'
|
||||
let { DATE_FMT, TIME_FMT } = process.env;
|
||||
if (!DATE_FMT) DATE_FMT = "MM/DD/YYYY";
|
||||
if (!TIME_FMT) TIME_FMT = "HH:mm";
|
||||
|
||||
export const language = process.env.store.app.language || 'en-US'
|
||||
export const language = process.env.store.app.language || "en-US";
|
||||
|
||||
export function calcDatetime (date, time) {
|
||||
if (!time) time = '00:00'
|
||||
const dt = moment(`${date} ${time}`, `${DATE_FMT} ${TIME_FMT}`)
|
||||
return dt.toDate()
|
||||
export function calcDatetime(date, time) {
|
||||
if (!time) time = "00:00";
|
||||
const dt = moment(`${date} ${time}`, `${DATE_FMT} ${TIME_FMT}`);
|
||||
return dt.toDate();
|
||||
}
|
||||
|
||||
export function getCoordinatesForPercent (radius, percent) {
|
||||
const x = radius * Math.cos(2 * Math.PI * percent)
|
||||
const y = radius * Math.sin(2 * Math.PI * percent)
|
||||
return [x, y]
|
||||
export function getCoordinatesForPercent(radius, percent) {
|
||||
const x = radius * Math.cos(2 * Math.PI * percent);
|
||||
const y = radius * Math.sin(2 * Math.PI * percent);
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,32 +26,34 @@ export function getCoordinatesForPercent (radius, percent) {
|
||||
*
|
||||
* Return value:
|
||||
* ex. {'#fff': 0.5, '#000': 0.5, ...} */
|
||||
export function zipColorsToPercentages (colors, percentages) {
|
||||
if (colors.length < percentages.length) throw new Error('You must declare an appropriate number of filter colors')
|
||||
export function zipColorsToPercentages(colors, percentages) {
|
||||
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
|
||||
return map
|
||||
}, {})
|
||||
map[colors[idx]] = percent;
|
||||
return map;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URI params to start with predefined set of
|
||||
* https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
|
||||
* @param {string} name: name of paramater to search
|
||||
* @param {string} url: url passed as variable, defaults to window.location.href
|
||||
*/
|
||||
export function getParameterByName (name, url) {
|
||||
if (!url) url = window.location.href
|
||||
name = name.replace(/[[\]]/g, `\\$&`)
|
||||
* Get URI params to start with predefined set of
|
||||
* https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
|
||||
* @param {string} name: name of paramater to search
|
||||
* @param {string} url: url passed as variable, defaults to window.location.href
|
||||
*/
|
||||
export function getParameterByName(name, url) {
|
||||
if (!url) url = window.location.href;
|
||||
name = name.replace(/[[\]]/g, "\\$&");
|
||||
|
||||
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`)
|
||||
const results = regex.exec(url)
|
||||
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
|
||||
const results = regex.exec(url);
|
||||
|
||||
if (!results) return null
|
||||
if (!results[2]) return ''
|
||||
if (!results) return null;
|
||||
if (!results[2]) return "";
|
||||
|
||||
return decodeURIComponent(results[2].replace(/\+/g, ' '))
|
||||
return decodeURIComponent(results[2].replace(/\+/g, " "));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,32 +61,35 @@ export function getParameterByName (name, url) {
|
||||
* @param {array} arr1: array of numbers
|
||||
* @param {array} arr2: array of numbers
|
||||
*/
|
||||
export function areEqual (arr1, arr2) {
|
||||
return ((arr1.length === arr2.length) && arr1.every((element, index) => {
|
||||
return element === arr2[index]
|
||||
}))
|
||||
export function areEqual(arr1, arr2) {
|
||||
return (
|
||||
arr1.length === arr2.length &&
|
||||
arr1.every((element, index) => {
|
||||
return element === arr2[index];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the variable is neither null nor undefined
|
||||
* @param {object} variable
|
||||
*/
|
||||
export function isNotNullNorUndefined (variable) {
|
||||
return (typeof variable !== 'undefined' && variable !== null)
|
||||
* Return whether the variable is neither null nor undefined
|
||||
* @param {object} variable
|
||||
*/
|
||||
export function isNotNullNorUndefined(variable) {
|
||||
return typeof variable !== "undefined" && variable !== null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Taken from: https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript
|
||||
*/
|
||||
export function capitalize (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||
* Taken from: https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript
|
||||
*/
|
||||
export function capitalize(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
export function trimAndEllipse (string, stringNum) {
|
||||
export function trimAndEllipse(string, stringNum) {
|
||||
if (string.length > stringNum) {
|
||||
return string.substring(0, 120) + '...'
|
||||
return string.substring(0, 120) + "...";
|
||||
}
|
||||
return string
|
||||
return string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,71 +99,72 @@ export function trimAndEllipse (string, stringNum) {
|
||||
* through every association's given path attribute to find its location.
|
||||
*
|
||||
* Returns the list of parents: ex. ['Chemical', 'Tear Gas', ...]
|
||||
*/
|
||||
export function getFilterParents (associations, filter) {
|
||||
for (let a of associations) {
|
||||
const { filter_paths: fp } = a
|
||||
*/
|
||||
export function getFilterParents(associations, filter) {
|
||||
for (const a of associations) {
|
||||
const { filter_paths: fp } = a;
|
||||
if (a.id === filter) {
|
||||
return fp.slice(0, fp.length - 1)
|
||||
return fp.slice(0, fp.length - 1);
|
||||
}
|
||||
const filterIndex = fp.indexOf(filter)
|
||||
if (filterIndex === 0) return []
|
||||
if (filterIndex > 0) return fp.slice(0, filterIndex)
|
||||
const filterIndex = fp.indexOf(filter);
|
||||
if (filterIndex === 0) return [];
|
||||
if (filterIndex > 0) return fp.slice(0, filterIndex);
|
||||
}
|
||||
throw new Error('Attempted to get parents of nonexistent filter')
|
||||
throw new Error("Attempted to get parents of nonexistent filter");
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs the second to last element in the paths array for a given existing filter.
|
||||
* This is the filter's most immediate ancestor.
|
||||
*/
|
||||
export function getImmediateFilterParent (associations, filter) {
|
||||
const parents = getFilterParents(associations, filter)
|
||||
if (parents.length === 0) return null
|
||||
return parents[parents.length - 1]
|
||||
*/
|
||||
export function getImmediateFilterParent(associations, filter) {
|
||||
const parents = getFilterParents(associations, filter);
|
||||
if (parents.length === 0) return null;
|
||||
return parents[parents.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Grab a meta filter's siblings, by way of the the `filter_path` hierarcy.
|
||||
*/
|
||||
export function getMetaFilterSiblings (allFilters, filterParent, filterKey) {
|
||||
const idxParent = allFilters.map(f => {
|
||||
return f.filter_paths.reduceRight((acc, path, idx) => {
|
||||
if (path === filterParent) return f.filter_paths[idx + 1]
|
||||
return acc
|
||||
}, null)
|
||||
})
|
||||
.filter(metaFilter => !!metaFilter && metaFilter !== filterKey)
|
||||
return [ ...(new Set(idxParent)) ]
|
||||
*/
|
||||
export function getMetaFilterSiblings(allFilters, filterParent, filterKey) {
|
||||
const idxParent = allFilters
|
||||
.map((f) => {
|
||||
return f.filter_paths.reduceRight((acc, path, idx) => {
|
||||
if (path === filterParent) return f.filter_paths[idx + 1];
|
||||
return acc;
|
||||
}, null);
|
||||
})
|
||||
.filter((metaFilter) => !!metaFilter && metaFilter !== filterKey);
|
||||
return [...new Set(idxParent)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs a given filter's siblings: the set of associations that share the same immediate filter parent.
|
||||
*/
|
||||
export function getFilterSiblings (allFilters, filterParent, filterKey) {
|
||||
const isMetaFilter = !allFilters.map(filt => filt.id).includes(filterKey)
|
||||
*/
|
||||
export function getFilterSiblings(allFilters, filterParent, filterKey) {
|
||||
const isMetaFilter = !allFilters.map((filt) => filt.id).includes(filterKey);
|
||||
|
||||
if (isMetaFilter) {
|
||||
return getMetaFilterSiblings(allFilters, filterParent, filterKey)
|
||||
return getMetaFilterSiblings(allFilters, filterParent, filterKey);
|
||||
}
|
||||
|
||||
return allFilters.reduce((acc, val) => {
|
||||
const valParent = getImmediateFilterParent(allFilters, val.id)
|
||||
if (valParent === filterParent && val.id !== filterKey) acc.push(val.id)
|
||||
return acc
|
||||
}, [])
|
||||
const valParent = getImmediateFilterParent(allFilters, val.id);
|
||||
if (valParent === filterParent && val.id !== filterKey) acc.push(val.id);
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function getEventCategories (event, categories) {
|
||||
const matchedCategories = []
|
||||
export function getEventCategories(event, categories) {
|
||||
const matchedCategories = [];
|
||||
if (event.associations && event.associations.length > 0) {
|
||||
event.associations.reduce((acc, val) => {
|
||||
const foundCategory = categories.find(cat => cat.id === val)
|
||||
if (foundCategory) acc.push(foundCategory)
|
||||
return acc
|
||||
}, matchedCategories)
|
||||
const foundCategory = categories.find((cat) => cat.id === val);
|
||||
if (foundCategory) acc.push(foundCategory);
|
||||
return acc;
|
||||
}, matchedCategories);
|
||||
}
|
||||
return matchedCategories
|
||||
return matchedCategories;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,186 +173,201 @@ export function getEventCategories (event, categories) {
|
||||
* source, call with two sets of parentheses:
|
||||
* const src = insetSourceFrom(sources)(anEvent)
|
||||
*/
|
||||
export function insetSourceFrom (allSources) {
|
||||
export function insetSourceFrom(allSources) {
|
||||
return (event) => {
|
||||
let sources
|
||||
let sources;
|
||||
if (!event.sources) {
|
||||
sources = []
|
||||
sources = [];
|
||||
} else {
|
||||
sources = event.sources.map(id => {
|
||||
return allSources.hasOwnProperty(id) ? allSources[id] : null
|
||||
})
|
||||
sources = event.sources.map((id) => {
|
||||
return allSources.hasOwnProperty(id) ? allSources[id] : null;
|
||||
});
|
||||
}
|
||||
return {
|
||||
...event,
|
||||
sources
|
||||
}
|
||||
}
|
||||
sources,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Debugging function: put in place of a mapStateToProps function to
|
||||
* view that source modal by default
|
||||
*/
|
||||
export function injectSource (id) {
|
||||
return state => {
|
||||
export function injectSource(id) {
|
||||
return (state) => {
|
||||
return {
|
||||
...state,
|
||||
app: {
|
||||
...state.app,
|
||||
source: state.domain.sources[id]
|
||||
}
|
||||
}
|
||||
}
|
||||
source: state.domain.sources[id],
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function urlFromEnv (ext) {
|
||||
export function urlFromEnv(ext) {
|
||||
if (process.env[ext]) {
|
||||
if (!Array.isArray(process.env[ext])) { return [`${process.env.SERVER_ROOT}${process.env[ext]}`] } else {
|
||||
return process.env[ext].map(suffix => `${process.env.SERVER_ROOT}${suffix}`)
|
||||
if (!Array.isArray(process.env[ext])) {
|
||||
return [`${process.env.SERVER_ROOT}${process.env[ext]}`];
|
||||
} else {
|
||||
return process.env[ext].map(
|
||||
(suffix) => `${process.env.SERVER_ROOT}${suffix}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleFlagAC (flag) {
|
||||
export function toggleFlagAC(flag) {
|
||||
return (appState) => ({
|
||||
...appState,
|
||||
flags: {
|
||||
...appState.flags,
|
||||
[flag]: !appState.flags[flag]
|
||||
}
|
||||
})
|
||||
[flag]: !appState.flags[flag],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function selectTypeFromPath (path) {
|
||||
let type
|
||||
export function selectTypeFromPath(path) {
|
||||
let type;
|
||||
switch (true) {
|
||||
case /\.(png|jpg)$/.test(path):
|
||||
type = 'Image'; break
|
||||
type = "Image";
|
||||
break;
|
||||
case /\.(mp4)$/.test(path):
|
||||
type = 'Video'; break
|
||||
type = "Video";
|
||||
break;
|
||||
case /\.(md)$/.test(path):
|
||||
type = 'Text'; break
|
||||
type = "Text";
|
||||
break;
|
||||
default:
|
||||
type = 'Unknown'; break
|
||||
type = "Unknown";
|
||||
break;
|
||||
}
|
||||
return { type, path }
|
||||
return { type, path };
|
||||
}
|
||||
|
||||
export function typeForPath (path) {
|
||||
let type
|
||||
path = path.trim()
|
||||
export function typeForPath(path) {
|
||||
let type;
|
||||
path = path.trim();
|
||||
switch (true) {
|
||||
case /\.((png)|(jpg)|(jpeg))$/.test(path):
|
||||
type = 'Image'; break
|
||||
type = "Image";
|
||||
break;
|
||||
case /\.(mp4)$/.test(path):
|
||||
type = 'Video'; break
|
||||
type = "Video";
|
||||
break;
|
||||
case /\.(md)$/.test(path):
|
||||
type = 'Text'; break
|
||||
type = "Text";
|
||||
break;
|
||||
case /\.(pdf)$/.test(path):
|
||||
type = 'Document'; break
|
||||
type = "Document";
|
||||
break;
|
||||
default:
|
||||
type = 'Unknown'; break
|
||||
type = "Unknown";
|
||||
break;
|
||||
}
|
||||
return type
|
||||
return type;
|
||||
}
|
||||
|
||||
export function selectTypeFromPathWithPoster (path, poster) {
|
||||
return { type: typeForPath(path), path, poster }
|
||||
export function selectTypeFromPathWithPoster(path, poster) {
|
||||
return { type: typeForPath(path), path, poster };
|
||||
}
|
||||
|
||||
export function isIdentical (obj1, obj2) {
|
||||
return hash(obj1) === hash(obj2)
|
||||
export function isIdentical(obj1, obj2) {
|
||||
return hash(obj1) === hash(obj2);
|
||||
}
|
||||
|
||||
export function calcOpacity (num) {
|
||||
export function calcOpacity(num) {
|
||||
/* Events have opacity 0.5 by default, and get added to according to how many
|
||||
* other events there are in the same render. The idea here is that the
|
||||
* overlaying of events builds up a 'heat map' of the event space, where
|
||||
* darker areas represent more events with proportion */
|
||||
const base = num >= 1 ? 0.9 : 0
|
||||
return base + (Math.min(0.5, 0.08 * (num - 1)))
|
||||
const base = num >= 1 ? 0.9 : 0;
|
||||
return base + Math.min(0.5, 0.08 * (num - 1));
|
||||
}
|
||||
|
||||
export function calcClusterOpacity (pointCount, totalPoints) {
|
||||
export function calcClusterOpacity(pointCount, totalPoints) {
|
||||
/* Clusters represent multiple events within a specific radius. The darker the cluster,
|
||||
the larger the number of underlying events. We use a multiplication factor (50) here as well
|
||||
to ensure that the larger clusters have an appropriately darker shading. */
|
||||
return Math.min(0.85, 0.08 + (pointCount / totalPoints) * 50)
|
||||
return Math.min(0.85, 0.08 + (pointCount / totalPoints) * 50);
|
||||
}
|
||||
|
||||
export function calcClusterSize (pointCount, totalPoints) {
|
||||
export function calcClusterSize(pointCount, totalPoints) {
|
||||
/* The larger the cluster size, the higher the count of points that the cluster represents.
|
||||
Just like with opacity, we use a multiplication factor to ensure that clusters with higher point
|
||||
counts appear larger. */
|
||||
const maxSize = totalPoints > 60 ? 40 : 20
|
||||
return Math.min(maxSize, 10 + (pointCount / totalPoints) * 150)
|
||||
const maxSize = totalPoints > 60 ? 40 : 20;
|
||||
return Math.min(maxSize, 10 + (pointCount / totalPoints) * 150);
|
||||
}
|
||||
|
||||
export function calculateTotalClusterPoints (clusters) {
|
||||
export function calculateTotalClusterPoints(clusters) {
|
||||
return clusters.reduce((total, cl) => {
|
||||
if (cl && cl.properties && cl.properties.cluster) {
|
||||
total += cl.properties.point_count
|
||||
total += cl.properties.point_count;
|
||||
}
|
||||
return total
|
||||
}, 0)
|
||||
return total;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
export function isLatitude (lat) {
|
||||
return !!lat && isFinite(lat) && Math.abs(lat) <= 90
|
||||
export function isLatitude(lat) {
|
||||
return !!lat && isFinite(lat) && Math.abs(lat) <= 90;
|
||||
}
|
||||
|
||||
export function isLongitude (lng) {
|
||||
return !!lng && isFinite(lng) && Math.abs(lng) <= 180
|
||||
export function isLongitude(lng) {
|
||||
return !!lng && isFinite(lng) && Math.abs(lng) <= 180;
|
||||
}
|
||||
|
||||
export function mapClustersToLocations (clusters, locations) {
|
||||
export function mapClustersToLocations(clusters, locations) {
|
||||
return clusters.reduce((acc, cl) => {
|
||||
const foundLocation = locations.find(location => location.label === cl.properties.id)
|
||||
if (foundLocation) acc.push(foundLocation)
|
||||
return acc
|
||||
}, [])
|
||||
const foundLocation = locations.find(
|
||||
(location) => location.label === cl.properties.id
|
||||
);
|
||||
if (foundLocation) acc.push(foundLocation);
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loops through a set of either locations or events
|
||||
* and calculates the proportionate percentage of every given association in relation to the coloring set
|
||||
*/
|
||||
export function calculateColorPercentages (set, coloringSet) {
|
||||
if (coloringSet.length === 0) return [1]
|
||||
const associationMap = {}
|
||||
*/
|
||||
export function calculateColorPercentages(set, coloringSet) {
|
||||
if (coloringSet.length === 0) return [1];
|
||||
const associationMap = {};
|
||||
|
||||
for (const [idx, value] of coloringSet.entries()) {
|
||||
for (let filter of value) {
|
||||
associationMap[filter] = idx
|
||||
for (const filter of value) {
|
||||
associationMap[filter] = idx;
|
||||
}
|
||||
}
|
||||
|
||||
const associationCounts = new Array(coloringSet.length)
|
||||
associationCounts.fill(0)
|
||||
const associationCounts = new Array(coloringSet.length);
|
||||
associationCounts.fill(0);
|
||||
|
||||
let totalAssociations = 0
|
||||
let totalAssociations = 0;
|
||||
|
||||
set.forEach(item => {
|
||||
let innerSet = 'events' in item ? item.events : item
|
||||
set.forEach((item) => {
|
||||
let innerSet = "events" in item ? item.events : item;
|
||||
|
||||
if (!Array.isArray(innerSet)) innerSet = [innerSet]
|
||||
if (!Array.isArray(innerSet)) innerSet = [innerSet];
|
||||
|
||||
innerSet.forEach(val => {
|
||||
val.associations.forEach(a => {
|
||||
const idx = associationMap[a]
|
||||
if (!idx && idx !== 0) return
|
||||
associationCounts[idx] += 1
|
||||
totalAssociations += 1
|
||||
})
|
||||
})
|
||||
})
|
||||
innerSet.forEach((val) => {
|
||||
val.associations.forEach((a) => {
|
||||
const idx = associationMap[a];
|
||||
if (!idx && idx !== 0) return;
|
||||
associationCounts[idx] += 1;
|
||||
totalAssociations += 1;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (totalAssociations === 0) return [1]
|
||||
if (totalAssociations === 0) return [1];
|
||||
|
||||
return associationCounts.map(count => count / totalAssociations)
|
||||
return associationCounts.map((count) => count / totalAssociations);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -354,73 +375,76 @@ export function calculateColorPercentages (set, coloringSet) {
|
||||
*
|
||||
* Example coloringSet = [['Chemical', 'Tear Gas'], ['Procedural', 'Destruction of property']]
|
||||
*/
|
||||
export function getFilterIdxFromColorSet (filter, coloringSet) {
|
||||
let filterIdx = -1
|
||||
export function getFilterIdxFromColorSet(filter, coloringSet) {
|
||||
let filterIdx = -1;
|
||||
coloringSet.map((set, idx) => {
|
||||
const foundIdx = set.indexOf(filter)
|
||||
if (foundIdx !== -1) filterIdx = idx
|
||||
})
|
||||
return filterIdx
|
||||
const foundIdx = set.indexOf(filter);
|
||||
if (foundIdx !== -1) filterIdx = idx;
|
||||
});
|
||||
return filterIdx;
|
||||
}
|
||||
|
||||
export const dateMin = function () {
|
||||
return Array.prototype.slice.call(arguments).reduce(function (a, b) {
|
||||
return a < b ? a : b
|
||||
})
|
||||
}
|
||||
return a < b ? a : b;
|
||||
});
|
||||
};
|
||||
|
||||
export const dateMax = function () {
|
||||
return Array.prototype.slice.call(arguments).reduce(function (a, b) {
|
||||
return a > b ? a : b
|
||||
})
|
||||
}
|
||||
return a > b ? a : b;
|
||||
});
|
||||
};
|
||||
|
||||
/** Taken from
|
||||
* https://stackoverflow.com/questions/22697936/binary-search-in-javascript
|
||||
* **/
|
||||
export function binarySearch (ar, el, compareFn) {
|
||||
var m = 0
|
||||
var n = ar.length - 1
|
||||
* https://stackoverflow.com/questions/22697936/binary-search-in-javascript
|
||||
* **/
|
||||
export function binarySearch(ar, el, compareFn) {
|
||||
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
|
||||
m = k + 1;
|
||||
} else if (cmp < 0) {
|
||||
n = k - 1
|
||||
n = k - 1;
|
||||
} else {
|
||||
return k
|
||||
return k;
|
||||
}
|
||||
}
|
||||
return -m - 1
|
||||
return -m - 1;
|
||||
}
|
||||
|
||||
export function makeNiceDate (datetime) {
|
||||
if (datetime === null) return null
|
||||
export function makeNiceDate(datetime) {
|
||||
if (datetime === null) return null;
|
||||
// see https://stackoverflow.com/questions/3552461/how-to-format-a-javascript-date
|
||||
const dateTimeFormat = new Intl.DateTimeFormat(
|
||||
language,
|
||||
{ year: 'numeric', month: 'long', day: '2-digit' }
|
||||
)
|
||||
const dateTimeFormat = new Intl.DateTimeFormat(language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "2-digit",
|
||||
});
|
||||
const [
|
||||
{ value: month },,
|
||||
{ value: day },,
|
||||
{ value: year }
|
||||
] = dateTimeFormat.formatToParts(datetime)
|
||||
{ value: month },
|
||||
,
|
||||
{ value: day },
|
||||
,
|
||||
{ value: year },
|
||||
] = dateTimeFormat.formatToParts(datetime);
|
||||
|
||||
return `${day} ${month}, ${year}`
|
||||
return `${day} ${month}, ${year}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default locale for d3 to format dates in each available language.
|
||||
* @param {Object} d3 - An instance of D3
|
||||
*/
|
||||
export function setD3Locale (d3) {
|
||||
export function setD3Locale(d3) {
|
||||
const languages = {
|
||||
'es-MX': require('./data/es-MX.json')
|
||||
}
|
||||
"es-MX": require("./data/es-MX.json"),
|
||||
};
|
||||
|
||||
if (language !== 'es-US' && languages[language]) {
|
||||
d3.timeFormatDefaultLocale(languages[language])
|
||||
if (language !== "es-US" && languages[language]) {
|
||||
d3.timeFormatDefaultLocale(languages[language]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
));
|
||||
|
||||
@@ -1,180 +1,186 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
Card,
|
||||
generateCardLayout,
|
||||
} from "@forensic-architecture/design-system/dist/react";
|
||||
|
||||
import * as selectors from '../selectors'
|
||||
import { getFilterIdxFromColorSet } from '../common/utilities'
|
||||
import * as selectors from "../selectors";
|
||||
import { getFilterIdxFromColorSet } from "../common/utilities";
|
||||
// import Card from './Card.jsx'
|
||||
import { Card } from '@forensic-architecture/design-system/react'
|
||||
import copy from '../common/data/copy.json'
|
||||
import copy from "../common/data/copy.json";
|
||||
|
||||
class CardStack extends React.Component {
|
||||
constructor () {
|
||||
super()
|
||||
this.refs = {}
|
||||
this.refCardStack = React.createRef()
|
||||
this.refCardStackContent = React.createRef()
|
||||
constructor() {
|
||||
super();
|
||||
this.refs = {};
|
||||
this.refCardStack = React.createRef();
|
||||
this.refCardStackContent = React.createRef();
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
const isNarrative = !!this.props.narrative
|
||||
componentDidUpdate() {
|
||||
const isNarrative = !!this.props.narrative;
|
||||
|
||||
if (isNarrative) {
|
||||
this.scrollToCard()
|
||||
this.scrollToCard();
|
||||
}
|
||||
}
|
||||
|
||||
scrollToCard () {
|
||||
const duration = 500
|
||||
const element = this.refCardStack.current
|
||||
scrollToCard() {
|
||||
const duration = 500;
|
||||
const element = this.refCardStack.current;
|
||||
const cardScroll = this.refs[this.props.narrative.current].current
|
||||
.offsetTop
|
||||
.offsetTop;
|
||||
|
||||
let start = element.scrollTop
|
||||
let change = cardScroll - start
|
||||
let currentTime = 0
|
||||
const increment = 20
|
||||
const start = element.scrollTop;
|
||||
const change = cardScroll - start;
|
||||
let currentTime = 0;
|
||||
const increment = 20;
|
||||
|
||||
// t = current time
|
||||
// b = start value
|
||||
// c = change in value
|
||||
// d = duration
|
||||
Math.easeInOutQuad = function (t, b, c, d) {
|
||||
t /= d / 2
|
||||
if (t < 1) return (c / 2) * t * t + b
|
||||
t -= 1
|
||||
return (-c / 2) * (t * (t - 2) - 1) + b
|
||||
}
|
||||
t /= d / 2;
|
||||
if (t < 1) return (c / 2) * t * t + b;
|
||||
t -= 1;
|
||||
return (-c / 2) * (t * (t - 2) - 1) + b;
|
||||
};
|
||||
|
||||
const animateScroll = function () {
|
||||
currentTime += increment
|
||||
const val = Math.easeInOutQuad(currentTime, start, change, duration)
|
||||
element.scrollTop = val
|
||||
if (currentTime < duration) setTimeout(animateScroll, increment)
|
||||
}
|
||||
animateScroll()
|
||||
currentTime += increment;
|
||||
const val = Math.easeInOutQuad(currentTime, start, change, duration);
|
||||
element.scrollTop = val;
|
||||
if (currentTime < duration) setTimeout(animateScroll, increment);
|
||||
};
|
||||
animateScroll();
|
||||
}
|
||||
|
||||
renderCards (events, selections) {
|
||||
renderCards(events, selections) {
|
||||
// if no selections provided, select all
|
||||
if (!selections) {
|
||||
selections = events.map((e) => true)
|
||||
selections = events.map((e) => true);
|
||||
}
|
||||
this.refs = []
|
||||
this.refs = [];
|
||||
|
||||
const generateTemplate =
|
||||
generateCardLayout[this.props.cardUI.layout.template];
|
||||
|
||||
return events.map((event, idx) => {
|
||||
const thisRef = React.createRef()
|
||||
this.refs[idx] = thisRef
|
||||
const thisRef = React.createRef();
|
||||
this.refs[idx] = thisRef;
|
||||
|
||||
return (
|
||||
<Card
|
||||
ref={thisRef}
|
||||
content={this.props.cardUI.layout({
|
||||
content={generateTemplate({
|
||||
event,
|
||||
colors: this.props.colors,
|
||||
coloringSet: this.props.coloringSet,
|
||||
getFilterIdxFromColorSet
|
||||
getFilterIdxFromColorSet,
|
||||
})}
|
||||
language={this.props.language}
|
||||
isLoading={this.props.isLoading}
|
||||
isSelected={selections[idx]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderSelectedCards () {
|
||||
const { selected } = this.props
|
||||
renderSelectedCards() {
|
||||
const { selected } = this.props;
|
||||
|
||||
if (selected.length > 0) {
|
||||
return this.renderCards(selected)
|
||||
return this.renderCards(selected);
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
renderNarrativeCards () {
|
||||
const { narrative } = this.props
|
||||
const showing = narrative.steps
|
||||
renderNarrativeCards() {
|
||||
const { narrative } = this.props;
|
||||
const showing = narrative.steps;
|
||||
|
||||
const selections = showing.map((_, idx) => idx === narrative.current)
|
||||
const selections = showing.map((_, idx) => idx === narrative.current);
|
||||
|
||||
return this.renderCards(showing, selections)
|
||||
return this.renderCards(showing, selections);
|
||||
}
|
||||
|
||||
renderCardStackHeader () {
|
||||
const headerLang = copy[this.props.language].cardstack.header
|
||||
renderCardStackHeader() {
|
||||
const headerLang = copy[this.props.language].cardstack.header;
|
||||
|
||||
return (
|
||||
<div
|
||||
id='card-stack-header'
|
||||
className='card-stack-header'
|
||||
id="card-stack-header"
|
||||
className="card-stack-header"
|
||||
onClick={() => this.props.onToggleCardstack()}
|
||||
>
|
||||
<button className='side-menu-burg is-active'>
|
||||
<button className="side-menu-burg is-active">
|
||||
<span />
|
||||
</button>
|
||||
<p className='header-copy top'>
|
||||
<p className="header-copy top">
|
||||
{`${this.props.selected.length} ${headerLang}`}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderCardStackContent () {
|
||||
renderCardStackContent() {
|
||||
return (
|
||||
<div id='card-stack-content' className='card-stack-content'>
|
||||
<div id="card-stack-content" className="card-stack-content">
|
||||
<ul>{this.renderSelectedCards()}</ul>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderNarrativeContent () {
|
||||
renderNarrativeContent() {
|
||||
return (
|
||||
<div
|
||||
id='card-stack-content'
|
||||
className='card-stack-content'
|
||||
id="card-stack-content"
|
||||
className="card-stack-content"
|
||||
ref={this.refCardStackContent}
|
||||
>
|
||||
<ul>{this.renderNarrativeCards()}</ul>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isCardstack, selected, narrative, timelineDims } = this.props
|
||||
render() {
|
||||
const { isCardstack, selected, narrative, timelineDims } = this.props;
|
||||
// TODO: make '237px', which is the narrative header, less hard-coded
|
||||
const height = `calc(100% - 237px - ${timelineDims.height}px)`
|
||||
const height = `calc(100% - 237px - ${timelineDims.height}px)`;
|
||||
if (selected.length > 0) {
|
||||
if (!narrative) {
|
||||
return (
|
||||
<div
|
||||
id='card-stack'
|
||||
id="card-stack"
|
||||
className={`card-stack
|
||||
${isCardstack ? '' : ' folded'}`}
|
||||
${isCardstack ? "" : " folded"}`}
|
||||
>
|
||||
{this.renderCardStackHeader()}
|
||||
{this.renderCardStackContent()}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
id='card-stack'
|
||||
id="card-stack"
|
||||
ref={this.refCardStack}
|
||||
className={`card-stack narrative-mode
|
||||
${isCardstack ? '' : ' folded'}`}
|
||||
${isCardstack ? "" : " folded"}`}
|
||||
style={{ height }}
|
||||
>
|
||||
{this.renderNarrativeContent()}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <div />
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
narrative: selectors.selectActiveNarrative(state),
|
||||
selected: selectors.selectSelected(state),
|
||||
@@ -185,8 +191,8 @@ function mapStateToProps (state) {
|
||||
cardUI: state.ui.card,
|
||||
colors: state.ui.coloring.colors,
|
||||
coloringSet: state.app.associations.coloringSet,
|
||||
features: state.features
|
||||
}
|
||||
features: state.features,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(CardStack)
|
||||
export default connect(mapStateToProps)(CardStack);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Portal } from 'react-portal'
|
||||
import colors from '../../../common/global.js'
|
||||
import ColoredMarkers from './ColoredMarkers.jsx'
|
||||
import React, { useState } from "react";
|
||||
import { Portal } from "react-portal";
|
||||
import colors from "../../../common/global.js";
|
||||
import ColoredMarkers from "./ColoredMarkers.jsx";
|
||||
import {
|
||||
calcClusterOpacity,
|
||||
calcClusterSize,
|
||||
@@ -9,18 +9,30 @@ import {
|
||||
isLongitude,
|
||||
calculateColorPercentages,
|
||||
zipColorsToPercentages,
|
||||
calculateTotalClusterPoints } from '../../../common/utilities'
|
||||
calculateTotalClusterPoints,
|
||||
} from "../../../common/utilities";
|
||||
|
||||
const DefsClusters = () => (
|
||||
<defs>
|
||||
<radialGradient id='clusterGradient'>
|
||||
<stop offset='10%' stop-color='red' />
|
||||
<stop offset='90%' stop-color='transparent' />
|
||||
<radialGradient id="clusterGradient">
|
||||
<stop offset="10%" stop-color="red" />
|
||||
<stop offset="90%" stop-color="transparent" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
)
|
||||
);
|
||||
|
||||
function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHover, onClick, getClusterChildren, coloringSet, filterColors }) {
|
||||
function Cluster({
|
||||
cluster,
|
||||
size,
|
||||
projectPoint,
|
||||
totalPoints,
|
||||
styles,
|
||||
renderHover,
|
||||
onClick,
|
||||
getClusterChildren,
|
||||
coloringSet,
|
||||
filterColors,
|
||||
}) {
|
||||
/**
|
||||
{
|
||||
geometry: {
|
||||
@@ -35,22 +47,25 @@ function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHove
|
||||
type: "Feature"
|
||||
}
|
||||
*/
|
||||
const { cluster_id: clusterId } = cluster.properties
|
||||
const { cluster_id: clusterId } = cluster.properties;
|
||||
|
||||
const individualChildren = getClusterChildren(clusterId)
|
||||
const colorPercentages = calculateColorPercentages(individualChildren, coloringSet)
|
||||
const individualChildren = getClusterChildren(clusterId);
|
||||
const colorPercentages = calculateColorPercentages(
|
||||
individualChildren,
|
||||
coloringSet
|
||||
);
|
||||
|
||||
const { coordinates } = cluster.geometry
|
||||
const [longitude, latitude] = coordinates
|
||||
if (!isLatitude(latitude) || !isLongitude(longitude)) return null
|
||||
const { x, y } = projectPoint([latitude, longitude])
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const { coordinates } = cluster.geometry;
|
||||
const [longitude, latitude] = coordinates;
|
||||
const { x, y } = projectPoint([latitude, longitude]);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
if (!isLatitude(latitude) || !isLongitude(longitude)) return null;
|
||||
|
||||
return (
|
||||
<g
|
||||
className={'cluster-event'}
|
||||
className="cluster-event"
|
||||
transform={`translate(${x}, ${y})`}
|
||||
onClick={e => onClick({ id: clusterId, latitude, longitude })}
|
||||
onClick={(e) => onClick({ id: clusterId, latitude, longitude })}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
@@ -58,16 +73,16 @@ function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHove
|
||||
radius={size}
|
||||
colorPercentMap={zipColorsToPercentages(filterColors, colorPercentages)}
|
||||
styles={{
|
||||
...styles
|
||||
...styles,
|
||||
}}
|
||||
className={'cluster-event-marker'}
|
||||
className="cluster-event-marker"
|
||||
/>
|
||||
{hovered ? renderHover(cluster) : null}
|
||||
</g>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ClusterEvents ({
|
||||
function ClusterEvents({
|
||||
projectPoint,
|
||||
onSelect,
|
||||
getClusterChildren,
|
||||
@@ -76,56 +91,66 @@ function ClusterEvents ({
|
||||
svg,
|
||||
clusters,
|
||||
filterColors,
|
||||
selected
|
||||
selected,
|
||||
}) {
|
||||
const totalPoints = calculateTotalClusterPoints(clusters)
|
||||
const totalPoints = calculateTotalClusterPoints(clusters);
|
||||
|
||||
const styles = {
|
||||
fill: isRadial ? "url('#clusterGradient')" : colors.fallbackEventColor,
|
||||
stroke: colors.darkBackground,
|
||||
strokeWidth: 0
|
||||
}
|
||||
strokeWidth: 0,
|
||||
};
|
||||
|
||||
function renderHover (txt, circleSize) {
|
||||
return <>
|
||||
<text text-anchor='middle' y='3px' style={{ fontWeight: 'bold', fill: 'black', zIndex: 10000 }}>{txt}</text>
|
||||
<circle
|
||||
class='event-hover'
|
||||
cx='0'
|
||||
cy='0'
|
||||
r={circleSize + 2}
|
||||
stroke={colors.primaryHighlight}
|
||||
fill-opacity='0.0'
|
||||
/>
|
||||
</>
|
||||
function renderHover(txt, circleSize) {
|
||||
return (
|
||||
<>
|
||||
<text
|
||||
text-anchor="middle"
|
||||
y="3px"
|
||||
style={{ fontWeight: "bold", fill: "black", zIndex: 10000 }}
|
||||
>
|
||||
{txt}
|
||||
</text>
|
||||
<circle
|
||||
class="event-hover"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r={circleSize + 2}
|
||||
stroke={colors.primaryHighlight}
|
||||
fill-opacity="0.0"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal node={svg}>
|
||||
<g className='cluster-locations'>
|
||||
<g className="cluster-locations">
|
||||
{isRadial ? <DefsClusters /> : null}
|
||||
{clusters.map(c => {
|
||||
const pointCount = c.properties.point_count
|
||||
const clusterSize = calcClusterSize(pointCount, totalPoints)
|
||||
return <Cluster
|
||||
onClick={onSelect}
|
||||
getClusterChildren={getClusterChildren}
|
||||
coloringSet={coloringSet}
|
||||
cluster={c}
|
||||
filterColors={filterColors}
|
||||
size={clusterSize}
|
||||
projectPoint={projectPoint}
|
||||
totalPoints={totalPoints}
|
||||
styles={{
|
||||
...styles,
|
||||
fillOpacity: calcClusterOpacity(pointCount, totalPoints)
|
||||
}}
|
||||
renderHover={() => renderHover(pointCount, clusterSize)}
|
||||
/>
|
||||
{clusters.map((c) => {
|
||||
const pointCount = c.properties.point_count;
|
||||
const clusterSize = calcClusterSize(pointCount, totalPoints);
|
||||
return (
|
||||
<Cluster
|
||||
onClick={onSelect}
|
||||
getClusterChildren={getClusterChildren}
|
||||
coloringSet={coloringSet}
|
||||
cluster={c}
|
||||
filterColors={filterColors}
|
||||
size={clusterSize}
|
||||
projectPoint={projectPoint}
|
||||
totalPoints={totalPoints}
|
||||
styles={{
|
||||
...styles,
|
||||
fillOpacity: calcClusterOpacity(pointCount, totalPoints),
|
||||
}}
|
||||
renderHover={() => renderHover(pointCount, clusterSize)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
</Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default ClusterEvents
|
||||
export default ClusterEvents;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,35 +1,47 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { Provider } from 'react-redux'
|
||||
import store from './store/index.js'
|
||||
import App from './components/App.jsx'
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Provider } from "react-redux";
|
||||
import store from "./store/index.js";
|
||||
import App from "./components/App.jsx";
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
document.getElementById('explore-app')
|
||||
)
|
||||
document.getElementById("explore-app")
|
||||
);
|
||||
|
||||
// Expressions from https://exceptionshub.com/how-to-detect-safari-chrome-ie-firefox-and-opera-browser.html
|
||||
|
||||
/* eslint-disable */
|
||||
// Opera 8.0+
|
||||
const isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0
|
||||
const isOpera =
|
||||
(!!window.opr && !!opr.addons) ||
|
||||
!!window.opera ||
|
||||
navigator.userAgent.indexOf(" OPR/") >= 0;
|
||||
// Firefox 1.0+
|
||||
const isFirefox = typeof InstallTrigger !== 'undefined'
|
||||
const isFirefox = typeof InstallTrigger !== "undefined";
|
||||
// Safari 3.0+ "[object HTMLElementConstructor]"
|
||||
const isSafari = /constructor/i.test(window.HTMLElement) || (function (p) { return p.toString() === '[object SafariRemoteNotification]' })(!window['safari'] || (typeof safari !== 'undefined' && safari.pushNotification))
|
||||
const isSafari =
|
||||
/constructor/i.test(window.HTMLElement) ||
|
||||
(function (p) {
|
||||
return p.toString() === "[object SafariRemoteNotification]";
|
||||
})(
|
||||
!window["safari"] ||
|
||||
(typeof safari !== "undefined" && safari.pushNotification)
|
||||
);
|
||||
// Internet Explorer 6-11
|
||||
const isIE = /* @cc_on!@ */false || !!document.documentMode
|
||||
const isIE = /* @cc_on!@ */ false || !!document.documentMode;
|
||||
// Edge 20+
|
||||
const isEdge = !isIE && !!window.StyleMedia
|
||||
const isEdge = !isIE && !!window.StyleMedia;
|
||||
// Chrome 1+
|
||||
const isChrome = !!window.chrome && !!window.chrome.webstore
|
||||
const isChrome = !!window.chrome && !!window.chrome.webstore;
|
||||
// Blink engine detection
|
||||
const isBlink = (isChrome || isOpera) && !!window.CSS
|
||||
const isBlink = (isChrome || isOpera) && !!window.CSS;
|
||||
|
||||
if (isEdge || isIE) {
|
||||
alert('Please view this website in Opera for best viewing. It is untested in your browser.')
|
||||
alert(
|
||||
"Please view this website in Opera for best viewing. It is untested in your browser."
|
||||
);
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
@@ -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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user