mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-07 19:08:37 +03:00
Ingesting config through Create React App
This commit is contained in:
File diff suppressed because one or more lines are too long
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")];
|
||||||
|
|
||||||
|
/** env variables from config.js */
|
||||||
|
const CONFIG = process.env.CONFIG || "config.js";
|
||||||
|
const envConfig = require("../" + CONFIG);
|
||||||
|
const userConfig = {};
|
||||||
|
const userFeatures = {};
|
||||||
|
for (const k in envConfig) {
|
||||||
|
userConfig[k] = JSON.stringify(envConfig[k]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const k in envConfig["features"]) {
|
||||||
|
userFeatures[k] = JSON.stringify(envConfig["features"][k]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
features: userFeatures,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { raw, stringified };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = getClientEnvironment;
|
||||||
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};`;
|
||||||
|
},
|
||||||
|
};
|
||||||
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
|
||||||
|
);
|
||||||
|
};
|
||||||
124
package.json
124
package.json
@@ -5,9 +5,9 @@
|
|||||||
"homepage": "",
|
"homepage": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"react-scripts:start": "react-scripts start",
|
"react-scripts:start": "node scripts/start.js",
|
||||||
"react-scripts:build": "react-scripts build",
|
"react-scripts:build": "node scripts/build.js",
|
||||||
"react-scripts:eject": "react-scripts eject",
|
"react-scripts:eject": "node scripts/eject.js",
|
||||||
"dev": "webpack-dev-server --content-base static --mode development",
|
"dev": "webpack-dev-server --content-base static --mode development",
|
||||||
"dev:wsl": "npm run dev -- --host 0.0.0.0",
|
"dev:wsl": "npm run dev -- --host 0.0.0.0",
|
||||||
"build": "NODE_ENV=production webpack --mode production",
|
"build": "NODE_ENV=production webpack --mode production",
|
||||||
@@ -17,28 +17,84 @@
|
|||||||
"lint:fix": "npm run lint -- --fix"
|
"lint:fix": "npm run lint -- --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/core": "7.12.3",
|
||||||
"@forensic-architecture/design-system": "0.6.1",
|
"@forensic-architecture/design-system": "0.6.1",
|
||||||
|
"@pmmmwh/react-refresh-webpack-plugin": "0.4.2",
|
||||||
|
"@svgr/webpack": "5.4.0",
|
||||||
|
"@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",
|
"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",
|
||||||
|
"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",
|
"joi": "^14.0.1",
|
||||||
"leaflet": "^1.0.3",
|
"leaflet": "^1.0.3",
|
||||||
"marked": "^0.7.0",
|
"marked": "^0.7.0",
|
||||||
|
"mini-css-extract-plugin": "0.11.3",
|
||||||
"moment": "^2.26.0",
|
"moment": "^2.26.0",
|
||||||
"object-hash": "^1.3.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",
|
||||||
|
"prompts": "2.4.0",
|
||||||
"ramda": "^0.26.1",
|
"ramda": "^0.26.1",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
|
"react-app-polyfill": "^2.0.0",
|
||||||
|
"react-dev-utils": "^11.0.1",
|
||||||
"react-device-detect": "^1.6.2",
|
"react-device-detect": "^1.6.2",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"react-image": "^1.5.1",
|
"react-image": "^1.5.1",
|
||||||
"react-portal": "^4.2.0",
|
"react-portal": "^4.2.0",
|
||||||
"react-redux": "^5.0.4",
|
"react-redux": "^5.0.4",
|
||||||
"react-scripts": "^4.0.1",
|
"react-refresh": "^0.8.3",
|
||||||
"react-tabs": "3.0.0",
|
"react-tabs": "3.0.0",
|
||||||
"redux": "^3.6.0",
|
"redux": "^3.6.0",
|
||||||
"redux-thunk": "^2.2.0",
|
"redux-thunk": "^2.2.0",
|
||||||
"reselect": "^3.0.1",
|
"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",
|
"supercluster": "^7.1.0",
|
||||||
|
"terser-webpack-plugin": "4.2.3",
|
||||||
|
"ts-pnp": "1.2.0",
|
||||||
|
"url-loader": "4.1.1",
|
||||||
"video-react": "^0.13.1",
|
"video-react": "^0.13.1",
|
||||||
"webpack": "^4.20.2"
|
"webpack": "4.44.2",
|
||||||
|
"webpack-dev-server": "3.11.0",
|
||||||
|
"webpack-manifest-plugin": "2.2.0",
|
||||||
|
"workbox-webpack-plugin": "5.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"ava": "1.0.0-beta.8",
|
"ava": "1.0.0-beta.8",
|
||||||
@@ -67,5 +123,63 @@
|
|||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"roots": [
|
||||||
|
"<rootDir>/src"
|
||||||
|
],
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"src/**/*.{js,jsx,ts,tsx}",
|
||||||
|
"!src/**/*.d.ts"
|
||||||
|
],
|
||||||
|
"setupFiles": [
|
||||||
|
"react-app-polyfill/jsdom"
|
||||||
|
],
|
||||||
|
"setupFilesAfterEnv": [],
|
||||||
|
"testMatch": [
|
||||||
|
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
|
||||||
|
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
|
||||||
|
],
|
||||||
|
"testEnvironment": "jsdom",
|
||||||
|
"testRunner": "/Users/zac/Developer/Forensic Architecture/timemap/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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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) || 3000;
|
||||||
|
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,22 +1,23 @@
|
|||||||
import moment from 'moment'
|
import moment from "moment";
|
||||||
import hash from 'object-hash'
|
import hash from "object-hash";
|
||||||
|
|
||||||
let { DATE_FMT, TIME_FMT } = process.env
|
let { DATE_FMT, TIME_FMT } = process.env;
|
||||||
if (!DATE_FMT) DATE_FMT = 'MM/DD/YYYY'
|
if (!DATE_FMT) DATE_FMT = "MM/DD/YYYY";
|
||||||
if (!TIME_FMT) TIME_FMT = 'HH:mm'
|
if (!TIME_FMT) TIME_FMT = "HH:mm";
|
||||||
|
|
||||||
export const language = process.env.store.app.language || 'en-US'
|
console.log(process.env);
|
||||||
|
export const language = process.env.store.app.language || "en-US";
|
||||||
|
|
||||||
export function calcDatetime (date, time) {
|
export function calcDatetime(date, time) {
|
||||||
if (!time) time = '00:00'
|
if (!time) time = "00:00";
|
||||||
const dt = moment(`${date} ${time}`, `${DATE_FMT} ${TIME_FMT}`)
|
const dt = moment(`${date} ${time}`, `${DATE_FMT} ${TIME_FMT}`);
|
||||||
return dt.toDate()
|
return dt.toDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCoordinatesForPercent (radius, percent) {
|
export function getCoordinatesForPercent(radius, percent) {
|
||||||
const x = radius * Math.cos(2 * Math.PI * percent)
|
const x = radius * Math.cos(2 * Math.PI * percent);
|
||||||
const y = radius * Math.sin(2 * Math.PI * percent)
|
const y = radius * Math.sin(2 * Math.PI * percent);
|
||||||
return [x, y]
|
return [x, y];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,32 +27,33 @@ export function getCoordinatesForPercent (radius, percent) {
|
|||||||
*
|
*
|
||||||
* Return value:
|
* Return value:
|
||||||
* ex. {'#fff': 0.5, '#000': 0.5, ...} */
|
* ex. {'#fff': 0.5, '#000': 0.5, ...} */
|
||||||
export function zipColorsToPercentages (colors, percentages) {
|
export function zipColorsToPercentages(colors, percentages) {
|
||||||
if (colors.length < percentages.length) throw new Error('You must declare an appropriate number of filter colors')
|
if (colors.length < percentages.length)
|
||||||
|
throw new Error("You must declare an appropriate number of filter colors");
|
||||||
|
|
||||||
return percentages.reduce((map, percent, idx) => {
|
return percentages.reduce((map, percent, idx) => {
|
||||||
map[colors[idx]] = percent
|
map[colors[idx]] = percent;
|
||||||
return map
|
return map;
|
||||||
}, {})
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get URI params to start with predefined set of
|
* Get URI params to start with predefined set of
|
||||||
* https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
|
* https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
|
||||||
* @param {string} name: name of paramater to search
|
* @param {string} name: name of paramater to search
|
||||||
* @param {string} url: url passed as variable, defaults to window.location.href
|
* @param {string} url: url passed as variable, defaults to window.location.href
|
||||||
*/
|
*/
|
||||||
export function getParameterByName (name, url) {
|
export function getParameterByName(name, url) {
|
||||||
if (!url) url = window.location.href
|
if (!url) url = window.location.href;
|
||||||
name = name.replace(/[[\]]/g, `\\$&`)
|
name = name.replace(/[[\]]/g, `\\$&`);
|
||||||
|
|
||||||
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`)
|
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
|
||||||
const results = regex.exec(url)
|
const results = regex.exec(url);
|
||||||
|
|
||||||
if (!results) return null
|
if (!results) return null;
|
||||||
if (!results[2]) return ''
|
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} arr1: array of numbers
|
||||||
* @param {array} arr2: array of numbers
|
* @param {array} arr2: array of numbers
|
||||||
*/
|
*/
|
||||||
export function areEqual (arr1, arr2) {
|
export function areEqual(arr1, arr2) {
|
||||||
return ((arr1.length === arr2.length) && arr1.every((element, index) => {
|
return (
|
||||||
return element === arr2[index]
|
arr1.length === arr2.length &&
|
||||||
}))
|
arr1.every((element, index) => {
|
||||||
|
return element === arr2[index];
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return whether the variable is neither null nor undefined
|
* Return whether the variable is neither null nor undefined
|
||||||
* @param {object} variable
|
* @param {object} variable
|
||||||
*/
|
*/
|
||||||
export function isNotNullNorUndefined (variable) {
|
export function isNotNullNorUndefined(variable) {
|
||||||
return (typeof variable !== 'undefined' && variable !== null)
|
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
|
* Taken from: https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript
|
||||||
*/
|
*/
|
||||||
export function capitalize (string) {
|
export function capitalize(string) {
|
||||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function trimAndEllipse (string, stringNum) {
|
export function trimAndEllipse(string, stringNum) {
|
||||||
if (string.length > 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.
|
* through every association's given path attribute to find its location.
|
||||||
*
|
*
|
||||||
* Returns the list of parents: ex. ['Chemical', 'Tear Gas', ...]
|
* Returns the list of parents: ex. ['Chemical', 'Tear Gas', ...]
|
||||||
*/
|
*/
|
||||||
export function getFilterParents (associations, filter) {
|
export function getFilterParents(associations, filter) {
|
||||||
for (let a of associations) {
|
for (let a of associations) {
|
||||||
const { filter_paths: fp } = a
|
const { filter_paths: fp } = a;
|
||||||
if (a.id === filter) {
|
if (a.id === filter) {
|
||||||
return fp.slice(0, fp.length - 1)
|
return fp.slice(0, fp.length - 1);
|
||||||
}
|
}
|
||||||
const filterIndex = fp.indexOf(filter)
|
const filterIndex = fp.indexOf(filter);
|
||||||
if (filterIndex === 0) return []
|
if (filterIndex === 0) return [];
|
||||||
if (filterIndex > 0) return fp.slice(0, filterIndex)
|
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.
|
* Grabs the second to last element in the paths array for a given existing filter.
|
||||||
* This is the filter's most immediate ancestor.
|
* This is the filter's most immediate ancestor.
|
||||||
*/
|
*/
|
||||||
export function getImmediateFilterParent (associations, filter) {
|
export function getImmediateFilterParent(associations, filter) {
|
||||||
const parents = getFilterParents(associations, filter)
|
const parents = getFilterParents(associations, filter);
|
||||||
if (parents.length === 0) return null
|
if (parents.length === 0) return null;
|
||||||
return parents[parents.length - 1]
|
return parents[parents.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grab a meta filter's siblings, by way of the the `filter_path` hierarcy.
|
* Grab a meta filter's siblings, by way of the the `filter_path` hierarcy.
|
||||||
*/
|
*/
|
||||||
export function getMetaFilterSiblings (allFilters, filterParent, filterKey) {
|
export function getMetaFilterSiblings(allFilters, filterParent, filterKey) {
|
||||||
const idxParent = allFilters.map(f => {
|
const idxParent = allFilters
|
||||||
return f.filter_paths.reduceRight((acc, path, idx) => {
|
.map((f) => {
|
||||||
if (path === filterParent) return f.filter_paths[idx + 1]
|
return f.filter_paths.reduceRight((acc, path, idx) => {
|
||||||
return acc
|
if (path === filterParent) return f.filter_paths[idx + 1];
|
||||||
}, null)
|
return acc;
|
||||||
})
|
}, null);
|
||||||
.filter(metaFilter => !!metaFilter && metaFilter !== filterKey)
|
})
|
||||||
return [ ...(new Set(idxParent)) ]
|
.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.
|
* Grabs a given filter's siblings: the set of associations that share the same immediate filter parent.
|
||||||
*/
|
*/
|
||||||
export function getFilterSiblings (allFilters, filterParent, filterKey) {
|
export function getFilterSiblings(allFilters, filterParent, filterKey) {
|
||||||
const isMetaFilter = !allFilters.map(filt => filt.id).includes(filterKey)
|
const isMetaFilter = !allFilters.map((filt) => filt.id).includes(filterKey);
|
||||||
|
|
||||||
if (isMetaFilter) {
|
if (isMetaFilter) {
|
||||||
return getMetaFilterSiblings(allFilters, filterParent, filterKey)
|
return getMetaFilterSiblings(allFilters, filterParent, filterKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
return allFilters.reduce((acc, val) => {
|
return allFilters.reduce((acc, val) => {
|
||||||
const valParent = getImmediateFilterParent(allFilters, val.id)
|
const valParent = getImmediateFilterParent(allFilters, val.id);
|
||||||
if (valParent === filterParent && val.id !== filterKey) acc.push(val.id)
|
if (valParent === filterParent && val.id !== filterKey) acc.push(val.id);
|
||||||
return acc
|
return acc;
|
||||||
}, [])
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEventCategories (event, categories) {
|
export function getEventCategories(event, categories) {
|
||||||
const matchedCategories = []
|
const matchedCategories = [];
|
||||||
if (event.associations && event.associations.length > 0) {
|
if (event.associations && event.associations.length > 0) {
|
||||||
event.associations.reduce((acc, val) => {
|
event.associations.reduce((acc, val) => {
|
||||||
const foundCategory = categories.find(cat => cat.id === val)
|
const foundCategory = categories.find((cat) => cat.id === val);
|
||||||
if (foundCategory) acc.push(foundCategory)
|
if (foundCategory) acc.push(foundCategory);
|
||||||
return acc
|
return acc;
|
||||||
}, matchedCategories)
|
}, matchedCategories);
|
||||||
}
|
}
|
||||||
return matchedCategories
|
return matchedCategories;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -167,186 +173,201 @@ export function getEventCategories (event, categories) {
|
|||||||
* source, call with two sets of parentheses:
|
* source, call with two sets of parentheses:
|
||||||
* const src = insetSourceFrom(sources)(anEvent)
|
* const src = insetSourceFrom(sources)(anEvent)
|
||||||
*/
|
*/
|
||||||
export function insetSourceFrom (allSources) {
|
export function insetSourceFrom(allSources) {
|
||||||
return (event) => {
|
return (event) => {
|
||||||
let sources
|
let sources;
|
||||||
if (!event.sources) {
|
if (!event.sources) {
|
||||||
sources = []
|
sources = [];
|
||||||
} else {
|
} else {
|
||||||
sources = event.sources.map(id => {
|
sources = event.sources.map((id) => {
|
||||||
return allSources.hasOwnProperty(id) ? allSources[id] : null
|
return allSources.hasOwnProperty(id) ? allSources[id] : null;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...event,
|
...event,
|
||||||
sources
|
sources,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debugging function: put in place of a mapStateToProps function to
|
* Debugging function: put in place of a mapStateToProps function to
|
||||||
* view that source modal by default
|
* view that source modal by default
|
||||||
*/
|
*/
|
||||||
export function injectSource (id) {
|
export function injectSource(id) {
|
||||||
return state => {
|
return (state) => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
app: {
|
app: {
|
||||||
...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 (process.env[ext]) {
|
||||||
if (!Array.isArray(process.env[ext])) { return [`${process.env.SERVER_ROOT}${process.env[ext]}`] } else {
|
if (!Array.isArray(process.env[ext])) {
|
||||||
return process.env[ext].map(suffix => `${process.env.SERVER_ROOT}${suffix}`)
|
return [`${process.env.SERVER_ROOT}${process.env[ext]}`];
|
||||||
|
} else {
|
||||||
|
return process.env[ext].map(
|
||||||
|
(suffix) => `${process.env.SERVER_ROOT}${suffix}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleFlagAC (flag) {
|
export function toggleFlagAC(flag) {
|
||||||
return (appState) => ({
|
return (appState) => ({
|
||||||
...appState,
|
...appState,
|
||||||
flags: {
|
flags: {
|
||||||
...appState.flags,
|
...appState.flags,
|
||||||
[flag]: !appState.flags[flag]
|
[flag]: !appState.flags[flag],
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectTypeFromPath (path) {
|
export function selectTypeFromPath(path) {
|
||||||
let type
|
let type;
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case /\.(png|jpg)$/.test(path):
|
case /\.(png|jpg)$/.test(path):
|
||||||
type = 'Image'; break
|
type = "Image";
|
||||||
|
break;
|
||||||
case /\.(mp4)$/.test(path):
|
case /\.(mp4)$/.test(path):
|
||||||
type = 'Video'; break
|
type = "Video";
|
||||||
|
break;
|
||||||
case /\.(md)$/.test(path):
|
case /\.(md)$/.test(path):
|
||||||
type = 'Text'; break
|
type = "Text";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
type = 'Unknown'; break
|
type = "Unknown";
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return { type, path }
|
return { type, path };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function typeForPath (path) {
|
export function typeForPath(path) {
|
||||||
let type
|
let type;
|
||||||
path = path.trim()
|
path = path.trim();
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case /\.((png)|(jpg)|(jpeg))$/.test(path):
|
case /\.((png)|(jpg)|(jpeg))$/.test(path):
|
||||||
type = 'Image'; break
|
type = "Image";
|
||||||
|
break;
|
||||||
case /\.(mp4)$/.test(path):
|
case /\.(mp4)$/.test(path):
|
||||||
type = 'Video'; break
|
type = "Video";
|
||||||
|
break;
|
||||||
case /\.(md)$/.test(path):
|
case /\.(md)$/.test(path):
|
||||||
type = 'Text'; break
|
type = "Text";
|
||||||
|
break;
|
||||||
case /\.(pdf)$/.test(path):
|
case /\.(pdf)$/.test(path):
|
||||||
type = 'Document'; break
|
type = "Document";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
type = 'Unknown'; break
|
type = "Unknown";
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return type
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectTypeFromPathWithPoster (path, poster) {
|
export function selectTypeFromPathWithPoster(path, poster) {
|
||||||
return { type: typeForPath(path), path, poster }
|
return { type: typeForPath(path), path, poster };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isIdentical (obj1, obj2) {
|
export function isIdentical(obj1, obj2) {
|
||||||
return hash(obj1) === hash(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
|
/* 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
|
* 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
|
* overlaying of events builds up a 'heat map' of the event space, where
|
||||||
* darker areas represent more events with proportion */
|
* darker areas represent more events with proportion */
|
||||||
const base = num >= 1 ? 0.9 : 0
|
const base = num >= 1 ? 0.9 : 0;
|
||||||
return base + (Math.min(0.5, 0.08 * (num - 1)))
|
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,
|
/* 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
|
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. */
|
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.
|
/* 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
|
Just like with opacity, we use a multiplication factor to ensure that clusters with higher point
|
||||||
counts appear larger. */
|
counts appear larger. */
|
||||||
const maxSize = totalPoints > 60 ? 40 : 20
|
const maxSize = totalPoints > 60 ? 40 : 20;
|
||||||
return Math.min(maxSize, 10 + (pointCount / totalPoints) * 150)
|
return Math.min(maxSize, 10 + (pointCount / totalPoints) * 150);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateTotalClusterPoints (clusters) {
|
export function calculateTotalClusterPoints(clusters) {
|
||||||
return clusters.reduce((total, cl) => {
|
return clusters.reduce((total, cl) => {
|
||||||
if (cl && cl.properties && cl.properties.cluster) {
|
if (cl && cl.properties && cl.properties.cluster) {
|
||||||
total += cl.properties.point_count
|
total += cl.properties.point_count;
|
||||||
}
|
}
|
||||||
return total
|
return total;
|
||||||
}, 0)
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isLatitude (lat) {
|
export function isLatitude(lat) {
|
||||||
return !!lat && isFinite(lat) && Math.abs(lat) <= 90
|
return !!lat && isFinite(lat) && Math.abs(lat) <= 90;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isLongitude (lng) {
|
export function isLongitude(lng) {
|
||||||
return !!lng && isFinite(lng) && Math.abs(lng) <= 180
|
return !!lng && isFinite(lng) && Math.abs(lng) <= 180;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapClustersToLocations (clusters, locations) {
|
export function mapClustersToLocations(clusters, locations) {
|
||||||
return clusters.reduce((acc, cl) => {
|
return clusters.reduce((acc, cl) => {
|
||||||
const foundLocation = locations.find(location => location.label === cl.properties.id)
|
const foundLocation = locations.find(
|
||||||
if (foundLocation) acc.push(foundLocation)
|
(location) => location.label === cl.properties.id
|
||||||
return acc
|
);
|
||||||
}, [])
|
if (foundLocation) acc.push(foundLocation);
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loops through a set of either locations or events
|
* Loops through a set of either locations or events
|
||||||
* and calculates the proportionate percentage of every given association in relation to the coloring set
|
* and calculates the proportionate percentage of every given association in relation to the coloring set
|
||||||
*/
|
*/
|
||||||
export function calculateColorPercentages (set, coloringSet) {
|
export function calculateColorPercentages(set, coloringSet) {
|
||||||
if (coloringSet.length === 0) return [1]
|
if (coloringSet.length === 0) return [1];
|
||||||
const associationMap = {}
|
const associationMap = {};
|
||||||
|
|
||||||
for (const [idx, value] of coloringSet.entries()) {
|
for (const [idx, value] of coloringSet.entries()) {
|
||||||
for (let filter of value) {
|
for (let filter of value) {
|
||||||
associationMap[filter] = idx
|
associationMap[filter] = idx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const associationCounts = new Array(coloringSet.length)
|
const associationCounts = new Array(coloringSet.length);
|
||||||
associationCounts.fill(0)
|
associationCounts.fill(0);
|
||||||
|
|
||||||
let totalAssociations = 0
|
let totalAssociations = 0;
|
||||||
|
|
||||||
set.forEach(item => {
|
set.forEach((item) => {
|
||||||
let innerSet = 'events' in item ? item.events : item
|
let innerSet = "events" in item ? item.events : item;
|
||||||
|
|
||||||
if (!Array.isArray(innerSet)) innerSet = [innerSet]
|
if (!Array.isArray(innerSet)) innerSet = [innerSet];
|
||||||
|
|
||||||
innerSet.forEach(val => {
|
innerSet.forEach((val) => {
|
||||||
val.associations.forEach(a => {
|
val.associations.forEach((a) => {
|
||||||
const idx = associationMap[a]
|
const idx = associationMap[a];
|
||||||
if (!idx && idx !== 0) return
|
if (!idx && idx !== 0) return;
|
||||||
associationCounts[idx] += 1
|
associationCounts[idx] += 1;
|
||||||
totalAssociations += 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']]
|
* Example coloringSet = [['Chemical', 'Tear Gas'], ['Procedural', 'Destruction of property']]
|
||||||
*/
|
*/
|
||||||
export function getFilterIdxFromColorSet (filter, coloringSet) {
|
export function getFilterIdxFromColorSet(filter, coloringSet) {
|
||||||
let filterIdx = -1
|
let filterIdx = -1;
|
||||||
coloringSet.map((set, idx) => {
|
coloringSet.map((set, idx) => {
|
||||||
const foundIdx = set.indexOf(filter)
|
const foundIdx = set.indexOf(filter);
|
||||||
if (foundIdx !== -1) filterIdx = idx
|
if (foundIdx !== -1) filterIdx = idx;
|
||||||
})
|
});
|
||||||
return filterIdx
|
return filterIdx;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dateMin = function () {
|
export const dateMin = function () {
|
||||||
return Array.prototype.slice.call(arguments).reduce(function (a, b) {
|
return Array.prototype.slice.call(arguments).reduce(function (a, b) {
|
||||||
return a < b ? a : b
|
return a < b ? a : b;
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const dateMax = function () {
|
export const dateMax = function () {
|
||||||
return Array.prototype.slice.call(arguments).reduce(function (a, b) {
|
return Array.prototype.slice.call(arguments).reduce(function (a, b) {
|
||||||
return a > b ? a : b
|
return a > b ? a : b;
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
/** Taken from
|
/** Taken from
|
||||||
* https://stackoverflow.com/questions/22697936/binary-search-in-javascript
|
* https://stackoverflow.com/questions/22697936/binary-search-in-javascript
|
||||||
* **/
|
* **/
|
||||||
export function binarySearch (ar, el, compareFn) {
|
export function binarySearch(ar, el, compareFn) {
|
||||||
var m = 0
|
var m = 0;
|
||||||
var n = ar.length - 1
|
var n = ar.length - 1;
|
||||||
while (m <= n) {
|
while (m <= n) {
|
||||||
var k = (n + m) >> 1
|
var k = (n + m) >> 1;
|
||||||
var cmp = compareFn(el, ar[k])
|
var cmp = compareFn(el, ar[k]);
|
||||||
if (cmp > 0) {
|
if (cmp > 0) {
|
||||||
m = k + 1
|
m = k + 1;
|
||||||
} else if (cmp < 0) {
|
} else if (cmp < 0) {
|
||||||
n = k - 1
|
n = k - 1;
|
||||||
} else {
|
} else {
|
||||||
return k
|
return k;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -m - 1
|
return -m - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeNiceDate (datetime) {
|
export function makeNiceDate(datetime) {
|
||||||
if (datetime === null) return null
|
if (datetime === null) return null;
|
||||||
// see https://stackoverflow.com/questions/3552461/how-to-format-a-javascript-date
|
// see https://stackoverflow.com/questions/3552461/how-to-format-a-javascript-date
|
||||||
const dateTimeFormat = new Intl.DateTimeFormat(
|
const dateTimeFormat = new Intl.DateTimeFormat(language, {
|
||||||
language,
|
year: "numeric",
|
||||||
{ year: 'numeric', month: 'long', day: '2-digit' }
|
month: "long",
|
||||||
)
|
day: "2-digit",
|
||||||
|
});
|
||||||
const [
|
const [
|
||||||
{ value: month },,
|
{ value: month },
|
||||||
{ value: day },,
|
,
|
||||||
{ value: year }
|
{ value: day },
|
||||||
] = dateTimeFormat.formatToParts(datetime)
|
,
|
||||||
|
{ 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.
|
* Sets the default locale for d3 to format dates in each available language.
|
||||||
* @param {Object} d3 - An instance of D3
|
* @param {Object} d3 - An instance of D3
|
||||||
*/
|
*/
|
||||||
export function setD3Locale (d3) {
|
export function setD3Locale(d3) {
|
||||||
const languages = {
|
const languages = {
|
||||||
'es-MX': require('./data/es-MX.json')
|
"es-MX": require("./data/es-MX.json"),
|
||||||
}
|
};
|
||||||
|
|
||||||
if (language !== 'es-US' && languages[language]) {
|
if (language !== "es-US" && languages[language]) {
|
||||||
d3.timeFormatDefaultLocale(languages[language])
|
d3.timeFormatDefaultLocale(languages[language]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,69 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
import { connect } from 'react-redux'
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
import * as selectors from '../selectors'
|
import * as selectors from "../selectors";
|
||||||
import { getFilterIdxFromColorSet } from '../common/utilities'
|
import { getFilterIdxFromColorSet } from "../common/utilities";
|
||||||
// import Card from './Card.jsx'
|
// import Card from './Card.jsx'
|
||||||
import { Card } from '@forensic-architecture/design-system/react'
|
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 {
|
class CardStack extends React.Component {
|
||||||
constructor () {
|
constructor() {
|
||||||
super()
|
super();
|
||||||
this.refs = {}
|
this.refs = {};
|
||||||
this.refCardStack = React.createRef()
|
this.refCardStack = React.createRef();
|
||||||
this.refCardStackContent = React.createRef()
|
this.refCardStackContent = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate () {
|
componentDidUpdate() {
|
||||||
const isNarrative = !!this.props.narrative
|
const isNarrative = !!this.props.narrative;
|
||||||
|
|
||||||
if (isNarrative) {
|
if (isNarrative) {
|
||||||
this.scrollToCard()
|
this.scrollToCard();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToCard () {
|
scrollToCard() {
|
||||||
const duration = 500
|
const duration = 500;
|
||||||
const element = this.refCardStack.current
|
const element = this.refCardStack.current;
|
||||||
const cardScroll = this.refs[this.props.narrative.current].current
|
const cardScroll = this.refs[this.props.narrative.current].current
|
||||||
.offsetTop
|
.offsetTop;
|
||||||
|
|
||||||
let start = element.scrollTop
|
let start = element.scrollTop;
|
||||||
let change = cardScroll - start
|
let change = cardScroll - start;
|
||||||
let currentTime = 0
|
let currentTime = 0;
|
||||||
const increment = 20
|
const increment = 20;
|
||||||
|
|
||||||
// t = current time
|
// t = current time
|
||||||
// b = start value
|
// b = start value
|
||||||
// c = change in value
|
// c = change in value
|
||||||
// d = duration
|
// d = duration
|
||||||
Math.easeInOutQuad = function (t, b, c, d) {
|
Math.easeInOutQuad = function (t, b, c, d) {
|
||||||
t /= d / 2
|
t /= d / 2;
|
||||||
if (t < 1) return (c / 2) * t * t + b
|
if (t < 1) return (c / 2) * t * t + b;
|
||||||
t -= 1
|
t -= 1;
|
||||||
return (-c / 2) * (t * (t - 2) - 1) + b
|
return (-c / 2) * (t * (t - 2) - 1) + b;
|
||||||
}
|
};
|
||||||
|
|
||||||
const animateScroll = function () {
|
const animateScroll = function () {
|
||||||
currentTime += increment
|
currentTime += increment;
|
||||||
const val = Math.easeInOutQuad(currentTime, start, change, duration)
|
const val = Math.easeInOutQuad(currentTime, start, change, duration);
|
||||||
element.scrollTop = val
|
element.scrollTop = val;
|
||||||
if (currentTime < duration) setTimeout(animateScroll, increment)
|
if (currentTime < duration) setTimeout(animateScroll, increment);
|
||||||
}
|
};
|
||||||
animateScroll()
|
animateScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCards (events, selections) {
|
renderCards(events, selections) {
|
||||||
// if no selections provided, select all
|
// if no selections provided, select all
|
||||||
if (!selections) {
|
if (!selections) {
|
||||||
selections = events.map((e) => true)
|
selections = events.map((e) => true);
|
||||||
}
|
}
|
||||||
this.refs = []
|
this.refs = [];
|
||||||
|
|
||||||
return events.map((event, idx) => {
|
return events.map((event, idx) => {
|
||||||
const thisRef = React.createRef()
|
const thisRef = React.createRef();
|
||||||
this.refs[idx] = thisRef
|
this.refs[idx] = thisRef;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@@ -72,109 +72,109 @@ class CardStack extends React.Component {
|
|||||||
event,
|
event,
|
||||||
colors: this.props.colors,
|
colors: this.props.colors,
|
||||||
coloringSet: this.props.coloringSet,
|
coloringSet: this.props.coloringSet,
|
||||||
getFilterIdxFromColorSet
|
getFilterIdxFromColorSet,
|
||||||
})}
|
})}
|
||||||
language={this.props.language}
|
language={this.props.language}
|
||||||
isLoading={this.props.isLoading}
|
isLoading={this.props.isLoading}
|
||||||
isSelected={selections[idx]}
|
isSelected={selections[idx]}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSelectedCards () {
|
renderSelectedCards() {
|
||||||
const { selected } = this.props
|
const { selected } = this.props;
|
||||||
|
|
||||||
if (selected.length > 0) {
|
if (selected.length > 0) {
|
||||||
return this.renderCards(selected)
|
return this.renderCards(selected);
|
||||||
}
|
}
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderNarrativeCards () {
|
renderNarrativeCards() {
|
||||||
const { narrative } = this.props
|
const { narrative } = this.props;
|
||||||
const showing = narrative.steps
|
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 () {
|
renderCardStackHeader() {
|
||||||
const headerLang = copy[this.props.language].cardstack.header
|
const headerLang = copy[this.props.language].cardstack.header;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id='card-stack-header'
|
id="card-stack-header"
|
||||||
className='card-stack-header'
|
className="card-stack-header"
|
||||||
onClick={() => this.props.onToggleCardstack()}
|
onClick={() => this.props.onToggleCardstack()}
|
||||||
>
|
>
|
||||||
<button className='side-menu-burg is-active'>
|
<button className="side-menu-burg is-active">
|
||||||
<span />
|
<span />
|
||||||
</button>
|
</button>
|
||||||
<p className='header-copy top'>
|
<p className="header-copy top">
|
||||||
{`${this.props.selected.length} ${headerLang}`}
|
{`${this.props.selected.length} ${headerLang}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCardStackContent () {
|
renderCardStackContent() {
|
||||||
return (
|
return (
|
||||||
<div id='card-stack-content' className='card-stack-content'>
|
<div id="card-stack-content" className="card-stack-content">
|
||||||
<ul>{this.renderSelectedCards()}</ul>
|
<ul>{this.renderSelectedCards()}</ul>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderNarrativeContent () {
|
renderNarrativeContent() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id='card-stack-content'
|
id="card-stack-content"
|
||||||
className='card-stack-content'
|
className="card-stack-content"
|
||||||
ref={this.refCardStackContent}
|
ref={this.refCardStackContent}
|
||||||
>
|
>
|
||||||
<ul>{this.renderNarrativeCards()}</ul>
|
<ul>{this.renderNarrativeCards()}</ul>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { isCardstack, selected, narrative, timelineDims } = this.props
|
const { isCardstack, selected, narrative, timelineDims } = this.props;
|
||||||
// TODO: make '237px', which is the narrative header, less hard-coded
|
// 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 (selected.length > 0) {
|
||||||
if (!narrative) {
|
if (!narrative) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id='card-stack'
|
id="card-stack"
|
||||||
className={`card-stack
|
className={`card-stack
|
||||||
${isCardstack ? '' : ' folded'}`}
|
${isCardstack ? "" : " folded"}`}
|
||||||
>
|
>
|
||||||
{this.renderCardStackHeader()}
|
{this.renderCardStackHeader()}
|
||||||
{this.renderCardStackContent()}
|
{this.renderCardStackContent()}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id='card-stack'
|
id="card-stack"
|
||||||
ref={this.refCardStack}
|
ref={this.refCardStack}
|
||||||
className={`card-stack narrative-mode
|
className={`card-stack narrative-mode
|
||||||
${isCardstack ? '' : ' folded'}`}
|
${isCardstack ? "" : " folded"}`}
|
||||||
style={{ height }}
|
style={{ height }}
|
||||||
>
|
>
|
||||||
{this.renderNarrativeContent()}
|
{this.renderNarrativeContent()}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div />
|
return <div />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps (state) {
|
function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
narrative: selectors.selectActiveNarrative(state),
|
narrative: selectors.selectActiveNarrative(state),
|
||||||
selected: selectors.selectSelected(state),
|
selected: selectors.selectSelected(state),
|
||||||
@@ -185,8 +185,8 @@ function mapStateToProps (state) {
|
|||||||
cardUI: state.ui.card,
|
cardUI: state.ui.card,
|
||||||
colors: state.ui.coloring.colors,
|
colors: state.ui.coloring.colors,
|
||||||
coloringSet: state.app.associations.coloringSet,
|
coloringSet: state.app.associations.coloringSet,
|
||||||
features: state.features
|
features: state.features,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps)(CardStack)
|
export default connect(mapStateToProps)(CardStack);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from "react";
|
||||||
import { Portal } from 'react-portal'
|
import { Portal } from "react-portal";
|
||||||
import colors from '../../../common/global.js'
|
import colors from "../../../common/global.js";
|
||||||
import ColoredMarkers from './ColoredMarkers.jsx'
|
import ColoredMarkers from "./ColoredMarkers.jsx";
|
||||||
import {
|
import {
|
||||||
calcClusterOpacity,
|
calcClusterOpacity,
|
||||||
calcClusterSize,
|
calcClusterSize,
|
||||||
@@ -9,18 +9,30 @@ import {
|
|||||||
isLongitude,
|
isLongitude,
|
||||||
calculateColorPercentages,
|
calculateColorPercentages,
|
||||||
zipColorsToPercentages,
|
zipColorsToPercentages,
|
||||||
calculateTotalClusterPoints } from '../../../common/utilities'
|
calculateTotalClusterPoints,
|
||||||
|
} from "../../../common/utilities";
|
||||||
|
|
||||||
const DefsClusters = () => (
|
const DefsClusters = () => (
|
||||||
<defs>
|
<defs>
|
||||||
<radialGradient id='clusterGradient'>
|
<radialGradient id="clusterGradient">
|
||||||
<stop offset='10%' stop-color='red' />
|
<stop offset="10%" stop-color="red" />
|
||||||
<stop offset='90%' stop-color='transparent' />
|
<stop offset="90%" stop-color="transparent" />
|
||||||
</radialGradient>
|
</radialGradient>
|
||||||
</defs>
|
</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: {
|
geometry: {
|
||||||
@@ -35,22 +47,25 @@ function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHove
|
|||||||
type: "Feature"
|
type: "Feature"
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
const { cluster_id: clusterId } = cluster.properties
|
const { cluster_id: clusterId } = cluster.properties;
|
||||||
|
|
||||||
const individualChildren = getClusterChildren(clusterId)
|
const individualChildren = getClusterChildren(clusterId);
|
||||||
const colorPercentages = calculateColorPercentages(individualChildren, coloringSet)
|
const colorPercentages = calculateColorPercentages(
|
||||||
|
individualChildren,
|
||||||
|
coloringSet
|
||||||
|
);
|
||||||
|
|
||||||
const { coordinates } = cluster.geometry
|
const { coordinates } = cluster.geometry;
|
||||||
const [longitude, latitude] = coordinates
|
const [longitude, latitude] = coordinates;
|
||||||
if (!isLatitude(latitude) || !isLongitude(longitude)) return null
|
const { x, y } = projectPoint([latitude, longitude]);
|
||||||
const { x, y } = projectPoint([latitude, longitude])
|
const [hovered, setHovered] = useState(false);
|
||||||
const [hovered, setHovered] = useState(false)
|
if (!isLatitude(latitude) || !isLongitude(longitude)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g
|
<g
|
||||||
className={'cluster-event'}
|
className={"cluster-event"}
|
||||||
transform={`translate(${x}, ${y})`}
|
transform={`translate(${x}, ${y})`}
|
||||||
onClick={e => onClick({ id: clusterId, latitude, longitude })}
|
onClick={(e) => onClick({ id: clusterId, latitude, longitude })}
|
||||||
onMouseEnter={() => setHovered(true)}
|
onMouseEnter={() => setHovered(true)}
|
||||||
onMouseLeave={() => setHovered(false)}
|
onMouseLeave={() => setHovered(false)}
|
||||||
>
|
>
|
||||||
@@ -58,16 +73,16 @@ function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHove
|
|||||||
radius={size}
|
radius={size}
|
||||||
colorPercentMap={zipColorsToPercentages(filterColors, colorPercentages)}
|
colorPercentMap={zipColorsToPercentages(filterColors, colorPercentages)}
|
||||||
styles={{
|
styles={{
|
||||||
...styles
|
...styles,
|
||||||
}}
|
}}
|
||||||
className={'cluster-event-marker'}
|
className={"cluster-event-marker"}
|
||||||
/>
|
/>
|
||||||
{hovered ? renderHover(cluster) : null}
|
{hovered ? renderHover(cluster) : null}
|
||||||
</g>
|
</g>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ClusterEvents ({
|
function ClusterEvents({
|
||||||
projectPoint,
|
projectPoint,
|
||||||
onSelect,
|
onSelect,
|
||||||
getClusterChildren,
|
getClusterChildren,
|
||||||
@@ -76,56 +91,66 @@ function ClusterEvents ({
|
|||||||
svg,
|
svg,
|
||||||
clusters,
|
clusters,
|
||||||
filterColors,
|
filterColors,
|
||||||
selected
|
selected,
|
||||||
}) {
|
}) {
|
||||||
const totalPoints = calculateTotalClusterPoints(clusters)
|
const totalPoints = calculateTotalClusterPoints(clusters);
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
fill: isRadial ? "url('#clusterGradient')" : colors.fallbackEventColor,
|
fill: isRadial ? "url('#clusterGradient')" : colors.fallbackEventColor,
|
||||||
stroke: colors.darkBackground,
|
stroke: colors.darkBackground,
|
||||||
strokeWidth: 0
|
strokeWidth: 0,
|
||||||
}
|
};
|
||||||
|
|
||||||
function renderHover (txt, circleSize) {
|
function renderHover(txt, circleSize) {
|
||||||
return <>
|
return (
|
||||||
<text text-anchor='middle' y='3px' style={{ fontWeight: 'bold', fill: 'black', zIndex: 10000 }}>{txt}</text>
|
<>
|
||||||
<circle
|
<text
|
||||||
class='event-hover'
|
text-anchor="middle"
|
||||||
cx='0'
|
y="3px"
|
||||||
cy='0'
|
style={{ fontWeight: "bold", fill: "black", zIndex: 10000 }}
|
||||||
r={circleSize + 2}
|
>
|
||||||
stroke={colors.primaryHighlight}
|
{txt}
|
||||||
fill-opacity='0.0'
|
</text>
|
||||||
/>
|
<circle
|
||||||
</>
|
class="event-hover"
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r={circleSize + 2}
|
||||||
|
stroke={colors.primaryHighlight}
|
||||||
|
fill-opacity="0.0"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal node={svg}>
|
<Portal node={svg}>
|
||||||
<g className='cluster-locations'>
|
<g className="cluster-locations">
|
||||||
{isRadial ? <DefsClusters /> : null}
|
{isRadial ? <DefsClusters /> : null}
|
||||||
{clusters.map(c => {
|
{clusters.map((c) => {
|
||||||
const pointCount = c.properties.point_count
|
const pointCount = c.properties.point_count;
|
||||||
const clusterSize = calcClusterSize(pointCount, totalPoints)
|
const clusterSize = calcClusterSize(pointCount, totalPoints);
|
||||||
return <Cluster
|
return (
|
||||||
onClick={onSelect}
|
<Cluster
|
||||||
getClusterChildren={getClusterChildren}
|
onClick={onSelect}
|
||||||
coloringSet={coloringSet}
|
getClusterChildren={getClusterChildren}
|
||||||
cluster={c}
|
coloringSet={coloringSet}
|
||||||
filterColors={filterColors}
|
cluster={c}
|
||||||
size={clusterSize}
|
filterColors={filterColors}
|
||||||
projectPoint={projectPoint}
|
size={clusterSize}
|
||||||
totalPoints={totalPoints}
|
projectPoint={projectPoint}
|
||||||
styles={{
|
totalPoints={totalPoints}
|
||||||
...styles,
|
styles={{
|
||||||
fillOpacity: calcClusterOpacity(pointCount, totalPoints)
|
...styles,
|
||||||
}}
|
fillOpacity: calcClusterOpacity(pointCount, totalPoints),
|
||||||
renderHover={() => renderHover(pointCount, clusterSize)}
|
}}
|
||||||
/>
|
renderHover={() => renderHover(pointCount, clusterSize)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</g>
|
</g>
|
||||||
</Portal>
|
</Portal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ClusterEvents
|
export default ClusterEvents;
|
||||||
|
|||||||
@@ -1,35 +1,49 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from "react-dom";
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from "react-redux";
|
||||||
import store from './store/index.js'
|
import store from "./store/index.js";
|
||||||
import App from './components/App.jsx'
|
import App from "./components/App.jsx";
|
||||||
|
|
||||||
|
console.log(process.env);
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<App />
|
<App />
|
||||||
</Provider>,
|
</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
|
// Expressions from https://exceptionshub.com/how-to-detect-safari-chrome-ie-firefox-and-opera-browser.html
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// Opera 8.0+
|
// 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+
|
// Firefox 1.0+
|
||||||
const isFirefox = typeof InstallTrigger !== 'undefined'
|
const isFirefox = typeof InstallTrigger !== "undefined";
|
||||||
// Safari 3.0+ "[object HTMLElementConstructor]"
|
// 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
|
// Internet Explorer 6-11
|
||||||
const isIE = /* @cc_on!@ */false || !!document.documentMode
|
const isIE = /* @cc_on!@ */ false || !!document.documentMode;
|
||||||
// Edge 20+
|
// Edge 20+
|
||||||
const isEdge = !isIE && !!window.StyleMedia
|
const isEdge = !isIE && !!window.StyleMedia;
|
||||||
// Chrome 1+
|
// Chrome 1+
|
||||||
const isChrome = !!window.chrome && !!window.chrome.webstore
|
const isChrome = !!window.chrome && !!window.chrome.webstore;
|
||||||
// Blink engine detection
|
// Blink engine detection
|
||||||
const isBlink = (isChrome || isOpera) && !!window.CSS
|
const isBlink = (isChrome || isOpera) && !!window.CSS;
|
||||||
|
|
||||||
if (isEdge || isIE) {
|
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 */
|
/* eslint-enable */
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
const webpack = require('webpack')
|
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
|
||||||
|
|
||||||
const devMode = process.env.NODE_ENV !== 'production'
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
const APP_DIR = path.resolve(__dirname, './src')
|
|
||||||
const BUILD_DIR = path.resolve(__dirname, './build')
|
|
||||||
|
|
||||||
/** env variables from config.js */
|
|
||||||
const CONFIG = process.env.CONFIG || 'config.js'
|
|
||||||
const envConfig = require('./' + CONFIG)
|
|
||||||
const userConfig = {}
|
|
||||||
const userFeatures = {}
|
|
||||||
for (const k in envConfig) {
|
|
||||||
userConfig[k] = JSON.stringify(envConfig[k])
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const k in envConfig['features']) {
|
|
||||||
userFeatures[k] = JSON.stringify(envConfig['features'][k])
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
entry: {
|
|
||||||
index: `${APP_DIR}/index.jsx`
|
|
||||||
},
|
|
||||||
devtool: 'source-map',
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.scss$/,
|
|
||||||
include: `${APP_DIR}`,
|
|
||||||
use: [
|
|
||||||
devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
|
|
||||||
'css-loader',
|
|
||||||
'sass-loader'
|
|
||||||
]
|
|
||||||
}, {
|
|
||||||
test: /\.js(x)?$/,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
include: `${APP_DIR}`,
|
|
||||||
use: {
|
|
||||||
loader: 'babel-loader'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
test: /\.(eot|svg|otf|ttf|woff|woff2|png)$/,
|
|
||||||
use: {
|
|
||||||
loader: 'file-loader'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
net: 'empty',
|
|
||||||
tls: 'empty',
|
|
||||||
dns: 'empty'
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['*', '.js']
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: BUILD_DIR,
|
|
||||||
filename: 'js/[name].bundle.js'
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env': {
|
|
||||||
...userConfig,
|
|
||||||
'NODE_ENV': JSON.stringify('production'),
|
|
||||||
'features': userFeatures
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
new MiniCssExtractPlugin({
|
|
||||||
filename: devMode ? '[name].css' : '[name].[hash].css',
|
|
||||||
chunkFilename: devMode ? '[id].css' : '[id].[hash].css'
|
|
||||||
}),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
template: './index.html'
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = config
|
|
||||||
Reference in New Issue
Block a user