Add types

This commit is contained in:
Jarosław Marek
2021-05-02 00:48:43 +12:00
parent e8eff6bdab
commit b1e63528dc
18 changed files with 189 additions and 113 deletions

View File

@@ -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<BadgeProps>) {
@@ -13,7 +13,7 @@ export default function Badge({ onClick, big, type, children }: React.PropsWithC
return (
<Comp
className={classNames('badge', `badge--${type}`, {
className={classNames('badge', `badge--${type != null ? Ring[type] : ''}`, {
'badge--big': big === true,
})}
onClick={onClick}

View File

@@ -2,7 +2,9 @@
import ReactFauxDOM from 'react-faux-dom';
import * as d3 from "d3";
export const YAxis = ({ scale }) => {
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);

View File

@@ -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,54 +32,17 @@ 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}) {
const enrichedBlips = blips.reduce((list, blip) => {
if (!blip.ring || !blip.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
};
let point;
let counter = 1;
do {
point = generateCoordinates(enrichedBlip, 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)
and quadrant borders (so that they don't overlap quadrants)
This feels pretty inefficient, but good enough for now.
*/
} 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)
));
enrichedBlip.x = point.x;
enrichedBlip.y = point.y;
list.push(enrichedBlip);
return list;
}, []);
const renderBlip = (blip, index) => {
function renderBlip(blip: Blip, index: number): JSX.Element {
const props = {
blip,
className: 'blip',
@@ -96,11 +60,55 @@ export default function BlipPoints({blips, xScale, yScale}) {
default:
return <DefaultBlip {...props} />;
}
};
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;
}
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(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)
and quadrant borders (so that they don't overlap quadrants)
This feels pretty inefficient, but good enough for now.
*/
} while (counter < 100
&& (Math.abs(point.x - xScale(0)) < 15
|| Math.abs(point.y - yScale(0)) < 15
|| list.some(b => distanceBetween(point, b.coordinates) < chartConfig.blipSize + chartConfig.blipSize / 2)
));
blip.coordinates = point;
list.push(blip);
return list;
}, []);
return (
<g className="blips">
{enrichedBlips.map((blip, index) => (
{blips.map((blip, index) => (
<Link pageName={`${blip.quadrant}/${blip.name}`} key={index}>
{renderBlip(blip, index)}
</Link>
@@ -108,3 +116,5 @@ export default function BlipPoints({blips, xScale, yScale}) {
</g>
);
};
export default BlipPoints;

View File

@@ -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 (
<rect
@@ -18,9 +30,11 @@ export const ChangedBlip = ({blip, ...props}) => {
);
};
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 (
<circle
r={chartConfig.blipSize / 2}
cx={blip.x}
cy={blip.y}
cx={blip.coordinates.x}
cy={blip.coordinates.y}
{...props}
/>
);

View File

@@ -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},
@@ -65,3 +69,5 @@ export default function QuadrantRings ({ quadrant, xScale}) {
</g>
);
}
export default QuadrantRings;

View File

@@ -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 }) {
<XAxis scale={xScale}/>
</g>
{Object.keys(quadrantsMap).map((id, index) => (
<QuadrantRings key={index} quadrant={quadrantsMap[id]} xScale={xScale} />
{[...quadrantsMap.values()].map((value, index) => (
<QuadrantRings key={index} quadrant={value} xScale={xScale} />
))}
{[1, 2, 3, 4].map((ring, index) => (
{[Ring.adopt, Ring.trial, Ring.assess, Ring.hold].map((ring, index) => (
<RingLabel key={index} ring={ring} xScale={xScale} yScale={yScale} />
))}
<BlipPoints blips={blips} xScale={xScale} yScale={yScale}/>
<BlipPoints items={items} xScale={xScale} yScale={yScale}/>
</svg>
<ReactTooltip className="tooltip" offset={{top: -5}}/>
</div>
);
}
export default RadarChart;

View File

@@ -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 (
<Fadeable leaving={leaving} onLeave={onLeave}>
@@ -28,7 +30,7 @@ export default function PageIndex({ leaving, onLeave, items, releases }: PageInd
<HeroHeadline alt={`Version #${numberOfReleases}`}>{radarName}</HeroHeadline>
</div>
{showChart && (
<RadarGrid blips={featuredOnly(items)} />
<RadarGrid items={featuredOnly(items)} />
)}
{showColumns && (
<QuadrantGrid items={featuredOnly(items)} />

View File

@@ -191,7 +191,7 @@ export default function PageItem({pageName, items, leaving, onLeave}: PageItemPr
<div className='item-page__nav'>
<div className='item-page__nav__inner'>
<div className='item-page__header' style={getAnimationState('navHeader')}>
<h3 className='headline'>{quadrantsMap[item.quadrant].displayName}</h3>
<h3 className='headline'>{quadrantsMap.get(item.quadrant).displayName}</h3>
</div>
<ItemList items={itemsInRing} activeItem={item} headerStyle={getAnimationState('navHeader')}

View File

@@ -40,7 +40,7 @@ export default function PageItemMobile({ pageName, items, leaving, onLeave }: Pa
<div className='mobile-item-page__header'>
<div className='split'>
<div className='split__left'>
<h3 className='headline'>{quadrantsMap[item.quadrant].displayName}</h3>
<h3 className='headline'>{quadrantsMap.get(item.quadrant).displayName}</h3>
<h1 className='hero-headline hero-headline--inverse'>{item.title}</h1>
</div>
<div className='split__right'>
@@ -59,7 +59,7 @@ export default function PageItemMobile({ pageName, items, leaving, onLeave }: Pa
<ItemList items={itemsInRing} activeItem={item}>
<div className='split'>
<div className='split__left'>
<h3 className='headline'>{quadrantsMap[item.quadrant].displayName}</h3>
<h3 className='headline'>{quadrantsMap.get(item.quadrant).displayName}</h3>
</div>
<div className='split__right'>
<IconLink pageName={item.quadrant} icon="pie" text="Zoom in" />

View File

@@ -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<Ring | 'all'>('all');
const [selectedRing, setRing] = useState<Ring>(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
</div>
<div className='split__right'>
<div className='nav'>
{Object.keys(rings).map((key) => (
<div className='nav__item' key={Ring[key]}>
<Badge big onClick={handleRingClick(Ring[key])} type={isRingActive(Ring[key]) ? Ring[key] : 'empty'}>
{Ring[key]}
{rings.map((ring) => (
<div className='nav__item' key={ring}>
<Badge big onClick={handleRingClick(ring)} type={isRingActive(ring) ? ring : null}>
{Ring[ring]}
</Badge>
</div>
))}
@@ -109,9 +107,10 @@ export default function PageOverview({ rings, search: searchProp, items, leaving
</div>
<div className='split__right'>
<div className='nav nav--relations'>
<div className='nav__item'>{quadrantsMap[item.quadrant].displayName}</div>
<div className='nav__item'>{quadrantsMap.get(item.quadrant).displayName}</div>
<div className='nav__item'>
<Badge type={item.ring}>{item.ring}</Badge>
{/* TODO get to the bottom of this */}
<Badge type={Ring[item.ring]}>{item.ring}</Badge>
</div>
</div>
</div>

View File

@@ -19,9 +19,9 @@ export default function PageQuadrant({ leaving, onLeave, pageName, items }: Page
const groups = groupByQuadrants(featuredOnly(items));
return (
<Fadeable leaving={leaving} onLeave={onLeave}>
<SetTitle title={quadrantsMap[pageName].displayName} />
<SetTitle title={quadrantsMap.get(pageName).displayName} />
<HeadlineGroup>
<HeroHeadline>{quadrantsMap[pageName].displayName}</HeroHeadline>
<HeroHeadline>{quadrantsMap.get(pageName).displayName}</HeroHeadline>
</HeadlineGroup>
<QuadrantSection groups={groups} quadrantName={pageName} big showTitle={false} />
</Fadeable>

View File

@@ -13,5 +13,5 @@ const renderQuadrant = (quadrantName: string, groups: Group) => {
export default function QuadrantGrid({ items }: { items: Item[] }) {
const groups = groupByQuadrants(items);
return <div className='quadrant-grid'>{Object.keys(quadrantsMap).map((quadrantName) => renderQuadrant(quadrantName, groups))}</div>;
return <div className='quadrant-grid'>{[...quadrantsMap.keys()].map((quadrantName) => renderQuadrant(quadrantName, groups))}</div>;
}

View File

@@ -55,7 +55,7 @@ export default function QuadrantSection({ quadrantName, groups, big = false, sho
<div className='split'>
{showTitle && (
<div className="split__left">
<h4 className="headline">{quadrantsMap[quadrantName].displayName}</h4>
<h4 className="headline">{quadrantsMap.get(quadrantName).displayName}</h4>
</div>
)}
{!big && (

View File

@@ -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 (
<div className="radar-legend">
<div className="wrapper">
@@ -49,15 +53,19 @@ const Legend = () => {
);
}
export default function RadarGrid({ blips }) {
const RadarGrid: React.FC<
{items: Item[]}
> = ({ items }) => {
return (
<div className="radar-grid">
<RadarChart blips={blips} />
{Object.keys(quadrantsMap).map((id, index) => (
<QuadrantLabel key={index} quadrant={quadrantsMap[id]} />
<RadarChart items={items} />
{[...quadrantsMap.values()].map((value, index) => (
<QuadrantLabel key={index} quadrant={value} />
))}
<Legend />
</div>
);
}
export default RadarGrid;

View File

@@ -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 <PageIndex leaving={leaving} items={items} onLeave={handlePageLeave} releases={releases}/>;
case page.overview:
return <PageOverview items={items} rings={Object.values(Ring)} search={search} leaving={leaving}
return <PageOverview items={items} rings={[Ring.all, Ring.adopt, Ring.assess, Ring.hold, Ring.trial]} search={search} leaving={leaving}
onLeave={handlePageLeave}/>;
case page.help:
return <PageHelp leaving={leaving} onLeave={handlePageLeave}/>;

View File

@@ -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<string, QuadrantConfig> = 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

View File

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

View File

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