create symlink for node_modules in generateJson and buildRadar scripts

This commit is contained in:
dennis.ludwig
2021-06-17 14:18:23 +02:00
parent 33e36674f7
commit 3b0aab8f08
63 changed files with 5238 additions and 861 deletions

3
.gitignore vendored
View File

@@ -11,6 +11,9 @@
# production
/build
# local development
/public/rd.json
# misc
.idea
.DS_Store

View File

@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn ts:check && yarn lint-staged
yarn ts:check && yarn lint-staged && yarn build:scripts

View File

@@ -1,5 +0,0 @@
{
"*.{json, md, yml, js, ts, tsx}": [
"prettier --write",
]
}

3
.lintstagedrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"*.{json, md, yml, js, ts, tsx}": ["prettier --write"]
}

View File

@@ -1,36 +1,45 @@
# AOE Technology Radar
A static site generator for AOE Technology Radar
## Looking for the AOE Tech Radar content?
The repository is now found here: https://github.com/AOEpeople/techradar
The AOE Tech radar is deployed here: https://www.aoe.com/techradar/index.html
## Usage for your own radar?
The generator is free to use under Open Source License - in fact there are already some other Radars published based on our Radar and there are also Contributions back.
(There is a list of planned features below in case someone wants to contribute :-)
However, please be aware:
* It would be nice to mention in radar that the generator is based on this repository.
* Also, when you want to reuse the CSS and Styling: Change the font (it is a licensed font) and the colors (It using AOE CI)
- It would be nice to mention in radar that the generator is based on this repository.
- Also, when you want to reuse the CSS and Styling: Change the font (it is a licensed font) and the colors (It using AOE CI)
## Use and build
Add the tech radar as a dependency
```
yarn add https://github.com/aoepeople/aoe_technology_radar.git
```
Generate json file based on md files
```
yarn aoe_technology_radar-generateJson
```
Build the radar
```
yarn aoe_technology_radar-buildRadar
```
Serve
```
cd build
python3 -m http.server 8080
@@ -39,6 +48,7 @@ python3 -m http.server 8080
Then open here: http://localhost:8080
## Run a prepared static version
In most cases you have the tech radar available at `/techradar`, and for reasons want all correct pages to be accessible.
Until this setup improves, you can use the following way to generate the correct tech radar:
@@ -53,31 +63,39 @@ cp -r build techradar
(This is rather workaroundish for now, but does the job.)
## Customize the tech radar
You can customize the following parts of the tech radar.
### Change title, description and headline
Set the environment variable `REACT_APP_RADAR_NAME`. The default is "AOE Technology Radar".
### Host the application under a sub path
To host the application under a sub path, set the environment variable `PUBLIC_URL`, e.g. "/techradar".
### Change the favicon
To change the favicon, create a public folder in your application and put your favicon.ico in it.
### Change the logo
To change the logo, create a public folder in your application and put your logo.svg in it.
For reference have a look at [public/logo.svg](./public/logo.svg).
### Change the index.html
To change the index.html, create a public folder in your application and put your index.html in it.
For reference have a look at [public/index.html](./public/index.html).
## Usage
For a new Technology Radar release, create a folder of the release date
(YYYY-MM-DD) under `/radar`. In each release folder create a folder for every
quadrant and place the items there.
### Maintaining items
The items are written in Markdown format (.md)
Each file has a [front-matter](https://github.com/jxson/front-matter) header
@@ -95,13 +113,13 @@ Text goes here. You can use **markdown** here.
Following front-matter attributes are possible:
* **title**: Name of the Item
* **quadrant**: Quadrant. One of `languages-and-frameworks`,
- **title**: Name of the Item
- **quadrant**: Quadrant. One of `languages-and-frameworks`,
`methods-and-patterns`, `platforms-and-aoe-services`, `tools`
* **ring**: Ring section in radar. One of `trial`, `assess`, `adopt`, `hold`
* **info**: (optional) A short textual description of the item (visible in
- **ring**: Ring section in radar. One of `trial`, `assess`, `adopt`, `hold`
- **info**: (optional) A short textual description of the item (visible in
overview pages)
* **featured**: (optional, default "true") If you set this to `false`, the item
- **featured**: (optional, default "true") If you set this to `false`, the item
will not be visible in the radar quadrants but still be available in the overview.
The name of the .md file acts as item identifier and may overwrite items with
@@ -111,11 +129,21 @@ If an item is overwritten in a new release, the attributes from the new item are
merged with the old ones, and a new history entry is created for that item.
You can integrate images in your markdown. Put the image files in your public folder and reference them
```
![nice image](/images/nice-image.png)
```
## Development
For local development you need a `rd.json` in the public folder. You can use `rd_example.json`.
Then simply start the dev server
```
yarn start
```
### Change scripts
If you change one of the scripts in the scripts' folder, you have to compile them to JavaScript.
Therefore, run `yarn build:scripts` and commit the results in dist_scripts.
To make it more robust the script will be executed on commit.

View File

@@ -17,7 +17,9 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from) {
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getFirstLetter = exports.groupByFirstLetter = exports.groupByQuadrants = exports.featuredOnly = void 0;
var featuredOnly = function (items) { return items.filter(function (item) { return item.featured; }); };
var featuredOnly = function (items) {
return items.filter(function (item) { return item.featured; });
};
exports.featuredOnly = featuredOnly;
var groupByQuadrants = function (items) {
return items.reduce(function (quadrants, item) {
@@ -52,5 +54,7 @@ var addItemToRing = function (ring, item) {
if (ring === void 0) { ring = []; }
return __spreadArray(__spreadArray([], ring), [item]);
};
var getFirstLetter = function (item) { return item.title.substr(0, 1).toUpperCase(); };
var getFirstLetter = function (item) {
return item.title.substr(0, 1).toUpperCase();
};
exports.getFirstLetter = getFirstLetter;

View File

@@ -10,13 +10,18 @@
<meta property="og:image" content="%PUBLIC_URL%/logo.svg" />
<meta name="format-detection" content="telephone=no" />
<meta name="viewport" content="width=device-width, maximum-scale=1.0, initial-scale=1.0, user-scalable=0" />
<meta
name="viewport"
content="width=device-width, maximum-scale=1.0, initial-scale=1.0, user-scalable=0"
/>
<title>%REACT_APP_RADAR_NAME%</title>
</head>
<body>
<noscript>You need to enable JavaScript to view the AOE Technology Radar.</noscript>
<noscript
>You need to enable JavaScript to view the AOE Technology Radar.</noscript
>
<div id="root"></div>
</body>
</html>

3875
rd_example.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,110 +1,131 @@
import React from 'react';
import React from "react";
export type Animations = {
[k: string]: Animation[]
}
[k: string]: Animation[];
};
export type AnimationStates = {
[k: string]: React.CSSProperties[]
}
[k: string]: React.CSSProperties[];
};
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
}
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
}
getState(): AnimationStates;
run(): any;
awaitAnimationComplete(callback: () => void): any;
};
export const createAnimation = (stateA: React.CSSProperties, stateB: React.CSSProperties, delay: number): Animation => ({
stateA,
stateB,
delay,
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 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 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);
}
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 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))))
);
const getMaxAnimationsDuration = (animations: Animations) =>
Math.max(
...Object.values(animations).map((animations) =>
getAnimationDuration(Object.values(animations))
)
);
export const createAnimationRunner = (animationsIn: { [k: string]: (Animation | Animation[]) }, subscriber: () => void = () => {
}): AnimationRunner => {
const animations = Object.entries(animationsIn).reduce((state, [name, animation]) => ({
...state,
[name]: animation instanceof Array ? animation : [animation] as Animation[],
}), {} as Animations);
export const createAnimationRunner = (
animationsIn: { [k: string]: Animation | Animation[] },
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);
let state = Object.entries(animations).reduce(
(state, [name, animation]) => ({
...state,
[name]: getAnimationStates(animation),
}),
{} as AnimationStates
);
const animationsDuration = getMaxAnimationsDuration(animations);
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);
});
});
}
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);
},
}
}
return {
getState() {
return state;
},
run() {
Object.entries(animations).forEach(([name, animation]) => {
animate(name, animation);
});
},
awaitAnimationComplete(callback) {
window.setTimeout(callback, animationsDuration);
},
};
};

View File

@@ -1,23 +1,28 @@
import React, { MouseEventHandler } from 'react';
import classNames from 'classnames';
import './badge.scss';
import {Ring} from "../../config";
import React, { MouseEventHandler } from "react";
import classNames from "classnames";
import "./badge.scss";
import { Ring } from "../../config";
type BadgeProps = {
onClick?: MouseEventHandler;
big?: boolean;
type: 'big' | 'all' | 'empty' | Ring;
type: "big" | "all" | "empty" | Ring;
};
export default function Badge({ onClick, big, type, children }: React.PropsWithChildren<BadgeProps>) {
const Comp = onClick ? 'a' : 'span';
export default function Badge({
onClick,
big,
type,
children,
}: React.PropsWithChildren<BadgeProps>) {
const Comp = onClick ? "a" : "span";
return (
<Comp
className={classNames('badge', `badge--${type}`, {
'badge--big': big === true,
className={classNames("badge", `badge--${type}`, {
"badge--big": big === true,
})}
onClick={onClick}
href={Comp === 'a' ? '#' : undefined}
href={Comp === "a" ? "#" : undefined}
>
{children}
</Comp>

View File

@@ -40,5 +40,4 @@
background: var(--color-marine);
border-color: var(--color-marine);
}
}

View File

@@ -1,16 +1,24 @@
import React from 'react';
import classNames from 'classnames';
import './branding.scss';
import React from "react";
import classNames from "classnames";
import "./branding.scss";
type BrandingProps = {
logoContent: React.ReactNode;
modifier?: 'backlink' | 'logo' | 'content' | 'footer';
modifier?: "backlink" | "logo" | "content" | "footer";
};
export default function Branding({ logoContent, modifier, children }: React.PropsWithChildren<BrandingProps>) {
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
className={classNames("branding", {
[`branding--${modifier}`]: modifier,
})}
>
<div className="branding__logo">{logoContent}</div>
<div className="branding__content">{children}</div>
</div>
);
}

View File

@@ -1,4 +1,4 @@
@import '../../styles/sccs-vars.scss';
@import "../../styles/sccs-vars.scss";
.branding {
margin: 40px 0;
@@ -43,4 +43,4 @@
@media (max-width: $until-xl) {
margin: 15px 0 0;
}
}
}

View File

@@ -1,13 +1,17 @@
import React, { useState, useEffect } from 'react';
import classNames from 'classnames';
import './fadeable.scss';
import React, { useState, useEffect } from "react";
import classNames from "classnames";
import "./fadeable.scss";
type FadeableProps = {
leaving: boolean;
onLeave: () => void;
};
export default function Fadeable({ leaving, onLeave, children }: React.PropsWithChildren<FadeableProps>) {
export default function Fadeable({
leaving,
onLeave,
children,
}: React.PropsWithChildren<FadeableProps>) {
const [faded, setFaded] = useState(leaving);
useEffect(() => {
@@ -25,7 +29,10 @@ export default function Fadeable({ leaving, onLeave, children }: React.PropsWith
};
return (
<div className={classNames('fadable', { 'is-faded': faded })} onTransitionEnd={handleTransitionEnd}>
<div
className={classNames("fadable", { "is-faded": faded })}
onTransitionEnd={handleTransitionEnd}
>
{children}
</div>
);

View File

@@ -3,6 +3,6 @@
transition: opacity 0.2s cubic-bezier(0.54, 0, 0.28, 1);
&.is-faded {
opacity: 0;
opacity: 0;
}
}

View File

@@ -1,18 +1,37 @@
import React from 'react';
import classNames from 'classnames';
import Branding from '../Branding/Branding';
import FooterEnd from '../FooterEnd/FooterEnd';
import { assetUrl, getItemPageNames, isMobileViewport } from '../../config';
import { Item } from '../../model';
import './footer.scss';
export default function Footer({ items, pageName }: { items: Item[]; pageName: string }) {
import React from "react";
import classNames from "classnames";
import Branding from "../Branding/Branding";
import FooterEnd from "../FooterEnd/FooterEnd";
import { assetUrl, getItemPageNames, isMobileViewport } from "../../config";
import { Item } from "../../model";
import "./footer.scss";
export default function Footer({
items,
pageName,
}: {
items: Item[];
pageName: string;
}) {
return (
<div className={classNames('footer', { 'is-hidden': !isMobileViewport() && getItemPageNames(items).includes(pageName) })}>
<Branding modifier='footer' logoContent={<img src={assetUrl('logo.svg')} width='150px' height='60px' alt='' />}>
<span className='footnote'>
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.
<div
className={classNames("footer", {
"is-hidden":
!isMobileViewport() && getItemPageNames(items).includes(pageName),
})}
>
<Branding
modifier="footer"
logoContent={
<img src={assetUrl("logo.svg")} width="150px" height="60px" alt="" />
}
>
<span className="footnote">
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.
</span>
</Branding>
<FooterEnd />

View File

@@ -1,37 +1,75 @@
import React from 'react';
import classNames from 'classnames';
import './footerend.scss';
export default function FooterEnd({ modifier }: { modifier?: 'in-sidebar' }) {
import React from "react";
import classNames from "classnames";
import "./footerend.scss";
export default function FooterEnd({ modifier }: { modifier?: "in-sidebar" }) {
return (
<div className={classNames('footer-end', { [`footer-end__${modifier}`]: modifier })}>
<div className='footer-social'>
<div className='footer-social__label'>
<div
className={classNames("footer-end", {
[`footer-end__${modifier}`]: modifier,
})}
>
<div className="footer-social">
<div className="footer-social__label">
<p>Follow us:</p>
</div>
<div className='footer-social__links'>
<a className='social-links-icon' href='https://www.facebook.com/aoepeople' target='_blank' rel='noopener noreferrer'>
<i className='socicon-facebook social-icon'></i>
<div className="footer-social__links">
<a
className="social-links-icon"
href="https://www.facebook.com/aoepeople"
target="_blank"
rel="noopener noreferrer"
>
<i className="socicon-facebook social-icon"></i>
</a>
<a className='social-links-icon' href='https://twitter.com/aoepeople' target='_blank' rel='noopener noreferrer'>
<i className='socicon-twitter social-icon'></i>
<a
className="social-links-icon"
href="https://twitter.com/aoepeople"
target="_blank"
rel="noopener noreferrer"
>
<i className="socicon-twitter social-icon"></i>
</a>
<a className='social-links-icon' href='https://www.linkedin.com/company/aoe' target='_blank' rel='noopener noreferrer'>
<i className='socicon-linkedin social-icon'></i>
<a
className="social-links-icon"
href="https://www.linkedin.com/company/aoe"
target="_blank"
rel="noopener noreferrer"
>
<i className="socicon-linkedin social-icon"></i>
</a>
<a className='social-links-icon' href='https://www.xing.com/company/aoe' target='_blank' rel='noopener noreferrer'>
<i className='socicon-xing social-icon'></i>
<a
className="social-links-icon"
href="https://www.xing.com/company/aoe"
target="_blank"
rel="noopener noreferrer"
>
<i className="socicon-xing social-icon"></i>
</a>
<a className='social-links-icon' href='https://www.youtube.com/user/aoepeople' target='_blank' rel='noopener noreferrer'>
<i className='socicon-youtube social-icon'></i>
<a
className="social-links-icon"
href="https://www.youtube.com/user/aoepeople"
target="_blank"
rel="noopener noreferrer"
>
<i className="socicon-youtube social-icon"></i>
</a>
<a className='social-links-icon' href='https://github.com/aoepeople' target='_blank' rel='noopener noreferrer'>
<i className='socicon-github social-icon'></i>
<a
className="social-links-icon"
href="https://github.com/aoepeople"
target="_blank"
rel="noopener noreferrer"
>
<i className="socicon-github social-icon"></i>
</a>
</div>
</div>
<div className='footer-copyright'>
<div className="footer-copyright">
<p>
<a href='https://www.aoe.com/en/copyright-meta/legal-information.html' target='_blank' rel='noopener noreferrer'>
<a
href="https://www.aoe.com/en/copyright-meta/legal-information.html"
target="_blank"
rel="noopener noreferrer"
>
Legal Information
</a>
</p>

View File

@@ -1,4 +1,4 @@
@import '../../styles/sccs-vars.scss';
@import "../../styles/sccs-vars.scss";
.footer-end {
font-size: 12px;
@@ -35,4 +35,4 @@
&__label {
margin: 0 10px 0 0;
}
}
}

View File

@@ -1,6 +1,17 @@
import React from 'react';
import classNames from 'classnames';
import './headline-group.scss';
export default function ({ children, secondary = false }: React.PropsWithChildren<{ secondary?: boolean }>) {
return <div className={classNames('headline-group', { 'headline-group--secondary': secondary })}>{children}</div>;
import React from "react";
import classNames from "classnames";
import "./headline-group.scss";
export default function ({
children,
secondary = false,
}: React.PropsWithChildren<{ secondary?: boolean }>) {
return (
<div
className={classNames("headline-group", {
"headline-group--secondary": secondary,
})}
>
{children}
</div>
);
}

View File

@@ -1,4 +1,4 @@
@import '../../styles/sccs-vars.scss';
@import "../../styles/sccs-vars.scss";
.headline-group {
margin: 0 0 60px;
@@ -14,4 +14,4 @@
margin: 5px 0;
}
}
}
}

View File

@@ -1,10 +1,13 @@
import React from 'react';
import './hero-headline.scss';
export default function ({ children, alt }: React.PropsWithChildren<{ alt?: string }>) {
import React from "react";
import "./hero-headline.scss";
export default function ({
children,
alt,
}: React.PropsWithChildren<{ alt?: string }>) {
return (
<div className='hero-headline'>
<div className="hero-headline">
{children}
<span className='hero-headline__alt'>{alt}</span>
<span className="hero-headline__alt">{alt}</span>
</div>
);
}

View File

@@ -1,4 +1,4 @@
@import '../../styles/sccs-vars.scss';
@import "../../styles/sccs-vars.scss";
.hero-headline {
font-size: 38px;
@@ -23,4 +23,4 @@
display: block;
}
}
}
}

View File

@@ -1,9 +1,9 @@
import React from 'react';
import classNames from 'classnames';
import Link from '../Link/Link';
import Flag from '../Flag/Flag';
import { Item as mItem } from '../../model';
import './item.scss';
import React from "react";
import classNames from "classnames";
import Link from "../Link/Link";
import Flag from "../Flag/Flag";
import { Item as mItem } from "../../model";
import "./item.scss";
type ItemProps = {
item: mItem;
noLeadingBorder?: boolean;
@@ -11,21 +11,26 @@ type ItemProps = {
style: React.CSSProperties;
};
export default function Item({ item, noLeadingBorder = false, active = false, style = {} }: ItemProps) {
export default function Item({
item,
noLeadingBorder = false,
active = false,
style = {},
}: ItemProps) {
return (
<Link
className={classNames('item', {
'item--no-leading-border': noLeadingBorder,
'is-active': active,
className={classNames("item", {
"item--no-leading-border": noLeadingBorder,
"is-active": active,
})}
pageName={`${item.quadrant}/${item.name}`}
style={style}
>
<div className='item__title'>
<div className="item__title">
{item.title}
<Flag item={item} />
</div>
{item.info && <div className='item__info'>{item.info}</div>}
{item.info && <div className="item__info">{item.info}</div>}
</Link>
);
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import Item from '../Item/Item';
import { Item as mItem } from '../../model';
import './item-list.scss';
import React from "react";
import Item from "../Item/Item";
import { Item as mItem } from "../../model";
import "./item-list.scss";
type ItemListProps = {
items: mItem[];
activeItem?: mItem;
@@ -10,19 +10,30 @@ type ItemListProps = {
itemStyle?: React.CSSProperties[];
};
export default function ItemList({ children, items, activeItem, noLeadingBorder, headerStyle = {}, itemStyle = [] }: React.PropsWithChildren<ItemListProps>) {
export default function ItemList({
children,
items,
activeItem,
noLeadingBorder,
headerStyle = {},
itemStyle = [],
}: React.PropsWithChildren<ItemListProps>) {
return (
<div className='item-list'>
<div className='item-list__header' style={headerStyle}>
<div className="item-list">
<div className="item-list__header" style={headerStyle}>
{children}
</div>
<div className='item-list__list'>
<div className="item-list__list">
{items.map((item, i) => (
<Item
key={item.name}
item={item}
noLeadingBorder={noLeadingBorder}
active={activeItem !== null && activeItem !== undefined && activeItem.name === item.name}
active={
activeItem !== null &&
activeItem !== undefined &&
activeItem.name === item.name
}
style={itemStyle[i]}
/>
))}

View File

@@ -1,17 +1,20 @@
import React from 'react';
import Badge from '../Badge/Badge';
import { formatRelease } from '../../date';
import { Revision } from '../../model';
import React from "react";
import Badge from "../Badge/Badge";
import { formatRelease } from "../../date";
import { Revision } from "../../model";
export default function ItemRevision({ revision }: { revision: Revision }) {
return (
<div className='item-revision'>
<div className="item-revision">
<div>
<Badge type={revision.ring}>
{revision.ring} | {formatRelease(revision.release)}
</Badge>
</div>
<div className='markdown' dangerouslySetInnerHTML={{ __html: revision.body }} />
<div
className="markdown"
dangerouslySetInnerHTML={{ __html: revision.body }}
/>
</div>
);
}

View File

@@ -1,5 +1,5 @@
.item-revision {
&+.item-revision {
& + .item-revision {
margin-top: 40px;
}
}

View File

@@ -1,13 +1,17 @@
import React from 'react';
import HeadlineGroup from '../HeadlineGroup/HeadlineGroup';
import ItemRevision from '../ItemRevision/ItemRevision';
import { Revision } from '../../model';
import './item-revisions.scss';
export default function ItemRevisions({ revisions }: { revisions: Revision[] }) {
import React from "react";
import HeadlineGroup from "../HeadlineGroup/HeadlineGroup";
import ItemRevision from "../ItemRevision/ItemRevision";
import { Revision } from "../../model";
import "./item-revisions.scss";
export default function ItemRevisions({
revisions,
}: {
revisions: Revision[];
}) {
return (
<div className='item-revisions'>
<div className="item-revisions">
<HeadlineGroup secondary>
<h4 className='headline headline--dark'>Revisions:</h4>
<h4 className="headline headline--dark">Revisions:</h4>
</HeadlineGroup>
{revisions.map((revision) => (

View File

@@ -1,15 +1,24 @@
import React from 'react';
import classNames from 'classnames';
import Link from '../Link/Link';
import { assetUrl, radarNameShort } from '../../config';
import './logo-link.scss';
import React from "react";
import classNames from "classnames";
import Link from "../Link/Link";
import { assetUrl, radarNameShort } from "../../config";
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>
<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>
);

View File

@@ -1,91 +1,139 @@
import React from 'react';
import HeroHeadline from '../HeroHeadline/HeroHeadline';
import Fadeable from '../Fadeable/Fadeable';
import SetTitle from '../SetTitle';
import { radarName } from '../../config';
import React from "react";
import HeroHeadline from "../HeroHeadline/HeroHeadline";
import Fadeable from "../Fadeable/Fadeable";
import SetTitle from "../SetTitle";
import { radarName } from "../../config";
export default function PageHelp({ leaving, onLeave }: { leaving: boolean; onLeave: () => void }) {
export default function PageHelp({
leaving,
onLeave,
}: {
leaving: boolean;
onLeave: () => void;
}) {
return (
<Fadeable leaving={leaving} onLeave={onLeave}>
<SetTitle title={'How to use the ' + radarName} />
<SetTitle title={"How to use the " + radarName} />
<HeroHeadline>How to use the {radarName}</HeroHeadline>
<div className='fullpage-content'>
<div className="fullpage-content">
<h3>Introduction</h3>
<p>Technology is moving fast and new technologies and innovations appear continuously.</p>
<p>
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.
Technology is moving fast and new technologies and innovations appear
continuously.
</p>
<p>
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.
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.
</p>
<p>
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.
</p>
<h3>What is the {radarName}</h3>
<p>
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.
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.
</p>
<h3>How it is created</h3>
<p>
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.
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.
</p>
<p>
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.
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.
</p>
<h3>How should it be used</h3>
<p>The radar acts as an overview of technologies that we think everyone in the teams should currently know about.</p>
<p>
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.
The radar acts as an overview of technologies that we think everyone
in the teams should currently know about.
</p>
<p>
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.
</p>
<p>
We also hope that developers outside of AOE find the informations in
our technologie overview inspirational.
</p>
<p>
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).
</p>
<p>We also hope that developers outside of AOE find the informations in our technologie overview inspirational.</p>
<p>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).</p>
<p>The quadrants are:</p>
<ul>
<li>
<strong>Languages and Frameworks:</strong> 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.{' '}
<strong>Languages and Frameworks:</strong> 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.{" "}
</li>
<li>
<strong>Tools:</strong> Here we put different software tools - from small helpers to bigger software projects
<strong>Tools:</strong> Here we put different software tools - from
small helpers to bigger software projects
</li>
<li>
<strong>Methods and Patterns:</strong> 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.
<strong>Methods and Patterns:</strong> 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.
</li>
<li>
<strong>Platforms and Services</strong> (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.
<strong>Platforms and Services</strong> (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.
</li>
</ul>
<p>Each of the items is classified in one of these rings:</p>
<ul>
<li>
<strong>Adopt:</strong> 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.
<strong>Adopt:</strong> 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.
</li>
<li>
<strong>Trial:</strong> 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.
<strong>Trial:</strong> 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.
</li>
<li>
<strong>Assess:</strong> 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.
<strong>Assess:</strong> 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.
</li>
<li>
<strong>Hold:</strong> 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.
<strong>Hold:</strong> 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.
</li>
</ul>
<p>
Contributions and source code of the radar are on github:{' '}
<a href='https://github.com/AOEpeople/aoe_technology_radar' target='_blank' rel='noopener noreferrer'>
Contributions and source code of the radar are on github:{" "}
<a
href="https://github.com/AOEpeople/aoe_technology_radar"
target="_blank"
rel="noopener noreferrer"
>
AOE Tech Radar on Github
</a>
</p>

View File

@@ -1,12 +1,12 @@
import React from 'react';
import { formatRelease } from '../../date';
import { featuredOnly, Item } from '../../model';
import HeroHeadline from '../HeroHeadline/HeroHeadline';
import QuadrantGrid from '../QuadrantGrid/QuadrantGrid';
import Fadeable from '../Fadeable/Fadeable';
import SetTitle from '../SetTitle';
import { radarName, radarNameShort } from '../../config';
import { MomentInput } from 'moment';
import React from "react";
import { formatRelease } from "../../date";
import { featuredOnly, Item } from "../../model";
import HeroHeadline from "../HeroHeadline/HeroHeadline";
import QuadrantGrid from "../QuadrantGrid/QuadrantGrid";
import Fadeable from "../Fadeable/Fadeable";
import SetTitle from "../SetTitle";
import { radarName, radarNameShort } from "../../config";
import { MomentInput } from "moment";
type PageIndexProps = {
leaving: boolean;
@@ -15,17 +15,26 @@ type PageIndexProps = {
releases: MomentInput[];
};
export default function PageIndex({ leaving, onLeave, items, releases }: PageIndexProps) {
export default function PageIndex({
leaving,
onLeave,
items,
releases,
}: PageIndexProps) {
const newestRelease = releases.slice(-1)[0];
const numberOfReleases = releases.length;
return (
<Fadeable leaving={leaving} onLeave={onLeave}>
<SetTitle title={radarNameShort} />
<div className='headline-group'>
<HeroHeadline alt={`Version #${numberOfReleases}`}>{radarName}</HeroHeadline>
<div className="headline-group">
<HeroHeadline alt={`Version #${numberOfReleases}`}>
{radarName}
</HeroHeadline>
</div>
<QuadrantGrid items={featuredOnly(items)} />
<div className='publish-date'>Published {formatRelease(newestRelease)}</div>
<div className="publish-date">
Published {formatRelease(newestRelease)}
</div>
</Fadeable>
);
}

View File

@@ -1,239 +1,271 @@
import React, {useEffect, useState} from 'react';
import Badge from '../Badge/Badge';
import ItemList from '../ItemList/ItemList';
import Link from '../Link/Link';
import FooterEnd from '../FooterEnd/FooterEnd';
import SetTitle from '../SetTitle';
import ItemRevisions from '../ItemRevisions/ItemRevisions';
import React, { useEffect, useState } from "react";
import Badge from "../Badge/Badge";
import ItemList from "../ItemList/ItemList";
import Link from "../Link/Link";
import FooterEnd from "../FooterEnd/FooterEnd";
import SetTitle from "../SetTitle";
import ItemRevisions from "../ItemRevisions/ItemRevisions";
import {
AnimationStates,
createAnimation,
createAnimationRunner
} from '../../animation';
import './item-page.scss';
import {translate} from '../../config';
import {groupByQuadrants, Item} from '../../model';
AnimationStates,
createAnimation,
createAnimationRunner,
} from "../../animation";
import "./item-page.scss";
import { translate } from "../../config";
import { groupByQuadrants, Item } from "../../model";
const getItem = (pageName: string, items: Item[]) => {
const [quadrantName, itemName] = pageName.split('/');
return items.filter((item) => item.quadrant === quadrantName && item.name === itemName)[0];
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];
const item = getItem(pageName, items);
return groupByQuadrants(items)[item.quadrant][item.ring];
};
type PageItemProps = {
pageName: string;
items: Item[];
leaving: boolean;
onLeave: () => void;
pageName: string;
items: Item[];
leaving: boolean;
onLeave: () => void;
};
export default function PageItem({pageName, items, leaving, onLeave}: PageItemProps) {
const itemsInRing = getItemsInRing(pageName, items);
export default function PageItem({
pageName,
items,
leaving,
onLeave,
}: PageItemProps) {
const itemsInRing = getItemsInRing(pageName, items);
const animationsIn = {
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
),
};
const animationsIn = {
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
),
};
const animationsOut = {
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
),
};
const animationsOut = {
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
),
};
const [animations, setAnimations] = useState<AnimationStates>(() => {
return leaving ? createAnimationRunner(animationsIn).getState() : {}
});
const [animations, setAnimations] = useState<AnimationStates>(() => {
return leaving ? createAnimationRunner(animationsIn).getState() : {};
});
const [stateLeaving, setStateLeaving] = useState(leaving);
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: string) => {
if (!animations) {
return undefined;
}
return animations[name];
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 getAnimationState = (name: string) => {
const animations = getAnimationStates(name)
if (animations === undefined || animations.length === 0) {
return undefined
}
return animations[0]
};
const getAnimationStates = (name: string) => {
if (!animations) {
return undefined;
}
return animations[name];
};
const item = getItem(pageName, items);
const getAnimationState = (name: string) => {
const animations = getAnimationStates(name);
if (animations === undefined || animations.length === 0) {
return undefined;
}
return animations[0];
};
return (
<div>
<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(item.quadrant)}</h3>
</div>
const item = getItem(pageName, items);
<ItemList items={itemsInRing} activeItem={item} headerStyle={getAnimationState('navHeader')}
itemStyle={getAnimationStates('items')}>
<div className='split'>
<div className='split__left'>
<Badge big type={item.ring}>
{item.ring}
</Badge>
</div>
<div className='split__right'>
<Link className='icon-link' pageName={item.quadrant}>
<span className='icon icon--pie icon-link__icon'/>
Quadrant Overview
</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'>
<h1 className='hero-headline hero-headline--inverse'>{item.title}</h1>
</div>
<div className='split__right'>
<Badge big type={item.ring}>
{item.ring}
</Badge>
</div>
</div>
</div>
<div className='markdown' dangerouslySetInnerHTML={{__html: item.body}}/>
{item.revisions.length > 1 && <ItemRevisions revisions={item.revisions.slice(1)}/>}
</div>
</div>
return (
<div>
<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(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}>
{item.ring}
</Badge>
</div>
<div className="split__right">
<Link className="icon-link" pageName={item.quadrant}>
<span className="icon icon--pie icon-link__icon" />
Quadrant Overview
</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">
<h1 className="hero-headline hero-headline--inverse">
{item.title}
</h1>
</div>
<div className="split__right">
<Badge big type={item.ring}>
{item.ring}
</Badge>
</div>
</div>
</div>
<div
className="markdown"
dangerouslySetInnerHTML={{ __html: item.body }}
/>
{item.revisions.length > 1 && (
<ItemRevisions revisions={item.revisions.slice(1)} />
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -57,6 +57,6 @@
min-height: 300px;
&__aside {
padding: 20px 0
padding: 20px 0;
}
}
}

View File

@@ -1,13 +1,13 @@
import React from 'react';
import Badge from '../Badge/Badge';
import ItemList from '../ItemList/ItemList';
import Link from '../Link/Link';
import Fadeable from '../Fadeable/Fadeable';
import SetTitle from '../SetTitle';
import ItemRevisions from '../ItemRevisions/ItemRevisions';
import React from "react";
import Badge from "../Badge/Badge";
import ItemList from "../ItemList/ItemList";
import Link from "../Link/Link";
import Fadeable from "../Fadeable/Fadeable";
import SetTitle from "../SetTitle";
import ItemRevisions from "../ItemRevisions/ItemRevisions";
import { translate } from '../../config';
import { groupByQuadrants, Item } from '../../model';
import { translate } from "../../config";
import { groupByQuadrants, Item } from "../../model";
type PageItemMobileProps = {
pageName: string;
@@ -16,10 +16,17 @@ type PageItemMobileProps = {
onLeave: () => void;
};
export default function PageItemMobile({ pageName, items, leaving, onLeave }: PageItemMobileProps) {
export default function PageItemMobile({
pageName,
items,
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];
const [quadrantName, itemName] = pageName.split("/");
const item = items.filter(
(item) => item.quadrant === quadrantName && item.name === itemName
)[0];
return item;
};
@@ -34,36 +41,43 @@ export default function PageItemMobile({ pageName, items, leaving, onLeave }: Pa
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(item.quadrant)}</h3>
<h1 className='hero-headline hero-headline--inverse'>{item.title}</h1>
<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(item.quadrant)}</h3>
<h1 className="hero-headline hero-headline--inverse">
{item.title}
</h1>
</div>
<div className='split__right'>
<div className="split__right">
<Badge big type={item.ring}>
{item.ring}
</Badge>
</div>
</div>
</div>
<div className='markdown' dangerouslySetInnerHTML={{ __html: item.body }} />
{item.revisions.length > 1 && <ItemRevisions revisions={item.revisions.slice(1)} />}
<div
className="markdown"
dangerouslySetInnerHTML={{ __html: item.body }}
/>
{item.revisions.length > 1 && (
<ItemRevisions revisions={item.revisions.slice(1)} />
)}
</div>
</div>
</div>
<aside className='mobile-item-page__aside'>
<aside className="mobile-item-page__aside">
<ItemList items={itemsInRing} activeItem={item}>
<div className='split'>
<div className='split__left'>
<h3 className='headline'>{translate(item.quadrant)}</h3>
<div className="split">
<div className="split__left">
<h3 className="headline">{translate(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
<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>

View File

@@ -1,30 +1,39 @@
import React, { useState, useEffect } from 'react';
import HeadlineGroup from '../HeadlineGroup/HeadlineGroup';
import HeroHeadline from '../HeroHeadline/HeroHeadline';
import Badge from '../Badge/Badge';
import Link from '../Link/Link';
import Search from '../Search/Search';
import Fadeable from '../Fadeable/Fadeable';
import SetTitle from '../SetTitle';
import Flag from '../Flag/Flag';
import { groupByFirstLetter, Item } from '../../model';
import { translate, Ring } from '../../config';
import React, { useState, useEffect } from "react";
import HeadlineGroup from "../HeadlineGroup/HeadlineGroup";
import HeroHeadline from "../HeroHeadline/HeroHeadline";
import Badge from "../Badge/Badge";
import Link from "../Link/Link";
import Search from "../Search/Search";
import Fadeable from "../Fadeable/Fadeable";
import SetTitle from "../SetTitle";
import Flag from "../Flag/Flag";
import { groupByFirstLetter, Item } from "../../model";
import { translate, Ring } from "../../config";
const containsSearchTerm = (text = '', term = '') => {
const containsSearchTerm = (text = "", term = "") => {
// TODO search refinement
return text.trim().toLocaleLowerCase().indexOf(term.trim().toLocaleLowerCase()) !== -1;
return (
text.trim().toLocaleLowerCase().indexOf(term.trim().toLocaleLowerCase()) !==
-1
);
};
type PageOverviewProps = {
rings: readonly ('all' | Ring)[];
rings: readonly ("all" | Ring)[];
search: string;
items: Item[];
leaving: boolean;
onLeave: () => void;
};
export default function PageOverview({ rings, search: searchProp, items, leaving, onLeave }: PageOverviewProps) {
const [ring, setRing] = useState<Ring | 'all'>('all');
export default function PageOverview({
rings,
search: searchProp,
items,
leaving,
onLeave,
}: PageOverviewProps) {
const [ring, setRing] = useState<Ring | "all">("all");
const [search, setSearch] = useState(searchProp);
useEffect(() => {
@@ -40,13 +49,19 @@ export default function PageOverview({ rings, search: searchProp, items, leaving
const isRingActive = (ringName: string) => ring === ringName;
const itemMatchesRing = (item: Item) => ring === 'all' || item.ring === ring;
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);
return (
search.trim() === "" ||
containsSearchTerm(item.title, search) ||
containsSearchTerm(item.body, search) ||
containsSearchTerm(item.info, search)
);
};
const isItemVisible = (item: Item) => itemMatchesRing(item) && itemMatchesSearch(item);
const isItemVisible = (item: Item) =>
itemMatchesRing(item) && itemMatchesSearch(item);
const getFilteredAndGroupedItems = () => {
const groups = groupByFirstLetter(items);
@@ -54,7 +69,9 @@ export default function PageOverview({ rings, search: searchProp, items, leaving
...group,
items: group.items.filter(isItemVisible),
}));
const nonEmptyGroups = groupsFiltered.filter((group) => group.items.length > 0);
const nonEmptyGroups = groupsFiltered.filter(
(group) => group.items.length > 0
);
return nonEmptyGroups;
};
@@ -64,20 +81,24 @@ export default function PageOverview({ rings, search: searchProp, items, leaving
return (
<Fadeable leaving={leaving} onLeave={onLeave}>
<SetTitle title='Technologies Overview' />
<SetTitle title="Technologies Overview" />
<HeadlineGroup>
<HeroHeadline>Technologies Overview</HeroHeadline>
</HeadlineGroup>
<div className='filter'>
<div className='split split--filter'>
<div className='split__left'>
<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'>
<div className="split__right">
<div className="nav">
{rings.map((ringName) => (
<div className='nav__item' key={ringName}>
<Badge big onClick={handleRingClick(ringName)} type={isRingActive(ringName) ? ringName : 'empty'}>
<div className="nav__item" key={ringName}>
<Badge
big
onClick={handleRingClick(ringName)}
type={isRingActive(ringName) ? ringName : "empty"}
>
{ringName}
</Badge>
</div>
@@ -87,30 +108,32 @@ export default function PageOverview({ rings, search: searchProp, items, leaving
</div>
</div>
<div className='letter-index'>
<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'>
<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'
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'>
<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(item.quadrant)}</div>
<div className='nav__item'>
<div className="split__right">
<div className="nav nav--relations">
<div className="nav__item">
{translate(item.quadrant)}
</div>
<div className="nav__item">
<Badge type={item.ring}>{item.ring}</Badge>
</div>
</div>

View File

@@ -1,12 +1,12 @@
import React from 'react';
import HeroHeadline from '../HeroHeadline/HeroHeadline';
import HeadlineGroup from '../HeadlineGroup/HeadlineGroup';
import QuadrantSection from '../QuadrantSection/QuadrantSection';
import Fadeable from '../Fadeable/Fadeable';
import SetTitle from '../SetTitle';
import React from "react";
import HeroHeadline from "../HeroHeadline/HeroHeadline";
import HeadlineGroup from "../HeadlineGroup/HeadlineGroup";
import QuadrantSection from "../QuadrantSection/QuadrantSection";
import Fadeable from "../Fadeable/Fadeable";
import SetTitle from "../SetTitle";
import { translate } from '../../config';
import { featuredOnly, groupByQuadrants, Item } from '../../model';
import { translate } from "../../config";
import { featuredOnly, groupByQuadrants, Item } from "../../model";
type PageQuadrantProps = {
leaving: boolean;
@@ -15,7 +15,12 @@ type PageQuadrantProps = {
items: Item[];
};
export default function PageQuadrant({ leaving, onLeave, pageName, items }: PageQuadrantProps) {
export default function PageQuadrant({
leaving,
onLeave,
pageName,
items,
}: PageQuadrantProps) {
const groups = groupByQuadrants(featuredOnly(items));
return (
<Fadeable leaving={leaving} onLeave={onLeave}>

View File

@@ -1,21 +1,34 @@
import React from 'react';
import HeroHeadline from '../HeroHeadline/HeroHeadline';
import Fadeable from '../Fadeable/Fadeable';
import SetTitle from '../SetTitle';
import React from "react";
import HeroHeadline from "../HeroHeadline/HeroHeadline";
import Fadeable from "../Fadeable/Fadeable";
import SetTitle from "../SetTitle";
export default function PageToolbox({ leaving, onLeave }: { leaving: boolean; onLeave: () => void }) {
export default function PageToolbox({
leaving,
onLeave,
}: {
leaving: boolean;
onLeave: () => void;
}) {
return (
<Fadeable leaving={leaving} onLeave={onLeave}>
<SetTitle title='Small AOE Toolbox' />
<SetTitle title="Small AOE Toolbox" />
<HeroHeadline>Small AOE Toolbox</HeroHeadline>
<div className='fullpage-content'>
<div className="fullpage-content">
<h3>Useful Tools</h3>
<ul>
<li>Fiddler - free web debugging proxy ( http://www.telerik.com/fiddler )</li>
<li>
Fiddler - free web debugging proxy ( http://www.telerik.com/fiddler
)
</li>
<li>SoapUI - Webservice Test Tool (https://www.soapui.org/ )</li>
<li>Postman - API Test Tool ( https://www.getpostman.com/ )</li>
<li> Modelio - Simple free UML Modelling tool ( https://www.modelio.org/ )</li>
<li>
{" "}
Modelio - Simple free UML Modelling tool ( https://www.modelio.org/
)
</li>
</ul>
<h3>Useful Tools (commercial)</h3>

View File

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

View File

@@ -1,4 +1,4 @@
@import '../../styles/sccs-vars.scss';
@import "../../styles/sccs-vars.scss";
.quadrant-grid {
display: flex;
@@ -13,4 +13,4 @@
flex-basis: 100%;
}
}
}
}

View File

@@ -1,12 +1,17 @@
import React from 'react';
import { translate, rings, Ring, showEmptyRings } from '../../config';
import Badge from '../Badge/Badge';
import Link from '../Link/Link';
import ItemList from '../ItemList/ItemList';
import Flag from '../Flag/Flag';
import { Group } from '../../model';
import './quadrant-section.scss';
const renderList = (ringName: Ring, quadrantName: string, groups: Group, big: boolean) => {
import React from "react";
import { translate, rings, Ring, showEmptyRings } from "../../config";
import Badge from "../Badge/Badge";
import Link from "../Link/Link";
import ItemList from "../ItemList/ItemList";
import Flag from "../Flag/Flag";
import { Group } from "../../model";
import "./quadrant-section.scss";
const renderList = (
ringName: Ring,
quadrantName: string,
groups: Group,
big: boolean
) => {
const itemsInRing = groups[quadrantName][ringName] || [];
if (big) {
@@ -20,13 +25,13 @@ const renderList = (ringName: Ring, quadrantName: string, groups: Group, big: bo
}
return (
<div className='ring-list'>
<div className='ring-list__header'>
<div className="ring-list">
<div className="ring-list__header">
<Badge type={ringName}>{ringName}</Badge>
</div>
{itemsInRing.map((item) => (
<span key={item.name} className='ring-list__item'>
<Link className='link' pageName={`${item.quadrant}/${item.name}`}>
<span key={item.name} className="ring-list__item">
<Link className="link" pageName={`${item.quadrant}/${item.name}`}>
{item.title}
<Flag item={item} short />
</Link>
@@ -36,36 +41,58 @@ const renderList = (ringName: Ring, quadrantName: string, groups: Group, big: bo
);
};
const renderRing = (ringName: Ring, quadrantName: string, groups: Group, big: boolean) => {
if (!showEmptyRings && (!groups[quadrantName] || !groups[quadrantName][ringName] || groups[quadrantName][ringName].length === 0)) {
const renderRing = (
ringName: Ring,
quadrantName: string,
groups: Group,
big: boolean
) => {
if (
!showEmptyRings &&
(!groups[quadrantName] ||
!groups[quadrantName][ringName] ||
groups[quadrantName][ringName].length === 0)
) {
return null;
}
return (
<div key={ringName} className='quadrant-section__ring'>
<div key={ringName} className="quadrant-section__ring">
{renderList(ringName, quadrantName, groups, big)}
</div>
);
};
export default function QuadrantSection({ quadrantName, groups, big = false }: { quadrantName: string; groups: Group; big?: boolean }) {
export default function QuadrantSection({
quadrantName,
groups,
big = false,
}: {
quadrantName: string;
groups: Group;
big?: boolean;
}) {
return (
<div className='quadrant-section'>
<div className='quadrant-section__header'>
<div className='split'>
<div className='split__left'>
<h4 className='headline'>{translate(quadrantName)}</h4>
<div className="quadrant-section">
<div className="quadrant-section__header">
<div className="split">
<div className="split__left">
<h4 className="headline">{translate(quadrantName)}</h4>
</div>
{!big && (
<div className='split__right'>
<Link className='icon-link' pageName={`${quadrantName}`}>
<span className='icon icon--pie icon-link__icon' />
<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'>{rings.map((ringName) => renderRing(ringName, quadrantName, groups, big))}</div>
<div className="quadrant-section__rings">
{rings.map((ringName) =>
renderRing(ringName, quadrantName, groups, big)
)}
</div>
</div>
);
}

View File

@@ -1,4 +1,4 @@
@import '../../styles/sccs-vars.scss';
@import "../../styles/sccs-vars.scss";
.quadrant-section {
&__header {
@@ -18,7 +18,6 @@
@media (max-width: $until-md) {
flex-basis: 50%;
}
}
}
}

View File

@@ -1,91 +1,137 @@
import React, {useState, useEffect} from 'react';
import PageIndex from './PageIndex/PageIndex';
import PageOverview from './PageOverview/PageOverview';
import PageHelp from './PageHelp/PageHelp';
import PageQuadrant from './PageQuadrant/PageQuadrant';
import PageItem from './PageItem/PageItem';
import PageItemMobile from './PageItemMobile/PageItemMobile';
import {quadrants, getItemPageNames, isMobileViewport, rings} from '../config';
import {Item} from '../model';
import React, { useState, useEffect } from "react";
import PageIndex from "./PageIndex/PageIndex";
import PageOverview from "./PageOverview/PageOverview";
import PageHelp from "./PageHelp/PageHelp";
import PageQuadrant from "./PageQuadrant/PageQuadrant";
import PageItem from "./PageItem/PageItem";
import PageItemMobile from "./PageItemMobile/PageItemMobile";
import {
quadrants,
getItemPageNames,
isMobileViewport,
rings,
} from "../config";
import { Item } from "../model";
type RouterProps = {
pageName: string
items: Item[]
releases: string[]
search: string
}
pageName: string;
items: Item[];
releases: string[];
search: string;
};
enum page {
index,
overview,
help,
quadrant,
itemMobile,
item,
notFound,
index,
overview,
help,
quadrant,
itemMobile,
item,
notFound,
}
const getPageByName = (items: Item[], pageName: string): page => {
if (pageName === 'index') {
return page.index;
}
if (pageName === 'overview') {
return page.overview;
}
if (pageName === 'help-and-about-tech-radar') {
return page.help;
}
if (quadrants.includes(pageName)) {
return page.quadrant;
}
if (getItemPageNames(items).includes(pageName)) {
return isMobileViewport() ? page.itemMobile : page.item;
}
if (pageName === "index") {
return page.index;
}
if (pageName === "overview") {
return page.overview;
}
if (pageName === "help-and-about-tech-radar") {
return page.help;
}
if (quadrants.includes(pageName)) {
return page.quadrant;
}
if (getItemPageNames(items).includes(pageName)) {
return isMobileViewport() ? page.itemMobile : page.item;
}
return page.notFound;
return page.notFound;
};
export default function Router({pageName, items, releases, search}: RouterProps) {
const [statePageName, setStatePageName] = useState(pageName);
const [leaving, setLeaving] = useState(false);
const [nextPageName, setNextPageName] = useState<string>('');
export default function Router({
pageName,
items,
releases,
search,
}: RouterProps) {
const [statePageName, setStatePageName] = useState(pageName);
const [leaving, setLeaving] = useState(false);
const [nextPageName, setNextPageName] = useState<string>("");
useEffect(() => {
const nowLeaving = getPageByName(items, pageName) !== getPageByName(items, statePageName);
if (nowLeaving) {
setLeaving(true);
setNextPageName(pageName);
} else {
setStatePageName(pageName);
}
}, [pageName, items, statePageName]);
const handlePageLeave = () => {
setStatePageName(nextPageName);
setNextPageName('');
window.setTimeout(() => {
window.requestAnimationFrame(() => {
setLeaving(false);
});
}, 0);
};
switch (getPageByName(items, statePageName)) {
case page.index:
return <PageIndex leaving={leaving} items={items} onLeave={handlePageLeave} releases={releases}/>;
case page.overview:
return <PageOverview items={items} rings={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} pageName={statePageName}/>;
case page.itemMobile:
return <PageItemMobile items={items} pageName={statePageName} leaving={leaving} onLeave={handlePageLeave}/>;
case page.item:
return <PageItem items={items} pageName={statePageName} leaving={leaving} onLeave={handlePageLeave}/>;
default:
return <div/>;
useEffect(() => {
const nowLeaving =
getPageByName(items, pageName) !== getPageByName(items, statePageName);
if (nowLeaving) {
setLeaving(true);
setNextPageName(pageName);
} else {
setStatePageName(pageName);
}
}, [pageName, items, statePageName]);
const handlePageLeave = () => {
setStatePageName(nextPageName);
setNextPageName("");
window.setTimeout(() => {
window.requestAnimationFrame(() => {
setLeaving(false);
});
}, 0);
};
switch (getPageByName(items, statePageName)) {
case page.index:
return (
<PageIndex
leaving={leaving}
items={items}
onLeave={handlePageLeave}
releases={releases}
/>
);
case page.overview:
return (
<PageOverview
items={items}
rings={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}
pageName={statePageName}
/>
);
case page.itemMobile:
return (
<PageItemMobile
items={items}
pageName={statePageName}
leaving={leaving}
onLeave={handlePageLeave}
/>
);
case page.item:
return (
<PageItem
items={items}
pageName={statePageName}
leaving={leaving}
onLeave={handlePageLeave}
/>
);
default:
return <div />;
}
}

View File

@@ -1,6 +1,6 @@
import React, { FormEvent } from 'react';
import classNames from 'classnames';
import './search.scss';
import React, { FormEvent } from "react";
import classNames from "classnames";
import "./search.scss";
type SearchProps = {
onClose?: () => void;
@@ -14,7 +14,10 @@ export default React.forwardRef((props: SearchProps, ref) => {
return Search(props, ref);
});
function Search({ value, onChange, onClose, open = false, onSubmit = () => {} }: SearchProps, ref: any) {
function Search(
{ value, onChange, onClose, open = false, onSubmit = () => {} }: SearchProps,
ref: any
) {
const closable = onClose !== undefined;
const handleSubmit = (e: FormEvent) => {
@@ -30,26 +33,34 @@ function Search({ value, onChange, onClose, open = false, onSubmit = () => {} }:
};
return (
<form className={classNames('search', { 'search--closable': closable })} onSubmit={handleSubmit}>
<form
className={classNames("search", { "search--closable": closable })}
onSubmit={handleSubmit}
>
<input
value={value}
type='text'
type="text"
onChange={(e) => {
onChange(e.target.value);
}}
className='search__field'
placeholder='What are you looking for?'
className="search__field"
placeholder="What are you looking for?"
ref={ref}
/>
<span className={classNames('search__button', { 'is-open': open })}>
<button type='submit' className='button'>
<span className='icon icon--search button__icon' />
<span className={classNames("search__button", { "is-open": open })}>
<button type="submit" className="button">
<span className="icon icon--search button__icon" />
Search
</button>
</span>
{closable && (
<button className={classNames('search__close link-button', { 'is-open': open })} onClick={handleClose}>
<span className='icon icon--close' />
<button
className={classNames("search__close link-button", {
"is-open": open,
})}
onClick={handleClose}
>
<span className="icon icon--close" />
</button>
)}
</form>

View File

@@ -11,13 +11,13 @@
width: 100%;
box-sizing: border-box;
padding: 10px 120px 10px 20px;
background: #3A444A;
background: #3a444a;
display: block;
border: none;
color: var(--color-white);
font-size: 16px;
line-height: 1;
font-family: 'DIN';
font-family: "DIN";
font-weight: normal;
&::placeholder {
@@ -26,7 +26,7 @@
&:focus {
outline: none;
background: #2F393F;
background: #2f393f;
}
}

View File

@@ -1,14 +1,14 @@
import {useEffect} from "react";
import {radarName} from "../config";
import { useEffect } from "react";
import { radarName } from "../config";
type SetTitleProps = {
title: string
}
title: string;
};
export default function SetTitle({title}: SetTitleProps) {
export default function SetTitle({ title }: SetTitleProps) {
useEffect(() => {
document.title = `${title} | ${radarName}`
}, [title])
document.title = `${title} | ${radarName}`;
}, [title]);
return null;
}

View File

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

View File

@@ -1,30 +1,29 @@
@font-face {
font-family: 'DIN';
src: url('./fonts/clanot-news.otf');
font-family: "DIN";
src: url("./fonts/clanot-news.otf");
font-weight: normal;
}
@font-face {
font-family: 'DIN';
src: url('./fonts/clanot-thin.otf');
font-family: "DIN";
src: url("./fonts/clanot-thin.otf");
font-weight: 300;
}
:root {
--color-gray-dark: #475157;
--color-gray-dark-alt: #4F585E;
--color-gray-dark-alt2: #434D53;
--color-gray-dark-alt: #4f585e;
--color-gray-dark-alt2: #434d53;
--color-gray-normal: #7f858a;
--color-gray-light: #7D878D;
--color-gray-light: #7d878d;
--color-white: #fff;
--color-green: #5CB449;
--color-orange: #FAA03D;
--color-blue: #40A7D1;
--color-green: #5cb449;
--color-orange: #faa03d;
--color-blue: #40a7d1;
--color-marine: #688190;
--color-red: #F1235A;
--color-brand: #F59134;
--color-red: #f1235a;
--color-brand: #f59134;
--until-sm: 30em;
--until-md: 48em;
--until-lg: 61.875em;
@@ -36,14 +35,12 @@
// @custom-media --until-lg (max-width: 61.875em);
// @custom-media --until-xl (max-width: 75em);
body {
background: var(--color-gray-dark);
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'DIN';
font-family: "DIN";
font-weight: normal;
font-size: 14px;
line-height: 1.5;
@@ -58,4 +55,4 @@ body {
overflow: hidden;
}
@import './styles/main.scss';
@import "./styles/main.scss";

View File

@@ -1,49 +1,85 @@
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
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];
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) {
};
var __spreadArray =
(this && this.__spreadArray) ||
function (to, from) {
for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
to[j] = from[i];
to[j] = from[i];
return to;
};
export var featuredOnly = function (items) {
return items.filter(function (item) {
return item.featured;
});
};
export var featuredOnly = function (items) { return items.filter(function (item) { return item.featured; }); };
export 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)));
}, {});
return items.reduce(function (quadrants, item) {
var _a;
return __assign(
__assign({}, quadrants),
((_a = {}),
(_a[item.quadrant] = addItemToQuadrant(quadrants[item.quadrant], item)),
_a)
);
}, {});
};
export var groupByFirstLetter = function (items) {
var index = items.reduce(function (letterIndex, item) {
var _a;
return (__assign(__assign({}, letterIndex), (_a = {}, _a[getFirstLetter(item)] = addItemToList(letterIndex[getFirstLetter(item)], item), _a)));
}, {});
return Object.keys(index)
.sort()
.map(function (letter) { return ({
var index = items.reduce(function (letterIndex, item) {
var _a;
return __assign(
__assign({}, letterIndex),
((_a = {}),
(_a[getFirstLetter(item)] = addItemToList(
letterIndex[getFirstLetter(item)],
item
)),
_a)
);
}, {});
return Object.keys(index)
.sort()
.map(function (letter) {
return {
letter: letter,
items: index[letter],
}); });
};
});
};
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 _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), [item]);
if (list === void 0) {
list = [];
}
return __spreadArray(__spreadArray([], list), [item]);
};
var addItemToRing = function (ring, item) {
if (ring === void 0) { ring = []; }
return __spreadArray(__spreadArray([], ring), [item]);
if (ring === void 0) {
ring = [];
}
return __spreadArray(__spreadArray([], ring), [item]);
};
export var getFirstLetter = function (item) {
return item.title.substr(0, 1).toUpperCase();
};
export var getFirstLetter = function (item) { return item.title.substr(0, 1).toUpperCase(); };

View File

@@ -1,43 +1,44 @@
import { Ring } from "./config"
import { Ring } from "./config";
export type ItemAttributes = {
name: string
ring: Ring
quadrant: string
title: string
featured: boolean
}
name: string;
ring: Ring;
quadrant: string;
title: string;
featured: boolean;
};
export type FlagType = 'new' | 'changed' | 'default'
export type FlagType = "new" | "changed" | "default";
export type Item = ItemAttributes & {
featured: boolean
body: string
info: string
flag: FlagType
revisions: Revision[]
}
featured: boolean;
body: string;
info: string;
flag: FlagType;
revisions: Revision[];
};
export type Revision = ItemAttributes & {
body: string
fileName: string
release: string
}
body: string;
fileName: string;
release: string;
};
export type Quadrant = {
[name: string]: Item[]
}
[name: string]: Item[];
};
export type Radar = {
items: Item[]
releases: string[]
}
items: Item[];
releases: string[];
};
export type Group = {
[quadrant: string]: Quadrant
}
[quadrant: string]: Quadrant;
};
export const featuredOnly = (items: Item[]) => items.filter(item => item.featured);
export const featuredOnly = (items: Item[]) =>
items.filter((item) => item.featured);
export const groupByQuadrants = (items: Item[]): Group =>
items.reduce(
@@ -45,7 +46,7 @@ export const groupByQuadrants = (items: Item[]): Group =>
...quadrants,
[item.quadrant]: addItemToQuadrant(quadrants[item.quadrant], item),
}),
{} as {[k: string]: Quadrant},
{} as { [k: string]: Quadrant }
);
export const groupByFirstLetter = (items: Item[]) => {
@@ -54,15 +55,15 @@ export const groupByFirstLetter = (items: Item[]) => {
...letterIndex,
[getFirstLetter(item)]: addItemToList(
letterIndex[getFirstLetter(item)],
item,
item
),
}),
{} as {[k: string]: Item[]},
{} as { [k: string]: Item[] }
);
return Object.keys(index)
.sort()
.map(letter => ({
.map((letter) => ({
letter,
items: index[letter],
}));
@@ -77,4 +78,5 @@ const addItemToList = (list: Item[] = [], item: Item) => [...list, item];
const addItemToRing = (ring: Item[] = [], item: Item) => [...ring, item];
export const getFirstLetter = (item: Item) => item.title.substr(0, 1).toUpperCase();
export const getFirstLetter = (item: Item) =>
item.title.substr(0, 1).toUpperCase();

View File

@@ -2,4 +2,4 @@
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
import "@testing-library/jest-dom";

View File

@@ -1,15 +1,13 @@
.fullpage-content {
font-size: 16px;
color: var(--color-white);
font-size: 16px;
& p,
& ul {
font-weight: lighter;
}
& a {
color: var(--color-white);
& p,
& ul {
font-weight: lighter
}
& a {
color: var(--color-white);
}
}
}
}

View File

@@ -1,4 +1,4 @@
@import '../../styles/sccs-vars.scss';
@import "../../styles/sccs-vars.scss";
.filter {
margin-bottom: 40px;
@@ -7,4 +7,4 @@
@media (max-width: $until-sm) {
display: none;
}
}
}

View File

@@ -1,4 +1,4 @@
.footnote {
font-size: 12px;
color: var(--color-gray-normal);
}
}

View File

@@ -1,2 +1,61 @@
/* copied from here: http://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.10.0/styles/darcula.min.css*/
.hljs{display:block;overflow-x:auto;padding:0.5em;background:#2b2b2b}.hljs{color:#bababa}.hljs-strong,.hljs-emphasis{color:#a8a8a2}.hljs-bullet,.hljs-quote,.hljs-link,.hljs-number,.hljs-regexp,.hljs-literal{color:#6896ba}.hljs-code,.hljs-selector-class{color:#a6e22e}.hljs-emphasis{font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-section,.hljs-attribute,.hljs-name,.hljs-variable{color:#cb7832}.hljs-params{color:#b9b9b9}.hljs-string{color:#6a8759}.hljs-subst,.hljs-type,.hljs-built_in,.hljs-builtin-name,.hljs-symbol,.hljs-selector-id,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-template-tag,.hljs-template-variable,.hljs-addition{color:#e0c46c}.hljs-comment,.hljs-deletion,.hljs-meta{color:#7f7f7f}
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #2b2b2b;
}
.hljs {
color: #bababa;
}
.hljs-strong,
.hljs-emphasis {
color: #a8a8a2;
}
.hljs-bullet,
.hljs-quote,
.hljs-link,
.hljs-number,
.hljs-regexp,
.hljs-literal {
color: #6896ba;
}
.hljs-code,
.hljs-selector-class {
color: #a6e22e;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-section,
.hljs-attribute,
.hljs-name,
.hljs-variable {
color: #cb7832;
}
.hljs-params {
color: #b9b9b9;
}
.hljs-string {
color: #6a8759;
}
.hljs-subst,
.hljs-type,
.hljs-built_in,
.hljs-builtin-name,
.hljs-symbol,
.hljs-selector-id,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-template-tag,
.hljs-template-variable,
.hljs-addition {
color: #e0c46c;
}
.hljs-comment,
.hljs-deletion,
.hljs-meta {
color: #7f7f7f;
}

View File

@@ -25,7 +25,7 @@
&:hover {
&:after {
content: '';
content: "";
position: absolute;
left: 0;
bottom: -8px;
@@ -33,4 +33,4 @@
border-bottom: 2px solid var(--color-gray-normal);
}
}
}
}

View File

@@ -8,26 +8,26 @@
vertical-align: middle;
&--pie {
background-image: url('../../icons/pie.svg');
background-image: url("../../icons/pie.svg");
}
&--question {
background-image: url('../../icons/question.svg');
background-image: url("../../icons/question.svg");
}
&--overview {
background-image: url('../../icons/overview.svg');
background-image: url("../../icons/overview.svg");
}
&--search {
background-image: url('../../icons/search.svg');
background-image: url("../../icons/search.svg");
}
&--back {
background-image: url('../../icons/back.svg');
background-image: url("../../icons/back.svg");
}
&--close {
background-image: url('../../icons/close.svg');
background-image: url("../../icons/close.svg");
}
}
}

View File

@@ -1,4 +1,4 @@
@import '../../styles/sccs-vars.scss';
@import "../../styles/sccs-vars.scss";
.letter-index {
margin-bottom: 60px;
@@ -26,4 +26,4 @@
left: 0;
}
}
}
}

View File

@@ -1,4 +1,4 @@
@import '../../styles/sccs-vars.scss';
@import "../../styles/sccs-vars.scss";
.nav {
white-space: nowrap;
@@ -7,7 +7,7 @@
display: inline-flex;
position: relative;
&+.nav__item {
& + .nav__item {
margin-left: 20px;
}
}
@@ -21,10 +21,8 @@
width: 0;
margin-top: -25px;
opacity: 0.8;
transition:
width 400ms cubic-bezier(0.24, 1.12, 0.71, 0.98) 100ms,
visibility 0s linear 500ms,
opacity 200ms linear;
transition: width 400ms cubic-bezier(0.24, 1.12, 0.71, 0.98) 100ms,
visibility 0s linear 500ms, opacity 200ms linear;
&.is-open {
opacity: 1;
@@ -42,4 +40,4 @@
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
@import '../../styles/sccs-vars.scss';
@import "../../styles/sccs-vars.scss";
.page {
max-width: 1200px;
@@ -16,7 +16,7 @@
z-index: 100;
@media (max-width: $until-sm) {
margin: 0
margin: 0;
}
@media (max-width: $until-lg) {
@@ -34,4 +34,4 @@
margin-top: 5px;
flex: 0 0 auto;
}
}
}

View File

@@ -1,4 +1,4 @@
@import '../../styles/sccs-vars.scss';
@import "../../styles/sccs-vars.scss";
.publish-date {
color: var(--color-gray-normal);
@@ -7,4 +7,4 @@
@media (max-width: $until-sm) {
text-align: center;
}
}
}

View File

@@ -1,8 +1,7 @@
.social-icon {
font-weight: normal;
font-style: normal;
font-size: 16px; /* Preferred icon size */
font-size: 16px; /* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
@@ -24,6 +23,5 @@
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: 'liga';
}
font-feature-settings: "liga";
}

View File

@@ -1,4 +1,4 @@
@import '../../styles/sccs-vars.scss';
@import "../../styles/sccs-vars.scss";
.split {
display: flex;
@@ -24,4 +24,4 @@
@media (max-width: $until-sm) {
display: block;
}
}
}

View File

@@ -13,4 +13,4 @@
@import "./components/publish-date";
@import "./components/ring-list";
@import "./components/social-icon";
@import "./components/split";
@import "./components/split";

View File

@@ -1,4 +1,4 @@
$until-sm: 30em;
$until-md: 48em;
$until-lg: 61.875em;
$until-xl: 75em;
$until-xl: 75em;