use typescript
4
.babelrc
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": ["@babel/preset-env", "@babel/preset-react"],
|
|
||||||
"plugins": ["@babel/plugin-proposal-class-properties"]
|
|
||||||
}
|
|
||||||
1
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -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();
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
export const NAVIGATE = 'navigate';
|
|
||||||
|
|
||||||
const actions = {
|
|
||||||
navigate: (pageName, pushToHistory = true, pageState = {}) => {
|
|
||||||
return {
|
|
||||||
type: NAVIGATE,
|
|
||||||
pageName,
|
|
||||||
pageState,
|
|
||||||
pushToHistory,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default actions;
|
|
||||||
85
js/client.js
@@ -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'),
|
|
||||||
);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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);
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
function Icon({ name, ...props }) {
|
|
||||||
return (
|
|
||||||
<span/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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');
|
|
||||||
109
js/details.js
@@ -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;
|
|
||||||
38
js/filter.js
@@ -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;
|
|
||||||
10
js/pjax.js
@@ -1,10 +0,0 @@
|
|||||||
import Pjax from 'pjax';
|
|
||||||
|
|
||||||
const applyPjax = () => {
|
|
||||||
new Pjax({
|
|
||||||
elements: '.js--body a',
|
|
||||||
selectors: ['title', '.js--body'],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default applyPjax;
|
|
||||||
@@ -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;
|
|
||||||
57
js/server.js
@@ -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>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
70
package.json
@@ -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",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 833 B After Width: | Height: | Size: 833 B |
|
Before Width: | Height: | Size: 830 B After Width: | Height: | Size: 830 B |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 239 KiB |
43
public/index.html
Normal 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>
|
||||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
BIN
public/logo192.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/logo512.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
25
public/manifest.json
Normal 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
@@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
1003
public/styles.css
Normal 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
@@ -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
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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">
|
||||||
30
src/components/Fadeable.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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.
|
||||||
@@ -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
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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})}>
|
||||||
10
src/components/HeroHeadline.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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', {
|
||||||
@@ -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]}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@@ -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>
|
||||||
@@ -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
@@ -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;
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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>
|
||||||
@@ -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}
|
||||||
273
src/components/PageItem.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
75
src/components/PageItemMobile.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
150
src/components/PageOverview.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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">
|
||||||
@@ -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
@@ -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
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
src/components/SetTitle.tsx
Normal 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;
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
1
src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="react-scripts" />
|
||||||
@@ -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");
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -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)
|
||||||
|
if (n.error) {
|
||||||
console.error(n.error.message || (n.error.code + ": " + n.error.path));
|
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);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
))
|
|
||||||
);
|
|
||||||
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
@@ -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'));
|
|
||||||
@@ -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
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||