From b1e63528dca8ed1acd6a141d8b8cb01eeb6b1051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Marek?= Date: Sun, 2 May 2021 00:48:43 +1200 Subject: [PATCH] Add types --- src/components/Badge/Badge.tsx | 6 +- src/components/Chart/Axes.tsx | 8 +- src/components/Chart/BlipPoints.tsx | 94 ++++++++++--------- src/components/Chart/BlipShapes.tsx | 34 +++++-- src/components/Chart/QuadrantRings.tsx | 12 ++- src/components/Chart/RadarChart.tsx | 25 +++-- src/components/PageIndex/PageIndex.tsx | 4 +- src/components/PageItem/PageItem.tsx | 2 +- .../PageItemMobile/PageItemMobile.tsx | 4 +- src/components/PageOverview/PageOverview.tsx | 25 +++-- src/components/PageQuadrant/PageQuadrant.tsx | 4 +- src/components/QuadrantGrid/QuadrantGrid.tsx | 2 +- .../QuadrantSection/QuadrantSection.tsx | 2 +- src/components/RadarGrid/RadarGrid.tsx | 22 +++-- src/components/Router.tsx | 4 +- src/config.ts | 22 ++--- src/model.ts | 22 +++++ tasks/create-static.ts | 10 +- 18 files changed, 189 insertions(+), 113 deletions(-) diff --git a/src/components/Badge/Badge.tsx b/src/components/Badge/Badge.tsx index 1570591..5d9a68e 100644 --- a/src/components/Badge/Badge.tsx +++ b/src/components/Badge/Badge.tsx @@ -1,11 +1,11 @@ import React, { MouseEventHandler } from 'react'; import classNames from 'classnames'; import './badge.scss'; -import {Ring} from "../../config"; +import {Ring} from "../../model"; type BadgeProps = { onClick?: MouseEventHandler; big?: boolean; - type: 'big' | 'all' | 'empty' | Ring; + type: Ring | null; }; export default function Badge({ onClick, big, type, children }: React.PropsWithChildren) { @@ -13,7 +13,7 @@ export default function Badge({ onClick, big, type, children }: React.PropsWithC return ( { +export const YAxis: React.FC<{ + scale: d3.ScaleLinear +}> = ({ scale }) => { const el = ReactFauxDOM.createElement('g'); const axisGenerator = d3.axisLeft(scale).ticks(6); @@ -15,7 +17,9 @@ export const YAxis = ({ scale }) => { return el.toReact(); }; -export const XAxis = ({ scale }) => { +export const XAxis: React.FC<{ + scale: d3.ScaleLinear +}> = ({ scale }) => { const el = ReactFauxDOM.createElement('g'); const axisGenerator = d3.axisBottom(scale).ticks(6); diff --git a/src/components/Chart/BlipPoints.tsx b/src/components/Chart/BlipPoints.tsx index c198f93..46e2089 100644 --- a/src/components/Chart/BlipPoints.tsx +++ b/src/components/Chart/BlipPoints.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import {FlagType, Ring} from '../../model'; +import { ScaleLinear } from 'd3'; +import { FlagType, Item, Blip, Point, Ring } from '../../model'; import { quadrantsMap, chartConfig } from '../../config'; import Link from '../Link/Link'; import { NewBlip, ChangedBlip, DefaultBlip } from './BlipShapes'; @@ -9,10 +10,10 @@ 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 */ -const generateCoordinates = (enrichedBlip, xScale, yScale) => { +function generateCoordinates(blip: Blip, xScale: ScaleLinear, yScale: ScaleLinear): Point { const pi = Math.PI, - ringRadius = chartConfig.ringsAttributes[enrichedBlip.ringPosition - 1].radius, - previousRingRadius = enrichedBlip.ringPosition == 1 ? 0 : chartConfig.ringsAttributes[enrichedBlip.ringPosition - 2].radius, + ringRadius = chartConfig.ringsAttributes[blip.ringPosition - 1].radius, + previousRingRadius = blip.ringPosition == 1 ? 0 : chartConfig.ringsAttributes[blip.ringPosition - 2].radius, ringPadding = 0.7; // radian between 0 and 90 degrees @@ -23,7 +24,7 @@ const generateCoordinates = (enrichedBlip, xScale, yScale) => { 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, 3, 2][enrichedBlip.quadrantPosition - 1] / 2; + const shift = pi * [1, 4, 3, 2][blip.quadrantPosition - 1] / 2; return { x: xScale(Math.cos(randomDegree + shift) * radius), @@ -31,34 +32,62 @@ const generateCoordinates = (enrichedBlip, xScale, yScale) => { }; }; -const randomBetween = (min, max) => { +function randomBetween (min: number, max: number): number { return Math.random() * (max - min) + min; }; -const distanceBetween = (point1, point2) => { +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)); }; -export default function BlipPoints({blips, xScale, yScale}) { +function renderBlip(blip: Blip, index: number): 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 ; + case FlagType.changed: + return ; + default: + return ; + } +}; - const enrichedBlips = blips.reduce((list, blip) => { - if (!blip.ring || !blip.quadrant) { +const BlipPoints: React.FC<{ + items: Item[] + xScale:ScaleLinear + yScale:ScaleLinear +}> = ({items, xScale, yScale}) => { + + 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; } - let enrichedBlip = { ...blip, - quadrantPosition: quadrantsMap[blip.quadrant].position, - ringPosition: Ring[blip.ring], - colour: quadrantsMap[blip.quadrant].colour, - txtColour: quadrantsMap[blip.quadrant].txtColour + const quadrantConfig = quadrantsMap.get(item.quadrant); + + let blip: Blip = { ...item, + quadrantPosition: quadrantConfig.position, + // TODO get to the bottom of this + // @ts-ignore + ringPosition: Ring[item.ring], + colour: quadrantConfig.colour, + txtColour: quadrantConfig.txtColour }; let point; let counter = 1; do { - point = generateCoordinates(enrichedBlip, xScale, yScale); + point = generateCoordinates(blip, xScale, yScale); 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) @@ -68,43 +97,24 @@ export default function BlipPoints({blips, xScale, yScale}) { } while (counter < 100 && (Math.abs(point.x - xScale(0)) < 15 || Math.abs(point.y - yScale(0)) < 15 - || list.some(item => distanceBetween(point, item) < chartConfig.blipSize + chartConfig.blipSize / 2) + || list.some(b => distanceBetween(point, b.coordinates) < chartConfig.blipSize + chartConfig.blipSize / 2) )); - enrichedBlip.x = point.x; - enrichedBlip.y = point.y; + blip.coordinates = point; - list.push(enrichedBlip); + list.push(blip); return list; }, []); - const renderBlip = (blip, index) => { - 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 ; - case FlagType.changed: - return ; - default: - return ; - } - } - return ( - {enrichedBlips.map((blip, index) => ( + {blips.map((blip, index) => ( {renderBlip(blip, index)} ))} ); -}; \ No newline at end of file +}; + +export default BlipPoints; \ No newline at end of file diff --git a/src/components/Chart/BlipShapes.tsx b/src/components/Chart/BlipShapes.tsx index ba19348..b977ead 100644 --- a/src/components/Chart/BlipShapes.tsx +++ b/src/components/Chart/BlipShapes.tsx @@ -1,9 +1,21 @@ import React from 'react'; +import { Blip } from '../../model'; import { chartConfig } from '../../config'; -export const ChangedBlip = ({blip, ...props}) => { - const centeredX = blip.x - chartConfig.blipSize/2, - centeredY = blip.y - chartConfig.blipSize/2; +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} & VisualBlipProps +> = ({blip, ...props}) => { + const centeredX = blip.coordinates.x - chartConfig.blipSize/2, + centeredY = blip.coordinates.y - chartConfig.blipSize/2; return ( { ); }; -export const NewBlip = ({blip, ...props}) => { - const centeredX = blip.x - chartConfig.blipSize/2, - centeredY = blip.y - chartConfig.blipSize/2; +export const NewBlip: React.FC< + {blip: Blip} & VisualBlipProps +> = ({blip, ...props}) => { + const centeredX = blip.coordinates.x - chartConfig.blipSize/2, + centeredY = blip.coordinates.y - chartConfig.blipSize/2; /* The below is a predefined path of a triangle with rounded corners. @@ -36,12 +50,14 @@ export const NewBlip = ({blip, ...props}) => { ); }; -export const DefaultBlip = ({blip, ...props}) => { +export const DefaultBlip: React.FC< + {blip: Blip} & VisualBlipProps +> = ({blip, ...props}) => { return ( ); diff --git a/src/components/Chart/QuadrantRings.tsx b/src/components/Chart/QuadrantRings.tsx index 7ad0c8a..5f28274 100644 --- a/src/components/Chart/QuadrantRings.tsx +++ b/src/components/Chart/QuadrantRings.tsx @@ -1,8 +1,9 @@ import React from 'react'; import * as d3 from 'd3'; import { chartConfig } from '../../config'; +import { QuadrantConfig } from '../../model'; -const arcPath = (quadrantPosition, ringPosition, xScale) => { +function arcPath(quadrantPosition: number, ringPosition: number, xScale: d3.ScaleLinear) { const startAngle = quadrantPosition == 1 ? 3 * Math.PI / 2 : (quadrantPosition - 2) * Math.PI / 2 @@ -21,7 +22,10 @@ const arcPath = (quadrantPosition, ringPosition, xScale) => { }); } -export default function QuadrantRings ({ quadrant, xScale}) { +const QuadrantRings: React.FC<{ + quadrant: QuadrantConfig + xScale: d3.ScaleLinear +}> = ({ quadrant, xScale}) => { // order from top-right clockwise const gradientAttributes = [ {x: 0, y: 0, cx: 1, cy: 1, r: 1}, @@ -64,4 +68,6 @@ export default function QuadrantRings ({ quadrant, xScale}) { ); - } \ No newline at end of file + } + + export default QuadrantRings; \ No newline at end of file diff --git a/src/components/Chart/RadarChart.tsx b/src/components/Chart/RadarChart.tsx index cbaf67c..18685de 100644 --- a/src/components/Chart/RadarChart.tsx +++ b/src/components/Chart/RadarChart.tsx @@ -1,7 +1,7 @@ import React from 'react'; import * as d3 from "d3"; import ReactTooltip from 'react-tooltip'; -import { Ring } from '../../model'; +import { Item, Ring } from '../../model'; import { chartConfig, quadrantsMap } from '../../config'; import { YAxis, XAxis } from './Axes'; import QuadrantRings from './QuadrantRings'; @@ -9,7 +9,11 @@ import BlipPoints from './BlipPoints'; import './chart.scss'; -const RingLabel = ({ring, xScale, yScale}) => { +const RingLabel: React.FC<{ + ring: Ring + xScale: d3.ScaleLinear + yScale: d3.ScaleLinear +}> = ({ring, xScale, yScale}) => { const ringRadius = chartConfig.ringsAttributes[ring - 1].radius, previousRingRadius = ring == 1 ? 0 : chartConfig.ringsAttributes[ring - 2].radius, @@ -30,7 +34,10 @@ const RingLabel = ({ring, xScale, yScale}) => { ); }; -export default function RadarChart({ blips }) { +const RadarChart: React.FC<{ + items: Item[] +}> = ({ items }) => { + const xScale = d3.scaleLinear() .domain(chartConfig.scale) .range([0, chartConfig.size]); @@ -48,17 +55,19 @@ export default function RadarChart({ blips }) { - {Object.keys(quadrantsMap).map((id, index) => ( - + {[...quadrantsMap.values()].map((value, index) => ( + ))} - {[1, 2, 3, 4].map((ring, index) => ( + {[Ring.adopt, Ring.trial, Ring.assess, Ring.hold].map((ring, index) => ( ))} - + ); -} \ No newline at end of file +} + +export default RadarChart; \ No newline at end of file diff --git a/src/components/PageIndex/PageIndex.tsx b/src/components/PageIndex/PageIndex.tsx index 03eab7f..066c0ae 100644 --- a/src/components/PageIndex/PageIndex.tsx +++ b/src/components/PageIndex/PageIndex.tsx @@ -19,7 +19,9 @@ type PageIndexProps = { export default function PageIndex({ leaving, onLeave, items, releases }: PageIndexProps) { const newestRelease = releases.slice(-1)[0]; const numberOfReleases = releases.length; + // @ts-ignore const showChart = homepageContent === HomepageOption.chart || homepageContent === HomepageOption.both; + // @ts-ignore const showColumns = homepageContent === HomepageOption.columns || homepageContent === HomepageOption.both; return ( @@ -28,7 +30,7 @@ export default function PageIndex({ leaving, onLeave, items, releases }: PageInd {radarName} {showChart && ( - + )} {showColumns && ( diff --git a/src/components/PageItem/PageItem.tsx b/src/components/PageItem/PageItem.tsx index f8a51d6..831c623 100644 --- a/src/components/PageItem/PageItem.tsx +++ b/src/components/PageItem/PageItem.tsx @@ -191,7 +191,7 @@ export default function PageItem({pageName, items, leaving, onLeave}: PageItemPr
-

{quadrantsMap[item.quadrant].displayName}

+

{quadrantsMap.get(item.quadrant).displayName}

-

{quadrantsMap[item.quadrant].displayName}

+

{quadrantsMap.get(item.quadrant).displayName}

{item.title}

@@ -59,7 +59,7 @@ export default function PageItemMobile({ pageName, items, leaving, onLeave }: Pa
-

{quadrantsMap[item.quadrant].displayName}

+

{quadrantsMap.get(item.quadrant).displayName}

diff --git a/src/components/PageOverview/PageOverview.tsx b/src/components/PageOverview/PageOverview.tsx index 6fc5784..31c740e 100644 --- a/src/components/PageOverview/PageOverview.tsx +++ b/src/components/PageOverview/PageOverview.tsx @@ -16,7 +16,7 @@ const containsSearchTerm = (text = '', term = '') => { }; type PageOverviewProps = { - rings: readonly ('all' | Ring)[]; + rings: readonly Ring[]; search: string; items: Item[]; leaving: boolean; @@ -24,13 +24,10 @@ type PageOverviewProps = { }; export default function PageOverview({ rings, search: searchProp, items, leaving, onLeave }: PageOverviewProps) { - const [ring, setRing] = useState('all'); + const [selectedRing, setRing] = useState(Ring.all); const [search, setSearch] = useState(searchProp); useEffect(() => { - if (rings.length > 0) { - setRing(rings[0]); - } setSearch(searchProp); }, [rings, searchProp]); @@ -38,9 +35,10 @@ export default function PageOverview({ rings, search: searchProp, items, leaving setRing(ring); }; - const isRingActive = (ringName: string) => ring === ringName; + const isRingActive = (ring: Ring) => selectedRing === ring; - const itemMatchesRing = (item: Item) => ring === 'all' || item.ring === ring; + // TODO get to the bottom of this + const itemMatchesRing = (item: Item) => selectedRing === Ring.all || Ring[item.ring] === selectedRing; const itemMatchesSearch = (item: Item) => { return search.trim() === '' || containsSearchTerm(item.title, search) || containsSearchTerm(item.body, search) || containsSearchTerm(item.info, search); @@ -75,10 +73,10 @@ export default function PageOverview({ rings, search: searchProp, items, leaving
- {Object.keys(rings).map((key) => ( -
- - {Ring[key]} + {rings.map((ring) => ( +
+ + {Ring[ring]}
))} @@ -109,9 +107,10 @@ export default function PageOverview({ rings, search: searchProp, items, leaving
-
{quadrantsMap[item.quadrant].displayName}
+
{quadrantsMap.get(item.quadrant).displayName}
- {item.ring} + {/* TODO get to the bottom of this */} + {item.ring}
diff --git a/src/components/PageQuadrant/PageQuadrant.tsx b/src/components/PageQuadrant/PageQuadrant.tsx index dcdbfed..52d43f8 100644 --- a/src/components/PageQuadrant/PageQuadrant.tsx +++ b/src/components/PageQuadrant/PageQuadrant.tsx @@ -19,9 +19,9 @@ export default function PageQuadrant({ leaving, onLeave, pageName, items }: Page const groups = groupByQuadrants(featuredOnly(items)); return ( - + - {quadrantsMap[pageName].displayName} + {quadrantsMap.get(pageName).displayName} diff --git a/src/components/QuadrantGrid/QuadrantGrid.tsx b/src/components/QuadrantGrid/QuadrantGrid.tsx index 216e802..5d301bd 100644 --- a/src/components/QuadrantGrid/QuadrantGrid.tsx +++ b/src/components/QuadrantGrid/QuadrantGrid.tsx @@ -13,5 +13,5 @@ const renderQuadrant = (quadrantName: string, groups: Group) => { export default function QuadrantGrid({ items }: { items: Item[] }) { const groups = groupByQuadrants(items); - return
{Object.keys(quadrantsMap).map((quadrantName) => renderQuadrant(quadrantName, groups))}
; + return
{[...quadrantsMap.keys()].map((quadrantName) => renderQuadrant(quadrantName, groups))}
; } diff --git a/src/components/QuadrantSection/QuadrantSection.tsx b/src/components/QuadrantSection/QuadrantSection.tsx index f8b35c7..7c73a37 100644 --- a/src/components/QuadrantSection/QuadrantSection.tsx +++ b/src/components/QuadrantSection/QuadrantSection.tsx @@ -55,7 +55,7 @@ export default function QuadrantSection({ quadrantName, groups, big = false, sho
{showTitle && (
-

{quadrantsMap[quadrantName].displayName}

+

{quadrantsMap.get(quadrantName).displayName}

)} {!big && ( diff --git a/src/components/RadarGrid/RadarGrid.tsx b/src/components/RadarGrid/RadarGrid.tsx index 161ae7a..e2a597d 100644 --- a/src/components/RadarGrid/RadarGrid.tsx +++ b/src/components/RadarGrid/RadarGrid.tsx @@ -2,10 +2,14 @@ import React from 'react'; import RadarChart from '../Chart/RadarChart'; import IconLink from '../IconLink/IconLink'; import { quadrantsMap } from '../../config'; +import { Item, QuadrantConfig } from '../../model'; import './radar-grid.scss'; -const QuadrantLabel = ({quadrant}) => { +const QuadrantLabel: React.FC<{ + quadrant: QuadrantConfig +}> = ({quadrant}) => { + const stylesMap = [ {top: 0, left: 0}, {top: 0, right: 0}, @@ -30,7 +34,7 @@ const QuadrantLabel = ({quadrant}) => { ); }; -const Legend = () => { +const Legend: React.FC = () => { return (
@@ -49,15 +53,19 @@ const Legend = () => { ); } -export default function RadarGrid({ blips }) { +const RadarGrid: React.FC< + {items: Item[]} +> = ({ items }) => { return (
- - {Object.keys(quadrantsMap).map((id, index) => ( - + + {[...quadrantsMap.values()].map((value, index) => ( + ))}
); -} \ No newline at end of file +} + +export default RadarGrid; \ No newline at end of file diff --git a/src/components/Router.tsx b/src/components/Router.tsx index ce0e93b..79b0486 100644 --- a/src/components/Router.tsx +++ b/src/components/Router.tsx @@ -35,7 +35,7 @@ const getPageByName = (items: Item[], pageName: string): page => { if (pageName === 'help-and-about-tech-radar') { return page.help; } - if (Object.keys(quadrantsMap).includes(pageName)) { + if (quadrantsMap.has(pageName)) { return page.quadrant; } if (getItemPageNames(items).includes(pageName)) { @@ -75,7 +75,7 @@ export default function Router({pageName, items, releases, search}: RouterProps) case page.index: return ; case page.overview: - return ; case page.help: return ; diff --git a/src/config.ts b/src/config.ts index 8327dae..d2c6dd0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,4 @@ -import {Item, HomepageOption} from './model'; +import {Item, HomepageOption, QuadrantConfig} from './model'; export const radarName = process.env.RADAR_NAME || 'AOE Technology Radar' export const radarNameShort = radarName; @@ -6,40 +6,40 @@ export const radarNameShort = radarName; export const homepageContent = HomepageOption.both; // by defaul show both versions so that people can choose which one they like more (or keep both) // Quadrants positions start from the top left and go clockwise -export const quadrantsMap = { - 'languages-and-frameworks': { +export const quadrantsMap: Map = new Map([ + ['languages-and-frameworks', { id: 'languages-and-frameworks', displayName: 'Languages & 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': { + }], + ['methods-and-patterns', { id: 'methods-and-patterns', displayName: 'Methods & 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': { + }], + ['platforms-and-aoe-services', { id: 'platforms-and-aoe-services', displayName: 'Platforms and Operations', colour: '#F25244', txtColour: '#444444', position: 3, description: '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.' - }, - 'tools': { + }], + ['tools', { id: 'tools', displayName: 'Tools', colour: '#F2A25C', txtColour: 'white', position: 4, description: 'Here we put different software tools - from small helpers to bigger software projects' - } -}; + }] +]); export const chartConfig = { size: 800, //in px diff --git a/src/model.ts b/src/model.ts index e639e83..96b3e94 100644 --- a/src/model.ts +++ b/src/model.ts @@ -34,6 +34,14 @@ export type Item = ItemAttributes & { revisions: Revision[] } +export type Blip = Item & { + quadrantPosition: number + ringPosition: number + colour: string + txtColour: string + coordinates?: Point +} + export type Revision = ItemAttributes & { body: string fileName: string @@ -44,6 +52,15 @@ export type Quadrant = { [name: string]: Item[] } +export type QuadrantConfig = { + id: string, + displayName: string, + colour: string, + txtColour: string, + position: number, + description: string +} + export type Radar = { items: Item[] releases: string[] @@ -53,6 +70,11 @@ export type Group = { [quadrant: string]: Quadrant } +export type Point = { + x: number, + y: number +} + export const featuredOnly = (items: Item[]) => items.filter(item => item.featured); export const groupByQuadrants = (items: Item[]): Group => diff --git a/tasks/create-static.ts b/tasks/create-static.ts index ce93c70..0c926c3 100644 --- a/tasks/create-static.ts +++ b/tasks/create-static.ts @@ -10,12 +10,12 @@ import {quadrantsMap} from "../src/config"; 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') + copyFileSync('build/index.html', 'build/overview.html'); + copyFileSync('build/index.html', 'build/help-and-about-tech-radar.html'); - Object.keys(quadrantsMap).forEach(quadrant => { - copyFileSync('build/index.html', 'build/' + quadrant + '.html') - mkdirSync('build/' + quadrant) + [...quadrantsMap.keys()].forEach(quadrant => { + copyFileSync('build/index.html', 'build/' + quadrant + '.html'); + mkdirSync('build/' + quadrant); }) radar.items.forEach(item => { copyFileSync('build/index.html', 'build/' + item.quadrant + '/' + item.name + '.html')