use typescript
This commit is contained in:
12
src/analytics.js
Normal file
12
src/analytics.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// Add Google Analytics
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', 'UA-839059-1', 'auto');
|
||||
|
||||
export function track() {
|
||||
ga('set', 'location', document.location.href);
|
||||
ga('send', 'pageview');
|
||||
}
|
||||
134
src/animation.ts
Normal file
134
src/animation.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
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 {
|
||||
animations,
|
||||
start: () => {
|
||||
Object.entries(animations).map(([name, animation]) => animation.run && animation.run((state) => {
|
||||
component.setState({
|
||||
...component.state,
|
||||
[name]: state,
|
||||
});
|
||||
}));
|
||||
},
|
||||
prepare: () => {
|
||||
Object.entries(animations).map(([name, animation]) => animation.prepare && animation.prepare((state) => {
|
||||
component.setState({
|
||||
...component.state,
|
||||
[name]: state,
|
||||
});
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const createAnimation = (stateA: React.CSSProperties, stateB: React.CSSProperties, delay: number): Animation => ({
|
||||
stateA,
|
||||
stateB,
|
||||
delay,
|
||||
});
|
||||
|
||||
const getAnimationState = (animation: Animation | Animation[], stateName: 'stateA' | 'stateB' = 'stateA'): React.CSSProperties => {
|
||||
if (animation instanceof Array) {
|
||||
return animation.map(a => getAnimationState(a, stateName))[0]; // todo fix
|
||||
}
|
||||
|
||||
return animation[stateName];
|
||||
};
|
||||
|
||||
const getMaxTransitionTime = (transition: string) => {
|
||||
const re = /(\d+)ms/g;
|
||||
const times: number[] = [];
|
||||
let matches;
|
||||
while ((matches = re.exec(transition)) != null) {
|
||||
times.push(parseInt(matches[1], 10));
|
||||
}
|
||||
return Math.max(...times);
|
||||
};
|
||||
|
||||
const getAnimationDuration = (animation: Animation | Animation[]): number => {
|
||||
if (animation instanceof Array) {
|
||||
return animation.reduce((maxDuration, a) => {
|
||||
const duration = getAnimationDuration(a);
|
||||
if (duration > maxDuration) {
|
||||
return duration;
|
||||
}
|
||||
return maxDuration;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
const state = animation.stateB;
|
||||
const maxTransition = state.transition ? getMaxTransitionTime(state.transition) : 0;
|
||||
return maxTransition + animation.delay;
|
||||
};
|
||||
|
||||
const getMaxAnimationsDuration = (animations: {[k: string]: Animation} | Animation[]) => (
|
||||
getAnimationDuration(Object.values(animations))
|
||||
);
|
||||
|
||||
export const createAnimationRunner = (animations: {[k: string]: Animation} | Animation[], subscriber: () => void = () => {}):AnimationRunner => {
|
||||
let state = Object.entries(animations).reduce((state, [name, animation]) => ({
|
||||
...state,
|
||||
[name]: getAnimationState(animation),
|
||||
}), {});
|
||||
|
||||
const animationsDuration = getMaxAnimationsDuration(animations);
|
||||
|
||||
const animate = (name: string, animation: Animation) => {
|
||||
if (animation instanceof Array) {
|
||||
animation.forEach((a, index) => {
|
||||
window.requestAnimationFrame(() => {
|
||||
window.setTimeout(() => {
|
||||
state = {
|
||||
...state,
|
||||
[name]: [
|
||||
// ...(state[name]?.slice(0, index)), // todo fix
|
||||
a.stateB,
|
||||
// ...(state[name]?.slice(index + 1, state[name].length)), // todo fix
|
||||
],
|
||||
};
|
||||
subscriber();
|
||||
}, a.delay);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
window.requestAnimationFrame(() => {
|
||||
window.setTimeout(() => {
|
||||
state = {
|
||||
...state,
|
||||
[name]: animation.stateB,
|
||||
};
|
||||
subscriber();
|
||||
}, animation.delay);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getState() {
|
||||
return state;
|
||||
},
|
||||
run() {
|
||||
Object.entries(animations).forEach(([name, animation]) => {
|
||||
animate(name, animation)
|
||||
});
|
||||
},
|
||||
awaitAnimationComplete(callback) {
|
||||
window.setTimeout(callback, animationsDuration);
|
||||
},
|
||||
}
|
||||
}
|
||||
45
src/components/App.tsx
Normal file
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
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>
|
||||
);
|
||||
}
|
||||
20
src/components/Branding.tsx
Normal file
20
src/components/Branding.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type BrandingProps = {
|
||||
logoContent: React.ReactNode
|
||||
modifier?: "backlink" | "logo" | "content" | "footer"
|
||||
}
|
||||
|
||||
export default function Branding({ logoContent, modifier, children }: React.PropsWithChildren<BrandingProps>) {
|
||||
return (
|
||||
<div className={classNames('branding', { [`branding--${modifier}`]: modifier })}>
|
||||
<div className="branding__logo">
|
||||
{logoContent}
|
||||
</div>
|
||||
<div className="branding__content">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
30
src/components/Fadeable.tsx
Normal file
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>
|
||||
);
|
||||
};
|
||||
20
src/components/Flag.tsx
Normal file
20
src/components/Flag.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ItemFlag {
|
||||
flag: "default" | "new" | "changed"
|
||||
}
|
||||
|
||||
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') {
|
||||
let name = item.flag.toUpperCase();
|
||||
let title = ucFirst(item.flag);
|
||||
if (short === true) {
|
||||
name = title[0]
|
||||
}
|
||||
return <span className={`flag flag--${item.flag}`} title={title}>{name}</span>;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
22
src/components/Footer.tsx
Normal file
22
src/components/Footer.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Branding from './Branding';
|
||||
import FooterEnd from './FooterEnd';
|
||||
import { assetUrl, getItemPageNames, isMobileViewport } from '../config';
|
||||
import { Item } from '../model';
|
||||
|
||||
export default function Footer({ items, pageName }: {items: Item[], pageName: string}) {
|
||||
return (
|
||||
<div className={classNames('footer', {'is-hidden': !isMobileViewport() && getItemPageNames(items).includes(pageName)})}>
|
||||
<Branding
|
||||
modifier="footer"
|
||||
logoContent={<img src={assetUrl('logo.svg')} width="150px" height="60px" alt="" />}
|
||||
>
|
||||
<span className="footnote">
|
||||
AOE is a leading global provider of services for digital transformation and digital business models. AOE relies exclusively on established Enterprise Open Source technologies. This leads to innovative solutions, digital products and portals in agile software projects, and helps build long-lasting, strategic partnerships with our customers.
|
||||
</span>
|
||||
</Branding>
|
||||
<FooterEnd/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
src/components/FooterEnd.tsx
Normal file
25
src/components/FooterEnd.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function FooterEnd({modifier}: {modifier?: "in-sidebar"}) {
|
||||
return (
|
||||
<div className={classNames('footer-end', {[`footer-end__${modifier}`]: modifier})}>
|
||||
<div className="footer-social">
|
||||
<div className="footer-social__label">
|
||||
<p>Follow us:</p>
|
||||
</div>
|
||||
<div className="footer-social__links">
|
||||
<a className="social-links-icon" href="https://www.facebook.com/aoepeople" target="_blank" rel="noopener noreferrer"><i className="socicon-facebook social-icon"></i></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" rel="noopener noreferrer"><i className="socicon-linkedin social-icon"></i></a>
|
||||
<a className="social-links-icon" href="https://www.xing.com/company/aoe" target="_blank" rel="noopener noreferrer"><i className="socicon-xing social-icon"></i></a>
|
||||
<a className="social-links-icon" href="https://www.youtube.com/user/aoepeople" target="_blank" rel="noopener noreferrer"><i className="socicon-youtube social-icon"></i></a>
|
||||
<a className="social-links-icon" href="https://github.com/aoepeople" target="_blank" rel="noopener noreferrer"><i className="socicon-github social-icon"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="footer-copyright">
|
||||
<p><a href="https://www.aoe.com/en/copyright-meta/legal-information.html" target="_blank" rel="noopener noreferrer">Legal Information</a></p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
76
src/components/Header.tsx
Normal file
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>
|
||||
);
|
||||
}
|
||||
11
src/components/HeadlineGroup.tsx
Normal file
11
src/components/HeadlineGroup.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function({ children, secondary = false }: React.PropsWithChildren<{secondary?: boolean}>) {
|
||||
return (
|
||||
<div
|
||||
className={classNames('headline-group', {'headline-group--secondary': secondary})}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/components/HeroHeadline.tsx
Normal file
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>
|
||||
);
|
||||
}
|
||||
31
src/components/Item.tsx
Normal file
31
src/components/Item.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Link from './Link';
|
||||
import Flag from './Flag';
|
||||
import { Item as mItem } from '../model';
|
||||
|
||||
type ItemProps = {
|
||||
item: mItem
|
||||
noLeadingBorder?: boolean
|
||||
active?: boolean
|
||||
style: React.CSSProperties
|
||||
}
|
||||
|
||||
export default function Item({ item, noLeadingBorder = false, active = false, style = {}}: ItemProps) {
|
||||
return (
|
||||
<Link
|
||||
className={classNames('item', {
|
||||
'item--no-leading-border': noLeadingBorder,
|
||||
'is-active': active,
|
||||
})}
|
||||
pageName={`${item.quadrant}/${item.name}`}
|
||||
style={style}
|
||||
>
|
||||
<div className="item__title">
|
||||
{item.title}
|
||||
<Flag item={item} />
|
||||
</div>
|
||||
{item.info && <div className="item__info">{item.info}</div>}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
34
src/components/ItemList.tsx
Normal file
34
src/components/ItemList.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import Item from './Item';
|
||||
import { Item as mItem } from '../model'
|
||||
|
||||
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 (
|
||||
<div className="item-list">
|
||||
<div className="item-list__header" style={headerStyle}>
|
||||
{children}
|
||||
</div>
|
||||
<div className="item-list__list">
|
||||
{
|
||||
items.map((item, i) => (
|
||||
<Item
|
||||
key={item.name}
|
||||
item={item}
|
||||
noLeadingBorder={noLeadingBorder}
|
||||
active={activeItem !== null && activeItem !== undefined && activeItem.name === item.name}
|
||||
style={itemStyle[i]}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
src/components/ItemRevision.tsx
Normal file
15
src/components/ItemRevision.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import Badge from './Badge';
|
||||
import { formatRelease } from '../date';
|
||||
import { Revision } from '../model';
|
||||
|
||||
export default function ItemRevision({ revision }: {revision: Revision}) {
|
||||
return (
|
||||
<div className="item-revision">
|
||||
<div>
|
||||
<Badge type={revision.ring}>{revision.ring} | {formatRelease(revision.release)}</Badge>
|
||||
</div>
|
||||
<div className="markdown" dangerouslySetInnerHTML={{__html: revision.body}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
src/components/ItemRevisions.tsx
Normal file
18
src/components/ItemRevisions.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import HeadlineGroup from './HeadlineGroup';
|
||||
import ItemRevision from './ItemRevision';
|
||||
import { Revision } from '../model';
|
||||
|
||||
export default function ItemRevisions({ revisions }: {revisions: Revision[]}) {
|
||||
return (
|
||||
<div className="item-revisions">
|
||||
<HeadlineGroup secondary>
|
||||
<h4 className="headline headline--dark">Revisions:</h4>
|
||||
</HeadlineGroup>
|
||||
|
||||
{revisions.map(revision => (
|
||||
<ItemRevision key={revision.release} revision={revision} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
src/components/Link.tsx
Normal file
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;
|
||||
19
src/components/LogoLink.tsx
Normal file
19
src/components/LogoLink.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Link from './Link';
|
||||
import { assetUrl, radarNameShort } from '../config';
|
||||
|
||||
export default function LogoLink({ small = false }: { small?: boolean }) {
|
||||
return (
|
||||
<Link pageName="index" className={classNames('logo-link', { 'logo-link--small': small })}>
|
||||
<span className="logo-link__icon icon icon--back"></span>
|
||||
<span className="logo-link__slide">
|
||||
<img className="logo-link__img" src={assetUrl('logo.svg')} width="150px" height="60px" alt={radarNameShort} />
|
||||
<span className="logo-link__text">
|
||||
{radarNameShort}
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
49
src/components/PageHelp.tsx
Normal file
49
src/components/PageHelp.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import HeroHeadline from './HeroHeadline';
|
||||
import Fadeable from './Fadeable';
|
||||
import SetTitle from './SetTitle';
|
||||
import { radarName } from '../config';
|
||||
|
||||
export default function PageHelp({ leaving, onLeave}: {leaving: boolean, onLeave: () => void}) {
|
||||
return (
|
||||
<Fadeable leaving={leaving} onLeave={onLeave}>
|
||||
<SetTitle title={ "How to use the " + radarName } />
|
||||
<HeroHeadline>How to use the {radarName}</HeroHeadline>
|
||||
<div className="fullpage-content">
|
||||
<h3>Introduction</h3>
|
||||
<p>Technology is moving fast and new technologies and innovations appear continuously.</p>
|
||||
<p>It's essential for a development and technology company such as AOE to constantly improve and keep track with the latest useful innovations.
|
||||
It is important to openly look for innovations and new technologies and to question established technologies and methods every now and then.</p>
|
||||
<p>But, it is also important to wisely choose which technologies to use in our daily work and in the different projects we are carrying out. As we all know: There is no silver bullet.</p>
|
||||
<h3>What is the {radarName}</h3>
|
||||
<p>The Tech Radar is an overview of different technologies - from languages, frameworks, tools and patterns to platforms - that we consider "new or mentionable".
|
||||
The radar therefore doesn't provide an overview of all established technologies - but it focuses on items that have recently gained in importance or changed.
|
||||
</p>
|
||||
<h3 >How it is created</h3><p>The items in the technology radar are raised by the different teams and therefore a lot of the items are related to the work and challenges the teams face in the different projects. In fact, we don't include anything on the radar, which we haven't already tried ourselves at least once.</p>
|
||||
<p>There have been a lot of valuable discussions in different expert groups about the classification and details of each of technologies and innovations. And the result of all this can be found in the latest technology radar.</p>
|
||||
<h3 >How should it be used</h3>
|
||||
<p>The radar acts as an overview of technologies that we think everyone in the teams should currently know about.</p>
|
||||
<p>Its goal is to act as a guide and inspiration for the daily work in the teams. Its purpose is also to provide helpful information and a bird's-eye perspective - so that decisions can be taken with a much deeper understanding of the subject matter. This results in more-informed and better-aligned decisions.</p>
|
||||
<p>We also hope that developers outside of AOE find the informations in our technologie overview inspirational.</p>
|
||||
<p>We group or categorize the items in 4 quadrants - (sometimes, when it's not 100% clear where a item belongs, we choose the best fit).</p>
|
||||
<p>The quadrants are:</p>
|
||||
<ul>
|
||||
<li><strong>Languages and Frameworks:</strong> We've placed development languages (such as Scala or Golang) here, as well as more low-level development frameworks (such as Play or Symfony), which are useful for implementing custom software of all kinds. </li>
|
||||
<li><strong>Tools:</strong> Here we put different software tools - from small helpers to bigger software projects</li>
|
||||
<li><strong>Methods and Patterns:</strong> Patterns are so important, and a lot of them are valid for a long time (compared to some tools or frameworks). So, this is the category where we put information on methods and patterns concerning development, continuous x, testing, organization, architecture, etc.</li>
|
||||
<li><strong>Platforms and Services</strong> (including AOE internal Services): Here we include infrastructure platforms and services. We also use this category to communicate news about AOE services that we want all AOE teams to be aware of.</li>
|
||||
</ul>
|
||||
<p>Each of the items is classified in one of these rings:</p>
|
||||
<ul>
|
||||
<li><strong>Adopt:</strong> We can clearly recommend this technology. We have used it for longer period of time in many teams and it has proven to be stable and useful.</li>
|
||||
<li><strong>Trial:</strong> We have used it with success and recommend to have a closer look at the technology in this ring. The goal of items here is to look at them more closely, with the goal to bring them to the adopt level.</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>
|
||||
</ul>
|
||||
<p>Contributions and source code of the radar are on github: <a href="https://github.com/AOEpeople/aoe_technology_radar" target="_blank" rel="noopener noreferrer">AOE Tech Radar on Github</a></p>
|
||||
|
||||
|
||||
</div>
|
||||
</Fadeable>
|
||||
);
|
||||
}
|
||||
35
src/components/PageIndex.tsx
Normal file
35
src/components/PageIndex.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { formatRelease } from '../date';
|
||||
import { featuredOnly, Item } from '../model';
|
||||
import HeroHeadline from './HeroHeadline';
|
||||
import QuadrantGrid from './QuadrantGrid';
|
||||
import Fadeable from './Fadeable';
|
||||
import SetTitle from './SetTitle';
|
||||
import { radarName, radarNameShort } from '../config';
|
||||
import { MomentInput } from 'moment';
|
||||
|
||||
type PageIndexProps = {
|
||||
leaving: boolean
|
||||
onLeave: () => void
|
||||
items: Item[]
|
||||
releases: MomentInput[]
|
||||
}
|
||||
|
||||
export default function PageIndex({ leaving, onLeave, items, releases }: PageIndexProps) {
|
||||
const newestRelease = releases.slice(-1)[0];
|
||||
const numberOfReleases = releases.length;
|
||||
return (
|
||||
<Fadeable leaving={leaving} onLeave={onLeave}>
|
||||
<SetTitle title={radarNameShort} />
|
||||
<div className="headline-group">
|
||||
<HeroHeadline alt={`Version #${numberOfReleases}`}>
|
||||
{radarName}
|
||||
</HeroHeadline>
|
||||
</div>
|
||||
<QuadrantGrid items={featuredOnly(items)} />
|
||||
<div className="publish-date">
|
||||
Published {formatRelease(newestRelease)}
|
||||
</div>
|
||||
</Fadeable>
|
||||
);
|
||||
}
|
||||
273
src/components/PageItem.tsx
Normal file
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
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
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>
|
||||
);
|
||||
}
|
||||
29
src/components/PageQuadrant.tsx
Normal file
29
src/components/PageQuadrant.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import HeroHeadline from './HeroHeadline';
|
||||
import HeadlineGroup from './HeadlineGroup';
|
||||
import QuadrantSection from './QuadrantSection';
|
||||
import Fadeable from './Fadeable';
|
||||
import SetTitle from './SetTitle';
|
||||
|
||||
import { translate } from '../config';
|
||||
import {featuredOnly, groupByQuadrants, Item} from '../model';
|
||||
|
||||
type PageQuadrantProps = {
|
||||
leaving: boolean
|
||||
onLeave: () => void
|
||||
pageName: string
|
||||
items: Item[]
|
||||
}
|
||||
|
||||
export default function PageQuadrant({ leaving, onLeave, pageName, items }: PageQuadrantProps) {
|
||||
const groups = groupByQuadrants(featuredOnly(items));
|
||||
return (
|
||||
<Fadeable leaving={leaving} onLeave={onLeave}>
|
||||
<SetTitle title={translate(pageName)} />
|
||||
<HeadlineGroup>
|
||||
<HeroHeadline>{translate(pageName)}</HeroHeadline>
|
||||
</HeadlineGroup>
|
||||
<QuadrantSection groups={groups} quadrantName={pageName} big />
|
||||
</Fadeable>
|
||||
);
|
||||
}
|
||||
41
src/components/PageToolbox.tsx
Normal file
41
src/components/PageToolbox.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import HeroHeadline from './HeroHeadline';
|
||||
import Fadeable from './Fadeable';
|
||||
import SetTitle from './SetTitle';
|
||||
|
||||
export default function PageToolbox({ leaving, onLeave }: {leaving: boolean, onLeave: () => void}) {
|
||||
return (
|
||||
<Fadeable leaving={leaving} onLeave={onLeave}>
|
||||
<SetTitle title="Small AOE Toolbox" />
|
||||
<HeroHeadline>Small AOE Toolbox</HeroHeadline>
|
||||
<div className="fullpage-content">
|
||||
<h3>Useful Tools</h3>
|
||||
|
||||
<ul>
|
||||
<li>Fiddler - free web debugging proxy ( http://www.telerik.com/fiddler )</li>
|
||||
<li>SoapUI - Webservice Test Tool (https://www.soapui.org/ )</li>
|
||||
<li>Postman - API Test Tool ( https://www.getpostman.com/ )</li>
|
||||
<li> Modelio - Simple free UML Modelling tool ( https://www.modelio.org/ )</li>
|
||||
</ul>
|
||||
|
||||
<h3>Useful Tools (commercial)</h3>
|
||||
|
||||
<ul>
|
||||
<li> Paw Rest Client</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<h3>Estabilshed Technologies</h3>
|
||||
<p>
|
||||
Not mentionable but adopted for a while now:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Jenkins</li>
|
||||
<li>Redis</li>
|
||||
<li>Varnish</li>
|
||||
<li>Symfony2</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Fadeable>
|
||||
);
|
||||
}
|
||||
23
src/components/QuadrantGrid.tsx
Normal file
23
src/components/QuadrantGrid.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { groupByQuadrants, Item, Group } from '../model';
|
||||
import { quadrants } from '../config';
|
||||
import QuadrantSection from './QuadrantSection';
|
||||
|
||||
const renderQuadrant = (quadrantName: string, groups: Group) => {
|
||||
return (
|
||||
<div key={quadrantName} className="quadrant-grid__quadrant">
|
||||
<QuadrantSection quadrantName={quadrantName} groups={groups} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function QuadrantGrid({ items }: { items: Item[] }) {
|
||||
const groups = groupByQuadrants(items);
|
||||
return (
|
||||
<div className="quadrant-grid">
|
||||
{
|
||||
quadrants.map((quadrantName) => renderQuadrant(quadrantName, groups))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
76
src/components/QuadrantSection.tsx
Normal file
76
src/components/QuadrantSection.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { translate, rings, ring } from '../config';
|
||||
import Badge from './Badge';
|
||||
import Link from './Link';
|
||||
import ItemList from './ItemList';
|
||||
import Flag from './Flag';
|
||||
import { Item, Group } from '../model';
|
||||
|
||||
const renderList = (ringName: ring, quadrantName: string, groups: Group, big: boolean) => {
|
||||
const itemsInRing = groups[quadrantName][ringName];
|
||||
|
||||
if (big) {
|
||||
return (
|
||||
<ItemList items={itemsInRing} noLeadingBorder>
|
||||
<Badge type={ringName} big={big}>
|
||||
{ringName}
|
||||
</Badge>
|
||||
</ItemList>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ring-list">
|
||||
<div className="ring-list__header">
|
||||
<Badge type={ringName}>{ringName}</Badge>
|
||||
</div>
|
||||
{itemsInRing.map(item => (
|
||||
<span key={item.name} className="ring-list__item">
|
||||
<Link className="link" pageName={`${item.quadrant}/${item.name}`}>
|
||||
{item.title}
|
||||
<Flag item={item} short />
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderRing = (ringName: ring, quadrantName: string, groups: Group, big: boolean) => {
|
||||
if (
|
||||
!groups[quadrantName] ||
|
||||
!groups[quadrantName][ringName] ||
|
||||
groups[quadrantName][ringName].length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div key={ringName} className="quadrant-section__ring">
|
||||
{renderList(ringName, quadrantName, groups, big)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function QuadrantSection({ quadrantName, groups, big = false }: { quadrantName: string, groups: Group, big?: boolean }) {
|
||||
return (
|
||||
<div className="quadrant-section">
|
||||
<div className="quadrant-section__header">
|
||||
<div className="split">
|
||||
<div className="split__left">
|
||||
<h4 className="headline">{translate(quadrantName)}</h4>
|
||||
</div>
|
||||
{!big && (
|
||||
<div className="split__right">
|
||||
<Link className="icon-link" pageName={`${quadrantName}`}>
|
||||
<span className="icon icon--pie icon-link__icon" />Zoom In
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="quadrant-section__rings">
|
||||
{rings.map(ringName => renderRing(ringName, quadrantName, groups, big))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
82
src/components/Router.tsx
Normal file
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
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
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;
|
||||
}
|
||||
58
src/config.ts
Normal file
58
src/config.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
// import moment from 'moment';
|
||||
import { Item, Radar } from './model';
|
||||
|
||||
export const radarName = 'AOE Technology Radar';
|
||||
export const radarNameShort = 'Technology Radar';
|
||||
|
||||
export const quadrants = [
|
||||
'languages-and-frameworks',
|
||||
'methods-and-patterns',
|
||||
'platforms-and-aoe-services',
|
||||
'tools',
|
||||
];
|
||||
|
||||
export function assetUrl(file: string) {
|
||||
return '/' + file;
|
||||
// return `/techradar/assets/${file}`
|
||||
}
|
||||
|
||||
export const getPageNames = (radar: Radar) => {
|
||||
return [
|
||||
'index',
|
||||
'overview',
|
||||
'help-and-about-tech-radar',
|
||||
'aoe-toolbox',
|
||||
...quadrants,
|
||||
...getItemPageNames(radar.items),
|
||||
]
|
||||
}
|
||||
|
||||
export const getItemPageNames = (items: Item[]) => items.map(item => `${item.quadrant}/${item.name}`);
|
||||
|
||||
export type ring = "adopt" | "trial" | "assess" | "hold"
|
||||
|
||||
export const rings: ring[] = [
|
||||
'adopt',
|
||||
'trial',
|
||||
'assess',
|
||||
'hold'
|
||||
];
|
||||
|
||||
const messages:{[k: string]: string} = {
|
||||
'languages-and-frameworks': 'Languages & Frameworks',
|
||||
'methods-and-patterns': 'Methods & Patterns',
|
||||
'platforms-and-aoe-services': 'Platforms and Operations',
|
||||
'tools': 'Tools',
|
||||
};
|
||||
|
||||
export const translate = (key: string) => (messages[key] || '-');
|
||||
|
||||
export function isMobileViewport() {
|
||||
// return false for server side rendering
|
||||
if (typeof window == 'undefined') return false;
|
||||
|
||||
const width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||
return width < 1200;
|
||||
}
|
||||
|
||||
// const formatRelease = (release: moment.MomentInput) => moment(release, 'YYYY-MM-DD').format('MMM YYYY');
|
||||
5
src/date.ts
Normal file
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
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
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
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/rd.json
Normal file
File diff suppressed because one or more lines are too long
1
src/react-app-env.d.ts
vendored
Normal file
1
src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
Reference in New Issue
Block a user