chore(codestyle): cleanup and reformat

This commit is contained in:
Bastian Ike
2022-09-13 11:18:36 +02:00
committed by Bastian
parent 6081f1edbb
commit 37c43712d1
56 changed files with 1142 additions and 560 deletions

View File

@@ -1,3 +1,3 @@
{ {
"extends": ["react-app", "react-app/jest"] "extends": ["react-app", "react-app/jest", "prettier"]
} }

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
save-exact=true

View File

@@ -24,8 +24,8 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result; return result;
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
var fs = __importStar(require("fs-extra"));
var child_process_1 = require("child_process"); var child_process_1 = require("child_process");
var fs = __importStar(require("fs-extra"));
var paths = __importStar(require("./paths")); var paths = __importStar(require("./paths"));
// Do this as the first thing so that any code reading it knows the right env. // Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = "production"; process.env.BABEL_ENV = "production";

View File

@@ -83,16 +83,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.createRadar = void 0; exports.createRadar = void 0;
var fs_extra_1 = require("fs-extra");
var fs_1 = require("fs");
var path = __importStar(require("path"));
var front_matter_1 = __importDefault(require("front-matter")); var front_matter_1 = __importDefault(require("front-matter"));
// @ts-ignore esModuleInterop is activated in tsconfig.scripts.json, but IDE typescript uses default typescript config var fs_1 = require("fs");
var marked_1 = require("marked"); var fs_extra_1 = require("fs-extra");
var highlight_js_1 = __importDefault(require("highlight.js")); var highlight_js_1 = __importDefault(require("highlight.js"));
var file_1 = require("./file"); var marked_1 = require("marked");
var path = __importStar(require("path"));
var model_1 = require("../../src/model"); var model_1 = require("../../src/model");
var paths_1 = require("../paths"); var paths_1 = require("../paths");
var file_1 = require("./file");
marked_1.marked.setOptions({ marked_1.marked.setOptions({
highlight: function (code) { return highlight_js_1.default.highlightAuto(code).value; }, highlight: function (code) { return highlight_js_1.default.highlightAuto(code).value; },
}); });
@@ -180,7 +179,7 @@ var createItems = function (revisions) {
return __assign(__assign({}, items), (_a = {}, _a[revision.name] = addRevisionToItem(items[revision.name], revision), _a)); return __assign(__assign({}, items), (_a = {}, _a[revision.name] = addRevisionToItem(items[revision.name], revision), _a));
}, {}); }, {});
return Object.values(itemMap) return Object.values(itemMap)
.map(function (item) { return (__assign(__assign({}, item), { "title": item.title || item.name })); }) .map(function (item) { return (__assign(__assign({}, item), { title: item.title || item.name })); })
.sort(function (x, y) { return (x.name > y.name ? 1 : -1); }); .sort(function (x, y) { return (x.name > y.name ? 1 : -1); });
}; };
var ignoreEmptyRevisionBody = function (revision, item) { var ignoreEmptyRevisionBody = function (revision, item) {
@@ -208,7 +207,10 @@ var addRevisionToItem = function (item, revision) {
return newItem; return newItem;
}; };
var revisionCreatesNewHistoryEntry = function (revision, item) { var revisionCreatesNewHistoryEntry = function (revision, item) {
return revision.body.trim() !== "" || (typeof revision.ring !== "undefined" && revision.ring !== item.ring) || (typeof revision.quadrant !== "undefined" && revision.quadrant !== item.quadrant); return (revision.body.trim() !== "" ||
(typeof revision.ring !== "undefined" && revision.ring !== item.ring) ||
(typeof revision.quadrant !== "undefined" &&
revision.quadrant !== item.quadrant));
}; };
var flagItem = function (items, allReleases) { var flagItem = function (items, allReleases) {
return items.map(function (item) { return items.map(function (item) {

View File

@@ -1,8 +1,8 @@
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.appNodeModules = exports.appYarnLock = exports.appPublic = exports.appBuild = exports.appRdJson = exports.templateNodeModules = exports.templateBuild = exports.template = exports.radarJson = void 0; exports.appNodeModules = exports.appYarnLock = exports.appPublic = exports.appBuild = exports.appRdJson = exports.templateNodeModules = exports.templateBuild = exports.template = exports.radarJson = void 0;
var path_1 = require("path");
var fs_1 = require("fs"); var fs_1 = require("fs");
var path_1 = require("path");
exports.radarJson = "rd.json"; exports.radarJson = "rd.json";
var appDirectory = (0, fs_1.realpathSync)(process.cwd()); var appDirectory = (0, fs_1.realpathSync)(process.cwd());
var resolveApp = function (relativePath) { var resolveApp = function (relativePath) {

389
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "aoe_technology_radar", "name": "aoe_technology_radar",
"version": "3.3.2", "version": "3.3.3",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "aoe_technology_radar", "name": "aoe_technology_radar",
"version": "3.3.2", "version": "3.3.3",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@apideck/better-ajv-errors": "0.3.6", "@apideck/better-ajv-errors": "0.3.6",
@@ -15,7 +15,6 @@
"@types/d3": "7.4.0", "@types/d3": "7.4.0",
"@types/fs-extra": "9.0.13", "@types/fs-extra": "9.0.13",
"@types/jest": "29.0.1", "@types/jest": "29.0.1",
"@types/node": "17.0.33",
"@types/react": "18.0.19", "@types/react": "18.0.19",
"@types/react-dom": "18.0.6", "@types/react-dom": "18.0.6",
"@types/sanitize-html": "2.6.2", "@types/sanitize-html": "2.6.2",
@@ -48,9 +47,14 @@
"aoe_technology_radar-generateJson": "dist_scripts/scripts/generateJson.js" "aoe_technology_radar-generateJson": "dist_scripts/scripts/generateJson.js"
}, },
"devDependencies": { "devDependencies": {
"@trivago/prettier-plugin-sort-imports": "3.3.0",
"@types/marked": "4.0.7",
"@types/node": "18.7.17",
"@typescript-eslint/parser": "5.37.0", "@typescript-eslint/parser": "5.37.0",
"eslint": "8.23.1", "eslint": "8.23.1",
"eslint-config-prettier": "8.5.0",
"eslint-config-react-app": "7.0.1", "eslint-config-react-app": "7.0.1",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.31.8", "eslint-plugin-react": "7.31.8",
"yallist": "4.0.0" "yallist": "4.0.0"
} }
@@ -3319,6 +3323,141 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/@trivago/prettier-plugin-sort-imports": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-3.3.0.tgz",
"integrity": "sha512-1y44bVZuIN0RsS3oIiGd5k8Vm3IZXYZnp4VsP2Z/S5L9WAOw43HE2clso66M2S/dDeJ+8sKPqnHsEfh39Vjs3w==",
"dev": true,
"dependencies": {
"@babel/core": "7.17.8",
"@babel/generator": "7.17.7",
"@babel/parser": "7.17.8",
"@babel/traverse": "7.17.3",
"@babel/types": "7.17.0",
"javascript-natural-sort": "0.7.1",
"lodash": "4.17.21"
},
"peerDependencies": {
"prettier": "2.x"
}
},
"node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/core": {
"version": "7.17.8",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.8.tgz",
"integrity": "sha512-OdQDV/7cRBtJHLSOBqqbYNkOcydOgnX59TZx4puf41fzcVtN3e/4yqY8lMQsK+5X2lJtAdmA+6OHqsj1hBJ4IQ==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.16.7",
"@babel/generator": "^7.17.7",
"@babel/helper-compilation-targets": "^7.17.7",
"@babel/helper-module-transforms": "^7.17.7",
"@babel/helpers": "^7.17.8",
"@babel/parser": "^7.17.8",
"@babel/template": "^7.16.7",
"@babel/traverse": "^7.17.3",
"@babel/types": "^7.17.0",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
"json5": "^2.1.2",
"semver": "^6.3.0"
},
"engines": {
"node": ">=6.9.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/babel"
}
},
"node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/generator": {
"version": "7.17.7",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz",
"integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==",
"dev": true,
"dependencies": {
"@babel/types": "^7.17.0",
"jsesc": "^2.5.1",
"source-map": "^0.5.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/parser": {
"version": "7.17.8",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.8.tgz",
"integrity": "sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/traverse": {
"version": "7.17.3",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz",
"integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.16.7",
"@babel/generator": "^7.17.3",
"@babel/helper-environment-visitor": "^7.16.7",
"@babel/helper-function-name": "^7.16.7",
"@babel/helper-hoist-variables": "^7.16.7",
"@babel/helper-split-export-declaration": "^7.16.7",
"@babel/parser": "^7.17.3",
"@babel/types": "^7.17.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/types": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
"integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.16.7",
"to-fast-properties": "^2.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@trivago/prettier-plugin-sort-imports/node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/@trivago/prettier-plugin-sort-imports/node_modules/semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@trivago/prettier-plugin-sort-imports/node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@trysound/sax": { "node_modules/@trysound/sax": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
@@ -3916,15 +4055,21 @@
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4="
}, },
"node_modules/@types/marked": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.7.tgz",
"integrity": "sha512-eEAhnz21CwvKVW+YvRvcTuFKNU9CV1qH+opcgVK3pIMI6YZzDm6gc8o2vHjldFk6MGKt5pueSB7IOpvpx5Qekw==",
"dev": true
},
"node_modules/@types/mime": { "node_modules/@types/mime": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw=="
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "17.0.33", "version": "18.7.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.33.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.17.tgz",
"integrity": "sha512-miWq2m2FiQZmaHfdZNcbpp9PuXg34W5JZ5CrJ/BaS70VuhoJENBEQybeiYSaPBRNq6KQGnjfEnc/F3PN++D+XQ==" "integrity": "sha512-0UyfUnt02zIuqp7yC8RYtDkp/vo8bFaQ13KkSEvUAohPOAlnVNbj5Fi3fgPSuwzakS+EvvnnZ4x9y7i6ASaSPQ=="
}, },
"node_modules/@types/parse-json": { "node_modules/@types/parse-json": {
"version": "4.0.0", "version": "4.0.0",
@@ -7292,6 +7437,18 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/eslint-config-prettier": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz",
"integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==",
"dev": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
"peerDependencies": {
"eslint": ">=7.0.0"
}
},
"node_modules/eslint-config-react-app": { "node_modules/eslint-config-react-app": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz",
@@ -7483,6 +7640,27 @@
"node": ">=6.0" "node": ">=6.0"
} }
}, },
"node_modules/eslint-plugin-prettier": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
"integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==",
"dev": true,
"dependencies": {
"prettier-linter-helpers": "^1.0.0"
},
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"eslint": ">=7.28.0",
"prettier": ">=2.0.0"
},
"peerDependenciesMeta": {
"eslint-config-prettier": {
"optional": true
}
}
},
"node_modules/eslint-plugin-react": { "node_modules/eslint-plugin-react": {
"version": "7.31.8", "version": "7.31.8",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz",
@@ -7958,6 +8136,12 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
}, },
"node_modules/fast-diff": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
"dev": true
},
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.2.11", "version": "3.2.11",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
@@ -9589,6 +9773,12 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/javascript-natural-sort": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
"dev": true
},
"node_modules/jest": { "node_modules/jest": {
"version": "27.5.1", "version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz",
@@ -12942,6 +13132,34 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/prettier": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
"dev": true,
"peer": true,
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-linter-helpers": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
"dev": true,
"dependencies": {
"fast-diff": "^1.1.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/pretty-bytes": { "node_modules/pretty-bytes": {
"version": "5.6.0", "version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
@@ -18345,6 +18563,109 @@
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
"integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw=="
}, },
"@trivago/prettier-plugin-sort-imports": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-3.3.0.tgz",
"integrity": "sha512-1y44bVZuIN0RsS3oIiGd5k8Vm3IZXYZnp4VsP2Z/S5L9WAOw43HE2clso66M2S/dDeJ+8sKPqnHsEfh39Vjs3w==",
"dev": true,
"requires": {
"@babel/core": "7.17.8",
"@babel/generator": "7.17.7",
"@babel/parser": "7.17.8",
"@babel/traverse": "7.17.3",
"@babel/types": "7.17.0",
"javascript-natural-sort": "0.7.1",
"lodash": "4.17.21"
},
"dependencies": {
"@babel/core": {
"version": "7.17.8",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.8.tgz",
"integrity": "sha512-OdQDV/7cRBtJHLSOBqqbYNkOcydOgnX59TZx4puf41fzcVtN3e/4yqY8lMQsK+5X2lJtAdmA+6OHqsj1hBJ4IQ==",
"dev": true,
"requires": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.16.7",
"@babel/generator": "^7.17.7",
"@babel/helper-compilation-targets": "^7.17.7",
"@babel/helper-module-transforms": "^7.17.7",
"@babel/helpers": "^7.17.8",
"@babel/parser": "^7.17.8",
"@babel/template": "^7.16.7",
"@babel/traverse": "^7.17.3",
"@babel/types": "^7.17.0",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
"json5": "^2.1.2",
"semver": "^6.3.0"
}
},
"@babel/generator": {
"version": "7.17.7",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz",
"integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==",
"dev": true,
"requires": {
"@babel/types": "^7.17.0",
"jsesc": "^2.5.1",
"source-map": "^0.5.0"
}
},
"@babel/parser": {
"version": "7.17.8",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.8.tgz",
"integrity": "sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ==",
"dev": true
},
"@babel/traverse": {
"version": "7.17.3",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz",
"integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.16.7",
"@babel/generator": "^7.17.3",
"@babel/helper-environment-visitor": "^7.16.7",
"@babel/helper-function-name": "^7.16.7",
"@babel/helper-hoist-variables": "^7.16.7",
"@babel/helper-split-export-declaration": "^7.16.7",
"@babel/parser": "^7.17.3",
"@babel/types": "^7.17.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
}
},
"@babel/types": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
"integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.16.7",
"to-fast-properties": "^2.0.0"
}
},
"globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
},
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
"dev": true
}
}
},
"@trysound/sax": { "@trysound/sax": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
@@ -18901,15 +19222,21 @@
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4="
}, },
"@types/marked": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.7.tgz",
"integrity": "sha512-eEAhnz21CwvKVW+YvRvcTuFKNU9CV1qH+opcgVK3pIMI6YZzDm6gc8o2vHjldFk6MGKt5pueSB7IOpvpx5Qekw==",
"dev": true
},
"@types/mime": { "@types/mime": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw=="
}, },
"@types/node": { "@types/node": {
"version": "17.0.33", "version": "18.7.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.33.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.17.tgz",
"integrity": "sha512-miWq2m2FiQZmaHfdZNcbpp9PuXg34W5JZ5CrJ/BaS70VuhoJENBEQybeiYSaPBRNq6KQGnjfEnc/F3PN++D+XQ==" "integrity": "sha512-0UyfUnt02zIuqp7yC8RYtDkp/vo8bFaQ13KkSEvUAohPOAlnVNbj5Fi3fgPSuwzakS+EvvnnZ4x9y7i6ASaSPQ=="
}, },
"@types/parse-json": { "@types/parse-json": {
"version": "4.0.0", "version": "4.0.0",
@@ -21450,6 +21777,13 @@
} }
} }
}, },
"eslint-config-prettier": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz",
"integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==",
"dev": true,
"requires": {}
},
"eslint-config-react-app": { "eslint-config-react-app": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz",
@@ -21599,6 +21933,15 @@
} }
} }
}, },
"eslint-plugin-prettier": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
"integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==",
"dev": true,
"requires": {
"prettier-linter-helpers": "^1.0.0"
}
},
"eslint-plugin-react": { "eslint-plugin-react": {
"version": "7.31.8", "version": "7.31.8",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz",
@@ -21861,6 +22204,12 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
}, },
"fast-diff": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
"dev": true
},
"fast-glob": { "fast-glob": {
"version": "3.2.11", "version": "3.2.11",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
@@ -23013,6 +23362,12 @@
} }
} }
}, },
"javascript-natural-sort": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
"dev": true
},
"jest": { "jest": {
"version": "27.5.1", "version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz",
@@ -25338,6 +25693,22 @@
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="
}, },
"prettier": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
"dev": true,
"peer": true
},
"prettier-linter-helpers": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
"dev": true,
"requires": {
"fast-diff": "^1.1.2"
}
},
"pretty-bytes": { "pretty-bytes": {
"version": "5.6.0", "version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",

View File

@@ -29,7 +29,6 @@
"@types/d3": "7.4.0", "@types/d3": "7.4.0",
"@types/fs-extra": "9.0.13", "@types/fs-extra": "9.0.13",
"@types/jest": "29.0.1", "@types/jest": "29.0.1",
"@types/node": "17.0.33",
"@types/react": "18.0.19", "@types/react": "18.0.19",
"@types/react-dom": "18.0.6", "@types/react-dom": "18.0.6",
"@types/sanitize-html": "2.6.2", "@types/sanitize-html": "2.6.2",
@@ -57,9 +56,14 @@
"yaml": "2.1.1" "yaml": "2.1.1"
}, },
"devDependencies": { "devDependencies": {
"@trivago/prettier-plugin-sort-imports": "3.3.0",
"@types/marked": "4.0.7",
"@types/node": "18.7.17",
"@typescript-eslint/parser": "5.37.0", "@typescript-eslint/parser": "5.37.0",
"eslint": "8.23.1", "eslint": "8.23.1",
"eslint-config-prettier": "8.5.0",
"eslint-config-react-app": "7.0.1", "eslint-config-react-app": "7.0.1",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.31.8", "eslint-plugin-react": "7.31.8",
"yallist": "4.0.0" "yallist": "4.0.0"
}, },

View File

@@ -1,4 +1,5 @@
module.exports = { module.exports = {
importOrder: ["^[./]"],
importOrderSeparation: true, importOrderSeparation: true,
importOrderSortSpecifiers: true, importOrderSortSpecifiers: true,
}; };

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
import * as fs from "fs-extra";
import { spawn } from "child_process"; import { spawn } from "child_process";
import * as fs from "fs-extra";
import * as paths from "./paths"; import * as paths from "./paths";
// Do this as the first thing so that any code reading it knows the right env. // Do this as the first thing so that any code reading it knows the right env.
@@ -52,7 +52,7 @@ const buildTemplate = () => {
buildTemplate().then(() => { buildTemplate().then(() => {
fs.copySync(paths.templateBuild, paths.appBuild); fs.copySync(paths.templateBuild, paths.appBuild);
fs.ensureDirSync(paths.appPublic) fs.ensureDirSync(paths.appPublic);
fs.copySync(paths.appPublic, paths.appBuild); fs.copySync(paths.appPublic, paths.appBuild);
console.log(`${paths.appBuild} was created and can be deployed.`); console.log(`${paths.appBuild} was created and can be deployed.`);
}); });

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "fs";
import { copyFileSync, mkdirSync, existsSync, readFileSync } from "fs";
import { createRadar } from "./generateJson/radar"; import { createRadar } from "./generateJson/radar";
// Do this as the first thing so that any code reading it knows the right env. // Do this as the first thing so that any code reading it knows the right env.

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
import * as fs from "fs-extra"; import * as fs from "fs-extra";
import * as paths from "./paths"; import * as paths from "./paths";
// Do this as the first thing so that any code reading it knows the right env. // Do this as the first thing so that any code reading it knows the right env.

View File

@@ -1,32 +1,39 @@
import { readFile } from "fs-extra";
import { readFileSync } from "fs";
import * as path from "path";
import frontMatter from "front-matter"; import frontMatter from "front-matter";
// @ts-ignore esModuleInterop is activated in tsconfig.scripts.json, but IDE typescript uses default typescript config import { readFileSync } from "fs";
import { marked } from "marked"; import { readFile } from "fs-extra";
import highlight from "highlight.js"; import highlight from "highlight.js";
import { marked } from "marked";
import * as path from "path";
import { radarPath, getAllMarkdownFiles } from "./file"; import {
import { Item, Revision, ItemAttributes, Radar, FlagType } from "../../src/model"; FlagType,
Item,
ItemAttributes,
Radar,
Revision,
} from "../../src/model";
import { appBuild } from "../paths"; import { appBuild } from "../paths";
import { getAllMarkdownFiles, radarPath } from "./file";
import type { ConfigData } from "../../src/config"; import type { ConfigData } from "../../src/config";
type FMAttributes = ItemAttributes;
marked.setOptions({ marked.setOptions({
highlight: (code: any) => highlight.highlightAuto(code).value, highlight: (code: any) => highlight.highlightAuto(code).value,
}); });
export const createRadar = async (): Promise<Radar> => { export const createRadar = async (): Promise<Radar> => {
const fileNames = await getAllMarkdownFiles(radarPath()); const fileNames = await getAllMarkdownFiles(radarPath());
const revisions: (Revision|undefined)[] = await createRevisionsFromFiles(fileNames); const revisions: (Revision | undefined)[] = await createRevisionsFromFiles(
const filterdRevisions : Revision[] = revisions.filter(r => r !== undefined) as Revision[]; fileNames
);
const filterdRevisions: Revision[] = revisions.filter(
(r) => r !== undefined
) as Revision[];
const allReleases = getAllReleases(filterdRevisions); const allReleases = getAllReleases(filterdRevisions);
const items = createItems(filterdRevisions); const items = createItems(filterdRevisions);
const flaggedItems = flagItem(items, allReleases); const flaggedItems = flagItem(items, allReleases);
items.forEach(item => checkAttributes(item.name, item)) items.forEach((item) => checkAttributes(item.name, item));
return { return {
items: flaggedItems, items: flaggedItems,
@@ -34,7 +41,7 @@ export const createRadar = async (): Promise<Radar> => {
}; };
}; };
const checkAttributes = (fileName: string, attributes: FMAttributes) => { const checkAttributes = (fileName: string, attributes: ItemAttributes) => {
const rawConf = readFileSync(path.resolve(appBuild, "config.json"), "utf-8"); const rawConf = readFileSync(path.resolve(appBuild, "config.json"), "utf-8");
const config = JSON.parse(rawConf) as ConfigData; const config = JSON.parse(rawConf) as ConfigData;
@@ -61,31 +68,29 @@ const checkAttributes = (fileName: string, attributes: FMAttributes) => {
} else { } else {
return attributes; return attributes;
} }
}; };
const createRevisionsFromFiles = (fileNames: string[]) => { const createRevisionsFromFiles = (fileNames: string[]) => {
const publicUrl = process.env.PUBLIC_URL; const publicUrl = process.env.PUBLIC_URL;
return Promise.all( return Promise.all(
fileNames.map( fileNames.map((fileName) =>
(fileName) => readFile(fileName, "utf8").then((data) => {
readFile(fileName, "utf8").then(data => { const fm = frontMatter<ItemAttributes>(data);
const fm = frontMatter<FMAttributes>(data); let html = marked(fm.body.replace(/\]\(\//g, `](${publicUrl}/`));
let html = marked(fm.body.replace(/\]\(\//g, `](${publicUrl}/`)); html = html.replace(
html = html.replace( /a href="http/g,
/a href="http/g, 'a target="_blank" rel="noopener noreferrer" href="http'
'a target="_blank" rel="noopener noreferrer" href="http' );
); const attributes = checkAttributes(fileName, fm.attributes);
const attributes = checkAttributes(fileName, fm.attributes); if (attributes) {
if (attributes) { return {
return { ...itemInfoFromFilename(fileName),
...itemInfoFromFilename(fileName), ...attributes,
...attributes, fileName,
fileName, body: html,
body: html, } as Revision;
} as Revision; }
} })
})
) )
); );
}; };
@@ -120,7 +125,7 @@ const createItems = (revisions: Revision[]) => {
); );
return Object.values(itemMap) return Object.values(itemMap)
.map((item) => ({ ...item, "title": item.title || item.name })) .map((item) => ({ ...item, title: item.title || item.name }))
.sort((x, y) => (x.name > y.name ? 1 : -1)); .sort((x, y) => (x.name > y.name ? 1 : -1));
}; };
@@ -162,7 +167,12 @@ const addRevisionToItem = (
}; };
const revisionCreatesNewHistoryEntry = (revision: Revision, item: Item) => { const revisionCreatesNewHistoryEntry = (revision: Revision, item: Item) => {
return revision.body.trim() !== "" || (typeof revision.ring !== "undefined" && revision.ring !== item.ring) || (typeof revision.quadrant !== "undefined" && revision.quadrant !== item.quadrant); return (
revision.body.trim() !== "" ||
(typeof revision.ring !== "undefined" && revision.ring !== item.ring) ||
(typeof revision.quadrant !== "undefined" &&
revision.quadrant !== item.quadrant)
);
}; };
const flagItem = (items: Item[], allReleases: string[]) => const flagItem = (items: Item[], allReleases: string[]) =>

View File

@@ -1,5 +1,5 @@
import { resolve } from "path";
import { realpathSync } from "fs"; import { realpathSync } from "fs";
import { resolve } from "path";
export const radarJson = "rd.json"; export const radarJson = "rd.json";
const appDirectory = realpathSync(process.cwd()); const appDirectory = realpathSync(process.cwd());

View File

@@ -1,19 +1,20 @@
import React from "react";
import classNames from "classnames"; import classNames from "classnames";
import Header from "./Header/Header"; import React from "react";
import Footer from "./Footer/Footer";
import Router from "./Router";
import { import {
BrowserRouter, BrowserRouter,
Routes,
Route,
Navigate, Navigate,
useParams, Route,
Routes,
useLocation, useLocation,
useParams,
} from "react-router-dom"; } from "react-router-dom";
import { Item } from "../model";
import { Messages, MessagesProvider } from "../context/MessagesContext";
import { ConfigData } from "../config"; import { ConfigData } from "../config";
import { Messages, MessagesProvider } from "../context/MessagesContext";
import { Item } from "../model";
import Footer from "./Footer/Footer";
import Header from "./Header/Header";
import Router from "./Router";
const useFetch = <D extends unknown>(url: string): D | undefined => { const useFetch = <D extends unknown>(url: string): D | undefined => {
const [data, setData] = React.useState<D>(); const [data, setData] = React.useState<D>();
@@ -35,7 +36,7 @@ const useFetch = <D extends unknown>(url: string): D | undefined => {
const useQuery = () => new URLSearchParams(useLocation().search); const useQuery = () => new URLSearchParams(useLocation().search);
const usePage = (params: Record<string, string | undefined>) => { const usePage = (params: Record<string, string | undefined>) => {
return (params['*'] || '').replace(".html", ""); return (params["*"] || "").replace(".html", "");
}; };
const RouterWithPageParam = ({ const RouterWithPageParam = ({

View File

@@ -1,5 +1,6 @@
import React, { MouseEventHandler } from "react";
import classNames from "classnames"; import classNames from "classnames";
import React, { MouseEventHandler } from "react";
import "./badge.scss"; import "./badge.scss";
type BadgeProps = { type BadgeProps = {

View File

@@ -1,6 +1,8 @@
import React from "react";
import classNames from "classnames"; import classNames from "classnames";
import React from "react";
import "./branding.scss"; import "./branding.scss";
type BrandingProps = { type BrandingProps = {
logoContent: React.ReactNode; logoContent: React.ReactNode;
modifier?: "backlink" | "logo" | "content" | "footer"; modifier?: "backlink" | "logo" | "content" | "footer";

View File

@@ -1,46 +1,44 @@
import React, { useRef, useLayoutEffect } from 'react';
import * as d3 from "d3"; import * as d3 from "d3";
import React, { useLayoutEffect, useRef } from "react";
export const YAxis: React.FC<{ export const YAxis: React.FC<{
scale: d3.ScaleLinear<number, number> scale: d3.ScaleLinear<number, number>;
}> = ({ scale }) => { }> = ({ scale }) => {
const ref = useRef<SVGSVGElement>(null);
const ref = useRef<SVGSVGElement>(null); useLayoutEffect(() => {
if (ref.current == null) {
return;
}
const axisGenerator = d3.axisLeft(scale).ticks(6);
d3.select(ref.current)
.attr("class", "y-axis")
.call(axisGenerator)
.call((g) => g.selectAll(".tick text").remove())
.call((g) => g.selectAll(".tick line").remove())
.call((g) => g.selectAll(".domain").remove());
}, [scale]);
useLayoutEffect(() => { return <g ref={ref} />;
if (ref.current == null) {
return
}
const axisGenerator = d3.axisLeft(scale).ticks(6);
d3.select(ref.current)
.attr('class', 'y-axis')
.call(axisGenerator)
.call(g => g.selectAll('.tick text').remove())
.call(g => g.selectAll('.tick line').remove())
.call(g => g.selectAll('.domain').remove());
}, [scale]);
return <g ref={ref} />;
}; };
export const XAxis: React.FC<{ export const XAxis: React.FC<{
scale: d3.ScaleLinear<number, number> scale: d3.ScaleLinear<number, number>;
}> = ({ scale }) => { }> = ({ scale }) => {
const ref = useRef<SVGSVGElement>(null);
const ref = useRef<SVGSVGElement>(null); useLayoutEffect(() => {
if (ref.current == null) {
return;
}
const axisGenerator = d3.axisBottom(scale).ticks(6);
d3.select(ref.current)
.attr("class", "x-axis")
.call(axisGenerator)
.call((g) => g.selectAll(".tick text").remove())
.call((g) => g.selectAll(".tick line").remove())
.call((g) => g.selectAll(".domain").remove());
}, [scale]);
useLayoutEffect(() => { return <g ref={ref} />;
if (ref.current == null) {
return
}
const axisGenerator = d3.axisBottom(scale).ticks(6);
d3.select(ref.current)
.attr('class', 'x-axis')
.call(axisGenerator)
.call(g => g.selectAll('.tick text').remove())
.call(g => g.selectAll('.tick line').remove())
.call(g => g.selectAll('.domain').remove());
}, [scale]);
return <g ref={ref} />;
}; };

View File

@@ -1,126 +1,147 @@
import React from 'react'; import { ScaleLinear } from "d3";
import { ScaleLinear } from 'd3'; import React from "react";
import { FlagType, Item, Blip, Point } from '../../model';
import Link from '../Link/Link'; import { ConfigData } from "../../config";
import { NewBlip, ChangedBlip, DefaultBlip } from './BlipShapes'; import { Blip, FlagType, Item, Point } from "../../model";
import { ConfigData } from '../../config'; import Link from "../Link/Link";
import { ChangedBlip, DefaultBlip, NewBlip } from "./BlipShapes";
/* /*
See https://medium.com/create-code/build-a-radar-diagram-with-d3-js-9db6458a9248 See https://medium.com/create-code/build-a-radar-diagram-with-d3-js-9db6458a9248
for a good explanation of formulas used to calculate various things in this component for a good explanation of formulas used to calculate various things in this component
*/ */
function generateCoordinates(blip: Blip, xScale: ScaleLinear<number, number>, yScale: ScaleLinear<number, number>, config: ConfigData): Point { function generateCoordinates(
const pi = Math.PI, blip: Blip,
ringRadius = config.chartConfig.ringsAttributes[blip.ringPosition].radius, xScale: ScaleLinear<number, number>,
previousRingRadius = blip.ringPosition === 0 ? 0 : config.chartConfig.ringsAttributes[blip.ringPosition - 1].radius, yScale: ScaleLinear<number, number>,
ringPadding = 0.7; config: ConfigData
): Point {
const pi = Math.PI,
ringRadius = config.chartConfig.ringsAttributes[blip.ringPosition].radius,
previousRingRadius =
blip.ringPosition === 0
? 0
: config.chartConfig.ringsAttributes[blip.ringPosition - 1].radius,
ringPadding = 0.7;
// radian between 0 and 90 degrees // radian between 0 and 90 degrees
const randomDegree = ((Math.random() * 90) * pi) / 180; const randomDegree = (Math.random() * 90 * pi) / 180;
// random distance from the centre of the radar, but within given ring. Also, with some "padding" so the points don't touch ring borders. // random distance from the centre of the radar, but within given ring. Also, with some "padding" so the points don't touch ring borders.
const radius = randomBetween(previousRingRadius + ringPadding, ringRadius - ringPadding); const radius = randomBetween(
/* previousRingRadius + ringPadding,
ringRadius - ringPadding
);
/*
Multiples of PI/2. To apply the calculated position to the specific quadrant. Multiples of PI/2. To apply the calculated position to the specific quadrant.
Order here is counter-clockwise, so we need to "invert" quadrant positions (i.e. swap 2 with 4) Order here is counter-clockwise, so we need to "invert" quadrant positions (i.e. swap 2 with 4)
*/ */
const shift = pi * [1, 4, 3, 2][blip.quadrantPosition - 1] / 2; const shift = (pi * [1, 4, 3, 2][blip.quadrantPosition - 1]) / 2;
return { return {
x: xScale(Math.cos(randomDegree + shift) * radius), x: xScale(Math.cos(randomDegree + shift) * radius),
y: yScale(Math.sin(randomDegree + shift) * radius) y: yScale(Math.sin(randomDegree + shift) * radius),
}; };
}; }
function randomBetween (min: number, max: number): number { function randomBetween(min: number, max: number): number {
return Math.random() * (max - min) + min; return Math.random() * (max - min) + min;
}; }
function distanceBetween(point1: Point, point2: Point): number { function distanceBetween(point1: Point, point2: Point): number {
const a = point2.x - point1.x; const a = point2.x - point1.x;
const b = point2.y - point1.y; const b = point2.y - point1.y;
return Math.sqrt((a * a) + (b * b)); return Math.sqrt(a * a + b * b);
}; }
function renderBlip(blip: Blip, index: number, config: ConfigData): JSX.Element { function renderBlip(
const props = { blip: Blip,
blip, index: number,
className: 'blip', config: ConfigData
fill: blip.colour, ): JSX.Element {
'data-background-color': blip.colour, const props = {
'data-text-color': blip.txtColour, blip,
'data-tip': blip.title, className: "blip",
key: index fill: blip.colour,
} "data-background-color": blip.colour,
switch (blip.flag) { "data-text-color": blip.txtColour,
case FlagType.new: "data-tip": blip.title,
return <NewBlip {...props} config={config} />; key: index,
case FlagType.changed: };
return <ChangedBlip {...props} config={config} />; switch (blip.flag) {
default: case FlagType.new:
return <DefaultBlip {...props} config={config} />; return <NewBlip {...props} config={config} />;
} case FlagType.changed:
}; return <ChangedBlip {...props} config={config} />;
default:
return <DefaultBlip {...props} config={config} />;
}
}
const BlipPoints: React.FC<{ const BlipPoints: React.FC<{
items: Item[] items: Item[];
xScale:ScaleLinear<number, number> xScale: ScaleLinear<number, number>;
yScale:ScaleLinear<number, number> yScale: ScaleLinear<number, number>;
config:ConfigData config: ConfigData;
}> = ({items, xScale, yScale, config}) => { }> = ({ items, xScale, yScale, config }) => {
const blips: Blip[] = items.reduce((list: Blip[], item: Item) => {
if (!item.ring || !item.quadrant) {
// skip the blip if it doesn't have a ring or quadrant assigned
return list;
}
const quadrantConfig = config.quadrantsMap[item.quadrant];
if (!quadrantConfig) {
return list;
}
const blips: Blip[] = items.reduce((list: Blip[], item: Item) => { let blip: Blip = {
if (!item.ring || !item.quadrant) { ...item,
// skip the blip if it doesn't have a ring or quadrant assigned quadrantPosition: quadrantConfig.position,
return list; ringPosition: config.rings.findIndex((r) => r === item.ring),
} colour: quadrantConfig.colour,
const quadrantConfig = config.quadrantsMap[item.quadrant]; txtColour: quadrantConfig.txtColour,
if (!quadrantConfig) { coordinates: { x: 0, y: 0 },
return list; };
}
let blip: Blip = { ...item, let point: Point;
quadrantPosition: quadrantConfig.position, let counter = 1;
ringPosition: config.rings.findIndex(r => r === item.ring), let distanceBetweenCheck: boolean;
colour: quadrantConfig.colour, do {
txtColour: quadrantConfig.txtColour, const localpoint = generateCoordinates(blip, xScale, yScale, config);
coordinates: {x: 0, y: 0}, point = localpoint;
}; counter++;
/*
let point: Point;
let counter = 1;
let distanceBetweenCheck: boolean;
do {
const localpoint = generateCoordinates(blip, xScale, yScale, config);
point = localpoint
counter++;
/*
Generate position of the new blip until it has a satisfactory distance to every other blip (so that they don't touch each other) Generate position of the new blip until it has a satisfactory distance to every other blip (so that they don't touch each other)
and quadrant borders (so that they don't overlap quadrants) and quadrant borders (so that they don't overlap quadrants)
This feels pretty inefficient, but good enough for now. This feels pretty inefficient, but good enough for now.
*/ */
distanceBetweenCheck = list.some(b => distanceBetween(localpoint, b.coordinates) < config.chartConfig.blipSize + config.chartConfig.blipSize / 2) distanceBetweenCheck = list.some(
} while (counter < 100 (b) =>
&& (Math.abs(point.x - xScale(0)) < 15 distanceBetween(localpoint, b.coordinates) <
|| Math.abs(point.y - yScale(0)) < 15 config.chartConfig.blipSize + config.chartConfig.blipSize / 2
|| distanceBetweenCheck );
)); } while (
counter < 100 &&
blip.coordinates = point; (Math.abs(point.x - xScale(0)) < 15 ||
Math.abs(point.y - yScale(0)) < 15 ||
list.push(blip); distanceBetweenCheck)
return list;
}, []);
return (
<g className="blips">
{blips.map((blip, index) => (
<Link pageName={`${blip.quadrant}/${blip.name}`} key={index}>
{renderBlip(blip, index, config)}
</Link>
))}
</g>
); );
blip.coordinates = point;
list.push(blip);
return list;
}, []);
return (
<g className="blips">
{blips.map((blip, index) => (
<Link pageName={`${blip.quadrant}/${blip.name}`} key={index}>
{renderBlip(blip, index, config)}
</Link>
))}
</g>
);
}; };
export default BlipPoints; export default BlipPoints;

View File

@@ -1,64 +1,65 @@
import React from 'react'; import React from "react";
import { ConfigData } from '../../config';
import { Blip } from '../../model'; import { ConfigData } from "../../config";
import { Blip } from "../../model";
type VisualBlipProps = { type VisualBlipProps = {
className: string, className: string;
fill: string, fill: string;
'data-background-color': string, "data-background-color": string;
'data-text-color': string, "data-text-color": string;
'data-tip': string, "data-tip": string;
key: number key: number;
} };
export const ChangedBlip: React.FC< export const ChangedBlip: React.FC<
{blip: Blip, config: ConfigData} & VisualBlipProps { blip: Blip; config: ConfigData } & VisualBlipProps
> = ({blip, config, ...props}) => { > = ({ blip, config, ...props }) => {
const centeredX = blip.coordinates.x - config.chartConfig.blipSize/2, const centeredX = blip.coordinates.x - config.chartConfig.blipSize / 2,
centeredY = blip.coordinates.y - config.chartConfig.blipSize/2; centeredY = blip.coordinates.y - config.chartConfig.blipSize / 2;
return ( return (
<rect <rect
transform={`rotate(-45 ${centeredX} ${centeredY})`} transform={`rotate(-45 ${centeredX} ${centeredY})`}
x={centeredX} x={centeredX}
y={centeredY} y={centeredY}
width={config.chartConfig.blipSize} width={config.chartConfig.blipSize}
height={config.chartConfig.blipSize} height={config.chartConfig.blipSize}
rx="3" rx="3"
{...props} {...props}
/> />
); );
}; };
export const NewBlip: React.FC< export const NewBlip: React.FC<
{blip: Blip, config: ConfigData} & VisualBlipProps { blip: Blip; config: ConfigData } & VisualBlipProps
> = ({blip, config, ...props}) => { > = ({ blip, config, ...props }) => {
const centeredX = blip.coordinates.x - config.chartConfig.blipSize/2, const centeredX = blip.coordinates.x - config.chartConfig.blipSize / 2,
centeredY = blip.coordinates.y - config.chartConfig.blipSize/2; centeredY = blip.coordinates.y - config.chartConfig.blipSize / 2;
/* /*
The below is a predefined path of a triangle with rounded corners. The below is a predefined path of a triangle with rounded corners.
I didn't find any more human friendly way of doing this as all examples I found have tons of lines of code I didn't find any more human friendly way of doing this as all examples I found have tons of lines of code
e.g. https://observablehq.com/@perlmonger42/interactive-rounded-corners-on-svg-polygons-using-d3-js e.g. https://observablehq.com/@perlmonger42/interactive-rounded-corners-on-svg-polygons-using-d3-js
*/ */
return ( return (
<path <path
transform={`translate(${centeredX}, ${centeredY})`} transform={`translate(${centeredX}, ${centeredY})`}
d="M.247 10.212l5.02-8.697a2 2 0 013.465 0l5.021 8.697a2 2 0 01-1.732 3H1.98a2 2 0 01-1.732-3z" d="M.247 10.212l5.02-8.697a2 2 0 013.465 0l5.021 8.697a2 2 0 01-1.732 3H1.98a2 2 0 01-1.732-3z"
{...props} {...props}
/> />
); );
}; };
export const DefaultBlip: React.FC< export const DefaultBlip: React.FC<
{blip: Blip, config: ConfigData} & VisualBlipProps { blip: Blip; config: ConfigData } & VisualBlipProps
> = ({blip, config, ...props}) => { > = ({ blip, config, ...props }) => {
return ( return (
<circle <circle
r={config.chartConfig.blipSize / 2} r={config.chartConfig.blipSize / 2}
cx={blip.coordinates.x} cx={blip.coordinates.x}
cy={blip.coordinates.y} cy={blip.coordinates.y}
{...props} {...props}
/> />
); );
}; };

View File

@@ -1,74 +1,92 @@
import React from 'react'; import * as d3 from "d3";
import * as d3 from 'd3'; import React from "react";
import { QuadrantConfig } from '../../model';
import { ConfigData } from '../../config';
function arcPath(quadrantPosition: number, ringPosition: number, xScale: d3.ScaleLinear<number, number>, config: ConfigData) { import { ConfigData } from "../../config";
const startAngle = quadrantPosition === 1 ? import { QuadrantConfig } from "../../model";
3 * Math.PI / 2
: (quadrantPosition - 2) * Math.PI / 2
const endAngle = quadrantPosition === 1 ?
4 * Math.PI / 2
: (quadrantPosition -1) * Math.PI / 2
const arcAttrs = config.chartConfig.ringsAttributes[ringPosition],
ringRadiusPx = xScale(arcAttrs.radius) - xScale(0),
arc = d3.arc();
return arc({ function arcPath(
innerRadius: ringRadiusPx - arcAttrs.arcWidth, quadrantPosition: number,
outerRadius: ringRadiusPx, ringPosition: number,
startAngle, xScale: d3.ScaleLinear<number, number>,
endAngle config: ConfigData
}) || undefined; ) {
const startAngle =
quadrantPosition === 1
? (3 * Math.PI) / 2
: ((quadrantPosition - 2) * Math.PI) / 2;
const endAngle =
quadrantPosition === 1
? (4 * Math.PI) / 2
: ((quadrantPosition - 1) * Math.PI) / 2;
const arcAttrs = config.chartConfig.ringsAttributes[ringPosition],
ringRadiusPx = xScale(arcAttrs.radius) - xScale(0),
arc = d3.arc();
return (
arc({
innerRadius: ringRadiusPx - arcAttrs.arcWidth,
outerRadius: ringRadiusPx,
startAngle,
endAngle,
}) || undefined
);
} }
const QuadrantRings: React.FC<{ const QuadrantRings: React.FC<{
quadrant: QuadrantConfig quadrant: QuadrantConfig;
xScale: d3.ScaleLinear<number, number> xScale: d3.ScaleLinear<number, number>;
config: ConfigData config: ConfigData;
}> = ({ quadrant, xScale, config}) => { }> = ({ quadrant, xScale, config }) => {
// order from top-right clockwise // order from top-right clockwise
const gradientAttributes = [ const gradientAttributes = [
{x: 0, y: 0, cx: 1, cy: 1, r: 1}, { x: 0, y: 0, cx: 1, cy: 1, r: 1 },
{x: xScale(0), y: 0, cx: 0, cy: 1, r: 1}, { x: xScale(0), y: 0, cx: 0, cy: 1, r: 1 },
{x: xScale(0), y: xScale(0), cx: 0, cy: 0, r: 1}, { x: xScale(0), y: xScale(0), cx: 0, cy: 0, r: 1 },
{x: 0, y: xScale(0), cx: 1, cy: 0, r: 1} { x: 0, y: xScale(0), cx: 1, cy: 0, r: 1 },
]; ];
const gradientId = `${quadrant.position}-radial-gradient`, const gradientId = `${quadrant.position}-radial-gradient`,
quadrantSize = config.chartConfig.size / 2; quadrantSize = config.chartConfig.size / 2;
return ( return (
<g className="quadrant-ring"> <g className="quadrant-ring">
{/* Definition of the quadrant gradient */} {/* Definition of the quadrant gradient */}
<defs> <defs>
<radialGradient id={gradientId} {...gradientAttributes[quadrant.position - 1]}> <radialGradient
<stop offset="0%" stopColor={quadrant.colour}></stop> id={gradientId}
<stop offset="100%" stopColor={quadrant.colour} stopOpacity="0"></stop> {...gradientAttributes[quadrant.position - 1]}
</radialGradient> >
</defs> <stop offset="0%" stopColor={quadrant.colour}></stop>
<stop
offset="100%"
stopColor={quadrant.colour}
stopOpacity="0"
></stop>
</radialGradient>
</defs>
{/* Gradient background area */} {/* Gradient background area */}
<rect <rect
width={quadrantSize} width={quadrantSize}
height={quadrantSize} height={quadrantSize}
x={gradientAttributes[quadrant.position - 1].x} x={gradientAttributes[quadrant.position - 1].x}
y={gradientAttributes[quadrant.position - 1].y} y={gradientAttributes[quadrant.position - 1].y}
fill={`url(#${gradientId})`} fill={`url(#${gradientId})`}
style={{opacity: 0.5}} style={{ opacity: 0.5 }}
/>
{/* Rings' arcs */}
{Array.from(config.rings).map((ringPosition, index) => (
<path
key={index}
fill={quadrant.colour}
d={arcPath(quadrant.position, index, xScale, config)}
style={{
transform: `translate(${quadrantSize}px, ${quadrantSize}px)`,
}}
/> />
))}
</g>
);
};
{/* Rings' arcs */} export default QuadrantRings;
{Array.from(config.rings).map((ringPosition, index) => (
<path
key={index}
fill={quadrant.colour}
d={arcPath(quadrant.position, index, xScale, config)}
style={{transform: `translate(${quadrantSize}px, ${quadrantSize}px)`}}
/>
))}
</g>
);
}
export default QuadrantRings;

View File

@@ -1,77 +1,109 @@
import React from 'react';
import * as d3 from "d3"; import * as d3 from "d3";
import ReactTooltip from 'react-tooltip'; import React from "react";
import { Item } from '../../model'; import ReactTooltip from "react-tooltip";
import { YAxis, XAxis } from './Axes';
import QuadrantRings from './QuadrantRings';
import BlipPoints from './BlipPoints';
import './chart.scss'; import { ConfigData } from "../../config";
import { ConfigData } from '../../config'; import { Item } from "../../model";
import { XAxis, YAxis } from "./Axes";
import BlipPoints from "./BlipPoints";
import QuadrantRings from "./QuadrantRings";
import "./chart.scss";
const RingLabel: React.FC<{ const RingLabel: React.FC<{
ring: string ring: string;
xScale: d3.ScaleLinear<number, number> xScale: d3.ScaleLinear<number, number>;
yScale: d3.ScaleLinear<number, number> yScale: d3.ScaleLinear<number, number>;
config: ConfigData config: ConfigData;
}> = ({ring, xScale, yScale, config}) => { }> = ({ ring, xScale, yScale, config }) => {
const ringIndex = config.rings.findIndex(r => r === ring) const ringIndex = config.rings.findIndex((r) => r === ring);
const ringRadius = config.chartConfig.ringsAttributes[ringIndex].radius, const ringRadius = config.chartConfig.ringsAttributes[ringIndex].radius,
previousRingRadius = ringIndex === 0 ? 0 : config.chartConfig.ringsAttributes[ringIndex - 1].radius, previousRingRadius =
ringIndex === 0
? 0
: config.chartConfig.ringsAttributes[ringIndex - 1].radius,
// middle point in between two ring arcs
distanceFromCentre =
previousRingRadius + (ringRadius - previousRingRadius) / 2;
// middle point in between two ring arcs return (
distanceFromCentre = previousRingRadius + (ringRadius - previousRingRadius) / 2; <g className="ring-label">
{/* Right hand-side label */}
return ( <text
<g className="ring-label"> x={xScale(distanceFromCentre)}
{/* Right hand-side label */} y={yScale(0)}
<text x={xScale(distanceFromCentre)} y={yScale(0)} textAnchor="middle" dy=".35em"> textAnchor="middle"
{ring} dy=".35em"
</text> >
{/* Left hand-side label */} {ring}
<text x={xScale(-distanceFromCentre)} y={yScale(0)} textAnchor="middle" dy=".35em"> </text>
{ring} {/* Left hand-side label */}
</text> <text
</g> x={xScale(-distanceFromCentre)}
); y={yScale(0)}
textAnchor="middle"
dy=".35em"
>
{ring}
</text>
</g>
);
}; };
const RadarChart: React.FC<{ const RadarChart: React.FC<{
items: Item[] items: Item[];
config: ConfigData config: ConfigData;
}> = ({ items, config }) => { }> = ({ items, config }) => {
const xScale = d3
const xScale = d3.scaleLinear() .scaleLinear()
.domain(config.chartConfig.scale) .domain(config.chartConfig.scale)
.range([0, config.chartConfig.size]); .range([0, config.chartConfig.size]);
const yScale = d3.scaleLinear() const yScale = d3
.scaleLinear()
.domain(config.chartConfig.scale) .domain(config.chartConfig.scale)
.range([config.chartConfig.size, 0]); .range([config.chartConfig.size, 0]);
return ( return (
<div className="chart" style={{maxWidth: `${config.chartConfig.size}px`}}> <div className="chart" style={{ maxWidth: `${config.chartConfig.size}px` }}>
<svg viewBox={`0 0 ${config.chartConfig.size} ${config.chartConfig.size}`}> <svg
<g transform={`translate(${xScale(0)}, 0)`}> viewBox={`0 0 ${config.chartConfig.size} ${config.chartConfig.size}`}
<YAxis scale={yScale}/> >
</g> <g transform={`translate(${xScale(0)}, 0)`}>
<g transform={`translate(0, ${yScale(0)})`}> <YAxis scale={yScale} />
<XAxis scale={xScale}/> </g>
</g> <g transform={`translate(0, ${yScale(0)})`}>
<XAxis scale={xScale} />
</g>
{Object.values(config.quadrantsMap).map((value, index) => ( {Object.values(config.quadrantsMap).map((value, index) => (
<QuadrantRings key={index} quadrant={value} xScale={xScale} config={config} /> <QuadrantRings
))} key={index}
quadrant={value}
xScale={xScale}
config={config}
/>
))}
{Array.from(config.rings).map((ring: string, index) => ( {Array.from(config.rings).map((ring: string, index) => (
<RingLabel key={index} ring={ring} xScale={xScale} yScale={yScale} config={config} /> <RingLabel
))} key={index}
ring={ring}
xScale={xScale}
yScale={yScale}
config={config}
/>
))}
<BlipPoints items={items} xScale={xScale} yScale={yScale} config={config} /> <BlipPoints
items={items}
xScale={xScale}
yScale={yScale}
config={config}
/>
</svg> </svg>
<ReactTooltip className="tooltip" offset={{top: -5}}/> <ReactTooltip className="tooltip" offset={{ top: -5 }} />
</div> </div>
); );
} };
export default RadarChart; export default RadarChart;

View File

@@ -1,16 +1,28 @@
import React from "react"; import React from "react";
import { Item } from "../../model"; import { Item } from "../../model";
import "./editButton.scss"; import "./editButton.scss";
type EditButtonProps = { type EditButtonProps = {
baseUrl: string; baseUrl: string;
item: Item & { release? : string } item: Item & { release?: string };
title?: string; title?: string;
}; };
export default function EditButton({baseUrl, item, title}: React.PropsWithChildren<EditButtonProps>) { export default function EditButton({
baseUrl,
item,
title,
}: React.PropsWithChildren<EditButtonProps>) {
const href = `${baseUrl}/${item.release}/${item.name}.md`; const href = `${baseUrl}/${item.release}/${item.name}.md`;
return ( return (
<a className="icon-link" href={href} target="_blank" rel="noopener noreferrer">{title || 'Edit'}</a> <a
className="icon-link"
href={href}
target="_blank"
rel="noopener noreferrer"
>
{title || "Edit"}
</a>
); );
} }

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from "react";
import classNames from "classnames"; import classNames from "classnames";
import React, { useEffect, useState } from "react";
import "./fadeable.scss"; import "./fadeable.scss";
type FadeableProps = { type FadeableProps = {

View File

@@ -1,12 +1,13 @@
import React from "react";
import classNames from "classnames"; import classNames from "classnames";
import React from "react";
import { assetUrl, getItemPageNames, isMobileViewport } from "../../config";
import { useMessages } from "../../context/MessagesContext";
import { Item } from "../../model";
import { sanitize } from "../../sanitize";
import Branding from "../Branding/Branding"; import Branding from "../Branding/Branding";
import FooterEnd from "../FooterEnd/FooterEnd"; import FooterEnd from "../FooterEnd/FooterEnd";
import { assetUrl, getItemPageNames, isMobileViewport } from "../../config";
import { Item } from "../../model";
import "./footer.scss"; import "./footer.scss";
import { useMessages } from "../../context/MessagesContext";
import { sanitize } from "../../sanitize";
interface Props { interface Props {
items: Item[]; items: Item[];
@@ -35,7 +36,10 @@ const Footer: React.FC<Props> = ({ items, pageName }) => {
/> />
} }
> >
<div className="footnote" dangerouslySetInnerHTML={sanitize(footerFootnote)}></div> <div
className="footnote"
dangerouslySetInnerHTML={sanitize(footerFootnote)}
></div>
</Branding> </Branding>
)} )}

View File

@@ -1,15 +1,21 @@
import React from "react";
import classNames from "classnames"; import classNames from "classnames";
import React from "react";
import { useMessages } from "../../context/MessagesContext";
import SocialLink from "../SocialLink/SocialLink"; import SocialLink from "../SocialLink/SocialLink";
import "./footerend.scss"; import "./footerend.scss";
import { useMessages } from "../../context/MessagesContext";
interface Props { interface Props {
modifier?: "in-sidebar"; modifier?: "in-sidebar";
} }
const FooterEnd: React.FC<Props> = ({ modifier }) => { const FooterEnd: React.FC<Props> = ({ modifier }) => {
const { socialLinksLabel, socialLinks, legalInformationLink, legalInformationLabel } = useMessages(); const {
socialLinksLabel,
socialLinks,
legalInformationLink,
legalInformationLabel,
} = useMessages();
return ( return (
<div <div
@@ -21,7 +27,7 @@ const FooterEnd: React.FC<Props> = ({ modifier }) => {
{socialLinks && ( {socialLinks && (
<> <>
<div className="footer-social__label"> <div className="footer-social__label">
<p>{socialLinksLabel ?? 'Follow us:' }</p> <p>{socialLinksLabel ?? "Follow us:"}</p>
</div> </div>
<div className="footer-social__links"> <div className="footer-social__links">
{socialLinks.map(({ href, iconName }) => ( {socialLinks.map(({ href, iconName }) => (
@@ -40,7 +46,7 @@ const FooterEnd: React.FC<Props> = ({ modifier }) => {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{legalInformationLabel || 'Legal Information'} {legalInformationLabel || "Legal Information"}
</a> </a>
</p> </p>
</div> </div>

View File

@@ -1,13 +1,14 @@
import React, { useState, useRef } from "react";
import classNames from "classnames"; import classNames from "classnames";
import qs from "query-string";
import React, { useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import { radarNameShort } from "../../config";
import { useMessages } from "../../context/MessagesContext";
import Branding from "../Branding/Branding"; import Branding from "../Branding/Branding";
import Link from "../Link/Link"; import Link from "../Link/Link";
import LogoLink from "../LogoLink/LogoLink"; import LogoLink from "../LogoLink/LogoLink";
import Search from "../Search/Search"; import Search from "../Search/Search";
import { radarNameShort } from "../../config";
import { useNavigate } from "react-router-dom";
import qs from "query-string";
import { useMessages } from "../../context/MessagesContext";
export default function Header({ pageName }: { pageName: string }) { export default function Header({ pageName }: { pageName: string }) {
const [searchOpen, setSearchOpen] = useState(false); const [searchOpen, setSearchOpen] = useState(false);

View File

@@ -1,5 +1,6 @@
import React from "react";
import classNames from "classnames"; import classNames from "classnames";
import React from "react";
import "./headline-group.scss"; import "./headline-group.scss";
interface Props { interface Props {

View File

@@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import "./hero-headline.scss"; import "./hero-headline.scss";
interface Props { interface Props {

View File

@@ -1,8 +1,9 @@
import React from "react";
import classNames from "classnames"; import classNames from "classnames";
import Link from "../Link/Link"; import React from "react";
import Flag from "../Flag/Flag";
import { Item as mItem } from "../../model"; import { Item as mItem } from "../../model";
import Flag from "../Flag/Flag";
import Link from "../Link/Link";
import "./item.scss"; import "./item.scss";
type Props = { type Props = {

View File

@@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import { featuredOnly, Item as mItem, nonFeaturedOnly } from "../../model";
import Item from "../Item/Item"; import Item from "../Item/Item";
import { featuredOnly, nonFeaturedOnly, Item as mItem } from "../../model";
import "./item-list.scss"; import "./item-list.scss";
type ItemListProps = { type ItemListProps = {

View File

@@ -1,13 +1,13 @@
import Badge from "../Badge/Badge";
import { formatRelease } from "../../date"; import { formatRelease } from "../../date";
import { Revision } from "../../model"; import { Revision } from "../../model";
import Badge from "../Badge/Badge";
export default function ItemRevision({ export default function ItemRevision({
revision, revision,
dateFormat, dateFormat,
}: { }: {
revision: Revision; revision: Revision;
dateFormat?: string dateFormat?: string;
}) { }) {
return ( return (
<div className="item-revision"> <div className="item-revision">

View File

@@ -1,25 +1,31 @@
import { useMessages } from "../../context/MessagesContext";
import { Revision } from "../../model";
import HeadlineGroup from "../HeadlineGroup/HeadlineGroup"; import HeadlineGroup from "../HeadlineGroup/HeadlineGroup";
import ItemRevision from "../ItemRevision/ItemRevision"; import ItemRevision from "../ItemRevision/ItemRevision";
import { Revision } from "../../model";
import "./item-revisions.scss"; import "./item-revisions.scss";
import { useMessages } from "../../context/MessagesContext";
export default function ItemRevisions({ export default function ItemRevisions({
revisions, revisions,
dateFormat, dateFormat,
}: { }: {
revisions: Revision[]; revisions: Revision[];
dateFormat?: string dateFormat?: string;
}) { }) {
const { revisionsText } = useMessages(); const { revisionsText } = useMessages();
return ( return (
<div className="item-revisions"> <div className="item-revisions">
<HeadlineGroup secondary> <HeadlineGroup secondary>
<h4 className="headline headline--dark">{revisionsText ?? 'Revisions:'}</h4> <h4 className="headline headline--dark">
{revisionsText ?? "Revisions:"}
</h4>
</HeadlineGroup> </HeadlineGroup>
{revisions.map((revision) => ( {revisions.map((revision) => (
<ItemRevision key={revision.release} revision={revision} dateFormat={dateFormat} /> <ItemRevision
key={revision.release}
revision={revision}
dateFormat={dateFormat}
/>
))} ))}
</div> </div>
); );

View File

@@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { Link as RLink } from "react-router-dom"; import { Link as RLink } from "react-router-dom";
import "./link.scss"; import "./link.scss";
type LinkProps = { type LinkProps = {

View File

@@ -1,8 +1,10 @@
import React from "react";
import classNames from "classnames"; import classNames from "classnames";
import Link from "../Link/Link"; import React from "react";
import { assetUrl, radarNameShort } from "../../config"; import { assetUrl, radarNameShort } from "../../config";
import Link from "../Link/Link";
import "./logo-link.scss"; import "./logo-link.scss";
export default function LogoLink({ small = false }: { small?: boolean }) { export default function LogoLink({ small = false }: { small?: boolean }) {
return ( return (
<Link <Link

View File

@@ -1,10 +1,11 @@
import React from "react"; import React from "react";
import HeroHeadline from "../HeroHeadline/HeroHeadline";
import Fadeable from "../Fadeable/Fadeable";
import SetTitle from "../SetTitle";
import { radarName } from "../../config"; import { radarName } from "../../config";
import { useMessages } from "../../context/MessagesContext"; import { useMessages } from "../../context/MessagesContext";
import { sanitize } from "../../sanitize"; import { sanitize } from "../../sanitize";
import Fadeable from "../Fadeable/Fadeable";
import HeroHeadline from "../HeroHeadline/HeroHeadline";
import SetTitle from "../SetTitle";
interface Props { interface Props {
leaving: boolean; leaving: boolean;
@@ -15,39 +16,58 @@ const PageHelp: React.FC<Props> = ({ leaving, onLeave }) => {
const { pageHelp } = useMessages(); const { pageHelp } = useMessages();
if (pageHelp) { if (pageHelp) {
const { paragraphs, quadrants, rings, sourcecodeLink, headlinePrefix, quadrantsPreDescription, ringsPreDescription } = pageHelp; const {
const title = `${headlinePrefix || 'How to use the'} ${radarName}`; paragraphs,
quadrants,
rings,
sourcecodeLink,
headlinePrefix,
quadrantsPreDescription,
ringsPreDescription,
} = pageHelp;
const title = `${headlinePrefix || "How to use the"} ${radarName}`;
return ( return (
<Fadeable leaving={leaving} onLeave={onLeave}> <Fadeable leaving={leaving} onLeave={onLeave}>
<SetTitle title={title}/> <SetTitle title={title} />
<HeroHeadline>{title}</HeroHeadline> <HeroHeadline>{title}</HeroHeadline>
<div className="fullpage-content"> <div className="fullpage-content">
{paragraphs.map(({ headline, values }) => ( {paragraphs.map(({ headline, values }) => (
<React.Fragment key={headline}> <React.Fragment key={headline}>
<h3>{headline}</h3> <h3>{headline}</h3>
{values.map((element, index) => { {values.map((element, index) => {
return ( return (
<p key={index} dangerouslySetInnerHTML={sanitize(element)}></p> <p
) key={index}
}) dangerouslySetInnerHTML={sanitize(element)}
} ></p>
</React.Fragment> );
})}
</React.Fragment>
))} ))}
<p>{quadrantsPreDescription ?? "The quadrants are:"}</p> <p>{quadrantsPreDescription ?? "The quadrants are:"}</p>
<ul> <ul>
{quadrants.map(({ name, description }) => ( {quadrants.map(({ name, description }) => (
<li key={name}> <li key={name}>
<strong>{name}:</strong> <span dangerouslySetInnerHTML={sanitize(description, {})}></span> <strong>{name}:</strong>{" "}
<span
dangerouslySetInnerHTML={sanitize(description, {})}
></span>
</li> </li>
))} ))}
</ul> </ul>
<p>{ringsPreDescription ?? "Each of the items is classified in one of these rings:"}</p> <p>
{ringsPreDescription ??
"Each of the items is classified in one of these rings:"}
</p>
<ul> <ul>
{rings.map(({ name, description }) => ( {rings.map(({ name, description }) => (
<li key={name}> <li key={name}>
<strong>{name}:</strong> <span dangerouslySetInnerHTML={sanitize(description, {})}></span> <strong>{name}:</strong>{" "}
<span
dangerouslySetInnerHTML={sanitize(description, {})}
></span>
</li> </li>
))} ))}
</ul> </ul>

View File

@@ -1,13 +1,14 @@
import { MomentInput } from "moment";
import { ConfigData, radarName, radarNameShort } from "../../config";
import { useMessages } from "../../context/MessagesContext";
import { formatRelease } from "../../date"; import { formatRelease } from "../../date";
import { featuredOnly, Item, HomepageOption } from "../../model"; import { HomepageOption, Item, featuredOnly } from "../../model";
import Fadeable from "../Fadeable/Fadeable";
import HeroHeadline from "../HeroHeadline/HeroHeadline"; import HeroHeadline from "../HeroHeadline/HeroHeadline";
import QuadrantGrid from "../QuadrantGrid/QuadrantGrid"; import QuadrantGrid from "../QuadrantGrid/QuadrantGrid";
import RadarGrid from '../RadarGrid/RadarGrid'; import RadarGrid from "../RadarGrid/RadarGrid";
import Fadeable from "../Fadeable/Fadeable";
import SetTitle from "../SetTitle"; import SetTitle from "../SetTitle";
import { ConfigData, radarName, radarNameShort } from "../../config";
import { MomentInput } from "moment";
import { useMessages } from "../../context/MessagesContext";
type PageIndexProps = { type PageIndexProps = {
leaving: boolean; leaving: boolean;
@@ -25,12 +26,16 @@ export default function PageIndex({
releases, releases,
}: PageIndexProps) { }: PageIndexProps) {
const { pageIndex } = useMessages(); const { pageIndex } = useMessages();
const publishedLabel = pageIndex?.publishedLabel || 'Published'; const publishedLabel = pageIndex?.publishedLabel || "Published";
const newestRelease = releases.slice(-1)[0]; const newestRelease = releases.slice(-1)[0];
const numberOfReleases = releases.length; const numberOfReleases = releases.length;
const showChart = config.homepageContent === HomepageOption.chart || config.homepageContent === HomepageOption.both; const showChart =
const showColumns = config.homepageContent === HomepageOption.columns || config.homepageContent === HomepageOption.both; config.homepageContent === HomepageOption.chart ||
config.homepageContent === HomepageOption.both;
const showColumns =
config.homepageContent === HomepageOption.columns ||
config.homepageContent === HomepageOption.both;
return ( return (
<Fadeable leaving={leaving} onLeave={onLeave}> <Fadeable leaving={leaving} onLeave={onLeave}>
<SetTitle title={radarNameShort} /> <SetTitle title={radarNameShort} />
@@ -39,9 +44,7 @@ export default function PageIndex({
{radarName} {radarName}
</HeroHeadline> </HeroHeadline>
</div> </div>
{showChart && ( {showChart && <RadarGrid items={featuredOnly(items)} config={config} />}
<RadarGrid items={featuredOnly(items)} config={config} />
)}
{showColumns && ( {showColumns && (
<QuadrantGrid items={featuredOnly(items)} config={config} /> <QuadrantGrid items={featuredOnly(items)} config={config} />
)} )}

View File

@@ -1,19 +1,17 @@
import React from "react"; import React from "react";
import { ConfigData, translate } from "../../config";
import { useMessages } from "../../context/MessagesContext";
import { Item, groupByQuadrants } from "../../model";
import Badge from "../Badge/Badge"; import Badge from "../Badge/Badge";
import EditButton from "../EditButton/EditButton"; import EditButton from "../EditButton/EditButton";
import ItemList from "../ItemList/ItemList";
import Link from "../Link/Link";
import FooterEnd from "../FooterEnd/FooterEnd"; import FooterEnd from "../FooterEnd/FooterEnd";
import SetTitle from "../SetTitle"; import ItemList from "../ItemList/ItemList";
import ItemRevisions from "../ItemRevisions/ItemRevisions"; import ItemRevisions from "../ItemRevisions/ItemRevisions";
import { useAnimations } from "./useAnimations"; import Link from "../Link/Link";
import SetTitle from "../SetTitle";
import "./item-page.scss"; import "./item-page.scss";
import { ConfigData, translate } from "../../config"; import { useAnimations } from "./useAnimations";
import {
groupByQuadrants,
Item,
} from "../../model";
import { useMessages } from "../../context/MessagesContext";
const getItem = (pageName: string, items: Item[]) => { const getItem = (pageName: string, items: Item[]) => {
const [quadrantName, itemName] = pageName.split("/"); const [quadrantName, itemName] = pageName.split("/");
@@ -35,9 +33,15 @@ type Props = {
onLeave: () => void; onLeave: () => void;
}; };
const PageItem: React.FC<Props> = ({ pageName, items, config, leaving, onLeave }) => { const PageItem: React.FC<Props> = ({
pageName,
items,
config,
leaving,
onLeave,
}) => {
const { pageItem } = useMessages(); const { pageItem } = useMessages();
const quadrantOverview = pageItem?.quadrantOverview || 'Quadrant Overview'; const quadrantOverview = pageItem?.quadrantOverview || "Quadrant Overview";
const itemsInRing = getItemsInRing(pageName, items); const itemsInRing = getItemsInRing(pageName, items);
@@ -48,7 +52,13 @@ const PageItem: React.FC<Props> = ({ pageName, items, config, leaving, onLeave }
}); });
const item = getItem(pageName, items); const item = getItem(pageName, items);
const editButton = config.editLink ? <EditButton baseUrl={config.editLink.radarLink} item={item} title={config.editLink.title}/> : null const editButton = config.editLink ? (
<EditButton
baseUrl={config.editLink.radarLink}
item={item}
title={config.editLink.title}
/>
) : null;
return ( return (
<> <>
@@ -119,7 +129,10 @@ const PageItem: React.FC<Props> = ({ pageName, items, config, leaving, onLeave }
dangerouslySetInnerHTML={{ __html: item.body }} dangerouslySetInnerHTML={{ __html: item.body }}
/> />
{item.revisions.length > 1 && ( {item.revisions.length > 1 && (
<ItemRevisions revisions={item.revisions.slice(1)} dateFormat={config.dateFormat} /> <ItemRevisions
revisions={item.revisions.slice(1)}
dateFormat={config.dateFormat}
/>
)} )}
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { useEffect, useState, useMemo } from "react"; import { useEffect, useMemo, useState } from "react";
import { import {
Animation, Animation,
AnimationStates, AnimationStates,
@@ -13,18 +14,14 @@ interface Props {
onLeave: () => void; onLeave: () => void;
} }
export const useAnimations = ({ export const useAnimations = ({ itemsInRing, leaving, onLeave }: Props) => {
itemsInRing,
leaving,
onLeave,
}: Props) => {
type AnimationConfig = { type AnimationConfig = {
background: Animation; background: Animation;
navHeader: Animation; navHeader: Animation;
text: Animation; text: Animation;
items: Animation[]; items: Animation[];
footer: Animation; footer: Animation;
} };
type AnimationNames = keyof AnimationConfig; type AnimationNames = keyof AnimationConfig;

View File

@@ -1,13 +1,12 @@
import { ConfigData, translate } from "../../config";
import { Item, groupByQuadrants } from "../../model";
import Badge from "../Badge/Badge"; import Badge from "../Badge/Badge";
import EditButton from "../EditButton/EditButton"; import EditButton from "../EditButton/EditButton";
import ItemList from "../ItemList/ItemList";
import Link from "../Link/Link";
import Fadeable from "../Fadeable/Fadeable"; import Fadeable from "../Fadeable/Fadeable";
import SetTitle from "../SetTitle"; import ItemList from "../ItemList/ItemList";
import ItemRevisions from "../ItemRevisions/ItemRevisions"; import ItemRevisions from "../ItemRevisions/ItemRevisions";
import Link from "../Link/Link";
import { ConfigData, translate } from "../../config"; import SetTitle from "../SetTitle";
import { groupByQuadrants, Item } from "../../model";
type PageItemMobileProps = { type PageItemMobileProps = {
pageName: string; pageName: string;
@@ -40,7 +39,13 @@ export default function PageItemMobile({
const item = getItem(pageName, items); const item = getItem(pageName, items);
const itemsInRing = getItemsInRing(pageName, items); const itemsInRing = getItemsInRing(pageName, items);
const editButton = config.editLink ? <EditButton baseUrl={config.editLink.radarLink} item={item} title={config.editLink.title}/> : null const editButton = config.editLink ? (
<EditButton
baseUrl={config.editLink.radarLink}
item={item}
title={config.editLink.title}
/>
) : null;
return ( return (
<Fadeable leaving={leaving} onLeave={onLeave}> <Fadeable leaving={leaving} onLeave={onLeave}>
<SetTitle title={item.title} /> <SetTitle title={item.title} />
@@ -50,7 +55,9 @@ export default function PageItemMobile({
<div className="mobile-item-page__header"> <div className="mobile-item-page__header">
<div className="split"> <div className="split">
<div className="split__left"> <div className="split__left">
<h3 className="headline">{translate(config, item.quadrant)}</h3> <h3 className="headline">
{translate(config, item.quadrant)}
</h3>
<h1 className="hero-headline hero-headline--inverse"> <h1 className="hero-headline hero-headline--inverse">
{item.title} {item.title}
</h1> </h1>

View File

@@ -1,15 +1,16 @@
import { useState, useEffect } from "react"; import { useEffect, useState } from "react";
import HeadlineGroup from "../HeadlineGroup/HeadlineGroup";
import HeroHeadline from "../HeroHeadline/HeroHeadline";
import Badge from "../Badge/Badge";
import Link from "../Link/Link";
import Search from "../Search/Search";
import Fadeable from "../Fadeable/Fadeable";
import SetTitle from "../SetTitle";
import Flag from "../Flag/Flag";
import { groupByFirstLetter, Item } from "../../model";
import { ConfigData, translate } from "../../config"; import { ConfigData, translate } from "../../config";
import { useMessages } from "../../context/MessagesContext"; import { useMessages } from "../../context/MessagesContext";
import { Item, groupByFirstLetter } from "../../model";
import Badge from "../Badge/Badge";
import Fadeable from "../Fadeable/Fadeable";
import Flag from "../Flag/Flag";
import HeadlineGroup from "../HeadlineGroup/HeadlineGroup";
import HeroHeadline from "../HeroHeadline/HeroHeadline";
import Link from "../Link/Link";
import Search from "../Search/Search";
import SetTitle from "../SetTitle";
const containsSearchTerm = (text = "", term = "") => { const containsSearchTerm = (text = "", term = "") => {
// TODO search refinement // TODO search refinement

View File

@@ -1,12 +1,12 @@
import React from "react"; import React from "react";
import HeroHeadline from "../HeroHeadline/HeroHeadline";
import HeadlineGroup from "../HeadlineGroup/HeadlineGroup";
import QuadrantSection from "../QuadrantSection/QuadrantSection";
import Fadeable from "../Fadeable/Fadeable";
import SetTitle from "../SetTitle";
import { ConfigData, translate } from "../../config"; import { ConfigData, translate } from "../../config";
import { featuredOnly, groupByQuadrants, Item } from "../../model"; import { Item, featuredOnly, groupByQuadrants } from "../../model";
import Fadeable from "../Fadeable/Fadeable";
import HeadlineGroup from "../HeadlineGroup/HeadlineGroup";
import HeroHeadline from "../HeroHeadline/HeroHeadline";
import QuadrantSection from "../QuadrantSection/QuadrantSection";
import SetTitle from "../SetTitle";
type PageQuadrantProps = { type PageQuadrantProps = {
leaving: boolean; leaving: boolean;

View File

@@ -1,21 +1,39 @@
import React from "react"; import React from "react";
import { groupByQuadrants, Item, Group } from "../../model";
import { ConfigData } from "../../config"; import { ConfigData } from "../../config";
import { Group, Item, groupByQuadrants } from "../../model";
import QuadrantSection from "../QuadrantSection/QuadrantSection"; import QuadrantSection from "../QuadrantSection/QuadrantSection";
import "./quadrant-grid.scss"; import "./quadrant-grid.scss";
const renderQuadrant = (quadrantName: string, groups: Group, config: ConfigData) => {
const renderQuadrant = (
quadrantName: string,
groups: Group,
config: ConfigData
) => {
return ( return (
<div key={quadrantName} className="quadrant-grid__quadrant"> <div key={quadrantName} className="quadrant-grid__quadrant">
<QuadrantSection quadrantName={quadrantName} groups={groups} config={config} /> <QuadrantSection
quadrantName={quadrantName}
groups={groups}
config={config}
/>
</div> </div>
); );
}; };
export default function QuadrantGrid({ items, config }: { items: Item[], config: ConfigData }) { export default function QuadrantGrid({
items,
config,
}: {
items: Item[];
config: ConfigData;
}) {
const groups = groupByQuadrants(items); const groups = groupByQuadrants(items);
return ( return (
<div className="quadrant-grid"> <div className="quadrant-grid">
{Object.keys(config.quadrants).map((quadrantName: string) => renderQuadrant(quadrantName, groups, config))} {Object.keys(config.quadrants).map((quadrantName: string) =>
renderQuadrant(quadrantName, groups, config)
)}
</div> </div>
); );
} }

View File

@@ -1,10 +1,11 @@
import { translate, ConfigData } from "../../config"; import { ConfigData, translate } from "../../config";
import Badge from "../Badge/Badge";
import Link from "../Link/Link";
import ItemList from "../ItemList/ItemList";
import Flag from "../Flag/Flag";
import { Group } from "../../model"; import { Group } from "../../model";
import Badge from "../Badge/Badge";
import Flag from "../Flag/Flag";
import ItemList from "../ItemList/ItemList";
import Link from "../Link/Link";
import "./quadrant-section.scss"; import "./quadrant-section.scss";
const renderList = ( const renderList = (
ringName: string, ringName: string,
quadrantName: string, quadrantName: string,

View File

@@ -1,10 +1,10 @@
import React from "react"; import React from "react";
import RadarChart from "../Chart/RadarChart";
import { ConfigData } from "../../config"; import { ConfigData } from "../../config";
import { Item, QuadrantConfig } from "../../model"; import { Item, QuadrantConfig } from "../../model";
import RadarChart from "../Chart/RadarChart";
import "./radar-grid.scss";
import Link from "../Link/Link"; import Link from "../Link/Link";
import "./radar-grid.scss";
const QuadrantLabel: React.FC<{ const QuadrantLabel: React.FC<{
quadrantConfig: QuadrantConfig; quadrantConfig: QuadrantConfig;
@@ -19,7 +19,10 @@ const QuadrantLabel: React.FC<{
]; ];
return ( return (
<div className="quadrant-label" style={stylesMap[quadrantConfig.position - 1]}> <div
className="quadrant-label"
style={stylesMap[quadrantConfig.position - 1]}
>
<div className="split"> <div className="split">
<div className="split__left"> <div className="split__left">
<small>Quadrant {quadrantConfig.position}</small> <small>Quadrant {quadrantConfig.position}</small>
@@ -65,7 +68,12 @@ const RadarGrid: React.FC<{ items: Item[]; config: ConfigData }> = ({
<div className="radar-grid"> <div className="radar-grid">
<RadarChart items={items} config={config} /> <RadarChart items={items} config={config} />
{Object.entries(config.quadrantsMap).map(([name, quadrant], index) => ( {Object.entries(config.quadrantsMap).map(([name, quadrant], index) => (
<QuadrantLabel key={index} quadrantConfig={quadrant} quadrantName={name} quadrantLabel={config.quadrants[name]} /> <QuadrantLabel
key={index}
quadrantConfig={quadrant}
quadrantName={name}
quadrantLabel={config.quadrants[name]}
/>
))} ))}
<Legend /> <Legend />
</div> </div>

View File

@@ -1,13 +1,13 @@
import { useState, useEffect } from "react"; import { useEffect, useState } from "react";
import PageIndex from "./PageIndex/PageIndex";
import PageOverview from "./PageOverview/PageOverview";
import PageHelp from "./PageHelp/PageHelp";
import PageQuadrant from "./PageQuadrant/PageQuadrant";
import PageItem from "./PageItem/PageItem";
import PageItemMobile from "./PageItemMobile/PageItemMobile";
import { ConfigData, getItemPageNames, isMobileViewport } from "../config"; import { ConfigData, getItemPageNames, isMobileViewport } from "../config";
import { Item } from "../model"; import { Item } from "../model";
import PageHelp from "./PageHelp/PageHelp";
import PageIndex from "./PageIndex/PageIndex";
import PageItem from "./PageItem/PageItem";
import PageItemMobile from "./PageItemMobile/PageItemMobile";
import PageOverview from "./PageOverview/PageOverview";
import PageQuadrant from "./PageQuadrant/PageQuadrant";
type RouterProps = { type RouterProps = {
pageName: string; pageName: string;

View File

@@ -1,6 +1,7 @@
import React, { FormEvent } from "react";
import { useMessages } from "../../context/MessagesContext";
import classNames from "classnames"; import classNames from "classnames";
import React, { FormEvent } from "react";
import { useMessages } from "../../context/MessagesContext";
import "./search.scss"; import "./search.scss";
type SearchProps = { type SearchProps = {
@@ -52,7 +53,7 @@ function Search(
<span className={classNames("search__button", { "is-open": open })}> <span className={classNames("search__button", { "is-open": open })}>
<button type="submit" className="button"> <button type="submit" className="button">
<span className="icon icon--search button__icon" /> <span className="icon icon--search button__icon" />
{ searchLabel } {searchLabel}
</button> </button>
</span> </span>
{closable && ( {closable && (

View File

@@ -1,4 +1,5 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { radarName } from "../config"; import { radarName } from "../config";
type SetTitleProps = { type SetTitleProps = {

View File

@@ -1,13 +1,13 @@
import React from "react"; import React from "react";
import { import {
FaExternalLinkAlt,
FaFacebookF, FaFacebookF,
FaTwitter,
FaLinkedinIn,
FaXing,
FaYoutube,
FaGithub, FaGithub,
FaInstagram, FaInstagram,
FaExternalLinkAlt FaLinkedinIn,
FaTwitter,
FaXing,
FaYoutube,
} from "react-icons/fa"; } from "react-icons/fa";
const icons = { const icons = {

View File

@@ -1,4 +1,4 @@
import {Item, HomepageOption, QuadrantConfig} from './model'; import { HomepageOption, Item, QuadrantConfig } from "./model";
export interface ConfigData { export interface ConfigData {
tags?: string[]; tags?: string[];
@@ -7,17 +7,17 @@ export interface ConfigData {
showEmptyRings: boolean; showEmptyRings: boolean;
quadrantsMap: { [quadrant: string]: QuadrantConfig }; quadrantsMap: { [quadrant: string]: QuadrantConfig };
chartConfig: { chartConfig: {
size: number, size: number;
scale: number[], scale: number[];
blipSize: number, blipSize: number;
ringsAttributes: {radius: number, arcWidth: number}[] ringsAttributes: { radius: number; arcWidth: number }[];
}; };
homepageContent: HomepageOption; homepageContent: HomepageOption;
dateFormat?: string; dateFormat?: string;
editLink?: { editLink?: {
radarLink: string, radarLink: string;
title?: string title?: string;
} };
} }
export const radarName = export const radarName =

View File

@@ -1,4 +1,5 @@
import { createContext, FC, useContext } from "react"; import { FC, createContext, useContext } from "react";
import { Props as SocialLink } from "../../components/SocialLink/SocialLink"; import { Props as SocialLink } from "../../components/SocialLink/SocialLink";
interface Quadrant { interface Quadrant {

View File

@@ -1,11 +1,14 @@
import moment from "moment"; import moment from "moment";
import { formatRelease } from "./date"; import { formatRelease } from "./date";
describe("formatRelease", () => { describe("formatRelease", () => {
it("should format a date object using default output format", () => { it("should format a date object using default output format", () => {
expect(formatRelease(moment('2022-01-05'))).toEqual('January 2022') expect(formatRelease(moment("2022-01-05"))).toEqual("January 2022");
}); });
it("should format a date object using a custom output format", () => { it("should format a date object using a custom output format", () => {
expect(formatRelease(moment('2022-01-05'), 'DD.MM.YYYY')).toEqual('05.01.2022') expect(formatRelease(moment("2022-01-05"), "DD.MM.YYYY")).toEqual(
"05.01.2022"
);
}); });
}); });

View File

@@ -3,5 +3,7 @@ import moment from "moment";
const isoDateToMoment = (isoDate: moment.MomentInput) => const isoDateToMoment = (isoDate: moment.MomentInput) =>
moment(isoDate, "YYYY-MM-DD"); moment(isoDate, "YYYY-MM-DD");
export const formatRelease = (isoDate: moment.MomentInput, format: string = "MMMM YYYY") => export const formatRelease = (
isoDateToMoment(isoDate).format(format); isoDate: moment.MomentInput,
format: string = "MMMM YYYY"
) => isoDateToMoment(isoDate).format(format);

View File

@@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import App from "./components/App";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import App from "./components/App";
import "./index.scss"; import "./index.scss";
const container = document.getElementById("root")!; const container = document.getElementById("root")!;

View File

@@ -1,7 +1,7 @@
export enum HomepageOption { export enum HomepageOption {
chart = "chart", chart = "chart",
columns = "columns", columns = "columns",
both = "both" both = "both",
} }
export type ItemAttributes = { export type ItemAttributes = {
@@ -14,9 +14,9 @@ export type ItemAttributes = {
}; };
export enum FlagType { export enum FlagType {
new = 'new', new = "new",
changed = 'changed', changed = "changed",
default = 'default' default = "default",
} }
export type Item = ItemAttributes & { export type Item = ItemAttributes & {
@@ -28,12 +28,12 @@ export type Item = ItemAttributes & {
}; };
export type Blip = Item & { export type Blip = Item & {
quadrantPosition: number quadrantPosition: number;
ringPosition: number ringPosition: number;
colour: string colour: string;
txtColour: string txtColour: string;
coordinates: Point coordinates: Point;
} };
export type Revision = ItemAttributes & { export type Revision = ItemAttributes & {
body: string; body: string;
@@ -46,11 +46,11 @@ export type Quadrant = {
}; };
export type QuadrantConfig = { export type QuadrantConfig = {
colour: string, colour: string;
txtColour: string, txtColour: string;
position: number, position: number;
description: string description: string;
} };
export type Radar = { export type Radar = {
items: Item[]; items: Item[];
@@ -67,9 +67,9 @@ export const nonFeaturedOnly = (items: Item[]) =>
items.filter((item) => !item.featured); items.filter((item) => !item.featured);
export type Point = { export type Point = {
x: number, x: number;
y: number y: number;
} };
export const groupByQuadrants = (items: Item[]): Group => export const groupByQuadrants = (items: Item[]): Group =>
items.reduce( items.reduce(

View File

@@ -2,17 +2,22 @@ import { sanitize } from "./sanitize";
describe("sanitize", () => { describe("sanitize", () => {
it("should sanitize the string input to HTML output", () => { it("should sanitize the string input to HTML output", () => {
let res = sanitize('foo'); let res = sanitize("foo");
expect(res.__html).toEqual("foo"); expect(res.__html).toEqual("foo");
res = sanitize('<a href="https://example.org">Example.org</a>'); res = sanitize('<a href="https://example.org">Example.org</a>');
expect(res.__html).toEqual("<a href=\"https://example.org\">Example.org</a>"); expect(res.__html).toEqual('<a href="https://example.org">Example.org</a>');
}); });
it("should not sanitize not allowed tags", () => { it("should not sanitize not allowed tags", () => {
let res = sanitize('Before <iframe src="https://example.org"></iframe> After'); let res = sanitize(
'Before <iframe src="https://example.org"></iframe> After'
);
expect(res.__html).toEqual("Before After"); expect(res.__html).toEqual("Before After");
}); });
it("should accept options for rendering", () => { it("should accept options for rendering", () => {
let res = sanitize('<a href="https://example.org" target="_blank">Example.org</a>', { allowedAttributes: { a: ['href']}}); let res = sanitize(
expect(res.__html).toEqual("<a href=\"https://example.org\">Example.org</a>"); '<a href="https://example.org" target="_blank">Example.org</a>',
{ allowedAttributes: { a: ["href"] } }
);
expect(res.__html).toEqual('<a href="https://example.org">Example.org</a>');
}); });
}); });

View File

@@ -1,15 +1,15 @@
import sanitizeHtml from 'sanitize-html'; import sanitizeHtml from "sanitize-html";
const defaultSanitizeOptions = { const defaultSanitizeOptions = {
allowedTags: ['b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li'], allowedTags: ["b", "i", "em", "strong", "a", "ul", "ol", "li"],
allowedAttributes: { allowedAttributes: {
'a': ['href', 'target'] a: ["href", "target"],
} },
} };
export const sanitize = (dirty: string, options: sanitizeHtml.IOptions = defaultSanitizeOptions) => ({ export const sanitize = (
__html: sanitizeHtml( dirty: string,
dirty, options: sanitizeHtml.IOptions = defaultSanitizeOptions
options ) => ({
) __html: sanitizeHtml(dirty, options),
}); });