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": "",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"react-scripts:start": "react-scripts start",
|
||||
"react-scripts:build": "react-scripts build",
|
||||
"react-scripts:eject": "react-scripts eject",
|
||||
"react-scripts:start": "node scripts/start.js",
|
||||
"react-scripts:build": "node scripts/build.js",
|
||||
"react-scripts:eject": "node scripts/eject.js",
|
||||
"dev": "webpack-dev-server --content-base static --mode development",
|
||||
"dev:wsl": "npm run dev -- --host 0.0.0.0",
|
||||
"build": "NODE_ENV=production webpack --mode production",
|
||||
@@ -17,28 +17,84 @@
|
||||
"lint:fix": "npm run lint -- --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "7.12.3",
|
||||
"@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",
|
||||
"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",
|
||||
"leaflet": "^1.0.3",
|
||||
"marked": "^0.7.0",
|
||||
"mini-css-extract-plugin": "0.11.3",
|
||||
"moment": "^2.26.0",
|
||||
"object-hash": "^1.3.0",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.4",
|
||||
"pnp-webpack-plugin": "1.6.4",
|
||||
"postcss-flexbugs-fixes": "4.2.1",
|
||||
"postcss-loader": "3.0.0",
|
||||
"postcss-normalize": "8.0.1",
|
||||
"postcss-preset-env": "6.7.0",
|
||||
"postcss-safe-parser": "5.0.2",
|
||||
"prompts": "2.4.0",
|
||||
"ramda": "^0.26.1",
|
||||
"react": "^16.13.1",
|
||||
"react-app-polyfill": "^2.0.0",
|
||||
"react-dev-utils": "^11.0.1",
|
||||
"react-device-detect": "^1.6.2",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-image": "^1.5.1",
|
||||
"react-portal": "^4.2.0",
|
||||
"react-redux": "^5.0.4",
|
||||
"react-scripts": "^4.0.1",
|
||||
"react-refresh": "^0.8.3",
|
||||
"react-tabs": "3.0.0",
|
||||
"redux": "^3.6.0",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"reselect": "^3.0.1",
|
||||
"resolve": "1.18.1",
|
||||
"resolve-url-loader": "^3.1.2",
|
||||
"sass-loader": "8.0.2",
|
||||
"semver": "7.3.2",
|
||||
"style-loader": "1.3.0",
|
||||
"supercluster": "^7.1.0",
|
||||
"terser-webpack-plugin": "4.2.3",
|
||||
"ts-pnp": "1.2.0",
|
||||
"url-loader": "4.1.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": {
|
||||
"ava": "1.0.0-beta.8",
|
||||
@@ -67,5 +123,63 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"roots": [
|
||||
"<rootDir>/src"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.{js,jsx,ts,tsx}",
|
||||
"!src/**/*.d.ts"
|
||||
],
|
||||
"setupFiles": [
|
||||
"react-app-polyfill/jsdom"
|
||||
],
|
||||
"setupFilesAfterEnv": [],
|
||||
"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 hash from 'object-hash'
|
||||
import moment from "moment";
|
||||
import hash from "object-hash";
|
||||
|
||||
let { DATE_FMT, TIME_FMT } = process.env
|
||||
if (!DATE_FMT) DATE_FMT = 'MM/DD/YYYY'
|
||||
if (!TIME_FMT) TIME_FMT = 'HH:mm'
|
||||
let { DATE_FMT, TIME_FMT } = process.env;
|
||||
if (!DATE_FMT) DATE_FMT = "MM/DD/YYYY";
|
||||
if (!TIME_FMT) TIME_FMT = "HH:mm";
|
||||
|
||||
export const language = process.env.store.app.language || 'en-US'
|
||||
console.log(process.env);
|
||||
export const language = process.env.store.app.language || "en-US";
|
||||
|
||||
export function calcDatetime(date, time) {
|
||||
if (!time) time = '00:00'
|
||||
const dt = moment(`${date} ${time}`, `${DATE_FMT} ${TIME_FMT}`)
|
||||
return dt.toDate()
|
||||
if (!time) time = "00:00";
|
||||
const dt = moment(`${date} ${time}`, `${DATE_FMT} ${TIME_FMT}`);
|
||||
return dt.toDate();
|
||||
}
|
||||
|
||||
export function getCoordinatesForPercent(radius, percent) {
|
||||
const x = radius * Math.cos(2 * Math.PI * percent)
|
||||
const y = radius * Math.sin(2 * Math.PI * percent)
|
||||
return [x, y]
|
||||
const x = radius * Math.cos(2 * Math.PI * percent);
|
||||
const y = radius * Math.sin(2 * Math.PI * percent);
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,12 +28,13 @@ export function getCoordinatesForPercent (radius, percent) {
|
||||
* Return value:
|
||||
* ex. {'#fff': 0.5, '#000': 0.5, ...} */
|
||||
export function zipColorsToPercentages(colors, percentages) {
|
||||
if (colors.length < percentages.length) throw new Error('You must declare an appropriate number of filter colors')
|
||||
if (colors.length < percentages.length)
|
||||
throw new Error("You must declare an appropriate number of filter colors");
|
||||
|
||||
return percentages.reduce((map, percent, idx) => {
|
||||
map[colors[idx]] = percent
|
||||
return map
|
||||
}, {})
|
||||
map[colors[idx]] = percent;
|
||||
return map;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,16 +44,16 @@ export function zipColorsToPercentages (colors, percentages) {
|
||||
* @param {string} url: url passed as variable, defaults to window.location.href
|
||||
*/
|
||||
export function getParameterByName(name, url) {
|
||||
if (!url) url = window.location.href
|
||||
name = name.replace(/[[\]]/g, `\\$&`)
|
||||
if (!url) url = window.location.href;
|
||||
name = name.replace(/[[\]]/g, `\\$&`);
|
||||
|
||||
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`)
|
||||
const results = regex.exec(url)
|
||||
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
|
||||
const results = regex.exec(url);
|
||||
|
||||
if (!results) return null
|
||||
if (!results[2]) return ''
|
||||
if (!results) return null;
|
||||
if (!results[2]) return "";
|
||||
|
||||
return decodeURIComponent(results[2].replace(/\+/g, ' '))
|
||||
return decodeURIComponent(results[2].replace(/\+/g, " "));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,9 +62,12 @@ export function getParameterByName (name, url) {
|
||||
* @param {array} arr2: array of numbers
|
||||
*/
|
||||
export function areEqual(arr1, arr2) {
|
||||
return ((arr1.length === arr2.length) && arr1.every((element, index) => {
|
||||
return element === arr2[index]
|
||||
}))
|
||||
return (
|
||||
arr1.length === arr2.length &&
|
||||
arr1.every((element, index) => {
|
||||
return element === arr2[index];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,21 +75,21 @@ export function areEqual (arr1, arr2) {
|
||||
* @param {object} 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
|
||||
*/
|
||||
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) {
|
||||
if (string.length > stringNum) {
|
||||
return string.substring(0, 120) + '...'
|
||||
return string.substring(0, 120) + "...";
|
||||
}
|
||||
return string
|
||||
return string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,15 +102,15 @@ export function trimAndEllipse (string, stringNum) {
|
||||
*/
|
||||
export function getFilterParents(associations, filter) {
|
||||
for (let a of associations) {
|
||||
const { filter_paths: fp } = a
|
||||
const { filter_paths: fp } = a;
|
||||
if (a.id === filter) {
|
||||
return fp.slice(0, fp.length - 1)
|
||||
return fp.slice(0, fp.length - 1);
|
||||
}
|
||||
const filterIndex = fp.indexOf(filter)
|
||||
if (filterIndex === 0) return []
|
||||
if (filterIndex > 0) return fp.slice(0, filterIndex)
|
||||
const filterIndex = fp.indexOf(filter);
|
||||
if (filterIndex === 0) return [];
|
||||
if (filterIndex > 0) return fp.slice(0, filterIndex);
|
||||
}
|
||||
throw new Error('Attempted to get parents of nonexistent filter')
|
||||
throw new Error("Attempted to get parents of nonexistent filter");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,52 +118,53 @@ export function getFilterParents (associations, filter) {
|
||||
* This is the filter's most immediate ancestor.
|
||||
*/
|
||||
export function getImmediateFilterParent(associations, filter) {
|
||||
const parents = getFilterParents(associations, filter)
|
||||
if (parents.length === 0) return null
|
||||
return parents[parents.length - 1]
|
||||
const parents = getFilterParents(associations, filter);
|
||||
if (parents.length === 0) return null;
|
||||
return parents[parents.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Grab a meta filter's siblings, by way of the the `filter_path` hierarcy.
|
||||
*/
|
||||
export function getMetaFilterSiblings(allFilters, filterParent, filterKey) {
|
||||
const idxParent = allFilters.map(f => {
|
||||
const idxParent = allFilters
|
||||
.map((f) => {
|
||||
return f.filter_paths.reduceRight((acc, path, idx) => {
|
||||
if (path === filterParent) return f.filter_paths[idx + 1]
|
||||
return acc
|
||||
}, null)
|
||||
if (path === filterParent) return f.filter_paths[idx + 1];
|
||||
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.
|
||||
*/
|
||||
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) {
|
||||
return getMetaFilterSiblings(allFilters, filterParent, filterKey)
|
||||
return getMetaFilterSiblings(allFilters, filterParent, filterKey);
|
||||
}
|
||||
|
||||
return allFilters.reduce((acc, val) => {
|
||||
const valParent = getImmediateFilterParent(allFilters, val.id)
|
||||
if (valParent === filterParent && val.id !== filterKey) acc.push(val.id)
|
||||
return acc
|
||||
}, [])
|
||||
const valParent = getImmediateFilterParent(allFilters, val.id);
|
||||
if (valParent === filterParent && val.id !== filterKey) acc.push(val.id);
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function getEventCategories(event, categories) {
|
||||
const matchedCategories = []
|
||||
const matchedCategories = [];
|
||||
if (event.associations && event.associations.length > 0) {
|
||||
event.associations.reduce((acc, val) => {
|
||||
const foundCategory = categories.find(cat => cat.id === val)
|
||||
if (foundCategory) acc.push(foundCategory)
|
||||
return acc
|
||||
}, matchedCategories)
|
||||
const foundCategory = categories.find((cat) => cat.id === val);
|
||||
if (foundCategory) acc.push(foundCategory);
|
||||
return acc;
|
||||
}, matchedCategories);
|
||||
}
|
||||
return matchedCategories
|
||||
return matchedCategories;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,19 +175,19 @@ export function getEventCategories (event, categories) {
|
||||
*/
|
||||
export function insetSourceFrom(allSources) {
|
||||
return (event) => {
|
||||
let sources
|
||||
let sources;
|
||||
if (!event.sources) {
|
||||
sources = []
|
||||
sources = [];
|
||||
} else {
|
||||
sources = event.sources.map(id => {
|
||||
return allSources.hasOwnProperty(id) ? allSources[id] : null
|
||||
})
|
||||
sources = event.sources.map((id) => {
|
||||
return allSources.hasOwnProperty(id) ? allSources[id] : null;
|
||||
});
|
||||
}
|
||||
return {
|
||||
...event,
|
||||
sources
|
||||
}
|
||||
}
|
||||
sources,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,24 +195,28 @@ export function insetSourceFrom (allSources) {
|
||||
* view that source modal by default
|
||||
*/
|
||||
export function injectSource(id) {
|
||||
return state => {
|
||||
return (state) => {
|
||||
return {
|
||||
...state,
|
||||
app: {
|
||||
...state.app,
|
||||
source: state.domain.sources[id]
|
||||
}
|
||||
}
|
||||
}
|
||||
source: state.domain.sources[id],
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function urlFromEnv(ext) {
|
||||
if (process.env[ext]) {
|
||||
if (!Array.isArray(process.env[ext])) { return [`${process.env.SERVER_ROOT}${process.env[ext]}`] } else {
|
||||
return process.env[ext].map(suffix => `${process.env.SERVER_ROOT}${suffix}`)
|
||||
if (!Array.isArray(process.env[ext])) {
|
||||
return [`${process.env.SERVER_ROOT}${process.env[ext]}`];
|
||||
} else {
|
||||
return process.env[ext].map(
|
||||
(suffix) => `${process.env.SERVER_ROOT}${suffix}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,50 +225,59 @@ export function toggleFlagAC (flag) {
|
||||
...appState,
|
||||
flags: {
|
||||
...appState.flags,
|
||||
[flag]: !appState.flags[flag]
|
||||
}
|
||||
})
|
||||
[flag]: !appState.flags[flag],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function selectTypeFromPath(path) {
|
||||
let type
|
||||
let type;
|
||||
switch (true) {
|
||||
case /\.(png|jpg)$/.test(path):
|
||||
type = 'Image'; break
|
||||
type = "Image";
|
||||
break;
|
||||
case /\.(mp4)$/.test(path):
|
||||
type = 'Video'; break
|
||||
type = "Video";
|
||||
break;
|
||||
case /\.(md)$/.test(path):
|
||||
type = 'Text'; break
|
||||
type = "Text";
|
||||
break;
|
||||
default:
|
||||
type = 'Unknown'; break
|
||||
type = "Unknown";
|
||||
break;
|
||||
}
|
||||
return { type, path }
|
||||
return { type, path };
|
||||
}
|
||||
|
||||
export function typeForPath(path) {
|
||||
let type
|
||||
path = path.trim()
|
||||
let type;
|
||||
path = path.trim();
|
||||
switch (true) {
|
||||
case /\.((png)|(jpg)|(jpeg))$/.test(path):
|
||||
type = 'Image'; break
|
||||
type = "Image";
|
||||
break;
|
||||
case /\.(mp4)$/.test(path):
|
||||
type = 'Video'; break
|
||||
type = "Video";
|
||||
break;
|
||||
case /\.(md)$/.test(path):
|
||||
type = 'Text'; break
|
||||
type = "Text";
|
||||
break;
|
||||
case /\.(pdf)$/.test(path):
|
||||
type = 'Document'; break
|
||||
type = "Document";
|
||||
break;
|
||||
default:
|
||||
type = 'Unknown'; break
|
||||
type = "Unknown";
|
||||
break;
|
||||
}
|
||||
return type
|
||||
return type;
|
||||
}
|
||||
|
||||
export function selectTypeFromPathWithPoster(path, poster) {
|
||||
return { type: typeForPath(path), path, poster }
|
||||
return { type: typeForPath(path), path, poster };
|
||||
}
|
||||
|
||||
export function isIdentical(obj1, obj2) {
|
||||
return hash(obj1) === hash(obj2)
|
||||
return hash(obj1) === hash(obj2);
|
||||
}
|
||||
|
||||
export function calcOpacity(num) {
|
||||
@@ -266,48 +285,50 @@ export function calcOpacity (num) {
|
||||
* other events there are in the same render. The idea here is that the
|
||||
* overlaying of events builds up a 'heat map' of the event space, where
|
||||
* darker areas represent more events with proportion */
|
||||
const base = num >= 1 ? 0.9 : 0
|
||||
return base + (Math.min(0.5, 0.08 * (num - 1)))
|
||||
const base = num >= 1 ? 0.9 : 0;
|
||||
return base + Math.min(0.5, 0.08 * (num - 1));
|
||||
}
|
||||
|
||||
export function calcClusterOpacity(pointCount, totalPoints) {
|
||||
/* Clusters represent multiple events within a specific radius. The darker the cluster,
|
||||
the larger the number of underlying events. We use a multiplication factor (50) here as well
|
||||
to ensure that the larger clusters have an appropriately darker shading. */
|
||||
return Math.min(0.85, 0.08 + (pointCount / totalPoints) * 50)
|
||||
return Math.min(0.85, 0.08 + (pointCount / totalPoints) * 50);
|
||||
}
|
||||
|
||||
export function calcClusterSize(pointCount, totalPoints) {
|
||||
/* The larger the cluster size, the higher the count of points that the cluster represents.
|
||||
Just like with opacity, we use a multiplication factor to ensure that clusters with higher point
|
||||
counts appear larger. */
|
||||
const maxSize = totalPoints > 60 ? 40 : 20
|
||||
return Math.min(maxSize, 10 + (pointCount / totalPoints) * 150)
|
||||
const maxSize = totalPoints > 60 ? 40 : 20;
|
||||
return Math.min(maxSize, 10 + (pointCount / totalPoints) * 150);
|
||||
}
|
||||
|
||||
export function calculateTotalClusterPoints(clusters) {
|
||||
return clusters.reduce((total, cl) => {
|
||||
if (cl && cl.properties && cl.properties.cluster) {
|
||||
total += cl.properties.point_count
|
||||
total += cl.properties.point_count;
|
||||
}
|
||||
return total
|
||||
}, 0)
|
||||
return total;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
export function isLatitude(lat) {
|
||||
return !!lat && isFinite(lat) && Math.abs(lat) <= 90
|
||||
return !!lat && isFinite(lat) && Math.abs(lat) <= 90;
|
||||
}
|
||||
|
||||
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) {
|
||||
return clusters.reduce((acc, cl) => {
|
||||
const foundLocation = locations.find(location => location.label === cl.properties.id)
|
||||
if (foundLocation) acc.push(foundLocation)
|
||||
return acc
|
||||
}, [])
|
||||
const foundLocation = locations.find(
|
||||
(location) => location.label === cl.properties.id
|
||||
);
|
||||
if (foundLocation) acc.push(foundLocation);
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -315,38 +336,38 @@ export function mapClustersToLocations (clusters, locations) {
|
||||
* and calculates the proportionate percentage of every given association in relation to the coloring set
|
||||
*/
|
||||
export function calculateColorPercentages(set, coloringSet) {
|
||||
if (coloringSet.length === 0) return [1]
|
||||
const associationMap = {}
|
||||
if (coloringSet.length === 0) return [1];
|
||||
const associationMap = {};
|
||||
|
||||
for (const [idx, value] of coloringSet.entries()) {
|
||||
for (let filter of value) {
|
||||
associationMap[filter] = idx
|
||||
associationMap[filter] = idx;
|
||||
}
|
||||
}
|
||||
|
||||
const associationCounts = new Array(coloringSet.length)
|
||||
associationCounts.fill(0)
|
||||
const associationCounts = new Array(coloringSet.length);
|
||||
associationCounts.fill(0);
|
||||
|
||||
let totalAssociations = 0
|
||||
let totalAssociations = 0;
|
||||
|
||||
set.forEach(item => {
|
||||
let innerSet = 'events' in item ? item.events : item
|
||||
set.forEach((item) => {
|
||||
let innerSet = "events" in item ? item.events : item;
|
||||
|
||||
if (!Array.isArray(innerSet)) innerSet = [innerSet]
|
||||
if (!Array.isArray(innerSet)) innerSet = [innerSet];
|
||||
|
||||
innerSet.forEach(val => {
|
||||
val.associations.forEach(a => {
|
||||
const idx = associationMap[a]
|
||||
if (!idx && idx !== 0) return
|
||||
associationCounts[idx] += 1
|
||||
totalAssociations += 1
|
||||
})
|
||||
})
|
||||
})
|
||||
innerSet.forEach((val) => {
|
||||
val.associations.forEach((a) => {
|
||||
const idx = associationMap[a];
|
||||
if (!idx && idx !== 0) return;
|
||||
associationCounts[idx] += 1;
|
||||
totalAssociations += 1;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (totalAssociations === 0) return [1]
|
||||
if (totalAssociations === 0) return [1];
|
||||
|
||||
return associationCounts.map(count => count / totalAssociations)
|
||||
return associationCounts.map((count) => count / totalAssociations);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -355,60 +376,63 @@ export function calculateColorPercentages (set, coloringSet) {
|
||||
* Example coloringSet = [['Chemical', 'Tear Gas'], ['Procedural', 'Destruction of property']]
|
||||
*/
|
||||
export function getFilterIdxFromColorSet(filter, coloringSet) {
|
||||
let filterIdx = -1
|
||||
let filterIdx = -1;
|
||||
coloringSet.map((set, idx) => {
|
||||
const foundIdx = set.indexOf(filter)
|
||||
if (foundIdx !== -1) filterIdx = idx
|
||||
})
|
||||
return filterIdx
|
||||
const foundIdx = set.indexOf(filter);
|
||||
if (foundIdx !== -1) filterIdx = idx;
|
||||
});
|
||||
return filterIdx;
|
||||
}
|
||||
|
||||
export const dateMin = function () {
|
||||
return Array.prototype.slice.call(arguments).reduce(function (a, b) {
|
||||
return a < b ? a : b
|
||||
})
|
||||
}
|
||||
return a < b ? a : b;
|
||||
});
|
||||
};
|
||||
|
||||
export const dateMax = function () {
|
||||
return Array.prototype.slice.call(arguments).reduce(function (a, b) {
|
||||
return a > b ? a : b
|
||||
})
|
||||
}
|
||||
return a > b ? a : b;
|
||||
});
|
||||
};
|
||||
|
||||
/** Taken from
|
||||
* https://stackoverflow.com/questions/22697936/binary-search-in-javascript
|
||||
* **/
|
||||
export function binarySearch(ar, el, compareFn) {
|
||||
var m = 0
|
||||
var n = ar.length - 1
|
||||
var m = 0;
|
||||
var n = ar.length - 1;
|
||||
while (m <= n) {
|
||||
var k = (n + m) >> 1
|
||||
var cmp = compareFn(el, ar[k])
|
||||
var k = (n + m) >> 1;
|
||||
var cmp = compareFn(el, ar[k]);
|
||||
if (cmp > 0) {
|
||||
m = k + 1
|
||||
m = k + 1;
|
||||
} else if (cmp < 0) {
|
||||
n = k - 1
|
||||
n = k - 1;
|
||||
} else {
|
||||
return k
|
||||
return k;
|
||||
}
|
||||
}
|
||||
return -m - 1
|
||||
return -m - 1;
|
||||
}
|
||||
|
||||
export function makeNiceDate(datetime) {
|
||||
if (datetime === null) return null
|
||||
if (datetime === null) return null;
|
||||
// see https://stackoverflow.com/questions/3552461/how-to-format-a-javascript-date
|
||||
const dateTimeFormat = new Intl.DateTimeFormat(
|
||||
language,
|
||||
{ year: 'numeric', month: 'long', day: '2-digit' }
|
||||
)
|
||||
const dateTimeFormat = new Intl.DateTimeFormat(language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "2-digit",
|
||||
});
|
||||
const [
|
||||
{ value: month },,
|
||||
{ value: day },,
|
||||
{ value: year }
|
||||
] = dateTimeFormat.formatToParts(datetime)
|
||||
{ value: month },
|
||||
,
|
||||
{ value: day },
|
||||
,
|
||||
{ value: year },
|
||||
] = dateTimeFormat.formatToParts(datetime);
|
||||
|
||||
return `${day} ${month}, ${year}`
|
||||
return `${day} ${month}, ${year}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -417,10 +441,10 @@ export function makeNiceDate (datetime) {
|
||||
*/
|
||||
export function setD3Locale(d3) {
|
||||
const languages = {
|
||||
'es-MX': require('./data/es-MX.json')
|
||||
}
|
||||
"es-MX": require("./data/es-MX.json"),
|
||||
};
|
||||
|
||||
if (language !== 'es-US' && languages[language]) {
|
||||
d3.timeFormatDefaultLocale(languages[language])
|
||||
if (language !== "es-US" && languages[language]) {
|
||||
d3.timeFormatDefaultLocale(languages[language]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +1,69 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import * as selectors from '../selectors'
|
||||
import { getFilterIdxFromColorSet } from '../common/utilities'
|
||||
import * as selectors from "../selectors";
|
||||
import { getFilterIdxFromColorSet } from "../common/utilities";
|
||||
// import Card from './Card.jsx'
|
||||
import { Card } from '@forensic-architecture/design-system/react'
|
||||
import copy from '../common/data/copy.json'
|
||||
import { Card } from "@forensic-architecture/design-system/react";
|
||||
import copy from "../common/data/copy.json";
|
||||
|
||||
class CardStack extends React.Component {
|
||||
constructor() {
|
||||
super()
|
||||
this.refs = {}
|
||||
this.refCardStack = React.createRef()
|
||||
this.refCardStackContent = React.createRef()
|
||||
super();
|
||||
this.refs = {};
|
||||
this.refCardStack = React.createRef();
|
||||
this.refCardStackContent = React.createRef();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const isNarrative = !!this.props.narrative
|
||||
const isNarrative = !!this.props.narrative;
|
||||
|
||||
if (isNarrative) {
|
||||
this.scrollToCard()
|
||||
this.scrollToCard();
|
||||
}
|
||||
}
|
||||
|
||||
scrollToCard() {
|
||||
const duration = 500
|
||||
const element = this.refCardStack.current
|
||||
const duration = 500;
|
||||
const element = this.refCardStack.current;
|
||||
const cardScroll = this.refs[this.props.narrative.current].current
|
||||
.offsetTop
|
||||
.offsetTop;
|
||||
|
||||
let start = element.scrollTop
|
||||
let change = cardScroll - start
|
||||
let currentTime = 0
|
||||
const increment = 20
|
||||
let start = element.scrollTop;
|
||||
let change = cardScroll - start;
|
||||
let currentTime = 0;
|
||||
const increment = 20;
|
||||
|
||||
// t = current time
|
||||
// b = start value
|
||||
// c = change in value
|
||||
// d = duration
|
||||
Math.easeInOutQuad = function (t, b, c, d) {
|
||||
t /= d / 2
|
||||
if (t < 1) return (c / 2) * t * t + b
|
||||
t -= 1
|
||||
return (-c / 2) * (t * (t - 2) - 1) + b
|
||||
}
|
||||
t /= d / 2;
|
||||
if (t < 1) return (c / 2) * t * t + b;
|
||||
t -= 1;
|
||||
return (-c / 2) * (t * (t - 2) - 1) + b;
|
||||
};
|
||||
|
||||
const animateScroll = function () {
|
||||
currentTime += increment
|
||||
const val = Math.easeInOutQuad(currentTime, start, change, duration)
|
||||
element.scrollTop = val
|
||||
if (currentTime < duration) setTimeout(animateScroll, increment)
|
||||
}
|
||||
animateScroll()
|
||||
currentTime += increment;
|
||||
const val = Math.easeInOutQuad(currentTime, start, change, duration);
|
||||
element.scrollTop = val;
|
||||
if (currentTime < duration) setTimeout(animateScroll, increment);
|
||||
};
|
||||
animateScroll();
|
||||
}
|
||||
|
||||
renderCards(events, selections) {
|
||||
// if no selections provided, select all
|
||||
if (!selections) {
|
||||
selections = events.map((e) => true)
|
||||
selections = events.map((e) => true);
|
||||
}
|
||||
this.refs = []
|
||||
this.refs = [];
|
||||
|
||||
return events.map((event, idx) => {
|
||||
const thisRef = React.createRef()
|
||||
this.refs[idx] = thisRef
|
||||
const thisRef = React.createRef();
|
||||
this.refs[idx] = thisRef;
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -72,105 +72,105 @@ class CardStack extends React.Component {
|
||||
event,
|
||||
colors: this.props.colors,
|
||||
coloringSet: this.props.coloringSet,
|
||||
getFilterIdxFromColorSet
|
||||
getFilterIdxFromColorSet,
|
||||
})}
|
||||
language={this.props.language}
|
||||
isLoading={this.props.isLoading}
|
||||
isSelected={selections[idx]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderSelectedCards() {
|
||||
const { selected } = this.props
|
||||
const { selected } = this.props;
|
||||
|
||||
if (selected.length > 0) {
|
||||
return this.renderCards(selected)
|
||||
return this.renderCards(selected);
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
renderNarrativeCards() {
|
||||
const { narrative } = this.props
|
||||
const showing = narrative.steps
|
||||
const { narrative } = this.props;
|
||||
const showing = narrative.steps;
|
||||
|
||||
const selections = showing.map((_, idx) => idx === narrative.current)
|
||||
const selections = showing.map((_, idx) => idx === narrative.current);
|
||||
|
||||
return this.renderCards(showing, selections)
|
||||
return this.renderCards(showing, selections);
|
||||
}
|
||||
|
||||
renderCardStackHeader() {
|
||||
const headerLang = copy[this.props.language].cardstack.header
|
||||
const headerLang = copy[this.props.language].cardstack.header;
|
||||
|
||||
return (
|
||||
<div
|
||||
id='card-stack-header'
|
||||
className='card-stack-header'
|
||||
id="card-stack-header"
|
||||
className="card-stack-header"
|
||||
onClick={() => this.props.onToggleCardstack()}
|
||||
>
|
||||
<button className='side-menu-burg is-active'>
|
||||
<button className="side-menu-burg is-active">
|
||||
<span />
|
||||
</button>
|
||||
<p className='header-copy top'>
|
||||
<p className="header-copy top">
|
||||
{`${this.props.selected.length} ${headerLang}`}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderCardStackContent() {
|
||||
return (
|
||||
<div id='card-stack-content' className='card-stack-content'>
|
||||
<div id="card-stack-content" className="card-stack-content">
|
||||
<ul>{this.renderSelectedCards()}</ul>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderNarrativeContent() {
|
||||
return (
|
||||
<div
|
||||
id='card-stack-content'
|
||||
className='card-stack-content'
|
||||
id="card-stack-content"
|
||||
className="card-stack-content"
|
||||
ref={this.refCardStackContent}
|
||||
>
|
||||
<ul>{this.renderNarrativeCards()}</ul>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isCardstack, selected, narrative, timelineDims } = this.props
|
||||
const { isCardstack, selected, narrative, timelineDims } = this.props;
|
||||
// TODO: make '237px', which is the narrative header, less hard-coded
|
||||
const height = `calc(100% - 237px - ${timelineDims.height}px)`
|
||||
const height = `calc(100% - 237px - ${timelineDims.height}px)`;
|
||||
if (selected.length > 0) {
|
||||
if (!narrative) {
|
||||
return (
|
||||
<div
|
||||
id='card-stack'
|
||||
id="card-stack"
|
||||
className={`card-stack
|
||||
${isCardstack ? '' : ' folded'}`}
|
||||
${isCardstack ? "" : " folded"}`}
|
||||
>
|
||||
{this.renderCardStackHeader()}
|
||||
{this.renderCardStackContent()}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
id='card-stack'
|
||||
id="card-stack"
|
||||
ref={this.refCardStack}
|
||||
className={`card-stack narrative-mode
|
||||
${isCardstack ? '' : ' folded'}`}
|
||||
${isCardstack ? "" : " folded"}`}
|
||||
style={{ height }}
|
||||
>
|
||||
{this.renderNarrativeContent()}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <div />
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,8 +185,8 @@ function mapStateToProps (state) {
|
||||
cardUI: state.ui.card,
|
||||
colors: state.ui.coloring.colors,
|
||||
coloringSet: state.app.associations.coloringSet,
|
||||
features: state.features
|
||||
}
|
||||
features: state.features,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(CardStack)
|
||||
export default connect(mapStateToProps)(CardStack);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Portal } from 'react-portal'
|
||||
import colors from '../../../common/global.js'
|
||||
import ColoredMarkers from './ColoredMarkers.jsx'
|
||||
import React, { useState } from "react";
|
||||
import { Portal } from "react-portal";
|
||||
import colors from "../../../common/global.js";
|
||||
import ColoredMarkers from "./ColoredMarkers.jsx";
|
||||
import {
|
||||
calcClusterOpacity,
|
||||
calcClusterSize,
|
||||
@@ -9,18 +9,30 @@ import {
|
||||
isLongitude,
|
||||
calculateColorPercentages,
|
||||
zipColorsToPercentages,
|
||||
calculateTotalClusterPoints } from '../../../common/utilities'
|
||||
calculateTotalClusterPoints,
|
||||
} from "../../../common/utilities";
|
||||
|
||||
const DefsClusters = () => (
|
||||
<defs>
|
||||
<radialGradient id='clusterGradient'>
|
||||
<stop offset='10%' stop-color='red' />
|
||||
<stop offset='90%' stop-color='transparent' />
|
||||
<radialGradient id="clusterGradient">
|
||||
<stop offset="10%" stop-color="red" />
|
||||
<stop offset="90%" stop-color="transparent" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
)
|
||||
);
|
||||
|
||||
function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHover, onClick, getClusterChildren, coloringSet, filterColors }) {
|
||||
function Cluster({
|
||||
cluster,
|
||||
size,
|
||||
projectPoint,
|
||||
totalPoints,
|
||||
styles,
|
||||
renderHover,
|
||||
onClick,
|
||||
getClusterChildren,
|
||||
coloringSet,
|
||||
filterColors,
|
||||
}) {
|
||||
/**
|
||||
{
|
||||
geometry: {
|
||||
@@ -35,22 +47,25 @@ function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHove
|
||||
type: "Feature"
|
||||
}
|
||||
*/
|
||||
const { cluster_id: clusterId } = cluster.properties
|
||||
const { cluster_id: clusterId } = cluster.properties;
|
||||
|
||||
const individualChildren = getClusterChildren(clusterId)
|
||||
const colorPercentages = calculateColorPercentages(individualChildren, coloringSet)
|
||||
const individualChildren = getClusterChildren(clusterId);
|
||||
const colorPercentages = calculateColorPercentages(
|
||||
individualChildren,
|
||||
coloringSet
|
||||
);
|
||||
|
||||
const { coordinates } = cluster.geometry
|
||||
const [longitude, latitude] = coordinates
|
||||
if (!isLatitude(latitude) || !isLongitude(longitude)) return null
|
||||
const { x, y } = projectPoint([latitude, longitude])
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const { coordinates } = cluster.geometry;
|
||||
const [longitude, latitude] = coordinates;
|
||||
const { x, y } = projectPoint([latitude, longitude]);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
if (!isLatitude(latitude) || !isLongitude(longitude)) return null;
|
||||
|
||||
return (
|
||||
<g
|
||||
className={'cluster-event'}
|
||||
className={"cluster-event"}
|
||||
transform={`translate(${x}, ${y})`}
|
||||
onClick={e => onClick({ id: clusterId, latitude, longitude })}
|
||||
onClick={(e) => onClick({ id: clusterId, latitude, longitude })}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
@@ -58,13 +73,13 @@ function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHove
|
||||
radius={size}
|
||||
colorPercentMap={zipColorsToPercentages(filterColors, colorPercentages)}
|
||||
styles={{
|
||||
...styles
|
||||
...styles,
|
||||
}}
|
||||
className={'cluster-event-marker'}
|
||||
className={"cluster-event-marker"}
|
||||
/>
|
||||
{hovered ? renderHover(cluster) : null}
|
||||
</g>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ClusterEvents({
|
||||
@@ -76,38 +91,47 @@ function ClusterEvents ({
|
||||
svg,
|
||||
clusters,
|
||||
filterColors,
|
||||
selected
|
||||
selected,
|
||||
}) {
|
||||
const totalPoints = calculateTotalClusterPoints(clusters)
|
||||
const totalPoints = calculateTotalClusterPoints(clusters);
|
||||
|
||||
const styles = {
|
||||
fill: isRadial ? "url('#clusterGradient')" : colors.fallbackEventColor,
|
||||
stroke: colors.darkBackground,
|
||||
strokeWidth: 0
|
||||
}
|
||||
strokeWidth: 0,
|
||||
};
|
||||
|
||||
function renderHover(txt, circleSize) {
|
||||
return <>
|
||||
<text text-anchor='middle' y='3px' style={{ fontWeight: 'bold', fill: 'black', zIndex: 10000 }}>{txt}</text>
|
||||
return (
|
||||
<>
|
||||
<text
|
||||
text-anchor="middle"
|
||||
y="3px"
|
||||
style={{ fontWeight: "bold", fill: "black", zIndex: 10000 }}
|
||||
>
|
||||
{txt}
|
||||
</text>
|
||||
<circle
|
||||
class='event-hover'
|
||||
cx='0'
|
||||
cy='0'
|
||||
class="event-hover"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r={circleSize + 2}
|
||||
stroke={colors.primaryHighlight}
|
||||
fill-opacity='0.0'
|
||||
fill-opacity="0.0"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal node={svg}>
|
||||
<g className='cluster-locations'>
|
||||
<g className="cluster-locations">
|
||||
{isRadial ? <DefsClusters /> : null}
|
||||
{clusters.map(c => {
|
||||
const pointCount = c.properties.point_count
|
||||
const clusterSize = calcClusterSize(pointCount, totalPoints)
|
||||
return <Cluster
|
||||
{clusters.map((c) => {
|
||||
const pointCount = c.properties.point_count;
|
||||
const clusterSize = calcClusterSize(pointCount, totalPoints);
|
||||
return (
|
||||
<Cluster
|
||||
onClick={onSelect}
|
||||
getClusterChildren={getClusterChildren}
|
||||
coloringSet={coloringSet}
|
||||
@@ -118,14 +142,15 @@ function ClusterEvents ({
|
||||
totalPoints={totalPoints}
|
||||
styles={{
|
||||
...styles,
|
||||
fillOpacity: calcClusterOpacity(pointCount, totalPoints)
|
||||
fillOpacity: calcClusterOpacity(pointCount, totalPoints),
|
||||
}}
|
||||
renderHover={() => renderHover(pointCount, clusterSize)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
</Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default ClusterEvents
|
||||
export default ClusterEvents;
|
||||
|
||||
@@ -1,35 +1,49 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { Provider } from 'react-redux'
|
||||
import store from './store/index.js'
|
||||
import App from './components/App.jsx'
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Provider } from "react-redux";
|
||||
import store from "./store/index.js";
|
||||
import App from "./components/App.jsx";
|
||||
|
||||
console.log(process.env);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
document.getElementById('explore-app')
|
||||
)
|
||||
document.getElementById("explore-app")
|
||||
);
|
||||
|
||||
// Expressions from https://exceptionshub.com/how-to-detect-safari-chrome-ie-firefox-and-opera-browser.html
|
||||
|
||||
/* eslint-disable */
|
||||
// Opera 8.0+
|
||||
const isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0
|
||||
const isOpera =
|
||||
(!!window.opr && !!opr.addons) ||
|
||||
!!window.opera ||
|
||||
navigator.userAgent.indexOf(" OPR/") >= 0;
|
||||
// Firefox 1.0+
|
||||
const isFirefox = typeof InstallTrigger !== 'undefined'
|
||||
const isFirefox = typeof InstallTrigger !== "undefined";
|
||||
// Safari 3.0+ "[object HTMLElementConstructor]"
|
||||
const isSafari = /constructor/i.test(window.HTMLElement) || (function (p) { return p.toString() === '[object SafariRemoteNotification]' })(!window['safari'] || (typeof safari !== 'undefined' && safari.pushNotification))
|
||||
const isSafari =
|
||||
/constructor/i.test(window.HTMLElement) ||
|
||||
(function (p) {
|
||||
return p.toString() === "[object SafariRemoteNotification]";
|
||||
})(
|
||||
!window["safari"] ||
|
||||
(typeof safari !== "undefined" && safari.pushNotification)
|
||||
);
|
||||
// Internet Explorer 6-11
|
||||
const isIE = /* @cc_on!@ */false || !!document.documentMode
|
||||
const isIE = /* @cc_on!@ */ false || !!document.documentMode;
|
||||
// Edge 20+
|
||||
const isEdge = !isIE && !!window.StyleMedia
|
||||
const isEdge = !isIE && !!window.StyleMedia;
|
||||
// Chrome 1+
|
||||
const isChrome = !!window.chrome && !!window.chrome.webstore
|
||||
const isChrome = !!window.chrome && !!window.chrome.webstore;
|
||||
// Blink engine detection
|
||||
const isBlink = (isChrome || isOpera) && !!window.CSS
|
||||
const isBlink = (isChrome || isOpera) && !!window.CSS;
|
||||
|
||||
if (isEdge || isIE) {
|
||||
alert('Please view this website in Opera for best viewing. It is untested in your browser.')
|
||||
alert(
|
||||
"Please view this website in Opera for best viewing. It is untested in your browser."
|
||||
);
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
@@ -1,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