Ingesting config through Create React App

This commit is contained in:
Zac Ioannidis
2020-12-07 19:28:07 +00:00
parent 00d840a65b
commit 3a54cd7df5
18 changed files with 1401 additions and 458 deletions

View File

@@ -1,3 +0,0 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}

1
.env Normal file
View File

@@ -0,0 +1 @@
FAST_REFRESH=true

File diff suppressed because one or more lines are too long

125
config/env.js Normal file
View 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 were 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;

View 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';
},
};

View 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
View 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
View 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
View 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
);
};

View File

@@ -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
View 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
View 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
View 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);

View File

@@ -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]);
} }
} }

View File

@@ -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);

View File

@@ -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;

View File

@@ -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 */

View File

@@ -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