chore: create demo page

This commit is contained in:
Roman Hotsiy 2018-03-13 12:58:22 +02:00
parent 28f239114b
commit 83eaa5f838
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
5 changed files with 491 additions and 5 deletions

215
demo/ComboBox.tsx Normal file
View File

@ -0,0 +1,215 @@
/**
* Could not find ready-to-use component with required behaviour so
* I quickly hacked my own. Will refactor into separate npm package later
*/
import * as React from 'react';
import styled, { StyledFunction } from 'styled-components';
function withProps<T, U extends HTMLElement = HTMLElement>(
styledFunction: StyledFunction<React.HTMLProps<U>>,
): StyledFunction<T & React.HTMLProps<U>> {
return styledFunction;
}
const DropDownItem = withProps<{ active: boolean }>(styled.li)`
${props => ((props as any).active ? 'background-color: #eee' : '')};
padding: 13px 16px;
&:hover {
background-color: #eee;
}
cursor: pointer;
text-overflow: ellipsis;
overflow: hidden;
`;
const DropDownList = styled.ul`
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12),
0 3px 1px -2px rgba(0, 0, 0, 0.2);
background: #fff;
border-radius: 0 0 2px 2px;
top: 100%;
left: 0;
right: 0;
z-index: 200;
overflow: hidden;
position: absolute;
list-style: none;
margin: 4px 0 0 0;
padding: 5px 0;
font-family: 'Lato';
overflow: hidden;
`;
const ComboBoxWrap = styled.div`
position: relative;
width: 100%;
max-width: 500px;
display: flex;
`;
const Input = styled.input`
box-sizing: border-box;
width: 100%;
padding: 0 10px;
color: #555;
background-color: #fff;
border: 1px solid #ccc;
font-size: 16px;
height: 28px;
box-sizing: border-box;
vertical-align: middle;
line-height: 1;
outline: none;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
&:focus {
border-color: #66afe9;
outline: 0;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
}
`;
const Button = styled.button`
background-color: #fff;
color: #333;
padding: 2px 10px;
touch-action: manipulation;
cursor: pointer;
user-select: none;
border: 1px solid #ccc;
border-left: 0;
font-size: 16px;
height: 28px;
box-sizing: border-box;
vertical-align: middle;
line-height: 1;
outline: none;
width: 80px;
`;
export interface ComboBoxProps {
onChange?: (val: string) => void;
options: { value: string; label: string }[];
placeholder?: string;
value?: string;
}
export interface ComboBoxState {
open: boolean;
value: string;
activeItemIdx: number;
}
export default class ComboBox extends React.Component<ComboBoxProps, ComboBoxState> {
state = {
open: false,
value: this.props.value || '',
activeItemIdx: -1,
};
open = () => {
this.setState({
open: true,
});
};
close = () => {
this.setState({
open: false,
activeItemIdx: -1,
});
};
handleChange = e => {
this.updateValue(e.currentTarget.value);
};
updateValue(value) {
this.setState({
value,
activeItemIdx: -1,
});
}
handleSelect(value: string) {
this.updateValue(value);
if (this.props.onChange) {
this.props.onChange(value);
}
this.close();
}
handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.keyCode === 13) {
this.handleSelect(e.currentTarget.value);
} else if (e.keyCode === 40) {
const activeItemIdx = Math.min(this.props.options.length - 1, ++this.state.activeItemIdx);
this.setState({
open: true,
activeItemIdx,
value: this.props.options[activeItemIdx].value,
});
e.preventDefault();
} else if (e.keyCode === 38) {
const activeItemIdx = Math.max(0, --this.state.activeItemIdx);
this.setState({
activeItemIdx,
value: this.props.options[activeItemIdx].value,
});
e.preventDefault();
} else if (e.keyCode === 27) {
this.close();
}
};
handleBlur = () => {
setTimeout(() => this.close(), 100);
};
handleItemClick = (val, idx) => {
this.handleSelect(val);
this.setState({
activeItemIdx: idx,
});
};
render() {
const { open, value, activeItemIdx } = this.state;
const { options, placeholder } = this.props;
return (
<ComboBoxWrap>
<Input
placeholder={placeholder}
onChange={this.handleChange}
value={value}
onFocus={this.open}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyPress}
/>
<Button onClick={() => this.handleSelect(this.state.value)}> TRY IT </Button>
{open && (
<DropDownList>
{options.map((option, idx) => (
<DropDownItem
active={idx == activeItemIdx}
key={option.value}
onMouseDown={() => {
this.handleItemClick(option.value, idx);
}}
>
<small>
<strong>{option.label}</strong>
</small>
<br />
{option.value}
</DropDownItem>
))}
</DropDownList>
)}
</ComboBoxWrap>
);
}
}

View File

@ -3,7 +3,9 @@
<head>
<meta charset="UTF-8" />
<title>ReDoc</title>
<title>ReDoc Interactive Demo</title>
<meta name="description" content="ReDoc Interactive Demo. OpenAPI/Swagger-generated API Reference Documentation" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
@ -18,8 +20,21 @@
</head>
<body>
<redoc spec-url="./openapi.yaml"></redoc>
<script src="../bundles/redoc.standalone.js"></script>
<div id="container"> </div>
<script>
(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');
if (window.location.host === 'rebilly.github.io') {
ga('create', 'UA-81703547-1', 'auto');
ga('send', 'pageview');
}
</script>
</body>
</html>
</html>

182
demo/index.tsx Normal file
View File

@ -0,0 +1,182 @@
import * as React from 'react';
import { render } from 'react-dom';
import styled from 'styled-components';
import { RedocStandalone } from '../';
import ComboBox from './ComboBox';
import * as url from 'url';
const demos = [
{ value: 'https://api.apis.guru/v2/specs/instagram.com/1.0.0/swagger.yaml', label: 'Instagram' },
{
value: 'https://api.apis.guru/v2/specs/googleapis.com/calendar/v3/swagger.yaml',
label: 'Google Calendar',
},
{ value: 'https://api.apis.guru/v2/specs/slack.com/1.0.3/swagger.yaml', label: 'Slack' },
{ value: 'https://api.apis.guru/v2/specs/zoom.us/2.0.0/swagger.yaml', label: 'Zoom.us' },
{
value: 'https://api.apis.guru/v2/specs/graphhopper.com/1.0/swagger.yaml',
label: 'GraphHopper',
},
];
const DEFAULT_SPEC = 'openapi.yaml';
class DemoApp extends React.Component<
{},
{ specUrl: string; dropdownOpen: boolean; cors: boolean }
> {
constructor(props) {
super(props);
let parts = window.location.search.match(/url=([^&]+)/);
let url = DEFAULT_SPEC;
if (parts && parts.length > 1) {
url = decodeURIComponent(parts[1]);
}
parts = window.location.search.match(/[?&]nocors(&|#|$)/);
let cors = true;
if (parts && parts.length > 1) {
cors = false;
}
this.state = {
specUrl: url,
dropdownOpen: false,
cors,
};
}
handleChange = (url: string) => {
this.setState({
specUrl: url,
});
window.history.pushState(
undefined,
'',
updateQueryStringParameter(location.search, 'url', url),
);
};
toggleCors = (e: React.ChangeEvent<HTMLInputElement>) => {
const cors = e.currentTarget.checked;
this.setState({
cors: cors,
});
window.history.pushState(
undefined,
'',
updateQueryStringParameter(location.search, 'nocors', cors ? undefined : ''),
);
};
render() {
const { specUrl, cors } = this.state;
let proxiedUrl = specUrl;
if (specUrl !== DEFAULT_SPEC) {
proxiedUrl = cors
? '\\\\cors.apis.guru/' + url.resolve(window.location.href, specUrl)
: specUrl;
}
return (
<>
<Heading>
<a href=".">
<Logo src="https://github.com/Rebilly/ReDoc/raw/master/docs/images/redoc-logo.png" />
</a>
<ControlsContainer>
<ComboBox
placeholder={'URL to a spec to try'}
options={demos}
onChange={this.handleChange}
value={specUrl === DEFAULT_SPEC ? '' : specUrl}
/>
<CorsCheckbox title="Use CORS proxy">
<input id="cors_checkbox" type="checkbox" onChange={this.toggleCors} checked={cors} />
<label htmlFor="cors_checkbox">CORS</label>
</CorsCheckbox>
</ControlsContainer>
<iframe
src="https://ghbtns.com/github-btn.html?user=Rebilly&amp;repo=ReDoc&amp;type=star&amp;count=true&amp;size=large"
frameBorder="0"
scrolling="0"
width="150px"
height="30px"
/>
</Heading>
<RedocStandalone specUrl={proxiedUrl} options={{ scrollYOffset: 'nav' }} />
</>
);
}
}
/* ====== Styled components ====== */
const ControlsContainer = styled.div`
display: flex;
justify-content: center;
flex: 1;
margin: 0 15px;
align-items: center;
`;
const CorsCheckbox = styled.div`
margin-left: 10px;
label {
font-size: 13px;
}
`;
const Heading = styled.nav`
position: sticky;
top: 0;
height: 50px;
box-sizing: border-box;
background: white;
border-bottom: 1px solid #cccccc;
z-index: 10;
padding: 5px;
display: flex;
align-items: center;
font-family: 'Lato';
`;
const Logo = styled.img`
height: 40px;
width: 124px;
display: inline-block;
margin-right: 15px;
`;
render(<DemoApp />, document.getElementById('container'));
/* ====== Helpers ====== */
function updateQueryStringParameter(uri, key, value) {
const keyValue = value === '' ? key : key + '=' + value;
var re = new RegExp('([?|&])' + key + '=?.*?(&|#|$)', 'i');
if (uri.match(re)) {
if (value !== undefined) {
return uri.replace(re, '$1' + keyValue + '$2');
} else {
return uri.replace(re, (_, separator: string, rest: string) => {
if (rest.startsWith('&')) {
rest = rest.substring(1);
}
return separator === '&' ? rest : separator + rest;
});
}
} else {
if (value === undefined) {
return uri;
}
var hash = '';
if (uri.indexOf('#') !== -1) {
hash = uri.replace(/.*#/, '#');
uri = uri.replace(/#.*/, '');
}
var separator = uri.indexOf('?') !== -1 ? '&' : '?';
return uri + separator + keyValue + hash;
}
}

73
demo/webpack.config.ts Normal file
View File

@ -0,0 +1,73 @@
import * as webpack from 'webpack';
import * as HtmlWebpackPlugin from 'html-webpack-plugin';
const VERSION = JSON.stringify(require('../package.json').version);
const REVISION = JSON.stringify(
require('child_process')
.execSync('git rev-parse --short HEAD')
.toString()
.trim(),
);
export default {
entry: __dirname + '/index.tsx',
output: {
filename: 'redoc-demo.bundle.js',
path: __dirname + '/dist',
},
devServer: {
contentBase: __dirname,
watchContentBase: true,
port: 8081,
stats: 'errors-only',
},
devtool: 'eval',
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'],
},
module: {
rules: [
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' },
{ test: [/\.eot$/, /\.gif$/, /\.woff$/, /\.svg$/, /\.ttf$/], use: 'null-loader' },
{
test: /\.tsx?$/,
use: [
'react-hot-loader/webpack',
{
loader: 'awesome-typescript-loader',
options: {
module: 'es2015',
},
},
],
exclude: ['node_modules'],
},
{
test: /\.css$/,
use: {
loader: 'css-loader',
options: {
sourceMap: true,
minimize: true,
},
},
},
],
},
plugins: [
new webpack.DefinePlugin({
__REDOC_VERSION__: VERSION,
__REDOC_REVISION__: REVISION,
__REDOC_DEV__: false,
}),
new webpack.NamedModulesPlugin(),
new webpack.optimize.ModuleConcatenationPlugin(),
new HtmlWebpackPlugin({
template: 'demo/index.html',
}),
],
};

View File

@ -19,7 +19,8 @@
"prettier": "prettier --write \"src/**/*.{ts,tsx}\"",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1",
"lint": "tslint --project tsconfig.json",
"benchmark": "node ./benchmark/benchmark.js"
"benchmark": "node ./benchmark/benchmark.js",
"start:demo": "webpack-dev-server --config demo/webpack.config.ts"
},
"author": "",
"license": "MIT",