feat: cleanup
This commit is contained in:
committed by
Mathias Schopmans
parent
1583363ab8
commit
ab005e03b3
26
.gitignore
vendored
26
.gitignore
vendored
@@ -1,26 +0,0 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
/.yarn
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
.vscode
|
|
||||||
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
1
.husky/.gitignore
vendored
1
.husky/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
_
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
npm run lint && npm rum prettier && npm run build:scripts && git add dist_scripts
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
"use strict";
|
|
||||||
var __assign = (this && this.__assign) || function () {
|
|
||||||
__assign = Object.assign || function(t) {
|
|
||||||
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
||||||
s = arguments[i];
|
|
||||||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
||||||
t[p] = s[p];
|
|
||||||
}
|
|
||||||
return t;
|
|
||||||
};
|
|
||||||
return __assign.apply(this, arguments);
|
|
||||||
};
|
|
||||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
||||||
if (k2 === undefined) k2 = k;
|
|
||||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
||||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
||||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
||||||
}
|
|
||||||
Object.defineProperty(o, k2, desc);
|
|
||||||
}) : (function(o, m, k, k2) {
|
|
||||||
if (k2 === undefined) k2 = k;
|
|
||||||
o[k2] = m[k];
|
|
||||||
}));
|
|
||||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
||||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
||||||
}) : function(o, v) {
|
|
||||||
o["default"] = v;
|
|
||||||
});
|
|
||||||
var __importStar = (this && this.__importStar) || function (mod) {
|
|
||||||
if (mod && mod.__esModule) return mod;
|
|
||||||
var result = {};
|
|
||||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
||||||
__setModuleDefault(result, mod);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
var child_process_1 = require("child_process");
|
|
||||||
var crypto_1 = require("crypto");
|
|
||||||
var fs = __importStar(require("fs-extra"));
|
|
||||||
var paths = __importStar(require("./paths"));
|
|
||||||
// Do this as the first thing so that any code reading it knows the right env.
|
|
||||||
process.env.BABEL_ENV = "production";
|
|
||||||
process.env.NODE_ENV = "production";
|
|
||||||
// Makes the script crash on unhandled rejections instead of silently
|
|
||||||
// ignoring them. In the future, promise rejections that are not handled will
|
|
||||||
// terminate the Node.js process with a non-zero exit code.
|
|
||||||
process.on("unhandledRejection", function (err) {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
fs.removeSync(paths.templateNodeModules);
|
|
||||||
fs.ensureSymlinkSync(paths.appNodeModules, paths.templateNodeModules);
|
|
||||||
var runCommand = function (command) {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
var executedCommand = (0, child_process_1.spawn)(command, {
|
|
||||||
stdio: "inherit",
|
|
||||||
shell: true,
|
|
||||||
env: __assign({ REACT_APP_RADAR_NAME: "AOE Technology Radar", REACT_APP_RADAR_TITLE_FORMAT: "%TECHNOLOGY_NAME% | %APP_TITLE%", REACT_APP_BUILDHASH: (0, crypto_1.randomBytes)(10).toString("hex"), GENERATE_SOURCEMAP: "false" }, process.env),
|
|
||||||
});
|
|
||||||
executedCommand.on("error", function (error) {
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
executedCommand.on("exit", function (code) {
|
|
||||||
if (code === 0) {
|
|
||||||
resolve(code);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
reject();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).catch(function (error) {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
var buildTemplate = function () {
|
|
||||||
var packageManager = fs.existsSync(paths.appYarnLock) ? "yarn" : "npm";
|
|
||||||
fs.emptyDirSync(paths.templateBuild);
|
|
||||||
process.chdir(paths.template);
|
|
||||||
return runCommand("".concat(packageManager, " run build"));
|
|
||||||
};
|
|
||||||
buildTemplate().then(function () {
|
|
||||||
fs.copySync(paths.templateBuild, paths.appBuild);
|
|
||||||
fs.ensureDirSync(paths.appPublic);
|
|
||||||
fs.copySync(paths.appPublic, paths.appBuild);
|
|
||||||
console.log("".concat(paths.appBuild, " was created and can be deployed."));
|
|
||||||
});
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
"use strict";
|
|
||||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
||||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
||||||
return new (P || (P = Promise))(function (resolve, reject) {
|
|
||||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
||||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
||||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
||||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
||||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
||||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
||||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
||||||
function step(op) {
|
|
||||||
if (f) throw new TypeError("Generator is already executing.");
|
|
||||||
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
||||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
||||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
||||||
switch (op[0]) {
|
|
||||||
case 0: case 1: t = op; break;
|
|
||||||
case 4: _.label++; return { value: op[1], done: false };
|
|
||||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
||||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
||||||
default:
|
|
||||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
||||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
||||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
||||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
||||||
if (t[2]) _.ops.pop();
|
|
||||||
_.trys.pop(); continue;
|
|
||||||
}
|
|
||||||
op = body.call(thisArg, _);
|
|
||||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
||||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
var fs_1 = require("fs");
|
|
||||||
var jsdom_1 = require("jsdom");
|
|
||||||
var xml_sitemap_1 = __importDefault(require("xml-sitemap"));
|
|
||||||
var config_1 = require("../src/config");
|
|
||||||
var radar_1 = require("./generateJson/radar");
|
|
||||||
// Do this as the first thing so that any code reading it knows the right env.
|
|
||||||
process.env.BABEL_ENV = "production";
|
|
||||||
process.env.NODE_ENV = "production";
|
|
||||||
// Makes the script crash on unhandled rejections instead of silently
|
|
||||||
// ignoring them. In the future, promise rejections that are not handled will
|
|
||||||
// terminate the Node.js process with a non-zero exit code.
|
|
||||||
process.on("unhandledRejection", function (err) {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
var createStaticFiles = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
||||||
var radar, rawConf, config, sitemap, sitemapOptions;
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
switch (_a.label) {
|
|
||||||
case 0:
|
|
||||||
console.log("starting static");
|
|
||||||
return [4 /*yield*/, (0, radar_1.createRadar)()];
|
|
||||||
case 1:
|
|
||||||
radar = _a.sent();
|
|
||||||
(0, fs_1.copyFileSync)("build/index.html", "build/overview.html");
|
|
||||||
(0, fs_1.copyFileSync)("build/index.html", "build/help-and-about-tech-radar.html");
|
|
||||||
rawConf = (0, fs_1.readFileSync)("build/config.json", "utf-8");
|
|
||||||
config = JSON.parse(rawConf);
|
|
||||||
Object.keys(config.quadrants).forEach(function (quadrant) {
|
|
||||||
var destFolder = "build/".concat(quadrant);
|
|
||||||
(0, fs_1.copyFileSync)("build/index.html", "".concat(destFolder, ".html"));
|
|
||||||
if (!(0, fs_1.existsSync)(destFolder)) {
|
|
||||||
(0, fs_1.mkdirSync)(destFolder);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
sitemap = new xml_sitemap_1.default();
|
|
||||||
sitemapOptions = {
|
|
||||||
lastmod: "now",
|
|
||||||
changefreq: "weekly",
|
|
||||||
};
|
|
||||||
sitemap.add("".concat(config_1.publicUrl, "index.html"), sitemapOptions);
|
|
||||||
radar.items.forEach(function (item) {
|
|
||||||
var targetPath = "build/".concat(item.quadrant, "/").concat(item.name, ".html");
|
|
||||||
(0, fs_1.copyFileSync)("build/index.html", targetPath);
|
|
||||||
jsdom_1.JSDOM.fromFile(targetPath).then(function (dom) {
|
|
||||||
var document = dom.window.document;
|
|
||||||
var rootEl = document.getElementById("root");
|
|
||||||
(0, config_1.setTitle)(document, item.title);
|
|
||||||
if (rootEl) {
|
|
||||||
var textNode = document.createElement("div");
|
|
||||||
var bodyFragment = jsdom_1.JSDOM.fragment(item.body);
|
|
||||||
textNode.appendChild(bodyFragment);
|
|
||||||
var headlineNode = document.createElement("h1");
|
|
||||||
var titleText = document.createTextNode(item.title);
|
|
||||||
headlineNode.appendChild(titleText);
|
|
||||||
rootEl.appendChild(headlineNode);
|
|
||||||
rootEl.appendChild(textNode);
|
|
||||||
// remove the <noscript> element as page has already been hydrated with static content
|
|
||||||
var noscriptEl = document.getElementsByTagName("noscript");
|
|
||||||
if (noscriptEl[0]) {
|
|
||||||
noscriptEl[0].remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.warn('Element with ID "root" not found. Static site content will be empty.');
|
|
||||||
}
|
|
||||||
(0, fs_1.writeFileSync)(targetPath, dom.serialize());
|
|
||||||
});
|
|
||||||
sitemap.add("".concat(config_1.publicUrl).concat(item.quadrant, "/").concat(item.name, ".html"), sitemapOptions);
|
|
||||||
});
|
|
||||||
(0, fs_1.writeFileSync)("build/sitemap.xml", sitemap.xml);
|
|
||||||
return [2 /*return*/];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}); };
|
|
||||||
createStaticFiles()
|
|
||||||
.then(function () {
|
|
||||||
console.log("created static files.");
|
|
||||||
})
|
|
||||||
.catch(function (err) {
|
|
||||||
if (err && err.message) {
|
|
||||||
console.error(err.message);
|
|
||||||
}
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
"use strict";
|
|
||||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
||||||
if (k2 === undefined) k2 = k;
|
|
||||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
||||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
||||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
||||||
}
|
|
||||||
Object.defineProperty(o, k2, desc);
|
|
||||||
}) : (function(o, m, k, k2) {
|
|
||||||
if (k2 === undefined) k2 = k;
|
|
||||||
o[k2] = m[k];
|
|
||||||
}));
|
|
||||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
||||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
||||||
}) : function(o, v) {
|
|
||||||
o["default"] = v;
|
|
||||||
});
|
|
||||||
var __importStar = (this && this.__importStar) || function (mod) {
|
|
||||||
if (mod && mod.__esModule) return mod;
|
|
||||||
var result = {};
|
|
||||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
||||||
__setModuleDefault(result, mod);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
||||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
||||||
return new (P || (P = Promise))(function (resolve, reject) {
|
|
||||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
||||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
||||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
||||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
||||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
||||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
||||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
||||||
function step(op) {
|
|
||||||
if (f) throw new TypeError("Generator is already executing.");
|
|
||||||
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
||||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
||||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
||||||
switch (op[0]) {
|
|
||||||
case 0: case 1: t = op; break;
|
|
||||||
case 4: _.label++; return { value: op[1], done: false };
|
|
||||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
||||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
||||||
default:
|
|
||||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
||||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
||||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
||||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
||||||
if (t[2]) _.ops.pop();
|
|
||||||
_.trys.pop(); continue;
|
|
||||||
}
|
|
||||||
op = body.call(thisArg, _);
|
|
||||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
||||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
var fs = __importStar(require("fs-extra"));
|
|
||||||
var file_1 = require("./generateJson/file");
|
|
||||||
var paths = __importStar(require("./paths"));
|
|
||||||
// Do this as the first thing so that any code reading it knows the right env.
|
|
||||||
process.env.BABEL_ENV = "production";
|
|
||||||
process.env.NODE_ENV = "production";
|
|
||||||
// Makes the script crash on unhandled rejections instead of silently
|
|
||||||
// ignoring them. In the future, promise rejections that are not handled will
|
|
||||||
// terminate the Node.js process with a non-zero exit code.
|
|
||||||
process.on("unhandledRejection", function (err) {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
fs.removeSync(paths.templateNodeModules);
|
|
||||||
fs.ensureSymlinkSync(paths.appNodeModules, paths.templateNodeModules);
|
|
||||||
try {
|
|
||||||
fs.statSync((0, file_1.radarPath)());
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error("".concat((0, file_1.radarPath)(), " not found. Please create ").concat((0, file_1.radarPath)(), " and add your markdown files to build the techradar."));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
var generateJson = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
||||||
var createRadar, save, radar;
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
switch (_a.label) {
|
|
||||||
case 0:
|
|
||||||
createRadar = require("./generateJson/radar").createRadar;
|
|
||||||
save = require("./generateJson/file").save;
|
|
||||||
return [4 /*yield*/, createRadar()];
|
|
||||||
case 1:
|
|
||||||
radar = _a.sent();
|
|
||||||
return [4 /*yield*/, save(JSON.stringify(radar), paths.radarJson)];
|
|
||||||
case 2:
|
|
||||||
_a.sent();
|
|
||||||
return [2 /*return*/];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}); };
|
|
||||||
generateJson()
|
|
||||||
.then(function () {
|
|
||||||
console.log("".concat(paths.appRdJson, " created."));
|
|
||||||
})
|
|
||||||
.catch(function (err) {
|
|
||||||
if (err && err.message) {
|
|
||||||
console.error(err.message);
|
|
||||||
}
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
||||||
if (k2 === undefined) k2 = k;
|
|
||||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
||||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
||||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
||||||
}
|
|
||||||
Object.defineProperty(o, k2, desc);
|
|
||||||
}) : (function(o, m, k, k2) {
|
|
||||||
if (k2 === undefined) k2 = k;
|
|
||||||
o[k2] = m[k];
|
|
||||||
}));
|
|
||||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
||||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
||||||
}) : function(o, v) {
|
|
||||||
o["default"] = v;
|
|
||||||
});
|
|
||||||
var __importStar = (this && this.__importStar) || function (mod) {
|
|
||||||
if (mod && mod.__esModule) return mod;
|
|
||||||
var result = {};
|
|
||||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
||||||
__setModuleDefault(result, mod);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
||||||
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
||||||
if (ar || !(i in from)) {
|
|
||||||
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
||||||
ar[i] = from[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return to.concat(ar || Array.prototype.slice.call(from));
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.save = exports.getAllMarkdownFiles = exports.buildPath = exports.jsPath = exports.faviconPath = exports.stylesPath = exports.radarPath = exports.relativePath = void 0;
|
|
||||||
var fs_extra_1 = require("fs-extra");
|
|
||||||
var path = __importStar(require("path"));
|
|
||||||
var walk_1 = require("walk");
|
|
||||||
var relativePath = function () {
|
|
||||||
var relativePath = [];
|
|
||||||
for (var _i = 0; _i < arguments.length; _i++) {
|
|
||||||
relativePath[_i] = arguments[_i];
|
|
||||||
}
|
|
||||||
return path.resolve.apply(path, relativePath);
|
|
||||||
};
|
|
||||||
exports.relativePath = relativePath;
|
|
||||||
var radarPath = function () {
|
|
||||||
var pathInSrc = [];
|
|
||||||
for (var _i = 0; _i < arguments.length; _i++) {
|
|
||||||
pathInSrc[_i] = arguments[_i];
|
|
||||||
}
|
|
||||||
return exports.relativePath.apply(void 0, __spreadArray(["radar"], pathInSrc, false));
|
|
||||||
};
|
|
||||||
exports.radarPath = radarPath;
|
|
||||||
var stylesPath = function () {
|
|
||||||
var pathInSrc = [];
|
|
||||||
for (var _i = 0; _i < arguments.length; _i++) {
|
|
||||||
pathInSrc[_i] = arguments[_i];
|
|
||||||
}
|
|
||||||
return exports.relativePath.apply(void 0, __spreadArray(["styles"], pathInSrc, false));
|
|
||||||
};
|
|
||||||
exports.stylesPath = stylesPath;
|
|
||||||
var faviconPath = function () {
|
|
||||||
var pathInSrc = [];
|
|
||||||
for (var _i = 0; _i < arguments.length; _i++) {
|
|
||||||
pathInSrc[_i] = arguments[_i];
|
|
||||||
}
|
|
||||||
return exports.relativePath.apply(void 0, __spreadArray(["assets/favicon.ico"], pathInSrc, false));
|
|
||||||
};
|
|
||||||
exports.faviconPath = faviconPath;
|
|
||||||
var jsPath = function () {
|
|
||||||
var pathInSrc = [];
|
|
||||||
for (var _i = 0; _i < arguments.length; _i++) {
|
|
||||||
pathInSrc[_i] = arguments[_i];
|
|
||||||
}
|
|
||||||
return exports.relativePath.apply(void 0, __spreadArray(["js"], pathInSrc, false));
|
|
||||||
};
|
|
||||||
exports.jsPath = jsPath;
|
|
||||||
var buildPath = function () {
|
|
||||||
var pathInDist = [];
|
|
||||||
for (var _i = 0; _i < arguments.length; _i++) {
|
|
||||||
pathInDist[_i] = arguments[_i];
|
|
||||||
}
|
|
||||||
return exports.relativePath.apply(void 0, __spreadArray(["build"], pathInDist, false));
|
|
||||||
};
|
|
||||||
exports.buildPath = buildPath;
|
|
||||||
var getAllMarkdownFiles = function (folder) {
|
|
||||||
return getAllFiles(folder, isMarkdownFile);
|
|
||||||
};
|
|
||||||
exports.getAllMarkdownFiles = getAllMarkdownFiles;
|
|
||||||
var getAllFiles = function (folder, predicate) {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
var walker = (0, walk_1.walk)(folder, { followLinks: false });
|
|
||||||
var files = [];
|
|
||||||
walker.on("file", function (root, fileStat, next) {
|
|
||||||
if (predicate(fileStat.name)) {
|
|
||||||
files.push(path.resolve(root, fileStat.name));
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
walker.on("errors", function (root, nodeStatsArray, next) {
|
|
||||||
nodeStatsArray.forEach(function (n) {
|
|
||||||
console.error("[ERROR] " + n.name);
|
|
||||||
if (n.error) {
|
|
||||||
console.error(n.error.message || n.error.code + ": " + n.error.path);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
walker.on("end", function () {
|
|
||||||
resolve(files.sort());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
var isMarkdownFile = function (name) { return name.match(/\.md$/) !== null; };
|
|
||||||
var save = function (data, fileName) {
|
|
||||||
return (0, fs_extra_1.outputFile)((0, exports.buildPath)(fileName), data);
|
|
||||||
};
|
|
||||||
exports.save = save;
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __assign = (this && this.__assign) || function () {
|
|
||||||
__assign = Object.assign || function(t) {
|
|
||||||
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
||||||
s = arguments[i];
|
|
||||||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
||||||
t[p] = s[p];
|
|
||||||
}
|
|
||||||
return t;
|
|
||||||
};
|
|
||||||
return __assign.apply(this, arguments);
|
|
||||||
};
|
|
||||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
||||||
if (k2 === undefined) k2 = k;
|
|
||||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
||||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
||||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
||||||
}
|
|
||||||
Object.defineProperty(o, k2, desc);
|
|
||||||
}) : (function(o, m, k, k2) {
|
|
||||||
if (k2 === undefined) k2 = k;
|
|
||||||
o[k2] = m[k];
|
|
||||||
}));
|
|
||||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
||||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
||||||
}) : function(o, v) {
|
|
||||||
o["default"] = v;
|
|
||||||
});
|
|
||||||
var __importStar = (this && this.__importStar) || function (mod) {
|
|
||||||
if (mod && mod.__esModule) return mod;
|
|
||||||
var result = {};
|
|
||||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
||||||
__setModuleDefault(result, mod);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
||||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
||||||
return new (P || (P = Promise))(function (resolve, reject) {
|
|
||||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
||||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
||||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
||||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
||||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
||||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
||||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
||||||
function step(op) {
|
|
||||||
if (f) throw new TypeError("Generator is already executing.");
|
|
||||||
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
||||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
||||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
||||||
switch (op[0]) {
|
|
||||||
case 0: case 1: t = op; break;
|
|
||||||
case 4: _.label++; return { value: op[1], done: false };
|
|
||||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
||||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
||||||
default:
|
|
||||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
||||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
||||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
||||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
||||||
if (t[2]) _.ops.pop();
|
|
||||||
_.trys.pop(); continue;
|
|
||||||
}
|
|
||||||
op = body.call(thisArg, _);
|
|
||||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
||||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
||||||
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
||||||
if (ar || !(i in from)) {
|
|
||||||
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
||||||
ar[i] = from[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return to.concat(ar || Array.prototype.slice.call(from));
|
|
||||||
};
|
|
||||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.createRadar = void 0;
|
|
||||||
var front_matter_1 = __importDefault(require("front-matter"));
|
|
||||||
var fs_1 = require("fs");
|
|
||||||
var fs_extra_1 = require("fs-extra");
|
|
||||||
var highlight_js_1 = __importDefault(require("highlight.js"));
|
|
||||||
var marked_1 = require("marked");
|
|
||||||
var path = __importStar(require("path"));
|
|
||||||
var config_1 = require("../../src/config");
|
|
||||||
var model_1 = require("../../src/model");
|
|
||||||
var paths_1 = require("../paths");
|
|
||||||
var file_1 = require("./file");
|
|
||||||
marked_1.marked.setOptions({
|
|
||||||
highlight: function (code) { return highlight_js_1.default.highlightAuto(code).value; },
|
|
||||||
});
|
|
||||||
var createRadar = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
||||||
var fileNames, revisions, filterdRevisions, allReleases, items, flaggedItems;
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
switch (_a.label) {
|
|
||||||
case 0: return [4 /*yield*/, (0, file_1.getAllMarkdownFiles)((0, file_1.radarPath)())];
|
|
||||||
case 1:
|
|
||||||
fileNames = _a.sent();
|
|
||||||
return [4 /*yield*/, createRevisionsFromFiles(fileNames)];
|
|
||||||
case 2:
|
|
||||||
revisions = _a.sent();
|
|
||||||
filterdRevisions = revisions.filter(function (r) { return r !== undefined; });
|
|
||||||
allReleases = getAllReleases(filterdRevisions);
|
|
||||||
items = createItems(filterdRevisions);
|
|
||||||
flaggedItems = flagItem(items, allReleases);
|
|
||||||
items.forEach(function (item) { return checkAttributes(item.name, item); });
|
|
||||||
return [2 /*return*/, {
|
|
||||||
items: flaggedItems,
|
|
||||||
releases: allReleases,
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}); };
|
|
||||||
exports.createRadar = createRadar;
|
|
||||||
var checkAttributes = function (fileName, attributes) {
|
|
||||||
var rawConf = (0, fs_1.readFileSync)(path.resolve(paths_1.appBuild, "config.json"), "utf-8");
|
|
||||||
var config = JSON.parse(rawConf);
|
|
||||||
if (!config.rings.includes(attributes.ring)) {
|
|
||||||
throw new Error("Error: ".concat(fileName, " has an illegal value for 'ring' - must be one of ").concat(config.rings));
|
|
||||||
}
|
|
||||||
var quadrants = Object.keys(config.quadrants);
|
|
||||||
if (!quadrants.includes(attributes.quadrant)) {
|
|
||||||
throw new Error("Error: ".concat(fileName, " has an illegal value for 'quadrant' - must be one of ").concat(quadrants));
|
|
||||||
}
|
|
||||||
if (config.tags) {
|
|
||||||
for (var _i = 0, _a = config.tags; _i < _a.length; _i++) {
|
|
||||||
var tag = _a[_i];
|
|
||||||
if (attributes.tags && attributes.tags.includes(tag)) {
|
|
||||||
return attributes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return attributes;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var createRevisionsFromFiles = function (fileNames) {
|
|
||||||
return Promise.all(fileNames.map(function (fileName) {
|
|
||||||
return (0, fs_extra_1.readFile)(fileName, "utf8").then(function (data) {
|
|
||||||
var fm = (0, front_matter_1.default)(data);
|
|
||||||
var html = (0, marked_1.marked)(fm.body.replace(/\]\(\//g, "](".concat(config_1.publicUrl)));
|
|
||||||
html = html.replace(/a href="http/g, 'a target="_blank" rel="noopener noreferrer" href="http');
|
|
||||||
var attributes = checkAttributes(fileName, fm.attributes);
|
|
||||||
if (attributes) {
|
|
||||||
return __assign(__assign(__assign({}, itemInfoFromFilename(fileName)), attributes), { fileName: fileName, body: html });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
var itemInfoFromFilename = function (fileName) {
|
|
||||||
var _a = fileName.split(path.sep).slice(-2), release = _a[0], name = _a[1];
|
|
||||||
return {
|
|
||||||
name: path.basename(name, ".md"),
|
|
||||||
release: release,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
var getAllReleases = function (revisions) {
|
|
||||||
return revisions
|
|
||||||
.reduce(function (allReleases, _a) {
|
|
||||||
var release = _a.release;
|
|
||||||
if (!allReleases.includes(release)) {
|
|
||||||
return __spreadArray(__spreadArray([], allReleases, true), [release], false);
|
|
||||||
}
|
|
||||||
return allReleases;
|
|
||||||
}, [])
|
|
||||||
.sort();
|
|
||||||
};
|
|
||||||
var createItems = function (revisions) {
|
|
||||||
var itemMap = revisions.reduce(function (items, revision) {
|
|
||||||
var _a;
|
|
||||||
return __assign(__assign({}, items), (_a = {}, _a[revision.name] = addRevisionToItem(items[revision.name], revision), _a));
|
|
||||||
}, {});
|
|
||||||
return Object.values(itemMap)
|
|
||||||
.map(function (item) { return (__assign(__assign({}, item), { title: item.title || item.name })); })
|
|
||||||
.sort(function (x, y) { return (x.name > y.name ? 1 : -1); });
|
|
||||||
};
|
|
||||||
var ignoreEmptyRevisionBody = function (revision, item) {
|
|
||||||
if (!revision.body || revision.body.trim() === "") {
|
|
||||||
return item.body;
|
|
||||||
}
|
|
||||||
return revision.body;
|
|
||||||
};
|
|
||||||
var addRevisionToItem = function (item, revision) {
|
|
||||||
if (item === void 0) { item = {
|
|
||||||
flag: model_1.FlagType.default,
|
|
||||||
featured: true,
|
|
||||||
revisions: [],
|
|
||||||
name: "",
|
|
||||||
title: "",
|
|
||||||
ring: "trial",
|
|
||||||
quadrant: "",
|
|
||||||
body: "",
|
|
||||||
info: "",
|
|
||||||
angleFraction: Math.random(),
|
|
||||||
radiusFraction: Math.random(),
|
|
||||||
}; }
|
|
||||||
var newItem = __assign(__assign(__assign({}, item), revision), { body: ignoreEmptyRevisionBody(revision, item) });
|
|
||||||
if (revisionCreatesNewHistoryEntry(revision, item)) {
|
|
||||||
newItem = __assign(__assign({}, newItem), { revisions: __spreadArray([revision], newItem.revisions, true) });
|
|
||||||
}
|
|
||||||
return newItem;
|
|
||||||
};
|
|
||||||
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));
|
|
||||||
};
|
|
||||||
var flagItem = function (items, allReleases) {
|
|
||||||
return items.map(function (item) {
|
|
||||||
return (__assign(__assign({}, item), { flag: getItemFlag(item, allReleases) }));
|
|
||||||
}, []);
|
|
||||||
};
|
|
||||||
var isInLastRelease = function (item, allReleases) {
|
|
||||||
return item.revisions[0].release === allReleases[allReleases.length - 1];
|
|
||||||
};
|
|
||||||
var isNewItem = function (item, allReleases) {
|
|
||||||
return item.revisions.length === 1 && isInLastRelease(item, allReleases);
|
|
||||||
};
|
|
||||||
var hasItemChanged = function (item, allReleases) {
|
|
||||||
return item.revisions.length > 1 && isInLastRelease(item, allReleases);
|
|
||||||
};
|
|
||||||
var getItemFlag = function (item, allReleases) {
|
|
||||||
if (isNewItem(item, allReleases)) {
|
|
||||||
return model_1.FlagType.new;
|
|
||||||
}
|
|
||||||
if (hasItemChanged(item, allReleases)) {
|
|
||||||
return model_1.FlagType.changed;
|
|
||||||
}
|
|
||||||
return model_1.FlagType.default;
|
|
||||||
};
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
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;
|
|
||||||
var fs_1 = require("fs");
|
|
||||||
var path_1 = require("path");
|
|
||||||
exports.radarJson = "rd.json";
|
|
||||||
var appDirectory = (0, fs_1.realpathSync)(process.cwd());
|
|
||||||
var resolveApp = function (relativePath) {
|
|
||||||
if (relativePath === void 0) { relativePath = ""; }
|
|
||||||
return (0, path_1.resolve)(appDirectory, relativePath);
|
|
||||||
};
|
|
||||||
var templateDirectory = (0, fs_1.realpathSync)(__dirname);
|
|
||||||
var resolveTemplate = function (relativePath) {
|
|
||||||
if (relativePath === void 0) { relativePath = ""; }
|
|
||||||
return (0, path_1.resolve)(templateDirectory, "../..", relativePath);
|
|
||||||
};
|
|
||||||
exports.template = resolveTemplate(); // this repository
|
|
||||||
exports.templateBuild = resolveTemplate("build"); // build folder in this repository
|
|
||||||
exports.templateNodeModules = resolveTemplate("node_modules"); // node_modules folder in this repository
|
|
||||||
exports.appRdJson = resolveApp("build/".concat(exports.radarJson)); // build/rd.json in project
|
|
||||||
exports.appBuild = resolveApp("build"); // build folder in project
|
|
||||||
exports.appPublic = resolveApp("public"); // public folder in project
|
|
||||||
exports.appYarnLock = resolveApp("yarn.lock"); // yarn.lock in project
|
|
||||||
exports.appNodeModules = resolveApp("node_modules"); // node_modules folder in project
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.translate = exports.assetUrl = exports.publicUrl = exports.isMobileViewport = exports.getItemPageNames = exports.setTitle = exports.titleFormat = exports.radarNameShort = exports.radarName = void 0;
|
|
||||||
exports.radarName = process.env.REACT_APP_RADAR_NAME || "AOE Technology Radar";
|
|
||||||
exports.radarNameShort = exports.radarName;
|
|
||||||
exports.titleFormat = process.env.REACT_APP_RADAR_TITLE_FORMAT || "%TECHNOLOGY_NAME% | %APP_TITLE%";
|
|
||||||
function setTitle(document, title) {
|
|
||||||
document.title = title
|
|
||||||
? exports.titleFormat
|
|
||||||
.replace("%TECHNOLOGY_NAME%", title)
|
|
||||||
.replace("%APP_TITLE%", exports.radarName)
|
|
||||||
: exports.radarName;
|
|
||||||
}
|
|
||||||
exports.setTitle = setTitle;
|
|
||||||
var getItemPageNames = function (items) {
|
|
||||||
return items.map(function (item) { return "".concat(item.quadrant, "/").concat(item.name); });
|
|
||||||
};
|
|
||||||
exports.getItemPageNames = getItemPageNames;
|
|
||||||
function isMobileViewport() {
|
|
||||||
// return false for server side rendering
|
|
||||||
if (typeof window == "undefined")
|
|
||||||
return false;
|
|
||||||
var width = window.innerWidth ||
|
|
||||||
document.documentElement.clientWidth ||
|
|
||||||
document.body.clientWidth;
|
|
||||||
return width < 1200;
|
|
||||||
}
|
|
||||||
exports.isMobileViewport = isMobileViewport;
|
|
||||||
exports.publicUrl = (process.env.PUBLIC_URL || "").replace(/\/$/, "") + "/";
|
|
||||||
function assetUrl(file) {
|
|
||||||
return exports.publicUrl + file;
|
|
||||||
}
|
|
||||||
exports.assetUrl = assetUrl;
|
|
||||||
function translate(config, key) {
|
|
||||||
return config.quadrants[key] || "-";
|
|
||||||
}
|
|
||||||
exports.translate = translate;
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __assign = (this && this.__assign) || function () {
|
|
||||||
__assign = Object.assign || function(t) {
|
|
||||||
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
||||||
s = arguments[i];
|
|
||||||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
||||||
t[p] = s[p];
|
|
||||||
}
|
|
||||||
return t;
|
|
||||||
};
|
|
||||||
return __assign.apply(this, arguments);
|
|
||||||
};
|
|
||||||
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
||||||
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
||||||
if (ar || !(i in from)) {
|
|
||||||
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
||||||
ar[i] = from[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return to.concat(ar || Array.prototype.slice.call(from));
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.getTags = exports.filteredOnly = exports.getFirstLetter = exports.groupByFirstLetter = exports.groupByQuadrants = exports.nonFeaturedOnly = exports.featuredOnly = exports.FlagType = exports.HomepageOption = void 0;
|
|
||||||
var HomepageOption;
|
|
||||||
(function (HomepageOption) {
|
|
||||||
HomepageOption["chart"] = "chart";
|
|
||||||
HomepageOption["columns"] = "columns";
|
|
||||||
HomepageOption["both"] = "both";
|
|
||||||
})(HomepageOption = exports.HomepageOption || (exports.HomepageOption = {}));
|
|
||||||
var FlagType;
|
|
||||||
(function (FlagType) {
|
|
||||||
FlagType["new"] = "new";
|
|
||||||
FlagType["changed"] = "changed";
|
|
||||||
FlagType["default"] = "default";
|
|
||||||
})(FlagType = exports.FlagType || (exports.FlagType = {}));
|
|
||||||
var featuredOnly = function (items) {
|
|
||||||
return items.filter(function (item) { return item.featured; });
|
|
||||||
};
|
|
||||||
exports.featuredOnly = featuredOnly;
|
|
||||||
var nonFeaturedOnly = function (items) {
|
|
||||||
return items.filter(function (item) { return !item.featured; });
|
|
||||||
};
|
|
||||||
exports.nonFeaturedOnly = nonFeaturedOnly;
|
|
||||||
var groupByQuadrants = function (items) {
|
|
||||||
return items.reduce(function (quadrants, item) {
|
|
||||||
var _a;
|
|
||||||
return (__assign(__assign({}, quadrants), (_a = {}, _a[item.quadrant] = addItemToQuadrant(quadrants[item.quadrant], item), _a)));
|
|
||||||
}, {});
|
|
||||||
};
|
|
||||||
exports.groupByQuadrants = groupByQuadrants;
|
|
||||||
var groupByFirstLetter = function (items) {
|
|
||||||
var index = items.reduce(function (letterIndex, item) {
|
|
||||||
var _a;
|
|
||||||
return (__assign(__assign({}, letterIndex), (_a = {}, _a[(0, exports.getFirstLetter)(item)] = addItemToList(letterIndex[(0, exports.getFirstLetter)(item)], item), _a)));
|
|
||||||
}, {});
|
|
||||||
return Object.keys(index)
|
|
||||||
.sort()
|
|
||||||
.map(function (letter) { return ({
|
|
||||||
letter: letter,
|
|
||||||
items: index[letter],
|
|
||||||
}); });
|
|
||||||
};
|
|
||||||
exports.groupByFirstLetter = groupByFirstLetter;
|
|
||||||
var addItemToQuadrant = function (quadrant, item) {
|
|
||||||
var _a;
|
|
||||||
if (quadrant === void 0) { quadrant = {}; }
|
|
||||||
return (__assign(__assign({}, quadrant), (_a = {}, _a[item.ring] = addItemToRing(quadrant[item.ring], item), _a)));
|
|
||||||
};
|
|
||||||
var addItemToList = function (list, item) {
|
|
||||||
if (list === void 0) { list = []; }
|
|
||||||
return __spreadArray(__spreadArray([], list, true), [item], false);
|
|
||||||
};
|
|
||||||
var addItemToRing = function (ring, item) {
|
|
||||||
if (ring === void 0) { ring = []; }
|
|
||||||
return __spreadArray(__spreadArray([], ring, true), [item], false);
|
|
||||||
};
|
|
||||||
var getFirstLetter = function (item) {
|
|
||||||
return item.title.substr(0, 1).toUpperCase();
|
|
||||||
};
|
|
||||||
exports.getFirstLetter = getFirstLetter;
|
|
||||||
var filteredOnly = function (items, tags) {
|
|
||||||
return items.filter(function (item) {
|
|
||||||
var itemTags = item.tags;
|
|
||||||
if (typeof itemTags === "undefined") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (Array.isArray(tags)) {
|
|
||||||
return tags.every(function (tag) { return itemTags.includes(tag); });
|
|
||||||
}
|
|
||||||
return itemTags.includes(tags);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
exports.filteredOnly = filteredOnly;
|
|
||||||
var getTags = function (items) {
|
|
||||||
var tags = items
|
|
||||||
.reduce(function (acc, item) {
|
|
||||||
return !item.tags ? acc : acc.concat(item.tags);
|
|
||||||
}, [])
|
|
||||||
.sort();
|
|
||||||
return Array.from(new Set(tags));
|
|
||||||
};
|
|
||||||
exports.getTags = getTags;
|
|
||||||
28943
package-lock.json
generated
28943
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
96
package.json
96
package.json
@@ -1,96 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "aoe_technology_radar",
|
|
||||||
"version": "3.6.0",
|
|
||||||
"description": "AOE Technology Radar",
|
|
||||||
"author": "AOE GmbH <contact-de@aoe.com> (http://www.aoe.com)",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/AOEpeople/aoe_technology_radar.git"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"aoe_technology_radar-buildRadar": "dist_scripts/scripts/buildRadar.js",
|
|
||||||
"aoe_technology_radar-createStaticFiles": "dist_scripts/scripts/createStaticFiles.js",
|
|
||||||
"aoe_technology_radar-generateJson": "dist_scripts/scripts/generateJson.js"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"prepare": "husky install && npm run build:scripts",
|
|
||||||
"start": "npx cross-env GENERATE_SOURCEMAP=false react-scripts start",
|
|
||||||
"build": "npx cross-env GENERATE_SOURCEMAP=false react-scripts build",
|
|
||||||
"build:scripts": "tsc --project tsconfig.scripts.json",
|
|
||||||
"test": "react-scripts test --watchAll=false",
|
|
||||||
"ts:check": "tsc --noEmit",
|
|
||||||
"lint": "npm run ts:check && eslint src/**/*.tsx",
|
|
||||||
"prettier": "prettier --write '{scripts,src}/**/*.{ts,tsx}'"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@apideck/better-ajv-errors": "0.3.6",
|
|
||||||
"@svgr/webpack": "6.5.1",
|
|
||||||
"@testing-library/jest-dom": "5.16.5",
|
|
||||||
"@testing-library/react": "14.0.0",
|
|
||||||
"@types/d3": "7.4.0",
|
|
||||||
"@types/fs-extra": "11.0.1",
|
|
||||||
"@types/jest": "29.5.2",
|
|
||||||
"@types/react": "18.2.14",
|
|
||||||
"@types/react-dom": "18.2.6",
|
|
||||||
"@types/react-modal": "3.16.0",
|
|
||||||
"@types/sanitize-html": "2.9.0",
|
|
||||||
"@types/walk": "2.3.1",
|
|
||||||
"classnames": "2.3.2",
|
|
||||||
"cross-env": "7.0.3",
|
|
||||||
"d3": "7.8.5",
|
|
||||||
"front-matter": "4.0.2",
|
|
||||||
"fs-extra": "11.1.1",
|
|
||||||
"highlight.js": "11.8.0",
|
|
||||||
"husky": "8.0.3",
|
|
||||||
"jsdom": "22.1.0",
|
|
||||||
"marked": "4.3.0",
|
|
||||||
"moment": "2.29.4",
|
|
||||||
"postcss-normalize": "10.0.1",
|
|
||||||
"query-string": "8.1.0",
|
|
||||||
"react": "18.2.0",
|
|
||||||
"react-dom": "18.2.0",
|
|
||||||
"react-icons": "4.10.1",
|
|
||||||
"react-modal": "3.16.1",
|
|
||||||
"react-router-dom": "6.14.0",
|
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"react-tooltip": "4.5.1",
|
|
||||||
"sanitize-html": "2.10.0",
|
|
||||||
"sass": "1.62.1",
|
|
||||||
"typescript": "4.9.5",
|
|
||||||
"walk": "2.3.15",
|
|
||||||
"xml-sitemap": "^0.3.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@trivago/prettier-plugin-sort-imports": "4.1.1",
|
|
||||||
"@types/jsdom": "21.1.1",
|
|
||||||
"@types/marked": "4.3.1",
|
|
||||||
"@types/node": "18.16.18",
|
|
||||||
"@typescript-eslint/parser": "5.59.8",
|
|
||||||
"eslint": "8.41.0",
|
|
||||||
"eslint-config-prettier": "8.8.0",
|
|
||||||
"eslint-config-react-app": "7.0.1",
|
|
||||||
"eslint-plugin-prettier": "4.2.1",
|
|
||||||
"eslint-plugin-react": "7.32.2",
|
|
||||||
"yallist": "4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"@svgr/webpack": "$@svgr/webpack",
|
|
||||||
"xml2js": "0.6.0"
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
importOrder: ["^[./]"],
|
|
||||||
importOrderSeparation: true,
|
|
||||||
importOrderSortSpecifiers: true,
|
|
||||||
};
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
{
|
|
||||||
"quadrants": {
|
|
||||||
"languages-and-frameworks": "Languages & Frameworks",
|
|
||||||
"methods-and-patterns": "Methods & Patterns",
|
|
||||||
"platforms-and-aoe-services": "Platforms & Operations",
|
|
||||||
"tools": "Tools"
|
|
||||||
},
|
|
||||||
"rings": [
|
|
||||||
"adopt",
|
|
||||||
"trial",
|
|
||||||
"assess",
|
|
||||||
"hold"
|
|
||||||
],
|
|
||||||
"showEmptyRings": false,
|
|
||||||
"homepageContent": "both",
|
|
||||||
"quadrantsMap": {
|
|
||||||
"languages-and-frameworks": {
|
|
||||||
"colour": "#84BFA4",
|
|
||||||
"txtColour": "#444444",
|
|
||||||
"position": 1,
|
|
||||||
"description": "We've placed development languages (such as Scala or Golang) here, as well as more low-level development frameworks (such as Play or Symfony), which are useful for implementing custom software of all kinds."
|
|
||||||
},
|
|
||||||
"methods-and-patterns": {
|
|
||||||
"colour": "#248EA6",
|
|
||||||
"txtColour": "white",
|
|
||||||
"position": 2,
|
|
||||||
"description": "Here we put information on methods and patterns concerning development, continuous x, testing, organization, architecture, etc."
|
|
||||||
},
|
|
||||||
"platforms-and-aoe-services": {
|
|
||||||
"colour": "#F25244",
|
|
||||||
"txtColour": "#444444",
|
|
||||||
"position": 3,
|
|
||||||
"description": "This quadrant clusters technologies around the operation of software and infrastructure related platforms, tools and services."
|
|
||||||
},
|
|
||||||
"tools": {
|
|
||||||
"colour": "#F2A25C",
|
|
||||||
"txtColour": "white",
|
|
||||||
"position": 4,
|
|
||||||
"description": "Here we put different software tools - from small helpers to bigger software projects"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"chartConfig": {
|
|
||||||
"size": 800,
|
|
||||||
"scale": [
|
|
||||||
-16,
|
|
||||||
16
|
|
||||||
],
|
|
||||||
"blipSize": 12,
|
|
||||||
"ringsAttributes": [
|
|
||||||
{
|
|
||||||
"radius": 8,
|
|
||||||
"arcWidth": 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"radius": 11,
|
|
||||||
"arcWidth": 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"radius": 14,
|
|
||||||
"arcWidth": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"radius": 16,
|
|
||||||
"arcWidth": 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dateFormat": "MMMM YYYY"
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,11 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: "DIN";
|
|
||||||
src: url("fonts/clanot-news.otf");
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "DIN";
|
|
||||||
src: url("fonts/clanot-thin.otf");
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,26 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<meta name="description" content="%REACT_APP_RADAR_NAME%" />
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo.svg" />
|
|
||||||
<link rel="stylesheet" href="%PUBLIC_URL%/fonts.css">
|
|
||||||
<link rel="stylesheet" href="%PUBLIC_URL%/styles.css">
|
|
||||||
<meta property="og:title" content="%REACT_APP_RADAR_NAME%" />
|
|
||||||
<meta property="og:image" content="%PUBLIC_URL%/logo.svg" />
|
|
||||||
|
|
||||||
<meta name="format-detection" content="telephone=no" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
|
|
||||||
<title>%REACT_APP_RADAR_NAME%</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<noscript>You need to enable JavaScript to view the %REACT_APP_RADAR_NAME%.</noscript>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 150 60" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><rect id="ArtBoard1" x="0" y="0" width="150" height="60" style="fill:none;"/><g><path d="M82.126,28.638c0.058,0.171 -0.042,0.309 -0.22,0.309l-5.318,0c-0.179,0 -0.276,-0.138 -0.217,-0.306l2.817,-8.047c0.059,-0.168 0.155,-0.168 0.212,0l2.726,8.044Zm-6.583,-16.289c-0.179,0 -0.375,0.139 -0.436,0.305l-10.103,27.727c-0.061,0.168 0.035,0.307 0.214,0.307l7.316,0c0.179,0 0.366,-0.141 0.415,-0.312l1.329,-4.609c0.05,-0.173 0.236,-0.311 0.415,-0.311l9.059,0c0.179,0 0.366,0.138 0.415,0.311l1.33,4.609c0.05,0.171 0.236,0.312 0.415,0.312l7.681,0c0.179,0 0.274,-0.139 0.213,-0.304l-10.216,-27.73c-0.062,-0.166 -0.259,-0.305 -0.437,-0.305l-7.61,0Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M107.736,34.36c-3.471,0 -4.925,-1.103 -4.925,-7.937c0,-7.088 1.565,-7.865 4.888,-7.865c3.322,0 4.887,0.777 4.887,7.865c0,6.834 -1.429,7.937 -4.85,7.937Zm-0.037,-22.373c-8.632,0 -12.481,4.453 -12.481,14.436c0,9.897 3.965,14.508 12.481,14.508c8.725,0 12.448,-4.338 12.448,-14.508c0,-9.983 -3.84,-14.436 -12.448,-14.436Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M124.72,40.35c0,0.175 0.142,0.318 0.317,0.318l21.646,0c0.174,0 0.317,-0.143 0.317,-0.318l0,-5.934c0,-0.175 -0.143,-0.319 -0.317,-0.319l-21.646,0c-0.175,0 -0.317,0.144 -0.317,0.319l0,5.934Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M125.032,12.349c-0.175,0 -0.318,0.144 -0.318,0.319l0,5.934c0,0.175 0.143,0.317 0.318,0.317l21.645,0c0.175,0 0.318,-0.142 0.318,-0.317l0,-5.934c0,-0.175 -0.143,-0.319 -0.318,-0.319l-21.645,0Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M124.714,29.476c0,0.175 0.143,0.319 0.318,0.319l21.645,0c0.175,0 0.318,-0.144 0.318,-0.319l0,-5.934c0,-0.175 -0.143,-0.319 -0.318,-0.319l-21.645,0c-0.175,0 -0.318,0.144 -0.318,0.319l0,5.934Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M29.494,9.418c-0.34,-0.148 -0.896,-0.148 -1.237,-0.002l-16.929,7.333c-0.341,0.146 -0.357,0.421 -0.036,0.609l16.962,9.88c0.321,0.188 0.583,0.643 0.584,1.016l0.01,19.427c0,0.372 0.246,0.496 0.545,0.277l14.717,-10.737c0.3,-0.217 0.58,-0.699 0.623,-1.067l2.132,-18.436c0.043,-0.367 -0.201,-0.791 -0.541,-0.94l-16.83,-7.36Zm25.668,2.35c0.343,0.141 0.581,0.557 0.529,0.922l-3.888,27.472c-0.052,0.368 -0.334,0.855 -0.626,1.084l-21.793,17.038c-0.292,0.229 -0.77,0.229 -1.063,0l-21.79,-17.038c-0.293,-0.229 -0.575,-0.716 -0.627,-1.084l-3.897,-27.472c-0.052,-0.365 0.187,-0.781 0.53,-0.922l25.714,-10.509c0.343,-0.141 0.905,-0.141 1.248,0l25.663,10.509Z" style="fill:#fff;"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -1,126 +0,0 @@
|
|||||||
{
|
|
||||||
"footerFootnote": "AOE is a leading global provider of services for digital transformation and digital business models. AOE relies exclusively on established Enterprise Open Source technologies. This leads to innovative solutions, digital products and portals in agile software projects, and helps build long-lasting, strategic partnerships with our customers.",
|
|
||||||
"legalInformationLabel": "Legal Information",
|
|
||||||
"legalInformationLink": "https://www.aoe.com/en/imprint.html",
|
|
||||||
"pageHelp": {
|
|
||||||
"headlinePrefix": "How to use the",
|
|
||||||
"paragraphs": [
|
|
||||||
{
|
|
||||||
"headline": "Introduction",
|
|
||||||
"values": [
|
|
||||||
"Technology is moving fast and new technologies and innovations appear continuously.",
|
|
||||||
"It's essential for a development and technology company such as AOE to constantly improve and keep track with the latest useful innovations. It is important to openly look for innovations and new technologies and to question established technologies and methods every now and then.",
|
|
||||||
"But, it is also important to wisely choose which technologies to use in our daily work and in the different projects we are carrying out. As we all know: There is no silver bullet."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"headline": "What is the AOE Technology Radar",
|
|
||||||
"values": [
|
|
||||||
"The Tech Radar is an overview of different technologies - from languages, frameworks, tools and patterns to platforms - that we consider \"new or mentionable\". The radar therefore doesn't provide an overview of all established technologies - but it focuses on items that have recently gained in importance or changed."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"headline": "How it is created",
|
|
||||||
"values": [
|
|
||||||
"The items in the technology radar are raised by the different teams and therefore a lot of the items are related to the work and challenges the teams face in the different projects. In fact, we don't include anything on the radar, which we haven't already tried ourselves at least once.",
|
|
||||||
"There have been a lot of valuable discussions in different expert groups about the classification and details of each of technologies and innovations. And the result of all this can be found in the latest technology radar."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"headline": "How should it be used",
|
|
||||||
"values": [
|
|
||||||
"The radar acts as an overview of technologies that we think everyone in the teams should currently know about.",
|
|
||||||
"Its goal is to act as a guide and inspiration for the daily work in the teams. Its purpose is also to provide helpful information and a bird's-eye perspective - so that decisions can be taken with a much deeper understanding of the subject matter. This results in more-informed and better-aligned decisions.",
|
|
||||||
"We also hope that developers outside of AOE find the information in our technology overview inspirational.",
|
|
||||||
"We group or categorize the items in 4 quadrants - (sometimes, when it's not 100% clear where a item belongs, we choose the best fit)."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"quadrants": [
|
|
||||||
{
|
|
||||||
"description": "We've placed development languages (such as Scala or Golang) here, as well as more low-level development frameworks (such as Play or Symfony), which are useful for implementing custom software of all kinds.",
|
|
||||||
"name": "Languages and Frameworks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Here we put different software tools - from small helpers to bigger software projects",
|
|
||||||
"name": "Tools"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Patterns are so important, and a lot of them are valid for a long time (compared to some tools or frameworks). So, this is the category where we put information on methods and patterns concerning development, continuous x, testing, organization, architecture, etc.",
|
|
||||||
"name": "Methods and Patterns"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "(including AOE internal Services): Here we include infrastructure platforms and services. We also use this category to communicate news about AOE services that we want all AOE teams to be aware of.",
|
|
||||||
"name": "Platforms and Operations"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"quadrantsPreDescription": "The quadrants are:",
|
|
||||||
"rings": [
|
|
||||||
{
|
|
||||||
"description": "We can clearly recommend this technology. We have used it for longer period of time in many teams and it has proven to be stable and useful.",
|
|
||||||
"name": "Adopt"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "We have used it with success and recommend to have a closer look at the technology in this ring. The goal of items here is to look at them more closely, with the goal to bring them to the adopt level.",
|
|
||||||
"name": "Trial"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "We have tried it out and we find it promising. We recommend having a look at these items when you face a specific need for the technology in your project.",
|
|
||||||
"name": "Assess"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "This category is a bit special. Unlike the others, we recommend to stop doing or using something. That does not mean that they are bad and it often might be ok to use them in existing projects. But we move things here if we think we shouldn't do them anymore - because we see better options or alternatives now.",
|
|
||||||
"name": "Hold"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"ringsPreDescription": "Each of the items is classified in one of these rings:",
|
|
||||||
"sourcecodeLink": {
|
|
||||||
"description": "Contributions and source code of the AOE Tech Radar are on github:",
|
|
||||||
"href": "https://github.com/AOEpeople/aoe_technology_radar",
|
|
||||||
"name": "AOE Tech Radar on Github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pageIndex": {
|
|
||||||
"publishedLabel": "Quadrant Overview"
|
|
||||||
},
|
|
||||||
"pageItem": {
|
|
||||||
"quadrantOverview": "Quadrant Overview"
|
|
||||||
},
|
|
||||||
"pageOverview": {
|
|
||||||
"title": "Technologies Overview"
|
|
||||||
},
|
|
||||||
"revisionsText": "Revisions:",
|
|
||||||
"searchLabel": "Search",
|
|
||||||
"searchPlaceholder": "What are you looking for?",
|
|
||||||
"socialLinks": [
|
|
||||||
{
|
|
||||||
"href": "https://www.facebook.com/aoepeople",
|
|
||||||
"iconName": "facebook"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"href": "https://twitter.com/aoepeople",
|
|
||||||
"iconName": "twitter"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"href": "https://www.linkedin.com/company/aoe",
|
|
||||||
"iconName": "linkedIn"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"href": "https://www.xing.com/company/aoe",
|
|
||||||
"iconName": "xing"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"href": "https://www.instagram.com/aoepeople",
|
|
||||||
"iconName": "instagram"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"href": "https://www.youtube.com/user/aoepeople",
|
|
||||||
"iconName": "youtube"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"href": "https://github.com/aoepeople",
|
|
||||||
"iconName": "github"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"socialLinksLabel": "Follow us:"
|
|
||||||
}
|
|
||||||
3875
public/rd.json
3875
public/rd.json
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
|||||||
/*
|
|
||||||
* Define your custom CSS Styles here.
|
|
||||||
*/
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": [
|
|
||||||
"config:base",
|
|
||||||
":semanticCommitTypeAll(chore)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
import { spawn } from "child_process";
|
|
||||||
import { randomBytes } from "crypto";
|
|
||||||
import * as fs from "fs-extra";
|
|
||||||
|
|
||||||
import * as paths from "./paths";
|
|
||||||
|
|
||||||
// Do this as the first thing so that any code reading it knows the right env.
|
|
||||||
process.env.BABEL_ENV = "production";
|
|
||||||
process.env.NODE_ENV = "production";
|
|
||||||
|
|
||||||
// Makes the script crash on unhandled rejections instead of silently
|
|
||||||
// ignoring them. In the future, promise rejections that are not handled will
|
|
||||||
// terminate the Node.js process with a non-zero exit code.
|
|
||||||
process.on("unhandledRejection", (err) => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
fs.removeSync(paths.templateNodeModules);
|
|
||||||
fs.ensureSymlinkSync(paths.appNodeModules, paths.templateNodeModules);
|
|
||||||
|
|
||||||
const runCommand = (command: string) =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const executedCommand = spawn(command, {
|
|
||||||
stdio: "inherit",
|
|
||||||
shell: true,
|
|
||||||
env: {
|
|
||||||
REACT_APP_RADAR_NAME: "AOE Technology Radar",
|
|
||||||
REACT_APP_RADAR_TITLE_FORMAT: "%TECHNOLOGY_NAME% | %APP_TITLE%",
|
|
||||||
REACT_APP_BUILDHASH: randomBytes(10).toString("hex"),
|
|
||||||
GENERATE_SOURCEMAP: "false",
|
|
||||||
...process.env,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
executedCommand.on("error", (error) => {
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
executedCommand.on("exit", (code) => {
|
|
||||||
if (code === 0) {
|
|
||||||
resolve(code);
|
|
||||||
} else {
|
|
||||||
reject();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
const buildTemplate = () => {
|
|
||||||
const packageManager = fs.existsSync(paths.appYarnLock) ? "yarn" : "npm";
|
|
||||||
|
|
||||||
fs.emptyDirSync(paths.templateBuild);
|
|
||||||
process.chdir(paths.template);
|
|
||||||
|
|
||||||
return runCommand(`${packageManager} run build`);
|
|
||||||
};
|
|
||||||
|
|
||||||
buildTemplate().then(() => {
|
|
||||||
fs.copySync(paths.templateBuild, paths.appBuild);
|
|
||||||
fs.ensureDirSync(paths.appPublic);
|
|
||||||
fs.copySync(paths.appPublic, paths.appBuild);
|
|
||||||
console.log(`${paths.appBuild} was created and can be deployed.`);
|
|
||||||
});
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
import {
|
|
||||||
copyFileSync,
|
|
||||||
existsSync,
|
|
||||||
mkdirSync,
|
|
||||||
readFileSync,
|
|
||||||
writeFileSync,
|
|
||||||
} from "fs";
|
|
||||||
import { JSDOM } from "jsdom";
|
|
||||||
import XmlSitemap from "xml-sitemap";
|
|
||||||
|
|
||||||
import { publicUrl, setTitle } from "../src/config";
|
|
||||||
import { createRadar } from "./generateJson/radar";
|
|
||||||
|
|
||||||
// Do this as the first thing so that any code reading it knows the right env.
|
|
||||||
process.env.BABEL_ENV = "production";
|
|
||||||
process.env.NODE_ENV = "production";
|
|
||||||
|
|
||||||
// Makes the script crash on unhandled rejections instead of silently
|
|
||||||
// ignoring them. In the future, promise rejections that are not handled will
|
|
||||||
// terminate the Node.js process with a non-zero exit code.
|
|
||||||
process.on("unhandledRejection", (err) => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
const createStaticFiles = async () => {
|
|
||||||
console.log("starting static");
|
|
||||||
const radar = await createRadar();
|
|
||||||
|
|
||||||
copyFileSync("build/index.html", "build/overview.html");
|
|
||||||
copyFileSync("build/index.html", "build/help-and-about-tech-radar.html");
|
|
||||||
const rawConf = readFileSync("build/config.json", "utf-8");
|
|
||||||
const config = JSON.parse(rawConf);
|
|
||||||
|
|
||||||
Object.keys(config.quadrants).forEach((quadrant) => {
|
|
||||||
const destFolder = `build/${quadrant}`;
|
|
||||||
copyFileSync("build/index.html", `${destFolder}.html`);
|
|
||||||
if (!existsSync(destFolder)) {
|
|
||||||
mkdirSync(destFolder);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const sitemap = new XmlSitemap();
|
|
||||||
const sitemapOptions = {
|
|
||||||
lastmod: "now",
|
|
||||||
changefreq: "weekly",
|
|
||||||
};
|
|
||||||
|
|
||||||
sitemap.add(`${publicUrl}index.html`, sitemapOptions);
|
|
||||||
|
|
||||||
radar.items.forEach((item) => {
|
|
||||||
const targetPath = `build/${item.quadrant}/${item.name}.html`;
|
|
||||||
copyFileSync("build/index.html", targetPath);
|
|
||||||
|
|
||||||
JSDOM.fromFile(targetPath).then((dom) => {
|
|
||||||
const document = dom.window.document;
|
|
||||||
const rootEl = document.getElementById("root");
|
|
||||||
|
|
||||||
setTitle(document, item.title);
|
|
||||||
|
|
||||||
if (rootEl) {
|
|
||||||
const textNode = document.createElement("div");
|
|
||||||
const bodyFragment = JSDOM.fragment(item.body);
|
|
||||||
textNode.appendChild(bodyFragment);
|
|
||||||
|
|
||||||
const headlineNode = document.createElement("h1");
|
|
||||||
const titleText = document.createTextNode(item.title);
|
|
||||||
headlineNode.appendChild(titleText);
|
|
||||||
|
|
||||||
rootEl.appendChild(headlineNode);
|
|
||||||
rootEl.appendChild(textNode);
|
|
||||||
|
|
||||||
// remove the <noscript> element as page has already been hydrated with static content
|
|
||||||
const noscriptEl = document.getElementsByTagName("noscript");
|
|
||||||
if (noscriptEl[0]) {
|
|
||||||
noscriptEl[0].remove();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
'Element with ID "root" not found. Static site content will be empty.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
writeFileSync(targetPath, dom.serialize());
|
|
||||||
});
|
|
||||||
|
|
||||||
sitemap.add(
|
|
||||||
`${publicUrl}${item.quadrant}/${item.name}.html`,
|
|
||||||
sitemapOptions
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
writeFileSync("build/sitemap.xml", sitemap.xml);
|
|
||||||
};
|
|
||||||
|
|
||||||
createStaticFiles()
|
|
||||||
.then(() => {
|
|
||||||
console.log(`created static files.`);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (err && err.message) {
|
|
||||||
console.error(err.message);
|
|
||||||
}
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
import * as fs from "fs-extra";
|
|
||||||
|
|
||||||
import { radarPath } from "./generateJson/file";
|
|
||||||
import * as paths from "./paths";
|
|
||||||
|
|
||||||
// Do this as the first thing so that any code reading it knows the right env.
|
|
||||||
process.env.BABEL_ENV = "production";
|
|
||||||
process.env.NODE_ENV = "production";
|
|
||||||
|
|
||||||
// Makes the script crash on unhandled rejections instead of silently
|
|
||||||
// ignoring them. In the future, promise rejections that are not handled will
|
|
||||||
// terminate the Node.js process with a non-zero exit code.
|
|
||||||
process.on("unhandledRejection", (err) => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
fs.removeSync(paths.templateNodeModules);
|
|
||||||
fs.ensureSymlinkSync(paths.appNodeModules, paths.templateNodeModules);
|
|
||||||
|
|
||||||
try {
|
|
||||||
fs.statSync(radarPath());
|
|
||||||
} catch (e) {
|
|
||||||
console.error(
|
|
||||||
`${radarPath()} not found. Please create ${radarPath()} and add your markdown files to build the techradar.`
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateJson = async () => {
|
|
||||||
const { createRadar } = require("./generateJson/radar");
|
|
||||||
const { save } = require("./generateJson/file");
|
|
||||||
|
|
||||||
const radar = await createRadar();
|
|
||||||
await save(JSON.stringify(radar), paths.radarJson);
|
|
||||||
};
|
|
||||||
|
|
||||||
generateJson()
|
|
||||||
.then(() => {
|
|
||||||
console.log(`${paths.appRdJson} created.`);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (err && err.message) {
|
|
||||||
console.error(err.message);
|
|
||||||
}
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import { outputFile } from "fs-extra";
|
|
||||||
import * as path from "path";
|
|
||||||
import { walk } from "walk";
|
|
||||||
|
|
||||||
export const relativePath = (...relativePath: string[]): string =>
|
|
||||||
path.resolve(...relativePath);
|
|
||||||
|
|
||||||
export const radarPath = (...pathInSrc: string[]) =>
|
|
||||||
relativePath("radar", ...pathInSrc);
|
|
||||||
|
|
||||||
export const stylesPath = (...pathInSrc: string[]) =>
|
|
||||||
relativePath("styles", ...pathInSrc);
|
|
||||||
|
|
||||||
export const faviconPath = (...pathInSrc: string[]) =>
|
|
||||||
relativePath("assets/favicon.ico", ...pathInSrc);
|
|
||||||
|
|
||||||
export const jsPath = (...pathInSrc: string[]) =>
|
|
||||||
relativePath("js", ...pathInSrc);
|
|
||||||
|
|
||||||
export const buildPath = (...pathInDist: string[]) =>
|
|
||||||
relativePath("build", ...pathInDist);
|
|
||||||
|
|
||||||
export const getAllMarkdownFiles = (folder: string) =>
|
|
||||||
getAllFiles(folder, isMarkdownFile);
|
|
||||||
|
|
||||||
const getAllFiles = (
|
|
||||||
folder: string,
|
|
||||||
predicate: (s: string) => boolean
|
|
||||||
): Promise<string[]> =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const walker = walk(folder, { followLinks: false });
|
|
||||||
const files: string[] = [];
|
|
||||||
|
|
||||||
walker.on("file", (root, fileStat, next) => {
|
|
||||||
if (predicate(fileStat.name)) {
|
|
||||||
files.push(path.resolve(root, fileStat.name));
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
walker.on("errors", (root, nodeStatsArray, next) => {
|
|
||||||
nodeStatsArray.forEach(function (n) {
|
|
||||||
console.error("[ERROR] " + n.name);
|
|
||||||
if (n.error) {
|
|
||||||
console.error(n.error.message || n.error.code + ": " + n.error.path);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
walker.on("end", () => {
|
|
||||||
resolve(files.sort());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const isMarkdownFile = (name: string) => name.match(/\.md$/) !== null;
|
|
||||||
|
|
||||||
export const save = (data: string | Buffer | DataView, fileName: string) =>
|
|
||||||
outputFile(buildPath(fileName), data);
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
import frontMatter from "front-matter";
|
|
||||||
import { readFileSync } from "fs";
|
|
||||||
import { readFile } from "fs-extra";
|
|
||||||
import highlight from "highlight.js";
|
|
||||||
import { marked } from "marked";
|
|
||||||
import * as path from "path";
|
|
||||||
|
|
||||||
import { ConfigData, publicUrl } from "../../src/config";
|
|
||||||
import {
|
|
||||||
FlagType,
|
|
||||||
Item,
|
|
||||||
ItemAttributes,
|
|
||||||
Radar,
|
|
||||||
Revision,
|
|
||||||
} from "../../src/model";
|
|
||||||
import { appBuild } from "../paths";
|
|
||||||
import { getAllMarkdownFiles, radarPath } from "./file";
|
|
||||||
|
|
||||||
marked.setOptions({
|
|
||||||
highlight: (code: any) => highlight.highlightAuto(code).value,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createRadar = async (): Promise<Radar> => {
|
|
||||||
const fileNames = await getAllMarkdownFiles(radarPath());
|
|
||||||
const revisions: (Revision | undefined)[] = await createRevisionsFromFiles(
|
|
||||||
fileNames
|
|
||||||
);
|
|
||||||
const filterdRevisions: Revision[] = revisions.filter(
|
|
||||||
(r) => r !== undefined
|
|
||||||
) as Revision[];
|
|
||||||
const allReleases = getAllReleases(filterdRevisions);
|
|
||||||
const items = createItems(filterdRevisions);
|
|
||||||
const flaggedItems = flagItem(items, allReleases);
|
|
||||||
|
|
||||||
items.forEach((item) => checkAttributes(item.name, item));
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: flaggedItems,
|
|
||||||
releases: allReleases,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkAttributes = (fileName: string, attributes: ItemAttributes) => {
|
|
||||||
const rawConf = readFileSync(path.resolve(appBuild, "config.json"), "utf-8");
|
|
||||||
const config = JSON.parse(rawConf) as ConfigData;
|
|
||||||
|
|
||||||
if (!config.rings.includes(attributes.ring)) {
|
|
||||||
throw new Error(
|
|
||||||
`Error: ${fileName} has an illegal value for 'ring' - must be one of ${config.rings}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const quadrants = Object.keys(config.quadrants);
|
|
||||||
if (!quadrants.includes(attributes.quadrant)) {
|
|
||||||
throw new Error(
|
|
||||||
`Error: ${fileName} has an illegal value for 'quadrant' - must be one of ${quadrants}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.tags) {
|
|
||||||
for (let tag of config.tags) {
|
|
||||||
if (attributes.tags && attributes.tags.includes(tag)) {
|
|
||||||
return attributes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
} else {
|
|
||||||
return attributes;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const createRevisionsFromFiles = (fileNames: string[]) => {
|
|
||||||
return Promise.all(
|
|
||||||
fileNames.map((fileName) =>
|
|
||||||
readFile(fileName, "utf8").then((data) => {
|
|
||||||
const fm = frontMatter<ItemAttributes>(data);
|
|
||||||
let html = marked(fm.body.replace(/\]\(\//g, `](${publicUrl}`));
|
|
||||||
html = html.replace(
|
|
||||||
/a href="http/g,
|
|
||||||
'a target="_blank" rel="noopener noreferrer" href="http'
|
|
||||||
);
|
|
||||||
const attributes = checkAttributes(fileName, fm.attributes);
|
|
||||||
if (attributes) {
|
|
||||||
return {
|
|
||||||
...itemInfoFromFilename(fileName),
|
|
||||||
...attributes,
|
|
||||||
fileName,
|
|
||||||
body: html,
|
|
||||||
} as Revision;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const itemInfoFromFilename = (fileName: string) => {
|
|
||||||
const [release, name] = fileName.split(path.sep).slice(-2);
|
|
||||||
return {
|
|
||||||
name: path.basename(name, ".md"),
|
|
||||||
release,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAllReleases = (revisions: Revision[]) =>
|
|
||||||
revisions
|
|
||||||
.reduce<string[]>((allReleases, { release }) => {
|
|
||||||
if (!allReleases.includes(release)) {
|
|
||||||
return [...allReleases, release];
|
|
||||||
}
|
|
||||||
return allReleases;
|
|
||||||
}, [])
|
|
||||||
.sort();
|
|
||||||
|
|
||||||
const createItems = (revisions: Revision[]) => {
|
|
||||||
const itemMap = revisions.reduce<{ [name: string]: Item }>(
|
|
||||||
(items, revision) => {
|
|
||||||
return {
|
|
||||||
...items,
|
|
||||||
[revision.name]: addRevisionToItem(items[revision.name], revision),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
return Object.values(itemMap)
|
|
||||||
.map((item) => ({ ...item, title: item.title || item.name }))
|
|
||||||
.sort((x, y) => (x.name > y.name ? 1 : -1));
|
|
||||||
};
|
|
||||||
|
|
||||||
const ignoreEmptyRevisionBody = (revision: Revision, item: Item) => {
|
|
||||||
if (!revision.body || revision.body.trim() === "") {
|
|
||||||
return item.body;
|
|
||||||
}
|
|
||||||
return revision.body;
|
|
||||||
};
|
|
||||||
|
|
||||||
const addRevisionToItem = (
|
|
||||||
item: Item = {
|
|
||||||
flag: FlagType.default,
|
|
||||||
featured: true,
|
|
||||||
revisions: [],
|
|
||||||
name: "",
|
|
||||||
title: "",
|
|
||||||
ring: "trial",
|
|
||||||
quadrant: "",
|
|
||||||
body: "",
|
|
||||||
info: "",
|
|
||||||
angleFraction: Math.random(),
|
|
||||||
radiusFraction: Math.random(),
|
|
||||||
},
|
|
||||||
revision: Revision
|
|
||||||
): Item => {
|
|
||||||
let newItem: Item = {
|
|
||||||
...item,
|
|
||||||
...revision,
|
|
||||||
body: ignoreEmptyRevisionBody(revision, item),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (revisionCreatesNewHistoryEntry(revision, item)) {
|
|
||||||
newItem = {
|
|
||||||
...newItem,
|
|
||||||
revisions: [revision, ...newItem.revisions],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return newItem;
|
|
||||||
};
|
|
||||||
|
|
||||||
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)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const flagItem = (items: Item[], allReleases: string[]) =>
|
|
||||||
items.map(
|
|
||||||
(item) =>
|
|
||||||
({
|
|
||||||
...item,
|
|
||||||
flag: getItemFlag(item, allReleases),
|
|
||||||
} as Item),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const isInLastRelease = (item: Item, allReleases: string[]) =>
|
|
||||||
item.revisions[0].release === allReleases[allReleases.length - 1];
|
|
||||||
|
|
||||||
const isNewItem = (item: Item, allReleases: string[]) =>
|
|
||||||
item.revisions.length === 1 && isInLastRelease(item, allReleases);
|
|
||||||
|
|
||||||
const hasItemChanged = (item: Item, allReleases: string[]) =>
|
|
||||||
item.revisions.length > 1 && isInLastRelease(item, allReleases);
|
|
||||||
|
|
||||||
const getItemFlag = (item: Item, allReleases: string[]): string => {
|
|
||||||
if (isNewItem(item, allReleases)) {
|
|
||||||
return FlagType.new;
|
|
||||||
}
|
|
||||||
if (hasItemChanged(item, allReleases)) {
|
|
||||||
return FlagType.changed;
|
|
||||||
}
|
|
||||||
return FlagType.default;
|
|
||||||
};
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { realpathSync } from "fs";
|
|
||||||
import { resolve } from "path";
|
|
||||||
|
|
||||||
export const radarJson = "rd.json";
|
|
||||||
const appDirectory = realpathSync(process.cwd());
|
|
||||||
const resolveApp = (relativePath = "") => resolve(appDirectory, relativePath);
|
|
||||||
const templateDirectory = realpathSync(__dirname);
|
|
||||||
const resolveTemplate = (relativePath = "") =>
|
|
||||||
resolve(templateDirectory, "../..", relativePath);
|
|
||||||
|
|
||||||
export const template = resolveTemplate(); // this repository
|
|
||||||
export const templateBuild = resolveTemplate("build"); // build folder in this repository
|
|
||||||
export const templateNodeModules = resolveTemplate("node_modules"); // node_modules folder in this repository
|
|
||||||
export const appRdJson = resolveApp(`build/${radarJson}`); // build/rd.json in project
|
|
||||||
export const appBuild = resolveApp("build"); // build folder in project
|
|
||||||
export const appPublic = resolveApp("public"); // public folder in project
|
|
||||||
export const appYarnLock = resolveApp("yarn.lock"); // yarn.lock in project
|
|
||||||
export const appNodeModules = resolveApp("node_modules"); // node_modules folder in project
|
|
||||||
1
scripts/types.d.ts
vendored
1
scripts/types.d.ts
vendored
@@ -1 +0,0 @@
|
|||||||
declare module "xml-sitemap";
|
|
||||||
135
src/animation.ts
135
src/animation.ts
@@ -1,135 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
export interface AnimationConfig {
|
|
||||||
[k: string]: Animation | Animation[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Animations = {
|
|
||||||
[k: string]: Animation[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AnimationStates = {
|
|
||||||
[k: string]: React.CSSProperties[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Animation = {
|
|
||||||
stateA: React.CSSProperties;
|
|
||||||
stateB: React.CSSProperties;
|
|
||||||
delay: number;
|
|
||||||
run?(callback: (state: any) => any): any; // todo fix
|
|
||||||
prepare?(callback: (state: any) => any): any; // todo fix
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AnimationRunner = {
|
|
||||||
getState(): AnimationStates;
|
|
||||||
run(): any;
|
|
||||||
awaitAnimationComplete(callback: () => void): any;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createAnimation = (
|
|
||||||
stateA: React.CSSProperties,
|
|
||||||
stateB: React.CSSProperties,
|
|
||||||
delay: number
|
|
||||||
): Animation => ({
|
|
||||||
stateA,
|
|
||||||
stateB,
|
|
||||||
delay,
|
|
||||||
});
|
|
||||||
|
|
||||||
const getAnimationStates = (
|
|
||||||
animations: Animation[],
|
|
||||||
stateName: "stateA" | "stateB" = "stateA"
|
|
||||||
): React.CSSProperties[] => {
|
|
||||||
return animations.map((animation) => animation[stateName]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMaxTransitionTime = (transition: string) => {
|
|
||||||
const re = /(\d+)ms/g;
|
|
||||||
const times: number[] = [];
|
|
||||||
let matches;
|
|
||||||
while ((matches = re.exec(transition)) != null) {
|
|
||||||
times.push(parseInt(matches[1], 10));
|
|
||||||
}
|
|
||||||
return Math.max(...times);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAnimationDuration = (animation: Animation | Animation[]): number => {
|
|
||||||
if (animation instanceof Array) {
|
|
||||||
return animation.reduce((maxDuration, a) => {
|
|
||||||
const duration = getAnimationDuration(a);
|
|
||||||
if (duration > maxDuration) {
|
|
||||||
return duration;
|
|
||||||
}
|
|
||||||
return maxDuration;
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = animation.stateB;
|
|
||||||
const maxTransition = state.transition
|
|
||||||
? getMaxTransitionTime(state.transition)
|
|
||||||
: 0;
|
|
||||||
return maxTransition + animation.delay;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMaxAnimationsDuration = (animations: Animations) =>
|
|
||||||
Math.max(
|
|
||||||
...Object.values(animations).map((animations) =>
|
|
||||||
getAnimationDuration(Object.values(animations))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export const createAnimationRunner = (
|
|
||||||
animationsIn: AnimationConfig,
|
|
||||||
subscriber: () => void = () => {}
|
|
||||||
): AnimationRunner => {
|
|
||||||
const animations = Object.entries(animationsIn).reduce(
|
|
||||||
(state, [name, animation]) => ({
|
|
||||||
...state,
|
|
||||||
[name]:
|
|
||||||
animation instanceof Array ? animation : ([animation] as Animation[]),
|
|
||||||
}),
|
|
||||||
{} as Animations
|
|
||||||
);
|
|
||||||
|
|
||||||
let state = Object.entries(animations).reduce(
|
|
||||||
(state, [name, animation]) => ({
|
|
||||||
...state,
|
|
||||||
[name]: getAnimationStates(animation),
|
|
||||||
}),
|
|
||||||
{} as AnimationStates
|
|
||||||
);
|
|
||||||
|
|
||||||
const animationsDuration = getMaxAnimationsDuration(animations);
|
|
||||||
|
|
||||||
const animate = (name: string, animation: Animation[]) => {
|
|
||||||
animation.forEach((a, index) => {
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
window.setTimeout(() => {
|
|
||||||
state = {
|
|
||||||
...state,
|
|
||||||
[name]: [
|
|
||||||
...state[name]?.slice(0, index),
|
|
||||||
a.stateB,
|
|
||||||
...state[name]?.slice(index + 1, state[name].length),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
subscriber();
|
|
||||||
}, a.delay);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
getState() {
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
run() {
|
|
||||||
Object.entries(animations).forEach(([name, animation]) => {
|
|
||||||
animate(name, animation);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
awaitAnimationComplete(callback) {
|
|
||||||
window.setTimeout(callback, animationsDuration);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
BrowserRouter,
|
|
||||||
Navigate,
|
|
||||||
Route,
|
|
||||||
Routes,
|
|
||||||
useParams,
|
|
||||||
} from "react-router-dom";
|
|
||||||
|
|
||||||
import { ConfigData, publicUrl } from "../config";
|
|
||||||
import { Messages, MessagesProvider } from "../context/MessagesContext";
|
|
||||||
import { useSearchParamState } from "../hooks/use-search-param-state";
|
|
||||||
import { Item, filteredOnly, getTags } 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 [data, setData] = React.useState<D>();
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
fetch(url)
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data: D) => {
|
|
||||||
setData(data);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(`fetch ${url} failed. Did the file exist?`, error);
|
|
||||||
});
|
|
||||||
}, [url]);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const usePage = (params: Record<string, string | undefined>) => {
|
|
||||||
return (params["*"] || "").replace(".html", "");
|
|
||||||
};
|
|
||||||
|
|
||||||
const useFilteredItems = ({ items }: { items: Item[] }) => {
|
|
||||||
const [searchParamState] = useSearchParamState();
|
|
||||||
const { tags } = searchParamState;
|
|
||||||
|
|
||||||
return tags ? filteredOnly(items, tags) : items;
|
|
||||||
};
|
|
||||||
|
|
||||||
const RouterWithPageParam = ({
|
|
||||||
items,
|
|
||||||
releases,
|
|
||||||
config,
|
|
||||||
}: {
|
|
||||||
items: Item[];
|
|
||||||
releases: string[];
|
|
||||||
config: ConfigData;
|
|
||||||
}) => {
|
|
||||||
const page = usePage(useParams());
|
|
||||||
const [searchParamState] = useSearchParamState();
|
|
||||||
const { search } = searchParamState;
|
|
||||||
const filteredItems = useFilteredItems({ items });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Router
|
|
||||||
pageName={page || ""}
|
|
||||||
search={search || ""}
|
|
||||||
items={filteredItems}
|
|
||||||
releases={releases}
|
|
||||||
config={config}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const HeaderWithPageParam = ({ items }: { items: Item[] }) => {
|
|
||||||
const page = usePage(useParams());
|
|
||||||
const tags = getTags(items);
|
|
||||||
|
|
||||||
return <Header pageName={page || ""} tags={tags} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FooterWithPageParam = ({ items }: { items: Item[] }) => {
|
|
||||||
const page = usePage(useParams());
|
|
||||||
const filteredItems = useFilteredItems({ items });
|
|
||||||
|
|
||||||
return <Footer pageName={page || ""} items={filteredItems} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Data {
|
|
||||||
items: Item[];
|
|
||||||
releases: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
const data = useFetch<Data>(
|
|
||||||
`${publicUrl}rd.json?${process.env.REACT_APP_BUILDHASH}`
|
|
||||||
);
|
|
||||||
const messages = useFetch<Messages>(
|
|
||||||
`${publicUrl}messages.json?${process.env.REACT_APP_BUILDHASH}`
|
|
||||||
);
|
|
||||||
const config = useFetch<ConfigData>(
|
|
||||||
`${publicUrl}config.json?${process.env.REACT_APP_BUILDHASH}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data && config) {
|
|
||||||
const { items, releases } = data;
|
|
||||||
return (
|
|
||||||
<MessagesProvider messages={messages}>
|
|
||||||
<BrowserRouter basename={`${publicUrl}`}>
|
|
||||||
<Routes>
|
|
||||||
<Route
|
|
||||||
path={"/*"}
|
|
||||||
element={
|
|
||||||
<div>
|
|
||||||
<div className="page">
|
|
||||||
<div className="page__header">
|
|
||||||
<HeaderWithPageParam items={items} />
|
|
||||||
</div>
|
|
||||||
<div className={classNames("page__content")}>
|
|
||||||
<RouterWithPageParam
|
|
||||||
config={config}
|
|
||||||
items={items}
|
|
||||||
releases={releases}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="page__footer">
|
|
||||||
<FooterWithPageParam items={items} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path={"/"}
|
|
||||||
element={<Navigate replace to={"/index.html"} />}
|
|
||||||
/>
|
|
||||||
</Routes>
|
|
||||||
</BrowserRouter>
|
|
||||||
</MessagesProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React, { MouseEventHandler } from "react";
|
|
||||||
|
|
||||||
import { ConfigData } from "../../config";
|
|
||||||
import "./badge.scss";
|
|
||||||
|
|
||||||
type BadgeProps = {
|
|
||||||
onClick?: MouseEventHandler;
|
|
||||||
big?: boolean;
|
|
||||||
type: "big" | "all" | "empty" | string;
|
|
||||||
config: ConfigData;
|
|
||||||
};
|
|
||||||
|
|
||||||
const badgeClass = (type: string, config: ConfigData) => {
|
|
||||||
if (!config.rings.includes(type)) {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
return ["first", "second", "third", "fourth"][config.rings.indexOf(type)];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Badge({
|
|
||||||
onClick,
|
|
||||||
big,
|
|
||||||
type,
|
|
||||||
config,
|
|
||||||
children,
|
|
||||||
}: React.PropsWithChildren<BadgeProps>) {
|
|
||||||
const Comp = onClick ? "a" : "span";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
className={classNames("badge", `badge--${badgeClass(type, config)}`, {
|
|
||||||
"badge--big": big === true,
|
|
||||||
})}
|
|
||||||
onClick={onClick}
|
|
||||||
href={Comp === "a" ? "#" : undefined}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Comp>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
.badge {
|
|
||||||
color: var(--color-white);
|
|
||||||
display: inline-block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0 15px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
border-radius: 13px;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 25px;
|
|
||||||
height: 25px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-decoration: none;
|
|
||||||
border: 1px solid var(--color-gray-normal);
|
|
||||||
|
|
||||||
&--big {
|
|
||||||
border-radius: 15px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 30px;
|
|
||||||
height: 30px;
|
|
||||||
padding: 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--all {
|
|
||||||
background: var(--color-gray-normal);
|
|
||||||
border-color: var(--color-gray-normal);
|
|
||||||
}
|
|
||||||
&--first {
|
|
||||||
background: var(--color-green);
|
|
||||||
border-color: var(--color-green);
|
|
||||||
}
|
|
||||||
&--second {
|
|
||||||
background: var(--color-orange);
|
|
||||||
border-color: var(--color-orange);
|
|
||||||
}
|
|
||||||
&--third {
|
|
||||||
background: var(--color-blue);
|
|
||||||
border-color: var(--color-blue);
|
|
||||||
}
|
|
||||||
&--fourth {
|
|
||||||
background: var(--color-marine);
|
|
||||||
border-color: var(--color-marine);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import "./branding.scss";
|
|
||||||
|
|
||||||
type BrandingProps = {
|
|
||||||
logoContent: React.ReactNode;
|
|
||||||
modifier?: "backlink" | "logo" | "content" | "footer";
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Branding({
|
|
||||||
logoContent,
|
|
||||||
modifier,
|
|
||||||
children,
|
|
||||||
}: React.PropsWithChildren<BrandingProps>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames("branding", {
|
|
||||||
[`branding--${modifier}`]: modifier,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div className="branding__logo">{logoContent}</div>
|
|
||||||
<div className="branding__content">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
@import "../../styles/sccs-vars.scss";
|
|
||||||
|
|
||||||
.branding {
|
|
||||||
margin: 40px 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
min-height: 60px;
|
|
||||||
transition: opacity 450ms cubic-bezier(0.24, 1.12, 0.71, 0.98);
|
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
&__backlink {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__logo {
|
|
||||||
flex: 0 0 200px;
|
|
||||||
|
|
||||||
& img {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
flex: 0 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-hidden {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: $until-sm) {
|
|
||||||
&--footer {
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
.branding__logo {
|
|
||||||
margin: 0 0 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: $until-xl) {
|
|
||||||
margin: 15px 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import * as d3 from "d3";
|
|
||||||
import React, { useLayoutEffect, useRef } from "react";
|
|
||||||
|
|
||||||
export const YAxis: React.FC<{
|
|
||||||
scale: d3.ScaleLinear<number, number>;
|
|
||||||
}> = ({ scale }) => {
|
|
||||||
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]);
|
|
||||||
|
|
||||||
return <g ref={ref} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const XAxis: React.FC<{
|
|
||||||
scale: d3.ScaleLinear<number, number>;
|
|
||||||
}> = ({ scale }) => {
|
|
||||||
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]);
|
|
||||||
|
|
||||||
return <g ref={ref} />;
|
|
||||||
};
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import { ScaleLinear } from "d3";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { ConfigData } from "../../config";
|
|
||||||
import { Blip, FlagType, Item, Point } from "../../model";
|
|
||||||
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
|
|
||||||
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 {
|
|
||||||
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
|
|
||||||
const randomDegree =
|
|
||||||
((0.1 + (blip.angleFraction || Math.random()) * 0.8) * 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.
|
|
||||||
const radius = pointBetween(
|
|
||||||
previousRingRadius + ringPadding,
|
|
||||||
ringRadius - ringPadding,
|
|
||||||
blip.radiusFraction || Math.random()
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
|
||||||
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)
|
|
||||||
*/
|
|
||||||
const shift = (pi * [1, 4, 2, 3][blip.quadrantPosition - 1]) / 2;
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: xScale(Math.cos(randomDegree + shift) * radius),
|
|
||||||
y: yScale(Math.sin(randomDegree + shift) * radius),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function pointBetween(min: number, max: number, amount: number): number {
|
|
||||||
return amount * (max - min) + min;
|
|
||||||
}
|
|
||||||
|
|
||||||
function distanceBetween(point1: Point, point2: Point): number {
|
|
||||||
const a = point2.x - point1.x;
|
|
||||||
const b = point2.y - point1.y;
|
|
||||||
return Math.sqrt(a * a + b * b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderBlip(
|
|
||||||
blip: Blip,
|
|
||||||
index: number,
|
|
||||||
config: ConfigData
|
|
||||||
): JSX.Element {
|
|
||||||
const props = {
|
|
||||||
blip,
|
|
||||||
className: "blip",
|
|
||||||
fill: blip.colour,
|
|
||||||
"data-background-color": blip.colour,
|
|
||||||
"data-text-color": blip.txtColour,
|
|
||||||
"data-tip": blip.title,
|
|
||||||
key: index,
|
|
||||||
};
|
|
||||||
switch (blip.flag) {
|
|
||||||
case FlagType.new:
|
|
||||||
return <NewBlip {...props} config={config} />;
|
|
||||||
case FlagType.changed:
|
|
||||||
return <ChangedBlip {...props} config={config} />;
|
|
||||||
default:
|
|
||||||
return <DefaultBlip {...props} config={config} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const BlipPoints: React.FC<{
|
|
||||||
items: Item[];
|
|
||||||
xScale: ScaleLinear<number, number>;
|
|
||||||
yScale: ScaleLinear<number, number>;
|
|
||||||
config: ConfigData;
|
|
||||||
}> = ({ 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
let blip: Blip = {
|
|
||||||
...item,
|
|
||||||
quadrantPosition: quadrantConfig.position,
|
|
||||||
ringPosition: config.rings.findIndex((r) => r === item.ring),
|
|
||||||
colour: quadrantConfig.colour,
|
|
||||||
txtColour: quadrantConfig.txtColour,
|
|
||||||
coordinates: { x: 0, y: 0 },
|
|
||||||
};
|
|
||||||
|
|
||||||
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)
|
|
||||||
and quadrant borders (so that they don't overlap quadrants)
|
|
||||||
This feels pretty inefficient, but good enough for now.
|
|
||||||
*/
|
|
||||||
distanceBetweenCheck = list.some(
|
|
||||||
(b) =>
|
|
||||||
distanceBetween(localpoint, b.coordinates) <
|
|
||||||
config.chartConfig.blipSize + config.chartConfig.blipSize / 2
|
|
||||||
);
|
|
||||||
} while (
|
|
||||||
counter < 100 &&
|
|
||||||
(Math.abs(point.x - xScale(0)) < 15 ||
|
|
||||||
Math.abs(point.y - yScale(0)) < 15 ||
|
|
||||||
distanceBetweenCheck)
|
|
||||||
);
|
|
||||||
|
|
||||||
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;
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { ConfigData } from "../../config";
|
|
||||||
import { Blip } from "../../model";
|
|
||||||
|
|
||||||
type VisualBlipProps = {
|
|
||||||
className: string;
|
|
||||||
fill: string;
|
|
||||||
"data-background-color": string;
|
|
||||||
"data-text-color": string;
|
|
||||||
"data-tip": string;
|
|
||||||
key: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ChangedBlip: React.FC<
|
|
||||||
{ blip: Blip; config: ConfigData } & VisualBlipProps
|
|
||||||
> = ({ blip, config, ...props }) => {
|
|
||||||
const centeredX = blip.coordinates.x - config.chartConfig.blipSize / 2,
|
|
||||||
centeredY = blip.coordinates.y - config.chartConfig.blipSize / 2;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<rect
|
|
||||||
transform={`rotate(-45 ${centeredX} ${centeredY})`}
|
|
||||||
x={centeredX}
|
|
||||||
y={centeredY}
|
|
||||||
width={config.chartConfig.blipSize}
|
|
||||||
height={config.chartConfig.blipSize}
|
|
||||||
rx="3"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NewBlip: React.FC<
|
|
||||||
{ blip: Blip; config: ConfigData } & VisualBlipProps
|
|
||||||
> = ({ blip, config, ...props }) => {
|
|
||||||
const centeredX = blip.coordinates.x - config.chartConfig.blipSize / 2,
|
|
||||||
centeredY = blip.coordinates.y - config.chartConfig.blipSize / 2;
|
|
||||||
|
|
||||||
/*
|
|
||||||
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
|
|
||||||
e.g. https://observablehq.com/@perlmonger42/interactive-rounded-corners-on-svg-polygons-using-d3-js
|
|
||||||
*/
|
|
||||||
return (
|
|
||||||
<path
|
|
||||||
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"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DefaultBlip: React.FC<
|
|
||||||
{ blip: Blip; config: ConfigData } & VisualBlipProps
|
|
||||||
> = ({ blip, config, ...props }) => {
|
|
||||||
return (
|
|
||||||
<circle
|
|
||||||
r={config.chartConfig.blipSize / 2}
|
|
||||||
cx={blip.coordinates.x}
|
|
||||||
cy={blip.coordinates.y}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import * as d3 from "d3";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { ConfigData } from "../../config";
|
|
||||||
import { QuadrantConfig } from "../../model";
|
|
||||||
|
|
||||||
const arcAngel = [
|
|
||||||
[(3 * Math.PI) / 2, (4 * Math.PI) / 2],
|
|
||||||
[0, Math.PI / 2],
|
|
||||||
[Math.PI, (Math.PI * 3) / 2],
|
|
||||||
[Math.PI / 2, Math.PI],
|
|
||||||
];
|
|
||||||
|
|
||||||
function arcPath(
|
|
||||||
quadrantPosition: number,
|
|
||||||
ringPosition: number,
|
|
||||||
xScale: d3.ScaleLinear<number, number>,
|
|
||||||
config: ConfigData
|
|
||||||
) {
|
|
||||||
const [startAngle, endAngle] = arcAngel[quadrantPosition - 1];
|
|
||||||
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<{
|
|
||||||
quadrant: QuadrantConfig;
|
|
||||||
xScale: d3.ScaleLinear<number, number>;
|
|
||||||
config: ConfigData;
|
|
||||||
}> = ({ quadrant, xScale, config }) => {
|
|
||||||
// order from top-right clockwise
|
|
||||||
const gradientAttributes = [
|
|
||||||
{ x: 0, y: 0, cx: 1, cy: 1, r: 1 },
|
|
||||||
{ x: xScale(0), y: 0, cx: 0, cy: 1, r: 1 },
|
|
||||||
{ x: 0, y: xScale(0), cx: 1, cy: 0, r: 1 },
|
|
||||||
{ x: xScale(0), y: xScale(0), cx: 0, cy: 0, r: 1 },
|
|
||||||
];
|
|
||||||
const gradientId = `${quadrant.position}-radial-gradient`,
|
|
||||||
quadrantSize = config.chartConfig.size / 2;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<g className="quadrant-ring">
|
|
||||||
{/* Definition of the quadrant gradient */}
|
|
||||||
<defs>
|
|
||||||
<radialGradient
|
|
||||||
id={gradientId}
|
|
||||||
{...gradientAttributes[quadrant.position - 1]}
|
|
||||||
>
|
|
||||||
<stop offset="0%" stopColor={quadrant.colour}></stop>
|
|
||||||
<stop
|
|
||||||
offset="100%"
|
|
||||||
stopColor={quadrant.colour}
|
|
||||||
stopOpacity="0"
|
|
||||||
></stop>
|
|
||||||
</radialGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
{/* Gradient background area */}
|
|
||||||
<rect
|
|
||||||
width={quadrantSize}
|
|
||||||
height={quadrantSize}
|
|
||||||
x={gradientAttributes[quadrant.position - 1].x}
|
|
||||||
y={gradientAttributes[quadrant.position - 1].y}
|
|
||||||
fill={`url(#${gradientId})`}
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QuadrantRings;
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import * as d3 from "d3";
|
|
||||||
import React from "react";
|
|
||||||
import ReactTooltip from "react-tooltip";
|
|
||||||
|
|
||||||
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<{
|
|
||||||
ring: string;
|
|
||||||
xScale: d3.ScaleLinear<number, number>;
|
|
||||||
yScale: d3.ScaleLinear<number, number>;
|
|
||||||
config: ConfigData;
|
|
||||||
}> = ({ ring, xScale, yScale, config }) => {
|
|
||||||
const ringIndex = config.rings.findIndex((r) => r === ring);
|
|
||||||
|
|
||||||
const ringRadius = config.chartConfig.ringsAttributes[ringIndex].radius,
|
|
||||||
previousRingRadius =
|
|
||||||
ringIndex === 0
|
|
||||||
? 0
|
|
||||||
: config.chartConfig.ringsAttributes[ringIndex - 1].radius,
|
|
||||||
// middle point in between two ring arcs
|
|
||||||
distanceFromCentre =
|
|
||||||
previousRingRadius + (ringRadius - previousRingRadius) / 2;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<g className="ring-label">
|
|
||||||
{/* Right hand-side label */}
|
|
||||||
<text
|
|
||||||
x={xScale(distanceFromCentre)}
|
|
||||||
y={yScale(0)}
|
|
||||||
textAnchor="middle"
|
|
||||||
dy=".35em"
|
|
||||||
>
|
|
||||||
{ring}
|
|
||||||
</text>
|
|
||||||
{/* Left hand-side label */}
|
|
||||||
<text
|
|
||||||
x={xScale(-distanceFromCentre)}
|
|
||||||
y={yScale(0)}
|
|
||||||
textAnchor="middle"
|
|
||||||
dy=".35em"
|
|
||||||
>
|
|
||||||
{ring}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const RadarChart: React.FC<{
|
|
||||||
items: Item[];
|
|
||||||
config: ConfigData;
|
|
||||||
}> = ({ items, config }) => {
|
|
||||||
const xScale = d3
|
|
||||||
.scaleLinear()
|
|
||||||
.domain(config.chartConfig.scale)
|
|
||||||
.range([0, config.chartConfig.size]);
|
|
||||||
const yScale = d3
|
|
||||||
.scaleLinear()
|
|
||||||
.domain(config.chartConfig.scale)
|
|
||||||
.range([config.chartConfig.size, 0]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="chart" style={{ maxWidth: `${config.chartConfig.size}px` }}>
|
|
||||||
<svg
|
|
||||||
viewBox={`0 0 ${config.chartConfig.size} ${config.chartConfig.size}`}
|
|
||||||
>
|
|
||||||
<g transform={`translate(${xScale(0)}, 0)`}>
|
|
||||||
<YAxis scale={yScale} />
|
|
||||||
</g>
|
|
||||||
<g transform={`translate(0, ${yScale(0)})`}>
|
|
||||||
<XAxis scale={xScale} />
|
|
||||||
</g>
|
|
||||||
|
|
||||||
{Object.values(config.quadrantsMap).map((value, index) => {
|
|
||||||
console.log(value);
|
|
||||||
return null;
|
|
||||||
})}
|
|
||||||
{Object.values(config.quadrantsMap).map((value, index) => (
|
|
||||||
<QuadrantRings
|
|
||||||
key={index}
|
|
||||||
quadrant={value}
|
|
||||||
xScale={xScale}
|
|
||||||
config={config}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{Array.from(config.rings).map((ring: string, index) => (
|
|
||||||
<RingLabel
|
|
||||||
key={index}
|
|
||||||
ring={ring}
|
|
||||||
xScale={xScale}
|
|
||||||
yScale={yScale}
|
|
||||||
config={config}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<BlipPoints
|
|
||||||
items={items}
|
|
||||||
xScale={xScale}
|
|
||||||
yScale={yScale}
|
|
||||||
config={config}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<ReactTooltip className="tooltip" offset={{ top: -5 }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RadarChart;
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
.chart {
|
|
||||||
fill: white;
|
|
||||||
font-size: 12px;
|
|
||||||
text-align: center;
|
|
||||||
position: relative;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart .blip:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart .tooltip {
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart .ring-label {
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { Item } from "../../model";
|
|
||||||
import "./editButton.scss";
|
|
||||||
|
|
||||||
type EditButtonProps = {
|
|
||||||
baseUrl: string;
|
|
||||||
item: Item & { release?: string };
|
|
||||||
title?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function EditButton({
|
|
||||||
baseUrl,
|
|
||||||
item,
|
|
||||||
title,
|
|
||||||
}: React.PropsWithChildren<EditButtonProps>) {
|
|
||||||
const href = `${baseUrl}/${item.release}/${item.name}.md`;
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
className="icon-link"
|
|
||||||
href={href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{title || "Edit"}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
.edit-button {
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import "./fadeable.scss";
|
|
||||||
|
|
||||||
type FadeableProps = {
|
|
||||||
leaving: boolean;
|
|
||||||
onLeave: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Fadeable({
|
|
||||||
leaving,
|
|
||||||
onLeave,
|
|
||||||
children,
|
|
||||||
}: React.PropsWithChildren<FadeableProps>) {
|
|
||||||
const [faded, setFaded] = useState(leaving);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!faded && leaving) {
|
|
||||||
setFaded(true);
|
|
||||||
} else if (faded && !leaving) {
|
|
||||||
setFaded(false);
|
|
||||||
}
|
|
||||||
}, [faded, leaving]);
|
|
||||||
|
|
||||||
const handleTransitionEnd = () => {
|
|
||||||
if (faded) {
|
|
||||||
onLeave();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames("fadable", { "is-faded": faded })}
|
|
||||||
onTransitionEnd={handleTransitionEnd}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.fadable {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.2s cubic-bezier(0.54, 0, 0.28, 1);
|
|
||||||
|
|
||||||
&.is-faded {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { FlagType } from "../../model";
|
|
||||||
import "./flag.scss";
|
|
||||||
|
|
||||||
interface ItemFlag {
|
|
||||||
flag: FlagType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Flag({
|
|
||||||
item,
|
|
||||||
short = false,
|
|
||||||
}: {
|
|
||||||
item: ItemFlag;
|
|
||||||
short?: boolean;
|
|
||||||
}) {
|
|
||||||
const ucFirst = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
||||||
|
|
||||||
if (item.flag !== FlagType.default) {
|
|
||||||
let name = item.flag.toUpperCase();
|
|
||||||
let title = ucFirst(item.flag);
|
|
||||||
if (short === true) {
|
|
||||||
name = title[0];
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<span className={`flag flag--${item.flag}`} title={title}>
|
|
||||||
{name}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
.flag {
|
|
||||||
font-size: 9px;
|
|
||||||
display: inline-block;
|
|
||||||
padding: 3px 8px;
|
|
||||||
border-radius: 10px;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: top;
|
|
||||||
margin-top: -2px;
|
|
||||||
left: 5px;
|
|
||||||
color: var(--color-white);
|
|
||||||
|
|
||||||
&--new {
|
|
||||||
background: var(--color-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--changed {
|
|
||||||
background: var(--color-blue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
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 FooterEnd from "../FooterEnd/FooterEnd";
|
|
||||||
import "./footer.scss";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
items: Item[];
|
|
||||||
pageName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Footer: React.FC<Props> = ({ items, pageName }) => {
|
|
||||||
const { footerFootnote } = useMessages();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames("footer", {
|
|
||||||
"is-hidden":
|
|
||||||
!isMobileViewport() && getItemPageNames(items).includes(pageName),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{footerFootnote && (
|
|
||||||
<Branding
|
|
||||||
modifier="footer"
|
|
||||||
logoContent={
|
|
||||||
<img
|
|
||||||
src={assetUrl("logo.svg")}
|
|
||||||
width="150px"
|
|
||||||
height="60px"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="footnote"
|
|
||||||
dangerouslySetInnerHTML={sanitize(footerFootnote)}
|
|
||||||
></div>
|
|
||||||
</Branding>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FooterEnd />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Footer;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
.footer {
|
|
||||||
border-top: 1px solid var(--color-gray-normal);
|
|
||||||
transition: opacity 450ms cubic-bezier(0.24, 1.12, 0.71, 0.98) 1500ms;
|
|
||||||
opacity: 1;
|
|
||||||
backface-visibility: hidden;
|
|
||||||
|
|
||||||
&.is-hidden {
|
|
||||||
opacity: 0;
|
|
||||||
transition-delay: 0s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { useMessages } from "../../context/MessagesContext";
|
|
||||||
import SocialLink from "../SocialLink/SocialLink";
|
|
||||||
import "./footerend.scss";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
modifier?: "in-sidebar";
|
|
||||||
}
|
|
||||||
|
|
||||||
const FooterEnd: React.FC<Props> = ({ modifier }) => {
|
|
||||||
const {
|
|
||||||
socialLinksLabel,
|
|
||||||
socialLinks,
|
|
||||||
legalInformationLink,
|
|
||||||
legalInformationLabel,
|
|
||||||
} = useMessages();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames("footer-end", {
|
|
||||||
[`footer-end__${modifier}`]: modifier,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div className="footer-social">
|
|
||||||
{socialLinks && (
|
|
||||||
<>
|
|
||||||
<div className="footer-social__label">
|
|
||||||
<p>{socialLinksLabel ?? "Follow us:"}</p>
|
|
||||||
</div>
|
|
||||||
<div className="footer-social__links">
|
|
||||||
{socialLinks.map(({ href, iconName }) => (
|
|
||||||
<SocialLink href={href} iconName={iconName} key={iconName} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{legalInformationLink && (
|
|
||||||
<div className="footer-copyright">
|
|
||||||
<p>
|
|
||||||
<a
|
|
||||||
href={legalInformationLink}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{legalInformationLabel || "Legal Information"}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FooterEnd;
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
@import "../../styles/sccs-vars.scss";
|
|
||||||
|
|
||||||
.footer-end {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--color-gray-normal);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 0 10px 0;
|
|
||||||
|
|
||||||
& a {
|
|
||||||
color: var(--color-gray-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__in-sidebar {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-top: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: $until-sm) {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
margin: 20px 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-social {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&__label {
|
|
||||||
margin: 0 10px 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
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 { useSearchParamState } from "../../hooks/use-search-param-state";
|
|
||||||
import { Tag } from "../../model";
|
|
||||||
import Branding from "../Branding/Branding";
|
|
||||||
import Link from "../Link/Link";
|
|
||||||
import LogoLink from "../LogoLink/LogoLink";
|
|
||||||
import Search from "../Search/Search";
|
|
||||||
import TagsModal from "../TagsModal/TagsModal";
|
|
||||||
|
|
||||||
export default function Header({
|
|
||||||
pageName,
|
|
||||||
tags,
|
|
||||||
}: {
|
|
||||||
pageName: string;
|
|
||||||
tags: Tag[];
|
|
||||||
}) {
|
|
||||||
const [searchOpen, setSearchOpen] = useState(false);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
const { searchLabel, pageHelp, pageOverview } = useMessages();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const searchRef = useRef<HTMLInputElement>(null);
|
|
||||||
const [searchParamState, setSearchParamsState] = useSearchParamState();
|
|
||||||
|
|
||||||
const openSearch = () => {
|
|
||||||
setSearchOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeSearch = () => {
|
|
||||||
setSearchOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSearchChange = (value: string) => {
|
|
||||||
setSearch(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSearchSubmit = () => {
|
|
||||||
let { tags } = searchParamState;
|
|
||||||
tags = Array.isArray(tags) ? tags.join("|") : tags;
|
|
||||||
|
|
||||||
navigate({
|
|
||||||
pathname: "/overview.html",
|
|
||||||
search: qs.stringify({ search: search, tags }),
|
|
||||||
});
|
|
||||||
|
|
||||||
setSearchOpen(false);
|
|
||||||
setSearch("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpenClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
// e.preventDefault(); // todo used to be a link
|
|
||||||
openSearch();
|
|
||||||
setTimeout(() => {
|
|
||||||
searchRef?.current?.focus();
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTagChange = (tag: Tag) => {
|
|
||||||
const { search, tags = [] } = searchParamState;
|
|
||||||
let newTags;
|
|
||||||
|
|
||||||
// Toggle changed item in tags searchParam depends on type Array or String
|
|
||||||
if (Array.isArray(tags)) {
|
|
||||||
newTags = tags.includes(tag)
|
|
||||||
? tags.filter((item: string) => item !== tag)
|
|
||||||
: [...tags, tag];
|
|
||||||
} else {
|
|
||||||
newTags = tags !== tag ? [tags, tag] : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
setSearchParamsState({
|
|
||||||
tags: newTags,
|
|
||||||
search,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
|
||||||
|
|
||||||
const toggleModal = function () {
|
|
||||||
setModalIsOpen(!modalIsOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
const smallLogo = pageName !== "index";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Branding logoContent={<LogoLink small={smallLogo} />}>
|
|
||||||
<div className="nav">
|
|
||||||
{pageHelp && (
|
|
||||||
<div className="nav__item">
|
|
||||||
<Link pageName="help-and-about-tech-radar" className="icon-link">
|
|
||||||
<span className="icon icon--question icon-link__icon" />
|
|
||||||
{pageHelp.headlinePrefix || "How to use"} {radarNameShort}?
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{Boolean(tags.length) && (
|
|
||||||
<div className="nav__item">
|
|
||||||
<button className="icon-link" onClick={toggleModal}>
|
|
||||||
<span className="icon icon--filter icon-link__icon" />
|
|
||||||
Filter
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="nav__item">
|
|
||||||
<Link pageName="overview" className="icon-link">
|
|
||||||
<span className="icon icon--overview icon-link__icon" />
|
|
||||||
{pageOverview?.title || "Technologies Overview"}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="nav__item">
|
|
||||||
<button className="icon-link" onClick={handleOpenClick}>
|
|
||||||
<span className="icon icon--search icon-link__icon" />
|
|
||||||
{searchLabel || "Search"}
|
|
||||||
</button>
|
|
||||||
<div className={classNames("nav__search", { "is-open": searchOpen })}>
|
|
||||||
<Search
|
|
||||||
value={search}
|
|
||||||
onClose={closeSearch}
|
|
||||||
onSubmit={handleSearchSubmit}
|
|
||||||
onChange={handleSearchChange}
|
|
||||||
open={searchOpen}
|
|
||||||
ref={searchRef}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{Boolean(tags.length) && (
|
|
||||||
<TagsModal
|
|
||||||
tags={tags}
|
|
||||||
isOpen={modalIsOpen}
|
|
||||||
closeModal={toggleModal}
|
|
||||||
handleTagChange={handleTagChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Branding>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import "./headline-group.scss";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
secondary?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const HeadlineGroup: React.FC<React.PropsWithChildren<Props>> = ({
|
|
||||||
children,
|
|
||||||
secondary = false,
|
|
||||||
}) => (
|
|
||||||
<div
|
|
||||||
className={classNames("headline-group", {
|
|
||||||
"headline-group--secondary": secondary,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default HeadlineGroup;
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
@import "../../styles/sccs-vars.scss";
|
|
||||||
|
|
||||||
.headline-group {
|
|
||||||
margin: 0 0 60px;
|
|
||||||
|
|
||||||
@media (max-width: $until-sm) {
|
|
||||||
margin: 30px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--secondary {
|
|
||||||
margin: 10px 0;
|
|
||||||
|
|
||||||
@media (max-width: $until-sm) {
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import "./hero-headline.scss";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
alt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const HeroHeadline: React.FC<React.PropsWithChildren<Props>> = ({
|
|
||||||
children,
|
|
||||||
alt,
|
|
||||||
}) => (
|
|
||||||
<div className="hero-headline">
|
|
||||||
{children} <span className="hero-headline__alt">{alt}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default HeroHeadline;
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
@import "../../styles/sccs-vars.scss";
|
|
||||||
|
|
||||||
.hero-headline {
|
|
||||||
font-size: 38px;
|
|
||||||
font-weight: 300;
|
|
||||||
line-height: 1.2;
|
|
||||||
color: var(--color-white);
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
&__alt {
|
|
||||||
color: var(--color-gray-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--inverse {
|
|
||||||
color: var(--color-gray-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: $until-sm) {
|
|
||||||
font-size: 26px;
|
|
||||||
|
|
||||||
&__alt {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import { MemoryRouter } from "react-router-dom";
|
|
||||||
|
|
||||||
import Item from "./Item";
|
|
||||||
import { item as testItem } from "./testData";
|
|
||||||
|
|
||||||
describe("Item", () => {
|
|
||||||
it("Should render the item", () => {
|
|
||||||
render(<Item item={testItem} />, { wrapper: MemoryRouter });
|
|
||||||
|
|
||||||
expect(screen.getByText(testItem.title)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { Item as mItem } from "../../model";
|
|
||||||
import Flag from "../Flag/Flag";
|
|
||||||
import Link from "../Link/Link";
|
|
||||||
import "./item.scss";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
item: mItem;
|
|
||||||
noLeadingBorder?: boolean;
|
|
||||||
active?: boolean;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
greyedOut?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Item: React.FC<Props> = ({
|
|
||||||
item,
|
|
||||||
noLeadingBorder = false,
|
|
||||||
active = false,
|
|
||||||
style = {},
|
|
||||||
greyedOut = false,
|
|
||||||
}) => (
|
|
||||||
<Link
|
|
||||||
className={classNames("item", {
|
|
||||||
"item--no-leading-border": noLeadingBorder,
|
|
||||||
"is-active": active,
|
|
||||||
})}
|
|
||||||
pageName={`${item.quadrant}/${item.name}`}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={classNames("item__title", {
|
|
||||||
"greyed-out": greyedOut,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item.title}
|
|
||||||
<Flag item={item} />
|
|
||||||
</div>
|
|
||||||
{item.info && <div className="item__info">{item.info}</div>}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Item;
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
.item {
|
|
||||||
padding: 10px;
|
|
||||||
border-bottom: 1px solid var(--color-gray-normal);
|
|
||||||
display: block;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: background 200ms ease-out;
|
|
||||||
color: var(--color-gray-normal);
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
&.is-active {
|
|
||||||
background: var(--color-gray-dark-alt);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--color-gray-dark-alt2);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-top: 1px solid var(--color-gray-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--big {
|
|
||||||
min-height: 80px;
|
|
||||||
padding: 20px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--no-leading-border {
|
|
||||||
&:first-child {
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--no-trailing-border {
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
font-size: 16px;
|
|
||||||
color: var(--color-white);
|
|
||||||
|
|
||||||
&.greyed-out {
|
|
||||||
color: var(--color-gray-light-alt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__info {
|
|
||||||
margin-top: 5px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--color-gray-normal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { FlagType, Item } from "../../model";
|
|
||||||
|
|
||||||
export const item: Item = {
|
|
||||||
flag: FlagType.default,
|
|
||||||
featured: false,
|
|
||||||
revisions: [
|
|
||||||
{
|
|
||||||
name: "yarn",
|
|
||||||
release: "2018-03-01",
|
|
||||||
title: "Yarn",
|
|
||||||
ring: "trial",
|
|
||||||
quadrant: "tools",
|
|
||||||
fileName: "C:\\projects\\techradar\\radar\\2018-03-01\\yarn.md",
|
|
||||||
body: "<p>Yarn is a dependency management tool for frontend (node) projects similar to npm. It also uses the npm registry and \ninfrastructure. According to Yarn, the benefits are that Yarn is much faster, automatically writes a .lock file and \nbuilds up a local cache to be even faster when installing packages again.</p>\n<p>At AOE, we started using Yarn in different projects to evaluate if we can switch to Yarn for all projects.</p>\n",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
name: "yarn",
|
|
||||||
title: "Yarn",
|
|
||||||
ring: "trial",
|
|
||||||
quadrant: "tools",
|
|
||||||
body: "<p>Yarn is a dependency management tool for frontend (node) projects similar to npm. It also uses the npm registry and \ninfrastructure. According to Yarn, the benefits are that Yarn is much faster, automatically writes a .lock file and \nbuilds up a local cache to be even faster when installing packages again.</p>\n<p>At AOE, we started using Yarn in different projects to evaluate if we can switch to Yarn for all projects.</p>\n",
|
|
||||||
info: "",
|
|
||||||
};
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { featuredOnly, Item as mItem, nonFeaturedOnly } from "../../model";
|
|
||||||
import Item from "../Item/Item";
|
|
||||||
import "./item-list.scss";
|
|
||||||
|
|
||||||
type ItemListProps = {
|
|
||||||
items: mItem[];
|
|
||||||
activeItem?: mItem;
|
|
||||||
noLeadingBorder?: boolean;
|
|
||||||
headerStyle?: React.CSSProperties;
|
|
||||||
itemStyle?: React.CSSProperties[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const ItemList: React.FC<React.PropsWithChildren<ItemListProps>> = ({
|
|
||||||
children,
|
|
||||||
items,
|
|
||||||
activeItem,
|
|
||||||
noLeadingBorder,
|
|
||||||
headerStyle = {},
|
|
||||||
itemStyle = [],
|
|
||||||
}) => {
|
|
||||||
const featuredItems = featuredOnly(items);
|
|
||||||
const nonFeaturedItems = nonFeaturedOnly(items);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="item-list">
|
|
||||||
<div className="item-list__header" style={headerStyle}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
<div className="item-list__list">
|
|
||||||
{featuredItems.map((item, i) => (
|
|
||||||
<Item
|
|
||||||
key={item.name}
|
|
||||||
item={item}
|
|
||||||
noLeadingBorder={noLeadingBorder}
|
|
||||||
active={activeItem?.name === item.name}
|
|
||||||
style={itemStyle[i]}
|
|
||||||
greyedOut={false}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{nonFeaturedItems.map((item, i) => (
|
|
||||||
<Item
|
|
||||||
key={item.name}
|
|
||||||
item={item}
|
|
||||||
noLeadingBorder={noLeadingBorder}
|
|
||||||
active={activeItem?.name === item.name}
|
|
||||||
style={itemStyle[featuredItems.length + i]}
|
|
||||||
greyedOut={true}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ItemList;
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
.item-list {
|
|
||||||
margin: 0 0 25px;
|
|
||||||
&__header {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { ConfigData } from "../../config";
|
|
||||||
import { formatRelease } from "../../date";
|
|
||||||
import { Revision } from "../../model";
|
|
||||||
import Badge from "../Badge/Badge";
|
|
||||||
|
|
||||||
export default function ItemRevision({
|
|
||||||
revision,
|
|
||||||
config,
|
|
||||||
dateFormat,
|
|
||||||
}: {
|
|
||||||
revision: Revision;
|
|
||||||
config: ConfigData;
|
|
||||||
dateFormat?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="item-revision">
|
|
||||||
<div>
|
|
||||||
<Badge type={revision.ring} config={config}>
|
|
||||||
{revision.ring} | {formatRelease(revision.release, dateFormat)}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="markdown"
|
|
||||||
dangerouslySetInnerHTML={{ __html: revision.body }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
.item-revision {
|
|
||||||
& + .item-revision {
|
|
||||||
margin-top: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { ConfigData } from "../../config";
|
|
||||||
import { useMessages } from "../../context/MessagesContext";
|
|
||||||
import { Revision } from "../../model";
|
|
||||||
import HeadlineGroup from "../HeadlineGroup/HeadlineGroup";
|
|
||||||
import ItemRevision from "../ItemRevision/ItemRevision";
|
|
||||||
import "./item-revisions.scss";
|
|
||||||
|
|
||||||
export default function ItemRevisions({
|
|
||||||
revisions,
|
|
||||||
config,
|
|
||||||
dateFormat,
|
|
||||||
}: {
|
|
||||||
revisions: Revision[];
|
|
||||||
config: ConfigData;
|
|
||||||
dateFormat?: string;
|
|
||||||
}) {
|
|
||||||
const { revisionsText } = useMessages();
|
|
||||||
return (
|
|
||||||
<div className="item-revisions">
|
|
||||||
<HeadlineGroup secondary>
|
|
||||||
<h4 className="headline headline--dark">
|
|
||||||
{revisionsText ?? "Revisions:"}
|
|
||||||
</h4>
|
|
||||||
</HeadlineGroup>
|
|
||||||
|
|
||||||
{revisions.map((revision) => (
|
|
||||||
<ItemRevision
|
|
||||||
key={revision.release}
|
|
||||||
revision={revision}
|
|
||||||
dateFormat={dateFormat}
|
|
||||||
config={config}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
.item-revisions {
|
|
||||||
margin-top: 60px;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Tag } from "../../model";
|
|
||||||
|
|
||||||
export default function ItemTags({ tags }: { tags?: Tag[] }) {
|
|
||||||
if (!tags) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="markdown">
|
|
||||||
{"Tags: "}
|
|
||||||
{tags.map((tag, id) => [
|
|
||||||
id !== 0 && ", ",
|
|
||||||
<span key={tag}>"{tag}"</span>,
|
|
||||||
])}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Link as RLink } from "react-router-dom";
|
|
||||||
|
|
||||||
import { useSearchParamState } from "../../hooks/use-search-param-state";
|
|
||||||
import "./link.scss";
|
|
||||||
|
|
||||||
type LinkProps = {
|
|
||||||
pageName: string;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function Link({
|
|
||||||
pageName,
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
style = {},
|
|
||||||
}: React.PropsWithChildren<LinkProps>) {
|
|
||||||
const [searchParamState] = useSearchParamState(undefined, {
|
|
||||||
parseOptions: { decode: false },
|
|
||||||
});
|
|
||||||
const { tags } = searchParamState;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RLink
|
|
||||||
to={tags ? `/${pageName}.html?tags=${tags}` : `/${pageName}.html`}
|
|
||||||
style={style}
|
|
||||||
{...{ className }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</RLink>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Link;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.link {
|
|
||||||
color: var(--color-white);
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { assetUrl, radarNameShort } from "../../config";
|
|
||||||
import Link from "../Link/Link";
|
|
||||||
import "./logo-link.scss";
|
|
||||||
|
|
||||||
export default function LogoLink({ small = false }: { small?: boolean }) {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
pageName="index"
|
|
||||||
className={classNames("logo-link", { "logo-link--small": small })}
|
|
||||||
>
|
|
||||||
<span className="logo-link__icon icon icon--back" />
|
|
||||||
<span className="logo-link__slide">
|
|
||||||
<img
|
|
||||||
className="logo-link__img"
|
|
||||||
src={assetUrl("logo.svg")}
|
|
||||||
width="150px"
|
|
||||||
height="60px"
|
|
||||||
alt={radarNameShort}
|
|
||||||
/>
|
|
||||||
<span className="logo-link__text">{radarNameShort}</span>
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
.logo-link {
|
|
||||||
display: inline-block;
|
|
||||||
transition: 200ms all ease-out;
|
|
||||||
width: 400px;
|
|
||||||
color: var(--color-white);
|
|
||||||
text-decoration: none;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&__slide {
|
|
||||||
transition: 400ms all cubic-bezier(0.54, 0, 0.28, 1);
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__icon {
|
|
||||||
transition: 400ms opacity ease-out;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__img {
|
|
||||||
transition: 400ms all cubic-bezier(0.54, 0, 0.28, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__text {
|
|
||||||
transition: 400ms all cubic-bezier(0.54, 0, 0.28, 1);
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 130px;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 1;
|
|
||||||
margin-top: -1px;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--small {
|
|
||||||
.logo-link__img {
|
|
||||||
transform: scale(0.55);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-link__text {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-link__slide {
|
|
||||||
transform: translateX(-32px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.logo-link__icon {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-link__slide {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { radarName } from "../../config";
|
|
||||||
import { useMessages } from "../../context/MessagesContext";
|
|
||||||
import { sanitize } from "../../sanitize";
|
|
||||||
import Fadeable from "../Fadeable/Fadeable";
|
|
||||||
import HeroHeadline from "../HeroHeadline/HeroHeadline";
|
|
||||||
import SetTitle from "../SetTitle";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
leaving: boolean;
|
|
||||||
onLeave: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PageHelp: React.FC<Props> = ({ leaving, onLeave }) => {
|
|
||||||
const { pageHelp } = useMessages();
|
|
||||||
|
|
||||||
if (pageHelp) {
|
|
||||||
const {
|
|
||||||
paragraphs,
|
|
||||||
quadrants,
|
|
||||||
rings,
|
|
||||||
sourcecodeLink,
|
|
||||||
headlinePrefix,
|
|
||||||
quadrantsPreDescription,
|
|
||||||
ringsPreDescription,
|
|
||||||
} = pageHelp;
|
|
||||||
const title = `${headlinePrefix || "How to use the"} ${radarName}`;
|
|
||||||
return (
|
|
||||||
<Fadeable leaving={leaving} onLeave={onLeave}>
|
|
||||||
<SetTitle title={title} />
|
|
||||||
<HeroHeadline>{title}</HeroHeadline>
|
|
||||||
<div className="fullpage-content">
|
|
||||||
{paragraphs.map(({ headline, values }) => (
|
|
||||||
<React.Fragment key={headline}>
|
|
||||||
<h3>{headline}</h3>
|
|
||||||
{values.map((element, index) => {
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
key={index}
|
|
||||||
dangerouslySetInnerHTML={sanitize(element)}
|
|
||||||
></p>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<p>{quadrantsPreDescription ?? "The quadrants are:"}</p>
|
|
||||||
<ul>
|
|
||||||
{quadrants.map(({ name, description }) => (
|
|
||||||
<li key={name}>
|
|
||||||
<strong>{name}:</strong>{" "}
|
|
||||||
<span
|
|
||||||
dangerouslySetInnerHTML={sanitize(description, {})}
|
|
||||||
></span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{ringsPreDescription ??
|
|
||||||
"Each of the items is classified in one of these rings:"}
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
{rings.map(({ name, description }) => (
|
|
||||||
<li key={name}>
|
|
||||||
<strong>{name}:</strong>{" "}
|
|
||||||
<span
|
|
||||||
dangerouslySetInnerHTML={sanitize(description, {})}
|
|
||||||
></span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{sourcecodeLink && (
|
|
||||||
<p>
|
|
||||||
{`${sourcecodeLink.description} `}
|
|
||||||
<a
|
|
||||||
href={sourcecodeLink.href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{sourcecodeLink.name}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Fadeable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PageHelp;
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { MomentInput } from "moment";
|
|
||||||
|
|
||||||
import { ConfigData, radarName } from "../../config";
|
|
||||||
import { useMessages } from "../../context/MessagesContext";
|
|
||||||
import { formatRelease } from "../../date";
|
|
||||||
import { HomepageOption, Item, featuredOnly } from "../../model";
|
|
||||||
import Fadeable from "../Fadeable/Fadeable";
|
|
||||||
import HeroHeadline from "../HeroHeadline/HeroHeadline";
|
|
||||||
import QuadrantGrid from "../QuadrantGrid/QuadrantGrid";
|
|
||||||
import RadarGrid from "../RadarGrid/RadarGrid";
|
|
||||||
import SetTitle from "../SetTitle";
|
|
||||||
|
|
||||||
type PageIndexProps = {
|
|
||||||
leaving: boolean;
|
|
||||||
onLeave: () => void;
|
|
||||||
items: Item[];
|
|
||||||
config: ConfigData;
|
|
||||||
releases: MomentInput[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PageIndex({
|
|
||||||
leaving,
|
|
||||||
onLeave,
|
|
||||||
items,
|
|
||||||
config,
|
|
||||||
releases,
|
|
||||||
}: PageIndexProps) {
|
|
||||||
const { pageIndex } = useMessages();
|
|
||||||
const publishedLabel = pageIndex?.publishedLabel || "Published";
|
|
||||||
|
|
||||||
const newestRelease = releases.slice(-1)[0];
|
|
||||||
const numberOfReleases = releases.length;
|
|
||||||
const showChart =
|
|
||||||
config.homepageContent === HomepageOption.chart ||
|
|
||||||
config.homepageContent === HomepageOption.both;
|
|
||||||
const showColumns =
|
|
||||||
config.homepageContent === HomepageOption.columns ||
|
|
||||||
config.homepageContent === HomepageOption.both;
|
|
||||||
return (
|
|
||||||
<Fadeable leaving={leaving} onLeave={onLeave}>
|
|
||||||
<SetTitle />
|
|
||||||
<div className="headline-group">
|
|
||||||
<HeroHeadline alt={`Version #${numberOfReleases}`}>
|
|
||||||
{radarName}
|
|
||||||
</HeroHeadline>
|
|
||||||
</div>
|
|
||||||
{showChart && <RadarGrid items={featuredOnly(items)} config={config} />}
|
|
||||||
{showColumns && (
|
|
||||||
<QuadrantGrid items={featuredOnly(items)} config={config} />
|
|
||||||
)}
|
|
||||||
<div className="publish-date">
|
|
||||||
{publishedLabel} {formatRelease(newestRelease, config.dateFormat)}
|
|
||||||
</div>
|
|
||||||
</Fadeable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
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 EditButton from "../EditButton/EditButton";
|
|
||||||
import FooterEnd from "../FooterEnd/FooterEnd";
|
|
||||||
import ItemList from "../ItemList/ItemList";
|
|
||||||
import ItemRevisions from "../ItemRevisions/ItemRevisions";
|
|
||||||
import ItemTags from "../ItemTags/ItemTags";
|
|
||||||
import Link from "../Link/Link";
|
|
||||||
import SetTitle from "../SetTitle";
|
|
||||||
import "./item-page.scss";
|
|
||||||
import { useAnimations } from "./useAnimations";
|
|
||||||
|
|
||||||
const getItem = (pageName: string, items: Item[]) => {
|
|
||||||
const [quadrantName, itemName] = pageName.split("/");
|
|
||||||
return items.filter(
|
|
||||||
(item) => item.quadrant === quadrantName && item.name === itemName
|
|
||||||
)[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getItemsInRing = (pageName: string, items: Item[]) => {
|
|
||||||
const item = getItem(pageName, items);
|
|
||||||
return groupByQuadrants(items)[item.quadrant][item.ring];
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
pageName: string;
|
|
||||||
items: Item[];
|
|
||||||
config: ConfigData;
|
|
||||||
leaving: boolean;
|
|
||||||
onLeave: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PageItem: React.FC<Props> = ({
|
|
||||||
pageName,
|
|
||||||
items,
|
|
||||||
config,
|
|
||||||
leaving,
|
|
||||||
onLeave,
|
|
||||||
}) => {
|
|
||||||
const { pageItem } = useMessages();
|
|
||||||
const quadrantOverview = pageItem?.quadrantOverview || "Quadrant Overview";
|
|
||||||
|
|
||||||
const itemsInRing = getItemsInRing(pageName, items);
|
|
||||||
|
|
||||||
const { getAnimationState, getAnimationStates } = useAnimations({
|
|
||||||
itemsInRing,
|
|
||||||
onLeave,
|
|
||||||
leaving,
|
|
||||||
});
|
|
||||||
|
|
||||||
const item = getItem(pageName, items);
|
|
||||||
const editButton = config.editLink ? (
|
|
||||||
<EditButton
|
|
||||||
baseUrl={config.editLink.radarLink}
|
|
||||||
item={item}
|
|
||||||
title={config.editLink.title}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SetTitle title={item.title} />
|
|
||||||
<div className="item-page">
|
|
||||||
<div className="item-page__nav">
|
|
||||||
<div className="item-page__nav__inner">
|
|
||||||
<div
|
|
||||||
className="item-page__header"
|
|
||||||
style={getAnimationState("navHeader")}
|
|
||||||
>
|
|
||||||
<h3 className="headline">{translate(config, item.quadrant)}</h3>
|
|
||||||
</div>
|
|
||||||
<ItemList
|
|
||||||
items={itemsInRing}
|
|
||||||
activeItem={item}
|
|
||||||
headerStyle={getAnimationState("navHeader")}
|
|
||||||
itemStyle={getAnimationStates("items")}
|
|
||||||
>
|
|
||||||
<div className="split">
|
|
||||||
<div className="split__left">
|
|
||||||
<Badge big type={item.ring} config={config}>
|
|
||||||
{item.ring}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="split__right">
|
|
||||||
<Link className="icon-link" pageName={item.quadrant}>
|
|
||||||
<span className="icon icon--pie icon-link__icon" />
|
|
||||||
{quadrantOverview}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ItemList>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="item-page__footer"
|
|
||||||
style={getAnimationState("footer")}
|
|
||||||
>
|
|
||||||
<FooterEnd modifier="in-sidebar" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="item-page__content"
|
|
||||||
style={getAnimationState("background")}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="item-page__content__inner"
|
|
||||||
style={getAnimationState("text")}
|
|
||||||
>
|
|
||||||
<div className="item-page__header">
|
|
||||||
<div className="split">
|
|
||||||
<div className="split__left hero-headline__wrapper">
|
|
||||||
<h1 className="hero-headline hero-headline--inverse">
|
|
||||||
{item.title}
|
|
||||||
</h1>
|
|
||||||
{editButton}
|
|
||||||
</div>
|
|
||||||
<div className="split__right">
|
|
||||||
<Badge big type={item.ring} config={config}>
|
|
||||||
{item.ring}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="markdown"
|
|
||||||
dangerouslySetInnerHTML={{ __html: item.body }}
|
|
||||||
/>
|
|
||||||
<ItemTags tags={item.tags} />
|
|
||||||
{item.revisions.length > 1 && (
|
|
||||||
<ItemRevisions
|
|
||||||
revisions={item.revisions.slice(1)}
|
|
||||||
dateFormat={config.dateFormat}
|
|
||||||
config={config}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PageItem;
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
.item-page {
|
|
||||||
display: flex;
|
|
||||||
min-height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 10;
|
|
||||||
|
|
||||||
&__nav,
|
|
||||||
&__content {
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding-top: 130px;
|
|
||||||
min-height: 100vh;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__header {
|
|
||||||
min-height: 40px;
|
|
||||||
margin: 0 0 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__nav {
|
|
||||||
background: var(--color-gray-dark);
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
flex-basis: calc(((100vw - 1200px) / 2) + 400px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__nav__inner {
|
|
||||||
box-sizing: border-box;
|
|
||||||
float: right;
|
|
||||||
width: 410px;
|
|
||||||
padding: 0 40px 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
//flex: 0 0 calc((100vw - 1200px) / 2 + 800px);
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
flex-basis: calc((100vw - 1200px) / 2 + 800px);
|
|
||||||
background: var(--color-white);
|
|
||||||
|
|
||||||
&__inner {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 810px;
|
|
||||||
padding: 0 10px 0 100px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-headline__wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-item-page {
|
|
||||||
background: #fff;
|
|
||||||
margin: 0 -15px;
|
|
||||||
padding: 15px;
|
|
||||||
min-height: 300px;
|
|
||||||
|
|
||||||
&__aside {
|
|
||||||
padding: 20px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Animation,
|
|
||||||
AnimationStates,
|
|
||||||
createAnimation,
|
|
||||||
createAnimationRunner,
|
|
||||||
} from "../../animation";
|
|
||||||
import { Item } from "../../model";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
itemsInRing: Item[];
|
|
||||||
leaving: boolean;
|
|
||||||
onLeave: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useAnimations = ({ itemsInRing, leaving, onLeave }: Props) => {
|
|
||||||
type AnimationConfig = {
|
|
||||||
background: Animation;
|
|
||||||
navHeader: Animation;
|
|
||||||
text: Animation;
|
|
||||||
items: Animation[];
|
|
||||||
footer: Animation;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AnimationNames = keyof AnimationConfig;
|
|
||||||
|
|
||||||
const animationsIn: AnimationConfig = useMemo(
|
|
||||||
() => ({
|
|
||||||
background: createAnimation(
|
|
||||||
{
|
|
||||||
transform: "translateX(calc((100vw - 1200px) / 2 + 800px))",
|
|
||||||
transition: "transform 450ms cubic-bezier(0.24, 1.12, 0.71, 0.98)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
transition: "transform 450ms cubic-bezier(0.24, 1.12, 0.71, 0.98)",
|
|
||||||
transform: "translateX(0)",
|
|
||||||
},
|
|
||||||
0
|
|
||||||
),
|
|
||||||
navHeader: createAnimation(
|
|
||||||
{
|
|
||||||
transform: "translateX(-40px)",
|
|
||||||
opacity: "0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
transition: "opacity 150ms ease-out, transform 300ms ease-out",
|
|
||||||
transform: "translateX(0px)",
|
|
||||||
opacity: "1",
|
|
||||||
},
|
|
||||||
300
|
|
||||||
),
|
|
||||||
text: createAnimation(
|
|
||||||
{
|
|
||||||
transform: "translateY(-20px)",
|
|
||||||
opacity: "0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
transition: "opacity 150ms ease-out, transform 300ms ease-out",
|
|
||||||
transform: "translateY(0px)",
|
|
||||||
opacity: "1",
|
|
||||||
},
|
|
||||||
600
|
|
||||||
),
|
|
||||||
items: itemsInRing.map((item, i) =>
|
|
||||||
createAnimation(
|
|
||||||
{
|
|
||||||
transform: "translateX(-40px)",
|
|
||||||
opacity: "0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
transition: "opacity 150ms ease-out, transform 300ms ease-out",
|
|
||||||
transform: "translateX(0px)",
|
|
||||||
opacity: "1",
|
|
||||||
},
|
|
||||||
400 + 100 * i
|
|
||||||
)
|
|
||||||
),
|
|
||||||
footer: createAnimation(
|
|
||||||
{
|
|
||||||
transition: "opacity 150ms ease-out, transform 300ms ease-out",
|
|
||||||
transform: "translateX(-40px)",
|
|
||||||
opacity: "0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
transition: "opacity 150ms ease-out, transform 300ms ease-out",
|
|
||||||
transform: "translateX(0px)",
|
|
||||||
opacity: "1",
|
|
||||||
},
|
|
||||||
600 + itemsInRing.length * 100
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
[itemsInRing]
|
|
||||||
);
|
|
||||||
|
|
||||||
const animationsOut: AnimationConfig = useMemo(
|
|
||||||
() => ({
|
|
||||||
background: createAnimation(
|
|
||||||
animationsIn.background.stateB,
|
|
||||||
animationsIn.background.stateA,
|
|
||||||
300 + itemsInRing.length * 50
|
|
||||||
),
|
|
||||||
navHeader: createAnimation(
|
|
||||||
animationsIn.navHeader.stateB,
|
|
||||||
{
|
|
||||||
transition: "opacity 150ms ease-out, transform 300ms ease-out",
|
|
||||||
transform: "translateX(40px)",
|
|
||||||
opacity: "0",
|
|
||||||
},
|
|
||||||
0
|
|
||||||
),
|
|
||||||
text: createAnimation(
|
|
||||||
animationsIn.text.stateB,
|
|
||||||
{
|
|
||||||
transform: "translateY(20px)",
|
|
||||||
transition: "opacity 150ms ease-out, transform 300ms ease-out",
|
|
||||||
opacity: "0",
|
|
||||||
},
|
|
||||||
0
|
|
||||||
),
|
|
||||||
items: itemsInRing.map((item, i) =>
|
|
||||||
createAnimation(
|
|
||||||
animationsIn.items[i].stateB,
|
|
||||||
{
|
|
||||||
transition: "opacity 150ms ease-out, transform 300ms ease-out",
|
|
||||||
transform: "translateX(40px)",
|
|
||||||
opacity: "0",
|
|
||||||
},
|
|
||||||
100 + 50 * i
|
|
||||||
)
|
|
||||||
),
|
|
||||||
footer: createAnimation(
|
|
||||||
animationsIn.text.stateB,
|
|
||||||
{
|
|
||||||
transition: "opacity 150ms ease-out, transform 300ms ease-out",
|
|
||||||
transform: "translateX(40px)",
|
|
||||||
opacity: "0",
|
|
||||||
},
|
|
||||||
200 + itemsInRing.length * 50
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
[itemsInRing, animationsIn]
|
|
||||||
);
|
|
||||||
|
|
||||||
const [animations, setAnimations] = useState<AnimationStates>(() => {
|
|
||||||
return leaving ? createAnimationRunner(animationsIn).getState() : {};
|
|
||||||
});
|
|
||||||
|
|
||||||
const [stateLeaving, setStateLeaving] = useState(leaving);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!stateLeaving && leaving) {
|
|
||||||
let animationRunner = createAnimationRunner(animationsOut, () =>
|
|
||||||
setAnimations(animationRunner.getState)
|
|
||||||
);
|
|
||||||
animationRunner.run();
|
|
||||||
animationRunner.awaitAnimationComplete(onLeave);
|
|
||||||
setStateLeaving(true);
|
|
||||||
}
|
|
||||||
if (stateLeaving && !leaving) {
|
|
||||||
let animationRunner = createAnimationRunner(animationsIn, () =>
|
|
||||||
setAnimations(animationRunner.getState)
|
|
||||||
);
|
|
||||||
animationRunner.run();
|
|
||||||
setStateLeaving(false);
|
|
||||||
}
|
|
||||||
}, [stateLeaving, leaving, animationsIn, animationsOut, onLeave]);
|
|
||||||
|
|
||||||
const getAnimationStates = (name: AnimationNames) => animations[name];
|
|
||||||
|
|
||||||
const getAnimationState = (name: AnimationNames) => {
|
|
||||||
const animations = getAnimationStates(name);
|
|
||||||
if (animations === undefined || animations.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return animations[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
getAnimationStates,
|
|
||||||
getAnimationState,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import { ConfigData, translate } from "../../config";
|
|
||||||
import { Item, groupByQuadrants } from "../../model";
|
|
||||||
import Badge from "../Badge/Badge";
|
|
||||||
import EditButton from "../EditButton/EditButton";
|
|
||||||
import Fadeable from "../Fadeable/Fadeable";
|
|
||||||
import ItemList from "../ItemList/ItemList";
|
|
||||||
import ItemRevisions from "../ItemRevisions/ItemRevisions";
|
|
||||||
import ItemTags from "../ItemTags/ItemTags";
|
|
||||||
import Link from "../Link/Link";
|
|
||||||
import SetTitle from "../SetTitle";
|
|
||||||
|
|
||||||
type PageItemMobileProps = {
|
|
||||||
pageName: string;
|
|
||||||
items: Item[];
|
|
||||||
config: ConfigData;
|
|
||||||
leaving: boolean;
|
|
||||||
onLeave: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PageItemMobile({
|
|
||||||
pageName,
|
|
||||||
items,
|
|
||||||
config,
|
|
||||||
leaving,
|
|
||||||
onLeave,
|
|
||||||
}: PageItemMobileProps) {
|
|
||||||
const getItem = (pageName: string, items: Item[]) => {
|
|
||||||
const [quadrantName, itemName] = pageName.split("/");
|
|
||||||
const item = items.filter(
|
|
||||||
(item) => item.quadrant === quadrantName && item.name === itemName
|
|
||||||
)[0];
|
|
||||||
return item;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getItemsInRing = (pageName: string, items: Item[]) => {
|
|
||||||
const item = getItem(pageName, items);
|
|
||||||
const itemsInRing = groupByQuadrants(items)[item.quadrant][item.ring];
|
|
||||||
return itemsInRing;
|
|
||||||
};
|
|
||||||
|
|
||||||
const item = getItem(pageName, items);
|
|
||||||
const itemsInRing = getItemsInRing(pageName, items);
|
|
||||||
const editButton = config.editLink ? (
|
|
||||||
<EditButton
|
|
||||||
baseUrl={config.editLink.radarLink}
|
|
||||||
item={item}
|
|
||||||
title={config.editLink.title}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
return (
|
|
||||||
<Fadeable leaving={leaving} onLeave={onLeave}>
|
|
||||||
<SetTitle title={item.title} />
|
|
||||||
<div className="mobile-item-page">
|
|
||||||
<div className="mobile-item-page__content">
|
|
||||||
<div className="mobile-item-page__content__inner">
|
|
||||||
<div className="mobile-item-page__header">
|
|
||||||
<div className="split">
|
|
||||||
<div className="split__left">
|
|
||||||
<h3 className="headline">
|
|
||||||
{translate(config, item.quadrant)}
|
|
||||||
</h3>
|
|
||||||
<h1 className="hero-headline hero-headline--inverse">
|
|
||||||
{item.title}
|
|
||||||
</h1>
|
|
||||||
{editButton}
|
|
||||||
</div>
|
|
||||||
<div className="split__right">
|
|
||||||
<Badge big type={item.ring} config={config}>
|
|
||||||
{item.ring}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="markdown"
|
|
||||||
dangerouslySetInnerHTML={{ __html: item.body }}
|
|
||||||
/>
|
|
||||||
<ItemTags tags={item.tags} />
|
|
||||||
{item.revisions.length > 1 && (
|
|
||||||
<ItemRevisions
|
|
||||||
revisions={item.revisions.slice(1)}
|
|
||||||
config={config}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<aside className="mobile-item-page__aside">
|
|
||||||
<ItemList items={itemsInRing} activeItem={item}>
|
|
||||||
<div className="split">
|
|
||||||
<div className="split__left">
|
|
||||||
<h3 className="headline">{translate(config, item.quadrant)}</h3>
|
|
||||||
</div>
|
|
||||||
<div className="split__right">
|
|
||||||
<Link className="icon-link" pageName={item.quadrant}>
|
|
||||||
<span className="icon icon--pie icon-link__icon"></span>Zoom In
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ItemList>
|
|
||||||
</aside>
|
|
||||||
</Fadeable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { ConfigData, translate } from "../../config";
|
|
||||||
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 = "") => {
|
|
||||||
// TODO search refinement
|
|
||||||
return (
|
|
||||||
text.trim().toLocaleLowerCase().indexOf(term.trim().toLocaleLowerCase()) !==
|
|
||||||
-1
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type PageOverviewProps = {
|
|
||||||
rings: readonly ("all" | string)[];
|
|
||||||
search: string;
|
|
||||||
items: Item[];
|
|
||||||
config: ConfigData;
|
|
||||||
leaving: boolean;
|
|
||||||
onLeave: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PageOverview({
|
|
||||||
rings,
|
|
||||||
search: searchProp,
|
|
||||||
items,
|
|
||||||
config,
|
|
||||||
leaving,
|
|
||||||
onLeave,
|
|
||||||
}: PageOverviewProps) {
|
|
||||||
const [ring, setRing] = useState<string | "all">("all");
|
|
||||||
const [search, setSearch] = useState(searchProp);
|
|
||||||
const { pageOverview } = useMessages();
|
|
||||||
const title = pageOverview?.title || "Technologies Overview";
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSearch(searchProp);
|
|
||||||
}, [searchProp]);
|
|
||||||
|
|
||||||
const handleRingClick = (ring: string) => () => {
|
|
||||||
setRing(ring);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isRingActive = (ringName: string) => ring === ringName;
|
|
||||||
|
|
||||||
const itemMatchesRing = (item: Item) => ring === "all" || item.ring === ring;
|
|
||||||
|
|
||||||
const itemMatchesSearch = (item: Item) => {
|
|
||||||
return (
|
|
||||||
search.trim() === "" ||
|
|
||||||
containsSearchTerm(item.title, search) ||
|
|
||||||
containsSearchTerm(item.body, search) ||
|
|
||||||
containsSearchTerm(item.info, search)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isItemVisible = (item: Item) =>
|
|
||||||
itemMatchesRing(item) && itemMatchesSearch(item);
|
|
||||||
|
|
||||||
const getFilteredAndGroupedItems = () => {
|
|
||||||
const groups = groupByFirstLetter(items);
|
|
||||||
const groupsFiltered = groups.map((group) => ({
|
|
||||||
...group,
|
|
||||||
items: group.items.filter(isItemVisible),
|
|
||||||
}));
|
|
||||||
const nonEmptyGroups = groupsFiltered.filter(
|
|
||||||
(group) => group.items.length > 0
|
|
||||||
);
|
|
||||||
return nonEmptyGroups;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSearchTermChange = setSearch;
|
|
||||||
|
|
||||||
const groups = getFilteredAndGroupedItems();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fadeable leaving={leaving} onLeave={onLeave}>
|
|
||||||
<SetTitle title={title} />
|
|
||||||
<HeadlineGroup>
|
|
||||||
<HeroHeadline>{title}</HeroHeadline>
|
|
||||||
</HeadlineGroup>
|
|
||||||
<div className="filter">
|
|
||||||
<div className="split split--filter">
|
|
||||||
<div className="split__left">
|
|
||||||
<Search onChange={handleSearchTermChange} value={search} />
|
|
||||||
</div>
|
|
||||||
<div className="split__right">
|
|
||||||
<div className="nav">
|
|
||||||
{["all", ...rings].map((ringName) => (
|
|
||||||
<div className="nav__item" key={ringName}>
|
|
||||||
<Badge
|
|
||||||
big
|
|
||||||
onClick={handleRingClick(ringName)}
|
|
||||||
type={isRingActive(ringName) ? ringName : "empty"}
|
|
||||||
config={config}
|
|
||||||
>
|
|
||||||
{ringName}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="letter-index">
|
|
||||||
{groups.map(({ letter, items }) => (
|
|
||||||
<div key={letter} className="letter-index__group">
|
|
||||||
<div className="letter-index__letter">{letter}</div>
|
|
||||||
<div className="letter-index__items">
|
|
||||||
<div className="item-list">
|
|
||||||
<div className="item-list__list">
|
|
||||||
{items.map((item) => (
|
|
||||||
<Link
|
|
||||||
key={item.name}
|
|
||||||
className="item item--big item--no-leading-border item--no-trailing-border"
|
|
||||||
pageName={`${item.quadrant}/${item.name}`}
|
|
||||||
>
|
|
||||||
<div className="split split--overview">
|
|
||||||
<div className="split__left">
|
|
||||||
<div className="item__title">
|
|
||||||
{item.title}
|
|
||||||
<Flag item={item} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="split__right">
|
|
||||||
<div className="nav nav--relations">
|
|
||||||
<div className="nav__item">
|
|
||||||
{translate(config, item.quadrant)}
|
|
||||||
</div>
|
|
||||||
<div className="nav__item">
|
|
||||||
<Badge type={item.ring} config={config}>
|
|
||||||
{item.ring}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Fadeable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { ConfigData, translate } from "../../config";
|
|
||||||
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 = {
|
|
||||||
leaving: boolean;
|
|
||||||
onLeave: () => void;
|
|
||||||
pageName: string;
|
|
||||||
items: Item[];
|
|
||||||
config: ConfigData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PageQuadrant({
|
|
||||||
leaving,
|
|
||||||
onLeave,
|
|
||||||
pageName,
|
|
||||||
items,
|
|
||||||
config,
|
|
||||||
}: PageQuadrantProps) {
|
|
||||||
const groups = groupByQuadrants(featuredOnly(items));
|
|
||||||
return (
|
|
||||||
<Fadeable leaving={leaving} onLeave={onLeave}>
|
|
||||||
<SetTitle title={translate(config, pageName)} />
|
|
||||||
<HeadlineGroup>
|
|
||||||
<HeroHeadline>{translate(config, pageName)}</HeroHeadline>
|
|
||||||
</HeadlineGroup>
|
|
||||||
<QuadrantSection
|
|
||||||
groups={groups}
|
|
||||||
quadrantName={pageName}
|
|
||||||
config={config}
|
|
||||||
big
|
|
||||||
/>
|
|
||||||
</Fadeable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { ConfigData } from "../../config";
|
|
||||||
import { Group, Item, groupByQuadrants } from "../../model";
|
|
||||||
import QuadrantSection from "../QuadrantSection/QuadrantSection";
|
|
||||||
import "./quadrant-grid.scss";
|
|
||||||
|
|
||||||
const renderQuadrant = (
|
|
||||||
quadrantName: string,
|
|
||||||
groups: Group,
|
|
||||||
config: ConfigData
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
<div key={quadrantName} className="quadrant-grid__quadrant">
|
|
||||||
<QuadrantSection
|
|
||||||
quadrantName={quadrantName}
|
|
||||||
groups={groups}
|
|
||||||
config={config}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function QuadrantGrid({
|
|
||||||
items,
|
|
||||||
config,
|
|
||||||
}: {
|
|
||||||
items: Item[];
|
|
||||||
config: ConfigData;
|
|
||||||
}) {
|
|
||||||
const groups = groupByQuadrants(items);
|
|
||||||
return (
|
|
||||||
<div className="quadrant-grid">
|
|
||||||
{Object.keys(config.quadrants).map((quadrantName: string) =>
|
|
||||||
renderQuadrant(quadrantName, groups, config)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
@import "../../styles/sccs-vars.scss";
|
|
||||||
|
|
||||||
.quadrant-grid {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
&__quadrant {
|
|
||||||
flex: 0 0 45%;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
|
|
||||||
@media (max-width: $until-lg) {
|
|
||||||
flex-basis: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import { ConfigData, translate } from "../../config";
|
|
||||||
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";
|
|
||||||
|
|
||||||
const renderList = (
|
|
||||||
ringName: string,
|
|
||||||
quadrantName: string,
|
|
||||||
groups: Group,
|
|
||||||
config: ConfigData,
|
|
||||||
big: boolean
|
|
||||||
) => {
|
|
||||||
const itemsInRing = groups[quadrantName][ringName] || [];
|
|
||||||
|
|
||||||
if (big) {
|
|
||||||
return (
|
|
||||||
<ItemList items={itemsInRing} noLeadingBorder>
|
|
||||||
<Badge type={ringName} big={big} config={config}>
|
|
||||||
{ringName}
|
|
||||||
</Badge>
|
|
||||||
</ItemList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="ring-list">
|
|
||||||
<div className="ring-list__header">
|
|
||||||
<Badge type={ringName} config={config}>
|
|
||||||
{ringName}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
{itemsInRing.map((item) => (
|
|
||||||
<span key={item.name} className="ring-list__item">
|
|
||||||
<Link className="link" pageName={`${item.quadrant}/${item.name}`}>
|
|
||||||
{item.title}
|
|
||||||
<Flag item={item} short />
|
|
||||||
</Link>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderRing = (
|
|
||||||
ringName: string,
|
|
||||||
quadrantName: string,
|
|
||||||
groups: Group,
|
|
||||||
config: ConfigData,
|
|
||||||
big: boolean
|
|
||||||
) => {
|
|
||||||
if (
|
|
||||||
!config.showEmptyRings &&
|
|
||||||
(!groups[quadrantName] ||
|
|
||||||
!groups[quadrantName][ringName] ||
|
|
||||||
groups[quadrantName][ringName].length === 0)
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div key={ringName} className="quadrant-section__ring">
|
|
||||||
{renderList(ringName, quadrantName, groups, config, big)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function QuadrantSection({
|
|
||||||
quadrantName,
|
|
||||||
groups,
|
|
||||||
config,
|
|
||||||
big = false,
|
|
||||||
}: {
|
|
||||||
quadrantName: string;
|
|
||||||
groups: Group;
|
|
||||||
config: ConfigData;
|
|
||||||
big?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="quadrant-section">
|
|
||||||
<div className="quadrant-section__header">
|
|
||||||
<div className="split">
|
|
||||||
<div className="split__left">
|
|
||||||
<h4 className="headline">{translate(config, quadrantName)}</h4>
|
|
||||||
</div>
|
|
||||||
{!big && (
|
|
||||||
<div className="split__right">
|
|
||||||
<Link className="icon-link" pageName={`${quadrantName}`}>
|
|
||||||
<span className="icon icon--pie icon-link__icon" />
|
|
||||||
Zoom In
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="quadrant-section__rings">
|
|
||||||
{config.rings.map((ringName: string) =>
|
|
||||||
renderRing(ringName, quadrantName, groups, config, big)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
@import "../../styles/sccs-vars.scss";
|
|
||||||
|
|
||||||
.quadrant-section {
|
|
||||||
&__header {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__rings {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__ring {
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0 8px;
|
|
||||||
flex: 0 0 25%;
|
|
||||||
margin: 0 0 25px;
|
|
||||||
|
|
||||||
@media (max-width: $until-md) {
|
|
||||||
flex-basis: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { ConfigData } from "../../config";
|
|
||||||
import { Item, QuadrantConfig } from "../../model";
|
|
||||||
import RadarChart from "../Chart/RadarChart";
|
|
||||||
import Link from "../Link/Link";
|
|
||||||
import "./radar-grid.scss";
|
|
||||||
|
|
||||||
const QuadrantLabel: React.FC<{
|
|
||||||
quadrantConfig: QuadrantConfig;
|
|
||||||
quadrantName: string;
|
|
||||||
quadrantLabel: string;
|
|
||||||
}> = ({ quadrantConfig, quadrantName, quadrantLabel }) => {
|
|
||||||
const stylesMap = [
|
|
||||||
{ top: 0, left: 0 },
|
|
||||||
{ top: 0, right: 0 },
|
|
||||||
{ bottom: 0, left: 0 },
|
|
||||||
{ bottom: 0, right: 0 },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="quadrant-label"
|
|
||||||
style={stylesMap[quadrantConfig.position - 1]}
|
|
||||||
>
|
|
||||||
<div className="split">
|
|
||||||
<div className="split__left">
|
|
||||||
<small>Quadrant {quadrantConfig.position}</small>
|
|
||||||
</div>
|
|
||||||
<div className="split__right">
|
|
||||||
<Link className="icon-link" pageName={`${quadrantName}`}>
|
|
||||||
<span className="icon icon--pie icon-link__icon" />
|
|
||||||
Zoom In
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr style={{ borderColor: quadrantConfig.colour }} />
|
|
||||||
<h4 className="headline">{quadrantLabel}</h4>
|
|
||||||
<div className="description">{quadrantConfig.description}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Legend: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<div className="radar-legend">
|
|
||||||
<div className="wrapper">
|
|
||||||
<span className="icon icon--blip_new"></span>
|
|
||||||
New in this version
|
|
||||||
</div>
|
|
||||||
<div className="wrapper">
|
|
||||||
<span className="icon icon--blip_changed"></span>
|
|
||||||
Recently changed
|
|
||||||
</div>
|
|
||||||
<div className="wrapper">
|
|
||||||
<span className="icon icon--blip_default"></span>
|
|
||||||
Unchanged
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const RadarGrid: React.FC<{ items: Item[]; config: ConfigData }> = ({
|
|
||||||
items,
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="radar-grid">
|
|
||||||
<RadarChart items={items} config={config} />
|
|
||||||
{Object.entries(config.quadrantsMap).map(([name, quadrant], index) => (
|
|
||||||
<QuadrantLabel
|
|
||||||
key={index}
|
|
||||||
quadrantConfig={quadrant}
|
|
||||||
quadrantName={name}
|
|
||||||
quadrantLabel={config.quadrants[name]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<Legend />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RadarGrid;
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
.radar-grid {
|
|
||||||
position: relative;
|
|
||||||
margin-bottom: 50px;
|
|
||||||
color: white;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 800px) {
|
|
||||||
.radar-grid {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.radar-grid .quadrant-label {
|
|
||||||
position: absolute;
|
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quadrant-label .split {
|
|
||||||
font-size: 12 px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quadrant-label hr {
|
|
||||||
width: 100%;
|
|
||||||
margin: 10px 0 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quadrant-label .description {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #a6b1bb
|
|
||||||
}
|
|
||||||
|
|
||||||
.quadrant-label .icon-link {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quadrant-label .icon-link .icon {
|
|
||||||
background-size: 18px 18px;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radar-grid .radar-legend {
|
|
||||||
position: absolute;
|
|
||||||
width: 15%;
|
|
||||||
right: 0;
|
|
||||||
top: 45%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radar-legend .wrapper {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radar-legend .icon {
|
|
||||||
|
|
||||||
background-position: center;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { ConfigData, getItemPageNames, isMobileViewport } from "../config";
|
|
||||||
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 = {
|
|
||||||
pageName: string;
|
|
||||||
items: Item[];
|
|
||||||
releases: string[];
|
|
||||||
search: string;
|
|
||||||
config: ConfigData;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum page {
|
|
||||||
index,
|
|
||||||
overview,
|
|
||||||
help,
|
|
||||||
quadrant,
|
|
||||||
itemMobile,
|
|
||||||
item,
|
|
||||||
notFound,
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPageByName = (
|
|
||||||
items: Item[],
|
|
||||||
pageName: string,
|
|
||||||
config: ConfigData
|
|
||||||
): page => {
|
|
||||||
if (pageName === "index") {
|
|
||||||
return page.index;
|
|
||||||
}
|
|
||||||
if (pageName === "overview") {
|
|
||||||
return page.overview;
|
|
||||||
}
|
|
||||||
if (pageName === "help-and-about-tech-radar") {
|
|
||||||
return page.help;
|
|
||||||
}
|
|
||||||
if (Object.keys(config.quadrants).includes(pageName)) {
|
|
||||||
return page.quadrant;
|
|
||||||
}
|
|
||||||
if (getItemPageNames(items).includes(pageName)) {
|
|
||||||
return isMobileViewport() ? page.itemMobile : page.item;
|
|
||||||
}
|
|
||||||
|
|
||||||
return page.notFound;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Router({
|
|
||||||
pageName,
|
|
||||||
items,
|
|
||||||
releases,
|
|
||||||
search,
|
|
||||||
config,
|
|
||||||
}: RouterProps) {
|
|
||||||
const [statePageName, setStatePageName] = useState(pageName);
|
|
||||||
const [leaving, setLeaving] = useState(false);
|
|
||||||
const [nextPageName, setNextPageName] = useState<string>("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const nowLeaving =
|
|
||||||
getPageByName(items, pageName, config) !==
|
|
||||||
getPageByName(items, statePageName, config);
|
|
||||||
if (nowLeaving) {
|
|
||||||
setLeaving(true);
|
|
||||||
setNextPageName(pageName);
|
|
||||||
} else {
|
|
||||||
setStatePageName(pageName);
|
|
||||||
}
|
|
||||||
}, [pageName, items, config, statePageName]);
|
|
||||||
|
|
||||||
const handlePageLeave = () => {
|
|
||||||
setStatePageName(nextPageName);
|
|
||||||
setNextPageName("");
|
|
||||||
|
|
||||||
window.setTimeout(() => {
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
setLeaving(false);
|
|
||||||
});
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (getPageByName(items, statePageName, config)) {
|
|
||||||
case page.index:
|
|
||||||
return (
|
|
||||||
<PageIndex
|
|
||||||
leaving={leaving}
|
|
||||||
items={items}
|
|
||||||
config={config}
|
|
||||||
onLeave={handlePageLeave}
|
|
||||||
releases={releases}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case page.overview:
|
|
||||||
return (
|
|
||||||
<PageOverview
|
|
||||||
items={items}
|
|
||||||
config={config}
|
|
||||||
rings={config.rings}
|
|
||||||
search={search}
|
|
||||||
leaving={leaving}
|
|
||||||
onLeave={handlePageLeave}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case page.help:
|
|
||||||
return <PageHelp leaving={leaving} onLeave={handlePageLeave} />;
|
|
||||||
case page.quadrant:
|
|
||||||
return (
|
|
||||||
<PageQuadrant
|
|
||||||
leaving={leaving}
|
|
||||||
onLeave={handlePageLeave}
|
|
||||||
items={items}
|
|
||||||
config={config}
|
|
||||||
pageName={statePageName}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case page.itemMobile:
|
|
||||||
return (
|
|
||||||
<PageItemMobile
|
|
||||||
items={items}
|
|
||||||
config={config}
|
|
||||||
pageName={statePageName}
|
|
||||||
leaving={leaving}
|
|
||||||
onLeave={handlePageLeave}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case page.item:
|
|
||||||
return (
|
|
||||||
<PageItem
|
|
||||||
items={items}
|
|
||||||
config={config}
|
|
||||||
pageName={statePageName}
|
|
||||||
leaving={leaving}
|
|
||||||
onLeave={handlePageLeave}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return <div />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React, { FormEvent } from "react";
|
|
||||||
|
|
||||||
import { useMessages } from "../../context/MessagesContext";
|
|
||||||
import "./search.scss";
|
|
||||||
|
|
||||||
type SearchProps = {
|
|
||||||
onClose?: () => void;
|
|
||||||
onSubmit?: () => void;
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
open?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default React.forwardRef((props: SearchProps, ref) => {
|
|
||||||
return Search(props, ref);
|
|
||||||
});
|
|
||||||
|
|
||||||
function Search(
|
|
||||||
{ value, onChange, onClose, open = false, onSubmit = () => {} }: SearchProps,
|
|
||||||
ref: any
|
|
||||||
) {
|
|
||||||
const { searchLabel, searchPlaceholder } = useMessages();
|
|
||||||
const closable = onClose !== undefined;
|
|
||||||
|
|
||||||
const handleSubmit = (e: FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
onSubmit();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = (e: React.MouseEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (onClose != null) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
className={classNames("search", { "search--closable": closable })}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
value={value}
|
|
||||||
type="text"
|
|
||||||
onChange={(e) => {
|
|
||||||
onChange(e.target.value);
|
|
||||||
}}
|
|
||||||
className="search__field"
|
|
||||||
placeholder={searchPlaceholder}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
<span className={classNames("search__button", { "is-open": open })}>
|
|
||||||
<button type="submit" className="button">
|
|
||||||
<span className="icon icon--search button__icon" />
|
|
||||||
{searchLabel}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
{closable && (
|
|
||||||
<button
|
|
||||||
className={classNames("search__close link-button", {
|
|
||||||
"is-open": open,
|
|
||||||
})}
|
|
||||||
onClick={handleClose}
|
|
||||||
>
|
|
||||||
<span className="icon icon--close" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
.search {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 600px;
|
|
||||||
height: 50px;
|
|
||||||
position: relative;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
&__field {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 10px 120px 10px 20px;
|
|
||||||
background: #3a444a;
|
|
||||||
display: block;
|
|
||||||
border: none;
|
|
||||||
color: var(--color-white);
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1;
|
|
||||||
font-family: "DIN";
|
|
||||||
font-weight: normal;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--color-gray-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
background: #2f393f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__button {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
margin-top: -18px;
|
|
||||||
right: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--closable {
|
|
||||||
.search {
|
|
||||||
&__field {
|
|
||||||
padding-right: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__button {
|
|
||||||
right: 50px;
|
|
||||||
transform: translateX(20px);
|
|
||||||
transition: transform 450ms cubic-bezier(0.24, 1.12, 0.71, 0.98) 100ms;
|
|
||||||
|
|
||||||
&.is-open {
|
|
||||||
transform: translateX(0);
|
|
||||||
transition-delay: 250ms;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__close {
|
|
||||||
position: absolute;
|
|
||||||
padding: 10px;
|
|
||||||
top: 50%;
|
|
||||||
margin-top: -21px;
|
|
||||||
right: 4px;
|
|
||||||
transform: scale(0.2);
|
|
||||||
transition: transform 400ms cubic-bezier(0.24, 1.12, 0.71, 0.98);
|
|
||||||
|
|
||||||
&.is-open {
|
|
||||||
transform: rotate(180deg) scale(1);
|
|
||||||
transition-delay: 300ms;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
import { setTitle } from "../config";
|
|
||||||
|
|
||||||
type SetTitleProps = {
|
|
||||||
title?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SetTitle({ title }: SetTitleProps) {
|
|
||||||
useEffect(() => {
|
|
||||||
setTitle(document, title);
|
|
||||||
}, [title]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
FaExternalLinkAlt,
|
|
||||||
FaFacebookF,
|
|
||||||
FaGithub,
|
|
||||||
FaInstagram,
|
|
||||||
FaLinkedinIn,
|
|
||||||
FaTwitter,
|
|
||||||
FaXing,
|
|
||||||
FaYoutube,
|
|
||||||
} from "react-icons/fa";
|
|
||||||
|
|
||||||
const icons = {
|
|
||||||
facebook: FaFacebookF,
|
|
||||||
twitter: FaTwitter,
|
|
||||||
linkedIn: FaLinkedinIn,
|
|
||||||
xing: FaXing,
|
|
||||||
instagram: FaInstagram,
|
|
||||||
youtube: FaYoutube,
|
|
||||||
github: FaGithub,
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
href: string;
|
|
||||||
iconName: keyof typeof icons;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SocialLink: React.FC<Props> = ({ href, iconName }) => {
|
|
||||||
const Icon = icons[iconName] || FaExternalLinkAlt;
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="social-icon-a"
|
|
||||||
aria-label={iconName}
|
|
||||||
>
|
|
||||||
<Icon className="social-icon" />
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SocialLink;
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import { ChangeEvent } from "react";
|
|
||||||
import ReactModal from "react-modal";
|
|
||||||
|
|
||||||
import { useSearchParamState } from "../../hooks/use-search-param-state";
|
|
||||||
import { Tag } from "../../model";
|
|
||||||
import "./tags-modal.scss";
|
|
||||||
|
|
||||||
const customStyles = {
|
|
||||||
content: {
|
|
||||||
top: "50%",
|
|
||||||
left: "50%",
|
|
||||||
right: "auto",
|
|
||||||
bottom: "auto",
|
|
||||||
marginRight: "-50%",
|
|
||||||
transform: "translate(-50%, -50%)",
|
|
||||||
backgroundColor: "var(--color-gray-dark)",
|
|
||||||
color: "var(--color-white)",
|
|
||||||
padding: 0,
|
|
||||||
borderRadius: "20px",
|
|
||||||
},
|
|
||||||
overlay: {
|
|
||||||
zIndex: 999,
|
|
||||||
backgroundColor: "rgba(71, 81, 87, 0.6)",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
ReactModal.setAppElement("#root");
|
|
||||||
|
|
||||||
type TagsModalProps = {
|
|
||||||
tags: Tag[];
|
|
||||||
isOpen: boolean;
|
|
||||||
closeModal: () => void;
|
|
||||||
handleTagChange: (tag: Tag) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function TagsModal({
|
|
||||||
tags,
|
|
||||||
isOpen,
|
|
||||||
closeModal,
|
|
||||||
handleTagChange,
|
|
||||||
}: TagsModalProps) {
|
|
||||||
const [searchParamState] = useSearchParamState();
|
|
||||||
let { tags: tagsFromURL } = searchParamState;
|
|
||||||
tagsFromURL = Array.isArray(tagsFromURL) ? tagsFromURL : [tagsFromURL];
|
|
||||||
|
|
||||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const { value } = event.target;
|
|
||||||
|
|
||||||
handleTagChange(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ReactModal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onRequestClose={closeModal}
|
|
||||||
style={customStyles}
|
|
||||||
contentLabel="Filters Modal"
|
|
||||||
closeTimeoutMS={300}
|
|
||||||
>
|
|
||||||
<div className="tags-modal">
|
|
||||||
<button
|
|
||||||
onClick={closeModal}
|
|
||||||
className="tags-modal__close-button link-button"
|
|
||||||
>
|
|
||||||
<span className="icon icon--close" />
|
|
||||||
</button>
|
|
||||||
<h4 className="headline tags-modal__title">Choose your filters</h4>
|
|
||||||
<ul
|
|
||||||
className="tags-modal__list"
|
|
||||||
style={{ columns: Math.ceil(tags.length / 8) }}
|
|
||||||
>
|
|
||||||
{tags.map((tag, index) => (
|
|
||||||
<li key={index} className="tags-modal__list-item">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id={`tag-checkbox-${index}`}
|
|
||||||
className="tags-modal__list-item-checkbox"
|
|
||||||
name={tag}
|
|
||||||
value={tag}
|
|
||||||
checked={tagsFromURL.includes(tag)}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<label htmlFor={`tag-checkbox-${index}`}>{tag}</label>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</ReactModal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
.tags-modal {
|
|
||||||
font-size: 16px;
|
|
||||||
position: relative;
|
|
||||||
padding: 50px;
|
|
||||||
|
|
||||||
&__close-button {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 30px;
|
|
||||||
transform: translateX(20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__list {
|
|
||||||
padding: 15px 0 0;
|
|
||||||
list-style-type: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__list-item {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__list-item-checkbox {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ReactModal__Overlay {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 300ms ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ReactModal__Overlay--after-open{
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ReactModal__Overlay--before-close{
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { HomepageOption, Item, QuadrantConfig } from "./model";
|
|
||||||
|
|
||||||
export interface ConfigData {
|
|
||||||
tags?: string[];
|
|
||||||
quadrants: { [key: string]: string };
|
|
||||||
rings: string[];
|
|
||||||
showEmptyRings: boolean;
|
|
||||||
quadrantsMap: { [quadrant: string]: QuadrantConfig };
|
|
||||||
chartConfig: {
|
|
||||||
size: number;
|
|
||||||
scale: number[];
|
|
||||||
blipSize: number;
|
|
||||||
ringsAttributes: { radius: number; arcWidth: number }[];
|
|
||||||
};
|
|
||||||
homepageContent: HomepageOption;
|
|
||||||
dateFormat?: string;
|
|
||||||
editLink?: {
|
|
||||||
radarLink: string;
|
|
||||||
title?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const radarName =
|
|
||||||
process.env.REACT_APP_RADAR_NAME || "AOE Technology Radar";
|
|
||||||
export const radarNameShort = radarName;
|
|
||||||
export const titleFormat =
|
|
||||||
process.env.REACT_APP_RADAR_TITLE_FORMAT || "%TECHNOLOGY_NAME% | %APP_TITLE%";
|
|
||||||
|
|
||||||
export function setTitle(document: Document, title?: string) {
|
|
||||||
document.title = title
|
|
||||||
? titleFormat
|
|
||||||
.replace("%TECHNOLOGY_NAME%", title)
|
|
||||||
.replace("%APP_TITLE%", radarName)
|
|
||||||
: radarName;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getItemPageNames = (items: Item[]) =>
|
|
||||||
items.map((item) => `${item.quadrant}/${item.name}`);
|
|
||||||
|
|
||||||
export function isMobileViewport() {
|
|
||||||
// return false for server side rendering
|
|
||||||
if (typeof window == "undefined") return false;
|
|
||||||
|
|
||||||
const width =
|
|
||||||
window.innerWidth ||
|
|
||||||
document.documentElement.clientWidth ||
|
|
||||||
document.body.clientWidth;
|
|
||||||
return width < 1200;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const publicUrl =
|
|
||||||
(process.env.PUBLIC_URL || "").replace(/\/$/, "") + "/";
|
|
||||||
|
|
||||||
export function assetUrl(file: string) {
|
|
||||||
return publicUrl + file;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function translate(config: ConfigData, key: string) {
|
|
||||||
return config.quadrants[key] || "-";
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import { FC, createContext, useContext } from "react";
|
|
||||||
|
|
||||||
import { Props as SocialLink } from "../../components/SocialLink/SocialLink";
|
|
||||||
|
|
||||||
interface Quadrant {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Paragraph {
|
|
||||||
headline: string;
|
|
||||||
values: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PageHelp {
|
|
||||||
paragraphs: Paragraph[];
|
|
||||||
quadrantsPreDescription?: string;
|
|
||||||
quadrants: Quadrant[];
|
|
||||||
rings: { name: string; description: string }[];
|
|
||||||
ringsPreDescription?: string;
|
|
||||||
sourcecodeLink?: {
|
|
||||||
href: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
};
|
|
||||||
headlinePrefix?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PageOverview {
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PageItem {
|
|
||||||
quadrantOverview: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PageIndex {
|
|
||||||
publishedLabel: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Messages {
|
|
||||||
footerFootnote?: string;
|
|
||||||
socialLinksLabel?: string;
|
|
||||||
socialLinks?: SocialLink[];
|
|
||||||
legalInformationLabel?: string;
|
|
||||||
legalInformationLink?: string;
|
|
||||||
pageHelp?: PageHelp;
|
|
||||||
pageOverview?: PageOverview;
|
|
||||||
pageItem?: PageItem;
|
|
||||||
pageIndex?: PageIndex;
|
|
||||||
searchLabel?: string;
|
|
||||||
searchPlaceholder?: string;
|
|
||||||
revisionsText?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MessagesContext = createContext<Messages | undefined>(undefined);
|
|
||||||
|
|
||||||
export const MessagesProvider: FC<
|
|
||||||
React.PropsWithChildren<{ messages?: Messages }>
|
|
||||||
> = ({ messages, children }) => (
|
|
||||||
<MessagesContext.Provider value={messages}>
|
|
||||||
{children}
|
|
||||||
</MessagesContext.Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const useMessages = () => useContext(MessagesContext) || {};
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import moment from "moment";
|
|
||||||
|
|
||||||
import { formatRelease } from "./date";
|
|
||||||
|
|
||||||
describe("formatRelease", () => {
|
|
||||||
it("should format a date object using default output format", () => {
|
|
||||||
expect(formatRelease(moment("2022-01-05"))).toEqual("January 2022");
|
|
||||||
});
|
|
||||||
it("should format a date object using a custom output format", () => {
|
|
||||||
expect(formatRelease(moment("2022-01-05"), "DD.MM.YYYY")).toEqual(
|
|
||||||
"05.01.2022"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user