Adding semi-functional rendering demo of app
This commit is contained in:
parent
0606325d6d
commit
a4450b34b3
@ -71,6 +71,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.2.2",
|
||||
"@ungap/url-search-params": "^0.1.2",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-jest": "^23.6.0",
|
||||
"babel-preset-gatsby": "^0.1.6",
|
||||
|
5
src/components/App/context.js
Normal file
5
src/components/App/context.js
Normal file
@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
const AppContext = React.createContext();
|
||||
|
||||
export default AppContext;
|
161
src/components/App/index.js
Normal file
161
src/components/App/index.js
Normal file
@ -0,0 +1,161 @@
|
||||
import React from 'react';
|
||||
import URLSearchParams from '@ungap/url-search-params';
|
||||
|
||||
import AppContext from 'components/App/context';
|
||||
import Form from 'components/Form';
|
||||
import Loader from 'components/Loader';
|
||||
import Message from 'components/Message';
|
||||
import SVG from 'components/SVG';
|
||||
|
||||
const toUrl = params => new URLSearchParams(params).toString();
|
||||
|
||||
const readURLHash = () => {
|
||||
const query = document.location.hash.slice(1);
|
||||
const params = new URLSearchParams(query);
|
||||
|
||||
if (params.get('syntax')) {
|
||||
return {
|
||||
syntax: params.get('syntax'),
|
||||
expr: params.get('expr')
|
||||
};
|
||||
} else {
|
||||
// Assuming old-style URL
|
||||
return {
|
||||
syntax: 'js',
|
||||
expr: query || ''
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const createSvgLink = async ({ svg }) => {
|
||||
try {
|
||||
const type = 'image/svg+xml';
|
||||
const blob = new Blob([svg], { type });
|
||||
|
||||
return {
|
||||
url: URL.createObjectURL(blob),
|
||||
label: 'Download SVG',
|
||||
filename: 'image.svg',
|
||||
type
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e); // eslint-disable-line no-console
|
||||
}
|
||||
};
|
||||
|
||||
const createPngLink = async ({ svg, width, height }) => {
|
||||
try {
|
||||
const type = 'image/png';
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
const loader = new Image();
|
||||
|
||||
loader.width = canvas.width = Number(width) * 2;
|
||||
loader.height = canvas.height = Number(height) * 2;
|
||||
|
||||
await new Promise(resolve => {
|
||||
loader.onload = resolve;
|
||||
loader.src = 'data:image/svg+xml,' + encodeURIComponent(svg);
|
||||
});
|
||||
|
||||
context.drawImage(loader, 0, 0, loader.width, loader.height);
|
||||
|
||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, type));
|
||||
|
||||
return {
|
||||
url: URL.createObjectURL(blob),
|
||||
label: 'Download PNG',
|
||||
filename: 'image.png',
|
||||
type
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e); // eslint-disable-line no-console
|
||||
}
|
||||
};
|
||||
|
||||
class App extends React.PureComponent {
|
||||
state={
|
||||
...readURLHash()
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('hashchange', this.handleHashChange);
|
||||
this.handleHashChange();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('hashchange', this.handleHashChange);
|
||||
}
|
||||
|
||||
handleHashChange = () => {
|
||||
const { expr, syntax } = readURLHash();
|
||||
|
||||
if (!expr) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
syntax,
|
||||
expr,
|
||||
permalinkUrl: document.location.toString()
|
||||
});
|
||||
console.log('Render:', syntax, expr); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
handleRetry = event => {
|
||||
event.preventDefault();
|
||||
this.handleHashChange();
|
||||
}
|
||||
|
||||
renderExpr = ({ expr, syntax }) => {
|
||||
if (expr) {
|
||||
document.location.hash = toUrl({ syntax, expr });
|
||||
}
|
||||
}
|
||||
|
||||
svgData = async ({ svg, width, height }) => {
|
||||
if (svg !== this.state.svg) {
|
||||
this.setState({
|
||||
svg,
|
||||
svgLink: await createSvgLink({ svg }),
|
||||
pngLink: await createPngLink({ svg, width, height })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
svgLink,
|
||||
pngLink,
|
||||
permalinkUrl,
|
||||
expr,
|
||||
syntax
|
||||
} = this.state;
|
||||
const context = {
|
||||
svgLink,
|
||||
pngLink,
|
||||
permalinkUrl,
|
||||
expr,
|
||||
syntax,
|
||||
renderExpr: this.renderExpr,
|
||||
svgData: this.svgData
|
||||
};
|
||||
|
||||
return <AppContext.Provider value={ context }>
|
||||
<Form />
|
||||
|
||||
<Loader />
|
||||
|
||||
<Message type="error" heading="Render Failure">
|
||||
<p>An error occurred while rendering the regular expression.</p>
|
||||
<a href="#retry" onClick={ this.handleRetry }>Retry</a>
|
||||
</Message>
|
||||
|
||||
<SVG />
|
||||
</AppContext.Provider>;
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
71
src/components/Form/index.js
Normal file
71
src/components/Form/index.js
Normal file
@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
|
||||
import ExpandIcon from 'react-feather/dist/icons/chevrons-down';
|
||||
|
||||
import style from './style.module.css';
|
||||
|
||||
import AppContext from 'components/App/context';
|
||||
import FormActions from 'components/FormActions';
|
||||
|
||||
const syntaxList = [
|
||||
{ id: 'js', label: 'JavaScript' },
|
||||
{ id: 'pcre', label: 'PCRE' }
|
||||
];
|
||||
|
||||
class Form extends React.PureComponent {
|
||||
static contextType = AppContext
|
||||
|
||||
state = {
|
||||
expr: this.context.expr,
|
||||
syntax: this.context.syntax
|
||||
}
|
||||
|
||||
handleSubmit = event => {
|
||||
event.preventDefault();
|
||||
|
||||
const { expr, syntax } = this.state;
|
||||
|
||||
this.context.renderExpr({ expr, syntax });
|
||||
}
|
||||
|
||||
handleKeyPress = event => {
|
||||
if (event.charCode === 13 && event.shiftKey) {
|
||||
this.handleSubmit(event);
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = event => this.setState({
|
||||
[event.target.name]: event.target.value
|
||||
})
|
||||
|
||||
render() {
|
||||
const { expr, syntax } = this.state;
|
||||
|
||||
return <div className={ style.form }>
|
||||
<form onSubmit={ this.handleSubmit }>
|
||||
<textarea
|
||||
name="expr"
|
||||
value={ expr }
|
||||
onKeyPress={ this.handleKeyPress }
|
||||
onChange={ this.handleChange }
|
||||
autoFocus
|
||||
placeholder="Enter regular expression to display"></textarea>
|
||||
<button type="submit">Display</button>
|
||||
<div className={ style.select }>
|
||||
<select
|
||||
name="syntax"
|
||||
value={ syntax }
|
||||
onChange={ this.handleChange } >
|
||||
{ syntaxList.map(({ id, label }) => (
|
||||
<option value={ id } key={ id }>{ label }</option>
|
||||
)) }
|
||||
</select>
|
||||
<ExpandIcon />
|
||||
</div>
|
||||
<FormActions />
|
||||
</form>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default Form;
|
49
src/components/Form/style.module.css
Normal file
49
src/components/Form/style.module.css
Normal file
@ -0,0 +1,49 @@
|
||||
@import url('../../globals.module.css');
|
||||
|
||||
:root {
|
||||
--control-gradient: var(--color-green) var(--gradient-green);
|
||||
--select-height: 2.8rem;
|
||||
--select-width: 12rem;
|
||||
--entry-line-height: 1.5em;
|
||||
}
|
||||
|
||||
.form {
|
||||
margin: var(--spacing-margin) 0;
|
||||
overflow: hidden; /* Keep floated content in the box */
|
||||
|
||||
& textarea {
|
||||
display: block;
|
||||
font-size: inherit;
|
||||
line-height: var(--entry-line-height);
|
||||
border: 0 none;
|
||||
outline: none;
|
||||
background: var(--color-tan);
|
||||
padding: 0 1rem;
|
||||
margin-bottom: var(--spacing-margin);
|
||||
width: 100% !important; /* "!important" to prevent user changing width */
|
||||
height: calc(3 * var(--entry-line-height));
|
||||
box-sizing: border-box;
|
||||
font-family: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace;
|
||||
}
|
||||
|
||||
& textarea::placeholder {
|
||||
color: var(--color-brown);
|
||||
}
|
||||
|
||||
& button {
|
||||
font-size: inherit;
|
||||
font-weight: bold;
|
||||
line-height: 2.8rem;
|
||||
width: 10rem;
|
||||
border: 0 none;
|
||||
background: var(--control-gradient);
|
||||
color: var(--color-black);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.select {
|
||||
composes: fancy-select;
|
||||
}
|
38
src/components/FormActions/index.js
Normal file
38
src/components/FormActions/index.js
Normal file
@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
|
||||
import DownloadIcon from 'react-feather/dist/icons/download';
|
||||
import LinkIcon from 'react-feather/dist/icons/link';
|
||||
|
||||
import style from './style.module.css';
|
||||
|
||||
import AppContext from 'components/App/context';
|
||||
|
||||
class FormActions extends React.PureComponent {
|
||||
static contextType = AppContext
|
||||
|
||||
downloadLink({ url, filename, type, label }) {
|
||||
return <li>
|
||||
<a href={ url } download={ filename } type={ type }>
|
||||
<DownloadIcon />{ label }
|
||||
</a>
|
||||
</li>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
permalinkUrl,
|
||||
svgLink,
|
||||
pngLink
|
||||
} = this.context;
|
||||
|
||||
return <ul className={ style.actions }>
|
||||
{ pngLink && this.downloadLink(pngLink) }
|
||||
{ svgLink && this.downloadLink(svgLink) }
|
||||
{ permalinkUrl && <li>
|
||||
<a href={ permalinkUrl }><LinkIcon />Permalink</a>
|
||||
</li> }
|
||||
</ul>;
|
||||
}
|
||||
}
|
||||
|
||||
export default FormActions;
|
19
src/components/FormActions/style.module.css
Normal file
19
src/components/FormActions/style.module.css
Normal file
@ -0,0 +1,19 @@
|
||||
@import url('../../globals.module.css');
|
||||
|
||||
.actions {
|
||||
composes: inline-list with-separator-left;
|
||||
float: right;
|
||||
|
||||
@media (max-width: 700px) {
|
||||
& li {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
& svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-right: 0.5rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
80
src/components/Loader/__snapshots__/test.js.snap
Normal file
80
src/components/Loader/__snapshots__/test.js.snap
Normal file
@ -0,0 +1,80 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Loader rendering 1`] = `
|
||||
<Loader>
|
||||
<div
|
||||
className="loader"
|
||||
>
|
||||
<Loader
|
||||
color="currentColor"
|
||||
size="24"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="2"
|
||||
y2="6"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="18"
|
||||
y2="22"
|
||||
/>
|
||||
<line
|
||||
x1="4.93"
|
||||
x2="7.76"
|
||||
y1="4.93"
|
||||
y2="7.76"
|
||||
/>
|
||||
<line
|
||||
x1="16.24"
|
||||
x2="19.07"
|
||||
y1="16.24"
|
||||
y2="19.07"
|
||||
/>
|
||||
<line
|
||||
x1="2"
|
||||
x2="6"
|
||||
y1="12"
|
||||
y2="12"
|
||||
/>
|
||||
<line
|
||||
x1="18"
|
||||
x2="22"
|
||||
y1="12"
|
||||
y2="12"
|
||||
/>
|
||||
<line
|
||||
x1="4.93"
|
||||
x2="7.76"
|
||||
y1="19.07"
|
||||
y2="16.24"
|
||||
/>
|
||||
<line
|
||||
x1="16.24"
|
||||
x2="19.07"
|
||||
y1="7.76"
|
||||
y2="4.93"
|
||||
/>
|
||||
</svg>
|
||||
</Loader>
|
||||
<div
|
||||
className="message"
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
</Loader>
|
||||
`;
|
14
src/components/Loader/index.js
Normal file
14
src/components/Loader/index.js
Normal file
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import LoaderIcon from 'react-feather/dist/icons/loader';
|
||||
|
||||
import style from './style.module.css';
|
||||
|
||||
const Loader = () => (
|
||||
<div className={ style.loader }>
|
||||
<LoaderIcon />
|
||||
<div className={ style.message }>Loading...</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Loader;
|
43
src/components/Loader/style.module.css
Normal file
43
src/components/Loader/style.module.css
Normal file
@ -0,0 +1,43 @@
|
||||
@import url('../../globals.module.css');
|
||||
|
||||
.loader {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: var(--spacing-margin) 0;
|
||||
padding: 2rem;
|
||||
background: var(--color-white);
|
||||
color: var(--color-black);
|
||||
|
||||
& .message {
|
||||
font-weight: bold;
|
||||
font-size: 2.5rem;
|
||||
margin-top: 2rem;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& svg {
|
||||
display: block;
|
||||
transform: scaleZ(1); /* Move to separate render layer in Chrome */
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
stroke: var(--color-black);
|
||||
animation: loader-spin 1s steps(8) infinite;
|
||||
|
||||
& line:nth-of-type(1) { stroke: color(var(--color-black) alpha(0.75)); }
|
||||
& line:nth-of-type(3) { stroke: color(var(--color-black) alpha(0.50)); }
|
||||
& line:nth-of-type(5) { stroke: color(var(--color-black) alpha(0.25)); }
|
||||
& line:nth-of-type(7) { stroke: color(var(--color-black) alpha(0)); }
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loader-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
15
src/components/Loader/test.js
Normal file
15
src/components/Loader/test.js
Normal file
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import Loader from 'components/Loader';
|
||||
|
||||
describe('Loader', () => {
|
||||
test('rendering', () => {
|
||||
// Using full rendering here since styles for this depend on the structure
|
||||
// of the SVG.
|
||||
const component = mount(
|
||||
<Loader />
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
43
src/components/SVG/index.js
Normal file
43
src/components/SVG/index.js
Normal file
@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
|
||||
import PlaceholderIcon from 'react-feather/dist/icons/file-text';
|
||||
|
||||
import style from './style.module.css';
|
||||
|
||||
import AppContext from 'components/App/context';
|
||||
|
||||
class SVG extends React.PureComponent {
|
||||
static contextType = AppContext
|
||||
|
||||
svgContainer = React.createRef()
|
||||
|
||||
provideSVGData() {
|
||||
if (!this.svgContainer.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const svg = this.svgContainer.current.querySelector('svg');
|
||||
this.context.svgData({
|
||||
svg: svg.outerHTML,
|
||||
width: svg.getAttribute('width'),
|
||||
height: svg.getAttribute('height')
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.provideSVGData();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.provideSVGData();
|
||||
}
|
||||
|
||||
render() {
|
||||
// Demo rendering for now
|
||||
return <div className={ style.render } ref={ this.svgContainer }>
|
||||
<PlaceholderIcon />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default SVG;
|
15
src/components/SVG/style.module.css
Normal file
15
src/components/SVG/style.module.css
Normal file
@ -0,0 +1,15 @@
|
||||
@import url('../../globals.module.css');
|
||||
|
||||
.render {
|
||||
width: 100%;
|
||||
background: var(--color-white);
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
margin: var(--spacing-margin) 0;
|
||||
|
||||
& svg {
|
||||
display: block;
|
||||
transform: scaleZ(1); /* Move to separate render layer in Chrome */
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
@ -22,10 +22,6 @@ exports[`Index Page rendering 1`] = `
|
||||
</p>
|
||||
</Message>
|
||||
</noscript>
|
||||
<div>
|
||||
<WithMergedOptions(TransComponent)>
|
||||
Hello world
|
||||
</WithMergedOptions(TransComponent)>
|
||||
</div>
|
||||
<App />
|
||||
</Fragment>
|
||||
`;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'gatsby';
|
||||
import { withNamespaces, Trans } from 'react-i18next';
|
||||
|
||||
import Metadata from 'components/Metadata';
|
||||
import Message from 'components/Message';
|
||||
import App from 'components/App';
|
||||
|
||||
export const IndexPage = () => <>
|
||||
<Metadata/>
|
||||
@ -14,7 +14,7 @@ export const IndexPage = () => <>
|
||||
please see the <Link to="/privacy">Privacy Policy</Link>.</p>
|
||||
</Message>
|
||||
</noscript>
|
||||
<div><Trans>Hello world</Trans></div>
|
||||
<App />
|
||||
</>;
|
||||
|
||||
export default withNamespaces()(IndexPage);
|
||||
export default IndexPage;
|
||||
|
@ -940,6 +940,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.0.32.tgz#0d3cb31022f8427ea58c008af32b80da126ca4e3"
|
||||
integrity sha1-DTyzECL4Qn6ljACK8yuA2hJspOM=
|
||||
|
||||
"@ungap/url-search-params@^0.1.2":
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@ungap/url-search-params/-/url-search-params-0.1.2.tgz#8ba8c0527543fe675d1c29ae0a2daca842e8ee4f"
|
||||
integrity sha512-WVk5+lJ+AoNLh2sIDMhnEAgLsVQuI067hWLJCzirErH1GYiy1gs09q4+XZxYWSvdAsslKsaO4q1iXXMx2c72dA==
|
||||
|
||||
"@webassemblyjs/ast@1.7.11":
|
||||
version "1.7.11"
|
||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.11.tgz#b988582cafbb2b095e8b556526f30c90d057cace"
|
||||
|
Loading…
Reference in New Issue
Block a user