use typescript

This commit is contained in:
Bastian Ike
2020-07-16 11:07:42 +02:00
committed by Bastian
parent 442d964180
commit be0241674c
93 changed files with 9750 additions and 2190 deletions

View File

@@ -1,4 +0,0 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": ["@babel/plugin-proposal-class-properties"]
}

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@ node_modules
npm-debug.log npm-debug.log
yarn-error.log yarn-error.log
aoe_technology_radar.iml aoe_technology_radar.iml
build

View File

@@ -1,50 +0,0 @@
export const featuredOnly = items => items.filter(item => item.featured);
export const groupByQuadrants = items =>
items.reduce(
(quadrants, item) => ({
...quadrants,
[item.quadrant]: addItemToQuadrant(quadrants[item.quadrant], item),
}),
{},
);
export const groupByRing = items =>
items.reduce(
(rings, item) => ({
...rings,
[item.ring]: addItemToList(rings[item.ring], item),
}),
{},
);
export const groupByFirstLetter = items => {
const index = items.reduce(
(letterIndex, item) => ({
...letterIndex,
[getFirstLetter(item)]: addItemToList(
letterIndex[getFirstLetter(item)],
item,
),
}),
{},
);
return Object.keys(index)
.sort()
.map(letter => ({
letter,
items: index[letter],
}));
};
const addItemToQuadrant = (quadrant = {}, item) => ({
...quadrant,
[item.ring]: addItemToRing(quadrant[item.ring], item),
});
const addItemToList = (list = [], item) => [...list, item];
const addItemToRing = (ring = [], item) => [...ring, item];
export const getFirstLetter = item => item.title.substr(0, 1).toUpperCase();

View File

@@ -1,14 +0,0 @@
export const NAVIGATE = 'navigate';
const actions = {
navigate: (pageName, pushToHistory = true, pageState = {}) => {
return {
type: NAVIGATE,
pageName,
pageState,
pushToHistory,
};
},
};
export default actions;

View File

@@ -1,85 +0,0 @@
import '@babel/polyfill';
import React from 'react';
import { hydrate } from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import createHistory from 'history/createBrowserHistory';
import App from './components/App';
import appReducer from './reducer';
import actions, { NAVIGATE } from './actions';
import { isMobileViewport, radarName } from '../common/config';
import { track } from './analytics';
// Remove .html and map / to index
const getPageNameFromPath = path => {
if (path === '/') {
return 'index';
}
return path.substring(1, path.length - 5);
};
const historyManager = store => {
const history = createHistory({
basename: '/techradar',
});
// If browser-back button is pressed, provide new pageName to store
history.listen((location, action) => {
if (action === 'POP') {
const pageName = getPageNameFromPath(location.pathname);
store.dispatch(actions.navigate(pageName, false));
}
});
return next => action => {
if (action.type === NAVIGATE && action.pushToHistory === true) {
window.scrollTo(0, 0);
history.push(`/${action.pageName}.html`);
}
return next(action);
};
};
let reloadTimeout;
let wasMobileViewport = isMobileViewport();
window.addEventListener('resize', function() {
clearTimeout(reloadTimeout);
reloadTimeout = setTimeout(() => {
if (wasMobileViewport != isMobileViewport()) window.location.reload();
}, 200);
});
// Grab the state from a global variable injected into the server-generated HTML
const preloadedState = window.__TECHRADAR__;
// Allow the passed state to be garbage-collected
delete window.__TECHRADAR__;
// Create Redux store with initial state
const store = createStore(
appReducer,
preloadedState,
applyMiddleware(historyManager),
);
const handleSetTitle = title => {
document.title = `${title} | ${radarName}`;
track();
};
// FIXME!
// This is a quiet ugly hack, that is needed to hydrate the react root correctly on mobile:
// The main issue is, that we render different components based on the view port - which means that
// the server-generated DOM differs from the DOM that would be rendered on mobile devices.
// We therefore completely ignore the pre-rendered DOM on mobile and let the client render.
// To solve this root of this issue, we need to remove the conditional renderings that check "isMobileViewport()"!
if (isMobileViewport()) {
document.getElementById('root').innerHTML = '';
}
hydrate(
<Provider store={store}>
<App onSetTitle={handleSetTitle} />
</Provider>,
document.getElementById('root'),
);

View File

@@ -1,38 +0,0 @@
import React from 'react';
import classNames from 'classnames';
import { connect } from 'react-redux';
import actions from '../actions';
import Header from './Header';
import Footer from './Footer';
import Router from './Router';
function App(props) {
return (
<div>
<div className="page">
<div className="page__header">
<Header {...props} />
</div>
<div
className={classNames('page__content', { 'is-faded': props.isFaded })}
>
<Router {...props} />
</div>
<div className="page__footer">
<Footer {...props} />
</div>
</div>
</div>
);
}
export default connect(
({ items, releases, pageName, pageState }) => ({
items,
releases,
pageName,
pageState,
}),
actions,
)(App);

View File

@@ -1,22 +0,0 @@
import React from 'react';
import classNames from 'classnames';
export default function Badge({ onClick, big, type, children }) {
const Comp = typeof onClick === 'function' ? 'a' : 'span';
return (
<Comp
className={classNames(
'badge',
`badge--${type}`,
{
'badge--big': big === true,
}
)}
onClick={onClick}
href={Comp === 'a' ? '#' : undefined}
>
{children}
</Comp>
);
}

View File

@@ -1,45 +0,0 @@
import React from 'react';
import classNames from 'classnames';
class Fadeable extends React.Component {
constructor(props) {
super(props);
this.state = {
faded: props.leaving,
};
}
componentWillReceiveProps({ leaving }) {
if (!this.props.leaving && leaving) {
this.setState({
...this.state,
faded: true,
});
}
if (this.props.leaving && !leaving) {
this.setState({
...this.state,
faded: false,
});
}
}
handleTransitionEnd = () => {
if (this.state.faded) {
this.props.onLeave();
}
};
render() {
return (
<div
className={classNames('fadable', { 'is-faded': this.state.faded })}
onTransitionEnd={this.handleTransitionEnd}
>
{this.props.children}
</div>
);
}
}
export default Fadeable;

View File

@@ -1,103 +0,0 @@
import React from 'react';
import classNames from 'classnames';
import { connect } from 'react-redux';
import Branding from './Branding';
import Link from './Link';
import LogoLink from './LogoLink';
import Search from './Search';
import actions from '../actions';
import { radarNameShort } from '../../common/config';
class Header extends React.Component {
constructor(...args) {
super(...args);
this.state = {
searchOpen: false,
search: '',
};
}
openSearch = () => {
this.setState({
searchOpen: true,
});
}
closeSearch = () => {
this.setState({
searchOpen: false,
});
}
handleSearchChange = (search) => {
this.setState({
search,
});
}
handleSearchSubmit = () => {
this.props.navigate('overview', true, {
search: this.state.search,
});
this.setState({
searchOpen: false,
search: '',
});
}
handleOpenClick = (e) => {
e.preventDefault();
this.openSearch();
setTimeout(() => {
this.search.focus();
}, 0);
}
render() {
const { pageName } = this.props;
const { searchOpen } = this.state;
const smallLogo = pageName !== 'index';
return (
<Branding
logoContent={<LogoLink small={smallLogo} />}
>
<div className="nav">
<div className="nav__item">
<Link pageName="help-and-about-tech-radar" className="icon-link">
<span className="icon icon--question icon-link__icon"></span>How to Use {radarNameShort}?
</Link>
</div>
<div className="nav__item">
<Link pageName="overview" className="icon-link">
<span className="icon icon--overview icon-link__icon"></span>Technologies Overview
</Link>
</div>
<div className="nav__item">
<a className="icon-link" href="#" onClick={this.handleOpenClick}>
<span className="icon icon--search icon-link__icon"></span>Search
</a>
<div className={classNames('nav__search', { 'is-open': searchOpen })}>
<Search
value={this.state.search}
onClose={this.closeSearch}
onSubmit={this.handleSearchSubmit}
onChange={this.handleSearchChange}
open={searchOpen}
ref={(s) => { this.search = s; }}
/>
</div>
</div>
</div>
</Branding>
);
}
}
export default connect(
undefined,
actions
)(Header);

View File

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

View File

@@ -1,7 +0,0 @@
import React from 'react';
function Icon({ name, ...props }) {
return (
<span/>
);
}

View File

@@ -1,21 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import actions from '../actions';
function Link({ pageName, children, navigate, className, style = {}}) {
const handleClick = (e) => {
e.preventDefault();
navigate(pageName);
};
return (
<a href={`/techradar/${pageName}.html`} onClick={handleClick} style={style} {...{ className }}>
{children}
</a>
);
}
export default connect(
undefined,
actions
)(Link);

View File

@@ -1,273 +0,0 @@
import React from 'react';
import Badge from './Badge';
import ItemList from './ItemList';
import Link from './Link';
import FooterEnd from './FooterEnd';
import SetTitle from './SetTitle';
import ItemRevisions from './ItemRevisions';
import { createAnimation, createAnimationRunner } from '../animation';
import { translate } from '../../common/config';
import { groupByQuadrants } from '../../common/model';
const setAnimations = (state, animations) => ({
...state,
animations,
});
class PageItem extends React.Component {
constructor(props) {
super(props);
const itemsInRing = this.getItemsInRing(props);
this.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,
),
};
this.animationsOut = {
background: createAnimation(
this.animationsIn.background.stateB,
this.animationsIn.background.stateA,
300 + itemsInRing.length * 50,
),
navHeader: createAnimation(
this.animationsIn.navHeader.stateB,
{
transition: 'opacity 150ms ease-out, transform 300ms ease-out',
transform: 'translateX(40px)',
opacity: '0',
},
0,
),
text: createAnimation(
this.animationsIn.text.stateB,
{
transform: 'translateY(20px)',
transition: 'opacity 150ms ease-out, transform 300ms ease-out',
opacity: '0',
},
0,
),
items: itemsInRing.map((item, i) =>
createAnimation(
this.animationsIn.items[i].stateB,
{
transition: 'opacity 150ms ease-out, transform 300ms ease-out',
transform: 'translateX(40px)',
opacity: '0',
},
100 + 50 * i,
),
),
footer: createAnimation(
this.animationsIn.text.stateB,
{
transition: 'opacity 150ms ease-out, transform 300ms ease-out',
transform: 'translateX(40px)',
opacity: '0',
},
200 + itemsInRing.length * 50,
),
};
if (props.leaving) {
// entering from an other page
this.state = setAnimations(
{},
createAnimationRunner(this.animationsIn).getState(),
);
} else {
// Hard refresh
this.state = {};
}
}
componentWillReceiveProps({ leaving }) {
if (!this.props.leaving && leaving) {
// page will be left
this.animationRunner = createAnimationRunner(
this.animationsOut,
this.handleAnimationsUpdate,
);
this.animationRunner.run();
this.animationRunner.awaitAnimationComplete(this.props.onLeave);
}
if (this.props.leaving && !leaving) {
// page is entered
this.animationRunner = createAnimationRunner(
this.animationsIn,
this.handleAnimationsUpdate,
);
this.animationRunner.run();
}
}
handleAnimationsUpdate = () => {
this.setState(setAnimations(this.state, this.animationRunner.getState()));
};
getAnimationState = name => {
if (!this.state.animations) {
return undefined;
}
return this.state.animations[name];
};
getItem = props => {
const [quadrantName, itemName] = props.pageName.split('/');
const item = props.items.filter(
item => item.quadrant === quadrantName && item.name === itemName,
)[0];
return item;
};
getItemsInRing = props => {
const item = this.getItem(props);
const itemsInRing = groupByQuadrants(props.items)[item.quadrant][item.ring];
return itemsInRing;
};
render() {
const item = this.getItem(this.props);
const itemsInRing = this.getItemsInRing(this.props);
return (
<div>
<SetTitle {...this.props} title={item.title} />
<div className="item-page">
<div className="item-page__nav">
<div className="item-page__nav__inner">
<div
className="item-page__header"
style={this.getAnimationState('navHeader')}
>
<h3 className="headline">{translate(item.quadrant)}</h3>
</div>
<ItemList
items={itemsInRing}
activeItem={item}
headerStyle={this.getAnimationState('navHeader')}
itemStyle={this.getAnimationState('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={this.getAnimationState('footer')}
>
<FooterEnd modifier="in-sidebar" />
</div>
</div>
</div>
<div
className="item-page__content"
style={this.getAnimationState('background')}
>
<div
className="item-page__content__inner"
style={this.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>
);
}
}
export default PageItem;

View File

@@ -1,73 +0,0 @@
import React from 'react';
import Badge from './Badge';
import ItemList from './ItemList';
import Link from './Link';
import Fadeable from './Fadeable';
import SetTitle from './SetTitle';
import ItemRevisions from './ItemRevisions';
import { translate } from '../../common/config';
import { groupByQuadrants } from '../../common/model';
class PageItem extends React.Component {
getItem = (props) => {
const [quadrantName, itemName] = props.pageName.split('/');
const item = props.items.filter(item => item.quadrant === quadrantName && item.name === itemName)[0];
return item;
}
getItemsInRing = (props) => {
const item = this.getItem(props);
const itemsInRing = groupByQuadrants(props.items)[item.quadrant][item.ring];
return itemsInRing;
};
render() {
const item = this.getItem(this.props);
const itemsInRing = this.getItemsInRing(this.props);
return (
<Fadeable leaving={this.props.leaving} onLeave={this.props.onLeave}>
<SetTitle {...this.props} 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>
<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>
<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>
<div className="split__right">
<Link className="icon-link" pageName={item.quadrant}>
<span className="icon icon--pie icon-link__icon"></span>Zoom In
</Link>
</div>
</div>
</ItemList>
</aside>
</Fadeable>
);
}
}
export default PageItem;

View File

@@ -1,169 +0,0 @@
import React from 'react';
import HeadlineGroup from './HeadlineGroup';
import HeroHeadline from './HeroHeadline';
import Badge from './Badge';
import Link from './Link';
import Search from './Search';
import Fadeable from './Fadeable';
import SetTitle from './SetTitle';
import Flag from './Flag';
import { groupByFirstLetter } from '../../common/model';
import { translate } from '../../common/config';
const rings = ['all', 'assess', 'trial', 'hold', 'adopt'];
const containsSearchTerm = (text = '', term = '') => {
// TODO search refinement
return (
text
.trim()
.toLocaleLowerCase()
.indexOf(term.trim().toLocaleLowerCase()) !== -1
);
};
class PageOverview extends React.Component {
constructor(props, ...args) {
super(props, ...args);
this.state = {
ring: rings[0],
search: props.pageState.search || '',
};
}
componentWillReceiveProps(nextProps) {
if (this.search !== nextProps.pageState.search) {
this.setState({
ring: rings[0],
search: nextProps.pageState.search,
});
}
}
handleRingClick = ring => e => {
e.preventDefault();
this.setState({
ring,
});
};
isRingActive(ringName) {
return this.state.ring === ringName;
}
itemMatchesRing = item => {
return this.state.ring === 'all' || item.ring === this.state.ring;
};
itemMatchesSearch = item => {
return (
this.state.search.trim() === '' ||
containsSearchTerm(item.title, this.state.search) ||
containsSearchTerm(item.body, this.state.search) ||
containsSearchTerm(item.info, this.state.search)
);
};
isItemVisible = item => {
return this.itemMatchesRing(item) && this.itemMatchesSearch(item);
};
getFilteredAndGroupedItems() {
const groups = groupByFirstLetter(this.props.items);
const groupsFiltered = groups.map(group => ({
...group,
items: group.items.filter(this.isItemVisible),
}));
const nonEmptyGroups = groupsFiltered.filter(
group => group.items.length > 0,
);
return nonEmptyGroups;
}
handleSearchTermChange = value => {
this.setState({
search: value,
});
};
render() {
const groups = this.getFilteredAndGroupedItems();
return (
<Fadeable leaving={this.props.leaving} onLeave={this.props.onLeave}>
<SetTitle {...this.props} title="Technologies Overview" />
<HeadlineGroup>
<HeroHeadline>Technologies Overview</HeroHeadline>
</HeadlineGroup>
<div className="filter">
<div className="split split--filter">
<div className="split__left">
<Search
onChange={this.handleSearchTermChange}
value={this.state.search}
/>
</div>
<div className="split__right">
<div className="nav">
{rings.map(ringName => (
<div className="nav__item" key={ringName}>
<Badge
big
onClick={this.handleRingClick(ringName)}
type={this.isRingActive(ringName) ? ringName : 'empty'}
>
{ringName}
</Badge>
</div>
))}
</div>
</div>
</div>
</div>
<div className="letter-index">
{groups.map(({ letter, items }) => (
<div key={letter} className="letter-index__group">
<div className="letter-index__letter">{letter}</div>
<div className="letter-index__items">
<div className="item-list">
<div className="item-list__list">
{items.map(item => (
<Link
key={item.name}
className="item item--big item--no-leading-border item--no-trailing-border"
pageName={`${item.quadrant}/${item.name}`}
>
<div className="split split--overview">
<div className="split__left">
<div className="item__title">
{item.title}
<Flag item={item} />
</div>
</div>
<div className="split__right">
<div className="nav nav--relations">
<div className="nav__item">
{translate(item.quadrant)}
</div>
<div className="nav__item">
<Badge type={item.ring}>{item.ring}</Badge>
</div>
</div>
</div>
</div>
</Link>
))}
</div>
</div>
</div>
</div>
))}
</div>
</Fadeable>
);
}
}
export default PageOverview;

View File

@@ -1,82 +0,0 @@
import React from 'react';
import PageIndex from './PageIndex';
import PageOverview from './PageOverview';
import PageHelp from './PageHelp';
import PageQuadrant from './PageQuadrant';
import PageItem from './PageItem';
import PageItemMobile from './PageItemMobile';
import { quadrants, getItemPageNames, isMobileViewport } from '../../common/config';
const getPageByName = (items, pageName) => {
if (pageName === 'index') {
return PageIndex;
}
if (pageName === 'overview') {
return PageOverview;
}
if (pageName === 'help-and-about-tech-radar') {
return PageHelp;
}
if (quadrants.includes(pageName)) {
return PageQuadrant;
}
if (getItemPageNames(items).includes(pageName)) {
return isMobileViewport() ? PageItemMobile : PageItem;
}
return 'div';
}
class Router extends React.Component {
constructor(props) {
super(props);
this.state = {
pageName: props.pageName,
leaving: false,
};
}
componentWillReceiveProps({ pageName }) {
const leaving = getPageByName(this.props.items, pageName) !== getPageByName(this.props.items, this.state.pageName);
if (leaving) {
this.setState({
...this.state,
nextPageName: pageName,
leaving: true,
});
} else {
// stay on same page
this.setState({
...this.state,
pageName,
});
}
}
handlePageLeave = () => {
this.setState({
...this.state,
pageName: this.state.nextPageName,
leaving: true,
nextPageName: null,
});
window.setTimeout(() => {
window.requestAnimationFrame(() => {
this.setState({
...this.state,
leaving: false,
});
});
}, 0);
};
render() {
const { pageName, leaving } = this.state;
const Comp = getPageByName(this.props.items, pageName);
return <Comp {...this.props} pageName={pageName} leaving={leaving} onLeave={this.handlePageLeave} />;
}
}
export default Router;

View File

@@ -1,52 +0,0 @@
import React from 'react';
import classNames from 'classnames';
class Search extends React.Component {
focus() {
this.input.focus();
}
render() {
const { value, onChange, onClose, open = false, onSubmit = () => {} } = this.props;
const closable = typeof onClose === 'function';
const handleSubmit = (e) => {
e.preventDefault();
onSubmit();
};
const handleClose = (e) => {
e.preventDefault();
onClose();
}
return (
<form className={classNames('search', { 'search--closable': closable })} onSubmit={handleSubmit}>
<input
value={value}
type="text"
onChange={(e) => { onChange(e.target.value); }}
className="search__field"
placeholder="What are you looking for?"
ref={(input) => { this.input = input; }}
/>
<span className={classNames('search__button', { 'is-open': open })}>
<button type="submit" className="button">
<span className="icon icon--search button__icon" />
Search
</button>
</span>
{
closable && (
<a href="#" className={classNames('search__close', { 'is-open': open })} onClick={handleClose}>
<span className="icon icon--close" />
</a>
)
}
</form>
);
}
}
export default Search;

View File

@@ -1,27 +0,0 @@
import React from 'react';
const callSetTitle = (props) => {
if (typeof props.onSetTitle === 'function' && props.title) {
props.onSetTitle(props.title);
}
};
class SetTitle extends React.Component {
constructor(props) {
super(props);
callSetTitle(props);
}
componentWillReceiveProps(nextProps) {
if (nextProps.title !== this.props.title) {
callSetTitle(nextProps);
}
}
render() {
return null;
}
}
export default SetTitle;

View File

@@ -1,6 +0,0 @@
import moment from 'moment';
const isoDateToMoment = isoDate => moment(isoDate, 'YYYY-MM-DD');
export const formatRelease = isoDate =>
isoDateToMoment(isoDate).format('MMMM YYYY');

View File

@@ -1,109 +0,0 @@
import Vue from 'vue';
import applyPjax from './pjax';
const af = (() => {
if (!window.requestAnimationFrame) {
return (
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
((callback, element) => {
window.setTimeout( callback, 1000 / 60 );
})
);
}
return window.requestAnimationFrame;
})();
const animation = (steps) => {
steps.map(([timeout, step]) => {
af(() => {
window.setTimeout(step, timeout)
});
});
}
const createAnimation = (stateA, stateB, delay) => ({
style: stateA,
prepare(target) {
this.style = stateA;
},
run(target) {
af(() => {
window.setTimeout(() => {
this.style = stateB;
}, delay)
});
},
});
const initDetails = (element, fromPjax) => {
if(!fromPjax) {
return;
}
const items = JSON.parse(element.getAttribute('data-items'));
const details = new Vue({
el: element,
template: element.outerHTML,
data: {
background: createAnimation({
transform: 'translateX(calc((100vw - 1200px) / 2 + 800px))',
}, {
transition: 'transform 450ms cubic-bezier(0.24, 1.12, 0.71, 0.98)',
transform: 'translateX(0)',
},
0
),
navHeader: createAnimation({
transform: 'translateY(-20px)',
opacity: '0',
}, {
transition: 'opacity 150ms ease-out, transform 300ms ease-out',
transform: 'translateY(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: items.map((item, i) => (createAnimation({
transform: 'translateX(-40px)',
opacity: '0',
}, {
transition: 'opacity 150ms ease-out, transform 300ms ease-out',
transform: 'translateX(0px)',
opacity: '1',
},
200 + 100 * i
)))
},
methods: {
createAnimation
},
mounted() {
applyPjax();
this.background.run();
this.navHeader.run();
this.text.run();
this.items.map(item => item.run());
},
updated() {
applyPjax();
},
});
};
export default initDetails;

View File

@@ -1,38 +0,0 @@
import Vue from 'vue';
import applyPjax from './pjax';
const initFilter = (element) => {
const index = JSON.parse(element.getAttribute('data-index'));
const filter = new Vue({
el: element,
template: element.outerHTML,
data: {
ring: 'all',
index,
},
methods: {
setRing(ring, event) {
if (event) event.preventDefault()
this.ring = ring;
},
isRingVisible(ring) {
return this.ring === 'all' || ring === this.ring;
},
isGroupVisible(letter) {
const itemsInLetter = this.index[letter] || [];
const visibleItems = itemsInLetter.filter((item) => this.isRingVisible(item.ring));
return visibleItems.length > 0;
},
},
mounted() {
applyPjax();
},
updated() {
applyPjax();
},
});
};
export default initFilter;

View File

@@ -1,10 +0,0 @@
import Pjax from 'pjax';
const applyPjax = () => {
new Pjax({
elements: '.js--body a',
selectors: ['title', '.js--body'],
});
};
export default applyPjax;

View File

@@ -1,24 +0,0 @@
import { NAVIGATE } from './actions';
const initialState = {
pageName: 'index',
pageState: {},
items: [],
releases: [],
};
const appReducer = (state = initialState, action = {}) => {
switch (action.type) {
case NAVIGATE:
return {
...state,
pageName: action.pageName,
pageState: action.pageState,
}
break;
default:
return state;
}
}
export default appReducer;

View File

@@ -1,57 +0,0 @@
import React from 'react';
import { renderToString } from 'react-dom/server';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { assetUrl, radarName } from '../common/config'
import App from './components/App';
import appReducer from './reducer';
export const renderPage = (radar, pageName) => {
// Create a new Redux store instance
const store = createStore(appReducer, {
...radar,
pageName,
pageState: {},
});
let pageTitle;
// Render the component to a string
const html = renderToString(
<Provider store={store}>
<App onSetTitle={(title) => { pageTitle = title; }} />
</Provider>
)
// Grab the initial state from our Redux store
const preloadedState = store.getState()
// Send the rendered page back to the client
return renderFullPage(html, pageTitle, preloadedState);
}
const renderFullPage = (html, pageTitle, preloadedState) => {
return `
<html>
<head>
<meta charset="utf-8">
<title>${pageTitle} | ${radarName}</title>
<link rel="stylesheet" href="${assetUrl('css/styles.css')}"/>
<link rel="stylesheet" href="https://d1azc1qln24ryf.cloudfront.net/114779/Socicon/style-cf.css?c2sn1i">
<link rel="shortcut icon" href="${assetUrl('favicon.ico')}" type="image/x-icon">
<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 property="og:title" content="${pageTitle} | ${radarName}" />
<meta property="og:image" content="${assetUrl('logo.svg')}" />
</head>
<body>
<div id="root">${html}</div>
<script>
window.__TECHRADAR__ = ${JSON.stringify(preloadedState)}
</script>
<script src="${assetUrl('js/bundle.js')}"></script>
</body>
</html>
`
}

View File

@@ -1,37 +1,51 @@
{ {
"name": "aoe_technology_radar", "name": "aoe_technology_radar",
"version": "1.0.0", "version": "2.0.0",
"description": "", "description": "AOE Technology Radar",
"main": "index.js",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "yarn clean && yarn build:pages && yarn build:jsprod && yarn build:css && yarn build:assets", "start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"_build": "yarn clean && yarn build:pages && yarn build:jsprod && yarn build:css && yarn build:assets",
"build:all": "yarn build", "build:all": "yarn build",
"build:pages": "cross-env RENDER_MODE=server babel-node ./tasks/build.js", "build:pages": "yarn run ts-node ./src/radarjson.ts",
"build:js": "cross-env RENDER_MODE=client webpack --config webpack.config.js",
"build:jsprod": "cross-env RENDER_MODE=client webpack -p --config webpack.config.js",
"build:css": "postcss -c postcss.config.js -o dist/techradar/assets/css/styles.css styles/main.css", "build:css": "postcss -c postcss.config.js -o dist/techradar/assets/css/styles.css styles/main.css",
"build:assets": "babel-node ./tasks/assets.js", "type-check": "tsc --noEmit"
"watch": "babel-node ./tasks/watch.js", },
"clean": "babel-node ./tasks/clean.js", "browserslist": {
"test": "echo \"Error: no test specified\" && exit 1" "production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}, },
"author": "AOE GmbH <contact-de@aoe.com> (http://www.aoe.com)", "author": "AOE GmbH <contact-de@aoe.com> (http://www.aoe.com)",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@babel/cli": "^7.10.5", "@testing-library/jest-dom": "^4.2.4",
"@babel/core": "^7.10.5", "@testing-library/react": "^9.3.2",
"@babel/node": "^7.10.5", "@testing-library/user-event": "^7.1.2",
"@babel/polyfill": "^7.10.4", "@types/classnames": "^2.2.10",
"@babel/preset-env": "^7.10.4", "@types/fs-extra": "^9.0.1",
"@babel/preset-react": "^7.10.4", "@types/highlight.js": "^9.12.4",
"autoprefixer": "7.1.6", "@types/jest": "^24.0.0",
"babel-loader": "^8.1.0", "@types/marked": "^1.1.0",
"classnames": "2.2.5", "@types/node": "^12.0.0",
"cross-env": "^5.1.1", "@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@types/react-router-dom": "^5.1.5",
"@types/walk": "^2.3.0",
"classnames": "^2.2.6",
"css-mqpacker": "^6.0.1", "css-mqpacker": "^6.0.1",
"csstype": "^2.6.11",
"front-matter": "2.3.0", "front-matter": "2.3.0",
"fs-extra": "4.0.2", "fs-extra": "^9.0.1",
"highlight.js": "9.12.0", "highlight.js": "9.12.0",
"history": "4.7.2", "history": "4.7.2",
"live-server": "1.2.1", "live-server": "1.2.1",
@@ -42,12 +56,16 @@
"postcss-custom-media": "^6.0.0", "postcss-custom-media": "^6.0.0",
"postcss-easy-import": "3.0.0", "postcss-easy-import": "3.0.0",
"postcss-nested": "2.1.2", "postcss-nested": "2.1.2",
"react": "16.1.1", "react": "^16.13.1",
"react-dom": "16.12.0", "react-dom": "16.12.0",
"react-redux": "5.0.6", "react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"redux": "3.7.2", "redux": "3.7.2",
"walk": "2.3.9", "ts-loader": "^8.0.1",
"webpack": "3.8.1" "ts-node": "^8.10.2",
"typescript": "^3.9.6",
"walk": "2.3.9"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 833 B

After

Width:  |  Height:  |  Size: 833 B

View File

Before

Width:  |  Height:  |  Size: 830 B

After

Width:  |  Height:  |  Size: 830 B

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 164 KiB

View File

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 239 KiB

43
public/index.html Normal file
View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using create-react-app" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<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">
<link rel="stylesheet" href="%PUBLIC_URL%/styles.css"/>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
public/manifest.json Normal file
View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

1003
public/styles.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,24 @@
export const createAnimationController = (animations, component) => { 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
}
type AnimationController = {}
type AnimationRunner = {
getState(): any
run(): any
awaitAnimationComplete(callback: () => void): any
}
export const createAnimationController = (animations: {[k: string]: Animation}, component: any): AnimationController => {
return { return {
animations, animations,
start: () => { start: () => {
Object.entries(animations).map(([name, animation]) => animation.run((state) => { Object.entries(animations).map(([name, animation]) => animation.run && animation.run((state) => {
component.setState({ component.setState({
...component.state, ...component.state,
[name]: state, [name]: state,
@@ -10,7 +26,7 @@ export const createAnimationController = (animations, component) => {
})); }));
}, },
prepare: () => { prepare: () => {
Object.entries(animations).map(([name, animation]) => animation.prepare((state) => { Object.entries(animations).map(([name, animation]) => animation.prepare && animation.prepare((state) => {
component.setState({ component.setState({
...component.state, ...component.state,
[name]: state, [name]: state,
@@ -20,31 +36,31 @@ export const createAnimationController = (animations, component) => {
}; };
} }
export const createAnimation = (stateA, stateB, delay) => ({ export const createAnimation = (stateA: React.CSSProperties, stateB: React.CSSProperties, delay: number): Animation => ({
stateA, stateA,
stateB, stateB,
delay, delay,
}); });
const getAnimationState = (animation, stateName = 'stateA') => { const getAnimationState = (animation: Animation | Animation[], stateName: 'stateA' | 'stateB' = 'stateA'): React.CSSProperties => {
if (animation instanceof Array) { if (animation instanceof Array) {
return animation.map(a => getAnimationState(a, stateName)); return animation.map(a => getAnimationState(a, stateName))[0]; // todo fix
} }
return animation[stateName]; return animation[stateName];
}; };
const getMaxTransitionTime = (transition) => { const getMaxTransitionTime = (transition: string) => {
const re = /(\d+)ms/g; const re = /(\d+)ms/g;
const times = []; const times: number[] = [];
let matches; let matches;
while ((matches = re.exec(transition)) != null) { while ((matches = re.exec(transition)) != null) {
times.push(parseInt(matches[1], 10)); times.push(parseInt(matches[1], 10));
} }
return Math.max.apply(null, times); return Math.max(...times);
}; };
const getAnimationDuration = (animation) => { const getAnimationDuration = (animation: Animation | Animation[]): number => {
if (animation instanceof Array) { if (animation instanceof Array) {
return animation.reduce((maxDuration, a) => { return animation.reduce((maxDuration, a) => {
const duration = getAnimationDuration(a); const duration = getAnimationDuration(a);
@@ -60,11 +76,11 @@ const getAnimationDuration = (animation) => {
return maxTransition + animation.delay; return maxTransition + animation.delay;
}; };
const getMaxAnimationsDuration = (animations) => ( const getMaxAnimationsDuration = (animations: {[k: string]: Animation} | Animation[]) => (
getAnimationDuration(Object.values(animations)) getAnimationDuration(Object.values(animations))
); );
export const createAnimationRunner = (animations, subscriber) => { export const createAnimationRunner = (animations: {[k: string]: Animation} | Animation[], subscriber: () => void = () => {}):AnimationRunner => {
let state = Object.entries(animations).reduce((state, [name, animation]) => ({ let state = Object.entries(animations).reduce((state, [name, animation]) => ({
...state, ...state,
[name]: getAnimationState(animation), [name]: getAnimationState(animation),
@@ -72,17 +88,17 @@ export const createAnimationRunner = (animations, subscriber) => {
const animationsDuration = getMaxAnimationsDuration(animations); const animationsDuration = getMaxAnimationsDuration(animations);
const animate = (name, animation) => { const animate = (name: string, animation: Animation) => {
if (animation instanceof Array) { if (animation instanceof Array) {
animation.map((a, index) => { animation.forEach((a, index) => {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
window.setTimeout(() => { window.setTimeout(() => {
state = { state = {
...state, ...state,
[name]: [ [name]: [
...(state[name].slice(0, index)), // ...(state[name]?.slice(0, index)), // todo fix
a.stateB, a.stateB,
...(state[name].slice(index + 1, state[name].length)), // ...(state[name]?.slice(index + 1, state[name].length)), // todo fix
], ],
}; };
subscriber(); subscriber();

45
src/components/App.tsx Normal file
View File

@@ -0,0 +1,45 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import Header from './Header';
import Footer from './Footer';
import Router from './Router';
import { BrowserRouter, Switch, Route, Redirect, useParams } from 'react-router-dom';
import radardata from '../rd.json'
import { Item } from '../model';
const A = () => {
const {page} = useParams()
return <Router pageName={page} items={radardata.items as Item[]} releases={radardata.releases as string[]}></Router>
}
export default function App() {
const [isFaded] = useState(false)
return (
<BrowserRouter>
<div>
<div className="page">
<div className="page__header">
<Header pageName="a" />
</div>
<div
className={classNames('page__content', { 'is-faded': isFaded })}
>
<Switch>
<Route path={"/techradar/:page(.+).html"}>
<A/>
</Route>
<Route path={"/"}>
<Redirect to={"/techradar/index.html"}/>
</Route>
</Switch>
</div>
<div className="page__footer">
<Footer items={radardata.items as Item[]} pageName="a" />
</div>
</div>
</div>
</BrowserRouter>
);
}

28
src/components/Badge.tsx Normal file
View File

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

View File

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

View File

@@ -0,0 +1,30 @@
import React, { useState, useEffect } from 'react';
import classNames from 'classnames';
type FadeableProps = {
leaving: boolean
onLeave: () => void
}
export default function Fadeable({ leaving, onLeave, children }: React.PropsWithChildren<FadeableProps>) {
const [faded, setFaded] = useState(leaving)
useEffect(() => {
setFaded(leaving)
}, [leaving])
const handleTransitionEnd = () => {
if (faded) {
onLeave();
}
};
return (
<div
className={classNames('fadable', { 'is-faded': faded })}
onTransitionEnd={handleTransitionEnd}
>
{children}
</div>
);
};

View File

@@ -1,20 +1,20 @@
import React from 'react'; import React from 'react';
function ucFirst(string) { interface ItemFlag {
return string.charAt(0).toUpperCase() + string.slice(1); flag: "default" | "new" | "changed"
} }
export default function Flag({ item, short = false }) { export default function Flag({ item, short = false }: {item: ItemFlag, short?: boolean}) {
const ucFirst = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
if (item.flag !== 'default') { if (item.flag !== 'default') {
let name = item.flag.toUpperCase(); let name = item.flag.toUpperCase();
let title = ucFirst(item.flag); let title = ucFirst(item.flag);
if (short === true) { if (short === true) {
name = { name = title[0]
new: 'N',
changed: 'C',
}[item.flag];
} }
return <span className={`flag flag--${item.flag}`} title={title}>{name}</span>; return <span className={`flag flag--${item.flag}`} title={title}>{name}</span>;
} }
return null; return null;
} }

View File

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

View File

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

76
src/components/Header.tsx Normal file
View File

@@ -0,0 +1,76 @@
import React, { useState, useRef, MouseEventHandler } from 'react';
import classNames from 'classnames';
import Branding from './Branding';
import Link from './Link';
import LogoLink from './LogoLink';
import Search from './Search';
import { radarNameShort } from '../config';
import { useHistory } from 'react-router-dom';
export default function Header({ pageName }: { pageName: string }) {
const [searchOpen, setSearchOpen] = useState(false)
const [search, setSearch] = useState('')
const history = useHistory()
const searchRef = useRef<HTMLInputElement>(null)
const openSearch = () => {
setSearchOpen(true)
}
const closeSearch = () => {
setSearchOpen(false)
}
const handleSearchChange = setSearch
const handleSearchSubmit = () => {
history.location.search = search
setSearchOpen(false)
setSearch('')
}
const handleOpenClick = (e: React.MouseEvent<HTMLButtonElement>) => {
// e.preventDefault(); // todo used to be a link
openSearch();
setTimeout(() => {
searchRef?.current?.focus();
}, 0);
}
const smallLogo = pageName !== 'index';
return (
<Branding
logoContent={<LogoLink small={smallLogo} />}
>
<div className="nav">
<div className="nav__item">
<Link pageName="help-and-about-tech-radar" className="icon-link">
<span className="icon icon--question icon-link__icon"></span>How to Use {radarNameShort}?
</Link>
</div>
<div className="nav__item">
<Link pageName="overview" className="icon-link">
<span className="icon icon--overview icon-link__icon"></span>Technologies Overview
</Link>
</div>
<div className="nav__item">
<button className="icon-link" onClick={handleOpenClick}>
<span className="icon icon--search icon-link__icon"></span>Search
</button>
<div className={classNames('nav__search', { 'is-open': searchOpen })}>
<Search
value={search}
onClose={closeSearch}
onSubmit={handleSearchSubmit}
onChange={handleSearchChange}
open={searchOpen}
ref={searchRef}
/>
</div>
</div>
</div>
</Branding>
);
}

View File

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

View File

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

View File

@@ -2,13 +2,16 @@ import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import Link from './Link'; import Link from './Link';
import Flag from './Flag'; import Flag from './Flag';
import { Item as mItem } from '../model';
export default function Item({ type ItemProps = {
item, item: mItem
noLeadingBorder = false, noLeadingBorder?: boolean
active = false, active?: boolean
style = {}, style: React.CSSProperties
}) { }
export default function Item({ item, noLeadingBorder = false, active = false, style = {}}: ItemProps) {
return ( return (
<Link <Link
className={classNames('item', { className={classNames('item', {

View File

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

View File

@@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import Badge from './Badge'; import Badge from './Badge';
import { formatRelease } from '../date'; import { formatRelease } from '../date';
import { Revision } from '../model';
export default function ItemRevision({ revision }) { export default function ItemRevision({ revision }: {revision: Revision}) {
return ( return (
<div className="item-revision"> <div className="item-revision">
<div> <div>

View File

@@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import HeadlineGroup from './HeadlineGroup'; import HeadlineGroup from './HeadlineGroup';
import ItemRevision from './ItemRevision'; import ItemRevision from './ItemRevision';
import { Revision } from '../model';
export default function ItemRevisions({ revisions }) { export default function ItemRevisions({ revisions }: {revisions: Revision[]}) {
return ( return (
<div className="item-revisions"> <div className="item-revisions">
<HeadlineGroup secondary> <HeadlineGroup secondary>

18
src/components/Link.tsx Normal file
View File

@@ -0,0 +1,18 @@
import React from 'react';
import { Link as RLink } from 'react-router-dom';
type LinkProps = {
pageName: string
style?: React.CSSProperties
className?: string
}
function Link({ pageName, children, className, style = {} }: React.PropsWithChildren<LinkProps>) {
return (
<RLink to={`/techradar/${pageName}.html`} style={style} {...{ className }}>
{children}
</RLink>
);
}
export default Link;

View File

@@ -1,14 +1,14 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import Link from './Link'; import Link from './Link';
import { assetUrl, radarNameShort } from '../../common/config'; import { assetUrl, radarNameShort } from '../config';
export default function LogoLink({ small=false }) { export default function LogoLink({ small = false }: { small?: boolean }) {
return ( return (
<Link pageName="index" className={classNames('logo-link', { 'logo-link--small': small })}> <Link pageName="index" className={classNames('logo-link', { 'logo-link--small': small })}>
<span className="logo-link__icon icon icon--back"></span> <span className="logo-link__icon icon icon--back"></span>
<span className="logo-link__slide"> <span className="logo-link__slide">
<img className="logo-link__img" src={assetUrl('logo.svg')} width="150px" height="60px" /> <img className="logo-link__img" src={assetUrl('logo.svg')} width="150px" height="60px" alt={radarNameShort} />
<span className="logo-link__text"> <span className="logo-link__text">
{radarNameShort} {radarNameShort}
</span> </span>
@@ -16,3 +16,4 @@ export default function LogoLink({ small=false }) {
</Link> </Link>
); );
} }

View File

@@ -2,12 +2,12 @@ import React from 'react';
import HeroHeadline from './HeroHeadline'; import HeroHeadline from './HeroHeadline';
import Fadeable from './Fadeable'; import Fadeable from './Fadeable';
import SetTitle from './SetTitle'; import SetTitle from './SetTitle';
import { radarName } from '../../common/config'; import { radarName } from '../config';
export default function PageHelp({ leaving, onLeave, ...props }) { export default function PageHelp({ leaving, onLeave}: {leaving: boolean, onLeave: () => void}) {
return ( return (
<Fadeable leaving={leaving} onLeave={onLeave}> <Fadeable leaving={leaving} onLeave={onLeave}>
<SetTitle {...props} title={ "How to use the " + radarName } /> <SetTitle title={ "How to use the " + radarName } />
<HeroHeadline>How to use the {radarName}</HeroHeadline> <HeroHeadline>How to use the {radarName}</HeroHeadline>
<div className="fullpage-content"> <div className="fullpage-content">
<h3>Introduction</h3> <h3>Introduction</h3>
@@ -40,7 +40,7 @@ export default function PageHelp({ leaving, onLeave, ...props }) {
<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.</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.</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 there 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> <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 there 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> </ul>
<p>Contributions and source code of the radar are on github: <a href="https://github.com/AOEpeople/aoe_technology_radar" target="_blank">AOE Tech Radar on Github</a></p> <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">AOE Tech Radar on Github</a></p>
</div> </div>

View File

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

273
src/components/PageItem.tsx Normal file
View File

@@ -0,0 +1,273 @@
import React, { useEffect, useState } from 'react';
import Badge from './Badge';
import ItemList from './ItemList';
import Link from './Link';
import FooterEnd from './FooterEnd';
import SetTitle from './SetTitle';
import ItemRevisions from './ItemRevisions';
import { createAnimation, createAnimationRunner } from '../animation';
import { translate } from '../config';
import { groupByQuadrants, Item } from '../model';
const getItem = (pageName: string, items: Item[]) => {
const [quadrantName, itemName] = pageName.split('/');
const item = items.filter(
item => item.quadrant === quadrantName && item.name === itemName,
)[0];
return item;
};
const getItemsInRing = (pageName: string, items: Item[]) => {
const item = getItem(pageName, items);
const itemsInRing = groupByQuadrants(items)[item.quadrant][item.ring];
return itemsInRing;
};
type PageItemProps = {
pageName: string
items: Item[]
leaving: boolean
onLeave: () => void
}
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 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<any>();
useEffect(()=>{
if (leaving) {
// entering from an other page
// setAnimations(createAnimationRunner(animationsIn).getState())
} else {
// Hard refresh
setAnimations(null)
}
}, [leaving])
const [stateLeaving, setStateLeaving] = useState(leaving)
let [animationRunner, setAnimationRunner] = useState<any>();
// useEffect(() => {
// if (!stateLeaving && leaving) {
// animationRunner = createAnimationRunner(
// animationsOut,
// handleAnimationsUpdate,
// );
// setAnimationRunner(animationRunner)
// animationRunner.run();
// animationRunner.awaitAnimationComplete(onLeave);
// }
// if (stateLeaving && !leaving) {
// animationRunner = createAnimationRunner(
// animationsIn,
// handleAnimationsUpdate,
// );
// setAnimationRunner(animationRunner)
// animationRunner.run();
// }
// setStateLeaving(leaving)
// }, [leaving])
const handleAnimationsUpdate = () => {
setAnimations(animationRunner.getState());
};
const getAnimationState = (name: string) => {
if (!animations) {
return undefined;
}
return animations[name];
};
const item = getItem(pageName, items);
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={getAnimationState('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

@@ -0,0 +1,75 @@
import React from 'react';
import Badge from './Badge';
import ItemList from './ItemList';
import Link from './Link';
import Fadeable from './Fadeable';
import SetTitle from './SetTitle';
import ItemRevisions from './ItemRevisions';
import { translate } from '../config';
import { groupByQuadrants, Item } from '../model';
type PageItemMobileProps = {
pageName: string
items: Item[]
leaving: boolean
onLeave: () => void
}
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];
return item;
}
const getItemsInRing = (pageName: string, items: Item[]) => {
const item = getItem(pageName, items);
const itemsInRing = groupByQuadrants(items)[item.quadrant][item.ring];
return itemsInRing;
};
const item = getItem(pageName, items);
const itemsInRing = getItemsInRing(pageName, items);
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>
<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>
<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>
<div className="split__right">
<Link className="icon-link" pageName={item.quadrant}>
<span className="icon icon--pie icon-link__icon"></span>Zoom In
</Link>
</div>
</div>
</ItemList>
</aside>
</Fadeable>
);
}

View File

@@ -0,0 +1,150 @@
import React, { useState, useEffect } from 'react';
import HeadlineGroup from './HeadlineGroup';
import HeroHeadline from './HeroHeadline';
import Badge from './Badge';
import Link from './Link';
import Search from './Search';
import Fadeable from './Fadeable';
import SetTitle from './SetTitle';
import Flag from './Flag';
import { groupByFirstLetter, Item } from '../model';
import { translate, ring } from '../config';
const containsSearchTerm = (text = '', term = '') => {
// TODO search refinement
return (
text
.trim()
.toLocaleLowerCase()
.indexOf(term.trim().toLocaleLowerCase()) !== -1
);
};
type PageOverviewProps = {
rings: 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")
const [search, setSearch] = useState(searchProp)
useEffect(() => {
if (rings.length > 0) {
setRing(rings[0])
}
setSearch(searchProp)
}, [rings, searchProp])
const handleRingClick = (ring: ring) => () => {
setRing(ring)
};
const isRingActive = (ringName: string) => ring === ringName
const itemMatchesRing = (item: Item) => ring === 'all' || item.ring === ring
const itemMatchesSearch = (item: Item) => {
return (
search.trim() === '' ||
containsSearchTerm(item.title, search) ||
containsSearchTerm(item.body, search) ||
containsSearchTerm(item.info, search)
);
};
const isItemVisible = (item: Item) => itemMatchesRing(item) && itemMatchesSearch(item)
const getFilteredAndGroupedItems = () => {
const groups = groupByFirstLetter(items);
const groupsFiltered = groups.map(group => ({
...group,
items: group.items.filter(isItemVisible),
}));
const nonEmptyGroups = groupsFiltered.filter(
group => group.items.length > 0,
);
return nonEmptyGroups;
}
const handleSearchTermChange = setSearch
const groups = getFilteredAndGroupedItems();
return (
<Fadeable leaving={leaving} onLeave={onLeave}>
<SetTitle title="Technologies Overview" />
<HeadlineGroup>
<HeroHeadline>Technologies Overview</HeroHeadline>
</HeadlineGroup>
<div className="filter">
<div className="split split--filter">
<div className="split__left">
<Search
onChange={handleSearchTermChange}
value={search}
/>
</div>
<div className="split__right">
<div className="nav">
{rings.map(ringName => (
<div className="nav__item" key={ringName}>
<Badge
big
onClick={handleRingClick(ringName)}
type={isRingActive(ringName) ? ringName : 'empty'}
>
{ringName}
</Badge>
</div>
))}
</div>
</div>
</div>
</div>
<div className="letter-index">
{groups.map(({ letter, items }) => (
<div key={letter} className="letter-index__group">
<div className="letter-index__letter">{letter}</div>
<div className="letter-index__items">
<div className="item-list">
<div className="item-list__list">
{items.map(item => (
<Link
key={item.name}
className="item item--big item--no-leading-border item--no-trailing-border"
pageName={`${item.quadrant}/${item.name}`}
>
<div className="split split--overview">
<div className="split__left">
<div className="item__title">
{item.title}
<Flag item={item} />
</div>
</div>
<div className="split__right">
<div className="nav nav--relations">
<div className="nav__item">
{translate(item.quadrant)}
</div>
<div className="nav__item">
<Badge type={item.ring}>{item.ring}</Badge>
</div>
</div>
</div>
</div>
</Link>
))}
</div>
</div>
</div>
</div>
))}
</div>
</Fadeable>
);
}

View File

@@ -5,14 +5,21 @@ import QuadrantSection from './QuadrantSection';
import Fadeable from './Fadeable'; import Fadeable from './Fadeable';
import SetTitle from './SetTitle'; import SetTitle from './SetTitle';
import { translate } from '../../common/config'; import { translate } from '../config';
import {featuredOnly, groupByQuadrants} from '../../common/model'; import {featuredOnly, groupByQuadrants, Item} from '../model';
export default function PageQuadrant({ leaving, onLeave, pageName, items, ...props }) { type PageQuadrantProps = {
leaving: boolean
onLeave: () => void
pageName: string
items: Item[]
}
export default function PageQuadrant({ leaving, onLeave, pageName, items }: PageQuadrantProps) {
const groups = groupByQuadrants(featuredOnly(items)); const groups = groupByQuadrants(featuredOnly(items));
return ( return (
<Fadeable leaving={leaving} onLeave={onLeave}> <Fadeable leaving={leaving} onLeave={onLeave}>
<SetTitle {...props} title={translate(pageName)} /> <SetTitle title={translate(pageName)} />
<HeadlineGroup> <HeadlineGroup>
<HeroHeadline>{translate(pageName)}</HeroHeadline> <HeroHeadline>{translate(pageName)}</HeroHeadline>
</HeadlineGroup> </HeadlineGroup>

View File

@@ -3,10 +3,10 @@ import HeroHeadline from './HeroHeadline';
import Fadeable from './Fadeable'; import Fadeable from './Fadeable';
import SetTitle from './SetTitle'; import SetTitle from './SetTitle';
export default function PageToolbox({ leaving, onLeave, ...props }) { export default function PageToolbox({ leaving, onLeave }: {leaving: boolean, onLeave: () => void}) {
return ( return (
<Fadeable leaving={leaving} onLeave={onLeave}> <Fadeable leaving={leaving} onLeave={onLeave}>
<SetTitle {...props} title="Small AOE Toolbox" /> <SetTitle title="Small AOE Toolbox" />
<HeroHeadline>Small AOE Toolbox</HeroHeadline> <HeroHeadline>Small AOE Toolbox</HeroHeadline>
<div className="fullpage-content"> <div className="fullpage-content">
<h3>Useful Tools</h3> <h3>Useful Tools</h3>

View File

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

View File

@@ -1,14 +1,15 @@
import React from 'react'; import React from 'react';
import { translate, rings } from '../../common/config'; import { translate, rings, ring } from '../config';
import Badge from './Badge'; import Badge from './Badge';
import Link from './Link'; import Link from './Link';
import ItemList from './ItemList'; import ItemList from './ItemList';
import Flag from './Flag'; import Flag from './Flag';
import { Item, Group } from '../model';
const renderList = (ringName, quadrantName, groups, big) => { const renderList = (ringName: ring, quadrantName: string, groups: Group, big: boolean) => {
const itemsInRing = groups[quadrantName][ringName]; const itemsInRing = groups[quadrantName][ringName];
if (big === true) { if (big) {
return ( return (
<ItemList items={itemsInRing} noLeadingBorder> <ItemList items={itemsInRing} noLeadingBorder>
<Badge type={ringName} big={big}> <Badge type={ringName} big={big}>
@@ -35,7 +36,7 @@ const renderList = (ringName, quadrantName, groups, big) => {
); );
}; };
const renderRing = (ringName, quadrantName, groups, big) => { const renderRing = (ringName: ring, quadrantName: string, groups: Group, big: boolean) => {
if ( if (
!groups[quadrantName] || !groups[quadrantName] ||
!groups[quadrantName][ringName] || !groups[quadrantName][ringName] ||
@@ -50,7 +51,7 @@ const renderRing = (ringName, quadrantName, groups, big) => {
); );
}; };
export default function QuadrantSection({ quadrantName, groups, big = false }) { export default function QuadrantSection({ quadrantName, groups, big = false }: { quadrantName: string, groups: Group, big?: boolean }) {
return ( return (
<div className="quadrant-section"> <div className="quadrant-section">
<div className="quadrant-section__header"> <div className="quadrant-section__header">

82
src/components/Router.tsx Normal file
View File

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

56
src/components/Search.tsx Normal file
View File

@@ -0,0 +1,56 @@
import React, { FormEvent } from 'react';
import classNames from 'classnames';
type SearchProps = {
onClose?: () => void
onSubmit?: () => void
value: string
onChange: (value: string) => void
open?: boolean
}
export default React.forwardRef((props: SearchProps, ref) => {
return Search(props, ref)
})
function Search({ value, onChange, onClose, open = false, onSubmit = () => { }}: SearchProps, ref: any) {
const closable = onClose !== undefined;
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
onSubmit();
};
const handleClose = (e: React.MouseEvent) => {
// e.preventDefault();
if (onClose != null) {
onClose();
}
}
return (
<form className={classNames('search', { 'search--closable': closable })} onSubmit={handleSubmit}>
<input
value={value}
type="text"
onChange={(e) => { onChange(e.target.value); }}
className="search__field"
placeholder="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" />
Search
</button>
</span>
{
closable && (
<button className={classNames('search__close', { 'is-open': open })} onClick={handleClose}>
<span className="icon icon--close" />
</button>
)
}
</form>
);
}

View File

@@ -0,0 +1,40 @@
// import React from 'react';
// todo fix this mess
// const _callSetTitle = (props) => {
// if (typeof props.onSetTitle === 'function' && props.title) {
// props.onSetTitle(props.title);
// }
// };
// class _SetTitle extends React.Component {
// constructor(props) {
// super(props);
// _callSetTitle(props);
// }
// componentWillReceiveProps(nextProps) {
// if (nextProps.title !== this.props.title) {
// _callSetTitle(nextProps);
// }
// }
// render() {
// return null;
// }
// }
type SetTitleProps = {
title: string
onSetTitle?: (title: string) => void
}
export default function SetTitle({title, onSetTitle}: SetTitleProps) {
if (onSetTitle) {
onSetTitle(title)
}
return null;
}

View File

@@ -1,3 +1,6 @@
// import moment from 'moment';
import { Item, Radar } from './model';
export const radarName = 'AOE Technology Radar'; export const radarName = 'AOE Technology Radar';
export const radarNameShort = 'Technology Radar'; export const radarNameShort = 'Technology Radar';
@@ -8,11 +11,12 @@ export const quadrants = [
'tools', 'tools',
]; ];
export function assetUrl(file) { export function assetUrl(file: string) {
return `/techradar/assets/${file}` return '/' + file;
// return `/techradar/assets/${file}`
} }
export const getPageNames = (radar) => { export const getPageNames = (radar: Radar) => {
return [ return [
'index', 'index',
'overview', 'overview',
@@ -23,23 +27,25 @@ export const getPageNames = (radar) => {
] ]
} }
export const getItemPageNames = (items) => items.map(item => `${item.quadrant}/${item.name}`); export const getItemPageNames = (items: Item[]) => items.map(item => `${item.quadrant}/${item.name}`);
export const rings = [ export type ring = "adopt" | "trial" | "assess" | "hold"
export const rings: ring[] = [
'adopt', 'adopt',
'trial', 'trial',
'assess', 'assess',
'hold' 'hold'
]; ];
const messages = { const messages:{[k: string]: string} = {
'languages-and-frameworks': 'Languages & Frameworks', 'languages-and-frameworks': 'Languages & Frameworks',
'methods-and-patterns': 'Methods & Patterns', 'methods-and-patterns': 'Methods & Patterns',
'platforms-and-aoe-services': 'Platforms and Operations', 'platforms-and-aoe-services': 'Platforms and Operations',
'tools': 'Tools', 'tools': 'Tools',
}; };
export const translate = (key) => (messages[key] || '-'); export const translate = (key: string) => (messages[key] || '-');
export function isMobileViewport() { export function isMobileViewport() {
// return false for server side rendering // return false for server side rendering
@@ -49,4 +55,4 @@ export function isMobileViewport() {
return width < 1200; return width < 1200;
} }
const formatRelease = (release) => moment(release, 'YYYY-MM-DD').format('MMM YYYY'); // const formatRelease = (release: moment.MomentInput) => moment(release, 'YYYY-MM-DD').format('MMM YYYY');

5
src/date.ts Normal file
View File

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

13
src/index.css Normal file
View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

11
src/index.tsx Normal file
View File

@@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>
,
document.getElementById('root')
);

78
src/model.ts Normal file
View File

@@ -0,0 +1,78 @@
import { ring } from "./config"
export type ItemAttributes = {
name: string
ring: ring
quadrant: string
title: string
featured: boolean
}
export type Item = ItemAttributes & {
featured: boolean
body: string
info: string
flag: 'new' | 'changed' | 'default'
revisions: Revision[]
}
export type Revision = ItemAttributes & {
body: string
fileName: string
release: string
}
export type Quadrant = {
[name: string]: Item[]
}
export type Radar = {
items: Item[]
releases: string[]
}
export type Group = {
[quadrant: string]: Quadrant
}
export const featuredOnly = (items: Item[]) => items.filter(item => item.featured);
export const groupByQuadrants = (items: Item[]): Group =>
items.reduce(
(quadrants, item: Item) => ({
...quadrants,
[item.quadrant]: addItemToQuadrant(quadrants[item.quadrant], item),
}),
{} as {[k: string]: Quadrant},
);
export const groupByFirstLetter = (items: Item[]) => {
const index = items.reduce(
(letterIndex, item) => ({
...letterIndex,
[getFirstLetter(item)]: addItemToList(
letterIndex[getFirstLetter(item)],
item,
),
}),
{} as {[k: string]: Item[]},
);
return Object.keys(index)
.sort()
.map(letter => ({
letter,
items: index[letter],
}));
};
const addItemToQuadrant = (quadrant: Quadrant = {}, item: Item): Quadrant => ({
...quadrant,
[item.ring]: addItemToRing(quadrant[item.ring], item),
});
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();

1
src/rd.json Normal file

File diff suppressed because one or more lines are too long

1
src/react-app-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@@ -1,12 +0,0 @@
import { copy } from 'fs-extra';
import {
assetsPath,
distPath,
} from '../common/file';
copy(assetsPath(), distPath('assets'), (err) => {
if (err) {
return console.error(err);
}
console.log("copied assets");
});

View File

@@ -1,19 +0,0 @@
import { createRadar } from '../common/radar';
import { save } from '../common/file';
import { getPageNames } from '../common/config';
import { renderPage } from '../js/server';
(async () => {
try {
const radar = await createRadar();
getPageNames(radar).map(pageName => {
const pageHtml = renderPage(radar, pageName);
save(pageHtml, `${pageName}.html`);
});
console.log('Built radar');
} catch (e) {
console.error('error:', e);
}
})();

View File

@@ -1,13 +0,0 @@
import { emptyDir } from 'fs-extra';
import { distPath } from '../common/file';
var distDir = distPath();
emptyDir(distDir, (err) => {
if (!err) {
console.log('Cleaned dist dir', distDir);
} else {
console.error(err);
}
});

View File

@@ -2,42 +2,42 @@ import { outputFile } from 'fs-extra';
import path from 'path'; import path from 'path';
import { walk } from 'walk'; import { walk } from 'walk';
export const relativePath = (...relativePath) => ( export const relativePath = (...relativePath: string[]): string => (
path.resolve(__dirname, '..', ...relativePath) path.resolve(__dirname, '..', ...relativePath)
); );
export const radarPath = (...pathInSrc) => ( export const radarPath = (...pathInSrc: string[]) => (
relativePath('radar', ...pathInSrc) relativePath('radar', ...pathInSrc)
); );
export const stylesPath = (...pathInSrc) => ( export const stylesPath = (...pathInSrc: string[]) => (
relativePath('styles', ...pathInSrc) relativePath('styles', ...pathInSrc)
); );
export const assetsPath = (...pathInSrc) => ( export const assetsPath = (...pathInSrc: string[]) => (
relativePath('assets', ...pathInSrc) relativePath('assets', ...pathInSrc)
); );
export const faviconPath = (...pathInSrc) => ( export const faviconPath = (...pathInSrc: string[]) => (
relativePath('assets/favicon.ico', ...pathInSrc) relativePath('assets/favicon.ico', ...pathInSrc)
); );
export const jsPath = (...pathInSrc) => ( export const jsPath = (...pathInSrc: string[]) => (
relativePath('js', ...pathInSrc) relativePath('js', ...pathInSrc)
); );
export const distPath = (...pathInDist) => ( export const distPath = (...pathInDist: string[]) => (
relativePath('dist', 'techradar', ...pathInDist) relativePath('dist', 'techradar', ...pathInDist)
); );
export const getAllMarkdownFiles = (folder) => ( export const getAllMarkdownFiles = (folder: string) => (
getAllFiles(folder, isMarkdownFile) getAllFiles(folder, isMarkdownFile)
); );
const getAllFiles = (folder, predicate) => ( const getAllFiles = (folder: string, predicate: (s: string) => boolean): Promise<string[]> => (
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const walker = walk(folder, { followLinks: false }); const walker = walk(folder, { followLinks: false });
const files = []; const files: string[] = [];
walker.on("file", (root, fileStat, next) => { walker.on("file", (root, fileStat, next) => {
if (predicate(fileStat.name)) { if (predicate(fileStat.name)) {
@@ -49,7 +49,9 @@ const getAllFiles = (folder, predicate) => (
walker.on("errors", (root, nodeStatsArray, next) => { walker.on("errors", (root, nodeStatsArray, next) => {
nodeStatsArray.forEach(function (n) { nodeStatsArray.forEach(function (n) {
console.error("[ERROR] " + n.name) console.error("[ERROR] " + n.name)
console.error(n.error.message || (n.error.code + ": " + n.error.path)); if (n.error) {
console.error(n.error.message || (n.error.code + ": " + n.error.path));
}
}); });
next(); next();
}); });
@@ -60,16 +62,6 @@ const getAllFiles = (folder, predicate) => (
}) })
); );
const isMarkdownFile = (name) => name.match(/\.md$/); const isMarkdownFile = (name: string) => name.match(/\.md$/) !== null;
export const save = (html, fileName) => ( export const save = (data: string | Buffer | DataView, fileName: string) => outputFile(distPath(fileName), data);
new Promise((resolve, reject) => (
outputFile(distPath(fileName), html, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
})
))
);

View File

@@ -3,14 +3,17 @@ import path from 'path';
import frontmatter from 'front-matter'; import frontmatter from 'front-matter';
import marked from 'marked'; import marked from 'marked';
import hljs from 'highlight.js'; import hljs from 'highlight.js';
import { quadrants, rings } from './config'; import { quadrants, rings } from '../src/config';
import { radarPath, getAllMarkdownFiles } from './file'; import { radarPath, getAllMarkdownFiles } from './file';
import { Item, Revision, ItemAttributes, Radar } from '../src/model';
type FMAttributes = ItemAttributes
marked.setOptions({ marked.setOptions({
highlight: code => hljs.highlightAuto(code).value, highlight: code => hljs.highlightAuto(code).value,
}); });
export const createRadar = async tree => { export const createRadar = async (): Promise<Radar> => {
const fileNames = await getAllMarkdownFiles(radarPath()); const fileNames = await getAllMarkdownFiles(radarPath());
const revisions = await createRevisionsFromFiles(fileNames); const revisions = await createRevisionsFromFiles(fileNames);
const allReleases = getAllReleases(revisions); const allReleases = getAllReleases(revisions);
@@ -23,7 +26,7 @@ export const createRadar = async tree => {
}; };
}; };
const checkAttributes = (fileName, attributes) => { const checkAttributes = (fileName: string, attributes: FMAttributes) => {
if (attributes.ring && !rings.includes(attributes.ring)) { if (attributes.ring && !rings.includes(attributes.ring)) {
throw new Error(`Error: ${fileName} has an illegal value for 'ring' - must be one of ${rings}`); throw new Error(`Error: ${fileName} has an illegal value for 'ring' - must be one of ${rings}`);
} }
@@ -32,52 +35,47 @@ const checkAttributes = (fileName, attributes) => {
throw new Error(`Error: ${fileName} has an illegal value for 'quadrant' - must be one of ${quadrants}`); throw new Error(`Error: ${fileName} has an illegal value for 'quadrant' - must be one of ${quadrants}`);
} }
if (!attributes.quadrant) { if (!attributes.quadrant || attributes.quadrant === '') {
const defaultQuadrant = quadrants[0]; // throw new Error(`Error: ${fileName} has no 'quadrant' set`);
console.warn(`${fileName} missing 'quadrant', using default: ${defaultQuadrant}`);
attributes.quadrant = defaultQuadrant;
} }
if (!attributes.title) { if (!attributes.title || attributes.title === '') {
attributes.title = path.basename(fileName); attributes.title = path.basename(fileName);
} }
return attributes
}; };
const createRevisionsFromFiles = fileNames => const createRevisionsFromFiles = (fileNames: string[]) =>
Promise.all( Promise.all(
fileNames.map(fileName => { fileNames.map(fileName => {
return new Promise((resolve, reject) => { return new Promise<Revision>((resolve, reject) => {
readFile(fileName, 'utf8', (err, data) => { readFile(fileName, 'utf8', (err, data) => {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
const fm = frontmatter(data); const fm = frontmatter<FMAttributes>(data);
// prepend subfolder to links
fm.body = fm.body.replace(/\]\(\//g, '](/techradar/');
// add target attribute to external links // add target attribute to external links
let html = marked(fm.body); // todo: check path
let html = marked(fm.body.replace(/\]\(\//g, '](/techradar/'));
html = html.replace( html = html.replace(
/a href="http/g, /a href="http/g,
'a target="_blank" href="http', 'a target="_blank" rel="noopener noreferrer" href="http',
); );
checkAttributes(fileName, fm.attributes);
resolve({ resolve({
...itemInfoFromFilename(fileName), ...itemInfoFromFilename(fileName),
...fm.attributes, ...checkAttributes(fileName, fm.attributes),
fileName, fileName,
body: html, body: html,
}); } as Revision);
} }
}); });
}); });
}), }),
); );
const itemInfoFromFilename = fileName => { const itemInfoFromFilename = (fileName: string) => {
const [release, nameWithSuffix] = fileName.split(path.sep).slice(-2); const [release, nameWithSuffix] = fileName.split(path.sep).slice(-2);
return { return {
name: nameWithSuffix.substr(0, nameWithSuffix.length - 3), name: nameWithSuffix.substr(0, nameWithSuffix.length - 3),
@@ -85,9 +83,9 @@ const itemInfoFromFilename = fileName => {
}; };
}; };
const getAllReleases = revisions => const getAllReleases = (revisions: Revision[]) =>
revisions revisions
.reduce((allReleases, { release }) => { .reduce<string[]>((allReleases, { release }) => {
if (!allReleases.includes(release)) { if (!allReleases.includes(release)) {
return [...allReleases, release]; return [...allReleases, release];
} }
@@ -95,13 +93,8 @@ const getAllReleases = revisions =>
}, []) }, [])
.sort(); .sort();
const addRevisionToQuadrant = (quadrant = {}, revision) => ({ const createItems = (revisions: Revision[]) => {
...quadrant, const itemMap = revisions.reduce<{[name: string]: Item}>((items, revision) => {
[revision.ring]: addRevisionToRing(quadrant[revision.ring], revision),
});
const createItems = revisions => {
const itemMap = revisions.reduce((items, revision) => {
return { return {
...items, ...items,
[revision.name]: addRevisionToItem(items[revision.name], revision), [revision.name]: addRevisionToItem(items[revision.name], revision),
@@ -111,7 +104,7 @@ const createItems = revisions => {
return Object.values(itemMap).sort((x, y) => (x.name > y.name ? 1 : -1)); return Object.values(itemMap).sort((x, y) => (x.name > y.name ? 1 : -1));
}; };
const ignoreEmptyRevisionBody = (revision, item) => { const ignoreEmptyRevisionBody = (revision: Revision, item: Item) => {
if (!revision.body || revision.body.trim() === '') { if (!revision.body || revision.body.trim() === '') {
return item.body; return item.body;
} }
@@ -119,57 +112,58 @@ const ignoreEmptyRevisionBody = (revision, item) => {
}; };
const addRevisionToItem = ( const addRevisionToItem = (
item = { item: Item = {
flag: 'default', flag: 'default',
featured: true, featured: true,
revisions: [], revisions: [],
name: '',
title: '',
ring: 'trial',
quadrant: '',
body: '',
info: '',
}, },
revision, revision: Revision,
) => { ): Item => {
const { fileName, ...rest } = revision; let newItem: Item = {
let newItem = {
...item, ...item,
...rest, ...revision,
body: ignoreEmptyRevisionBody(rest, item), body: ignoreEmptyRevisionBody(revision, item),
attributes: {
...item.attributes,
...revision.attributes,
},
}; };
if (revisionCreatesNewHistoryEntry(revision)) { if (revisionCreatesNewHistoryEntry(revision)) {
newItem = { newItem = {
...newItem, ...newItem,
revisions: [rest, ...newItem.revisions], revisions: [revision, ...newItem.revisions],
}; };
} }
return newItem; return newItem;
}; };
const revisionCreatesNewHistoryEntry = revision => { const revisionCreatesNewHistoryEntry = (revision: Revision) => {
return revision.body.trim() !== '' || typeof revision.ring !== 'undefined'; return revision.body.trim() !== '' || typeof revision.ring !== 'undefined';
}; };
const flagItem = (items, allReleases) => const flagItem = (items: Item[], allReleases: string[]) =>
items.map( items.map(
item => ({ item => ({
...item, ...item,
flag: getItemFlag(item, allReleases), flag: getItemFlag(item, allReleases),
}), } as Item),
[], [],
); );
const isInLastRelease = (item, allReleases) => const isInLastRelease = (item: Item, allReleases: string[]) =>
item.revisions[0].release === allReleases[allReleases.length - 1]; item.revisions[0].release === allReleases[allReleases.length - 1];
const isNewItem = (item, allReleases) => const isNewItem = (item: Item, allReleases: string[]) =>
item.revisions.length === 1 && isInLastRelease(item, allReleases); item.revisions.length === 1 && isInLastRelease(item, allReleases);
const hasItemChanged = (item, allReleases) => const hasItemChanged = (item: Item, allReleases: string[]) =>
item.revisions.length > 1 && isInLastRelease(item, allReleases); item.revisions.length > 1 && isInLastRelease(item, allReleases);
const getItemFlag = (item, allReleases) => { const getItemFlag = (item: Item, allReleases: string[]): string => {
if (isNewItem(item, allReleases)) { if (isNewItem(item, allReleases)) {
return 'new'; return 'new';
} }

25
tasks/radarjson.ts Normal file
View File

@@ -0,0 +1,25 @@
import { createRadar } from "./radar";
import { save } from "./file";
export const r = (async () => {
try {
console.log('start')
const radar = await createRadar();
// console.log(radar);
save(JSON.stringify(radar), 'radar.json')
// getPageNames(radar).map(pageName => {
// // const pageHtml = renderPage(radar, pageName);
// // save(pageHtml, `${pageName}.html`);
// save([pageName, radar], `${pageName}.html`)
// });
console.log('Built radar');
} catch (e) {
console.error('error:', e);
}
})();

View File

@@ -1,24 +0,0 @@
import pug from 'pug';
import moment from 'moment';
import { translate } from '../common/config';
import { relativePath } from '../common/file';
import {
groupByQuadrants,
groupByFirstLetter,
groupByRing,
} from './radar';
const templateFolder = 'templates';
export const vars = (vars) => ({
translate: translate,
formatRelease: (release) => moment(release, 'YYYY-MM-DD').format('MMM YYYY'),
groupByQuadrants,
groupByFirstLetter,
groupByRing,
...vars,
});
export const item = pug.compileFile(relativePath(templateFolder, 'item-page.pug'));
export const quadrant = pug.compileFile(relativePath(templateFolder, 'quadrant-page.pug'));

View File

@@ -1,41 +0,0 @@
import { watch } from 'fs';
import { exec } from 'child_process';
import liveServer from 'live-server';
import {
stylesPath,
assetsPath,
jsPath,
radarPath,
relativePath,
} from '../common/file';
const runBuild = name =>
exec(`yarn run build:${name}`, (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
console.log(stdout);
console.error(stderr);
});
const watchBuild = name => (eventType, fileName) => runBuild(name);
const options = {
recursive: true,
};
runBuild('all');
watch(stylesPath(), options, watchBuild('css'));
watch(jsPath(), options, watchBuild('js'));
watch(jsPath(), options, watchBuild('pages'));
watch(assetsPath(), options, watchBuild('assets'));
watch(radarPath(), options, watchBuild('pages'));
const params = {
root: relativePath('dist'),
logLevel: 0, // 0 = errors only, 1 = some, 2 = lots
};
liveServer.start(params);

25
tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
},
"include": [
"src"
]
}

View File

@@ -1,24 +0,0 @@
var path = require('path');
module.exports = {
entry: {
bundle: './js/client.js',
},
output: {
path: path.join(__dirname, 'dist'),
filename: 'techradar/assets/js/[name].js',
},
module: {
rules: [
{
test: /\.js?$/,
include: [
path.resolve(__dirname, "js"),
path.resolve(__dirname, "common"),
],
loader: "babel-loader",
},
],
},
}

7934
yarn.lock

File diff suppressed because it is too large Load Diff