diff --git a/src/components/App/index.js b/src/components/App/index.js index b9f7845..9e7c49e 100644 --- a/src/components/App/index.js +++ b/src/components/App/index.js @@ -1,11 +1,62 @@ import React from 'react'; +import PropTypes from 'prop-types'; import * as Sentry from '@sentry/browser'; +import URLSearchParams from '@ungap/url-search-params'; -import { AppContextProvider } from 'components/AppContext'; import Form from 'components/Form'; import Loader from 'components/Loader'; import Message from 'components/Message'; +const toUrl = params => new URLSearchParams(params).toString(); + +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 = { loading: false, @@ -13,15 +64,43 @@ class App extends React.PureComponent { Render: null } - handleRender = async ({ syntax, expr }) => { + componentDidMount() { + if (this.props.expr) { + this.handleRender(); + } + } + + componentDidUpdate(prevProps) { + const { syntax, expr } = this.props; + + if (syntax !== prevProps.syntax || expr !== prevProps.expr) { + this.handleRender(); + } + } + + handleSubmit = ({ syntax, expr }) => { + if (expr) { + document.location.hash = toUrl({ syntax, expr }); + } + } + + handleRender = async () => { + const { syntax, expr } = this.props; + this.setState({ - syntax, - expr, - loading: true, + loading: false, loadingError: null, Render: null }); + if (!expr) { + return; + } + + this.setState({ + loading: true + }); + try { const Render = await import( /* webpackChunkName: "render-[index]" */ @@ -51,18 +130,51 @@ class App extends React.PureComponent { handleRetry = event => { event.preventDefault(); - this.handleRender(this.state); + this.handleRender(); + } + + handleSvgMarkup = async ({ svg, width, height }) => { + if (svg !== this.state.svg) { + this.setState({ + svg, + svgLink: await createSvgLink({ svg }), + pngLink: await createPngLink({ svg, width, height }) + }); + } } render() { + const { + syntax, + expr, + permalinkUrl + } = this.props; const { loading, loadingError, - Render + Render, + svgLink, + pngLink } = this.state; - return -
+ const formProps = { + onSubmit: this.handleSubmit, + syntax, + expr, + actions: { + permalinkUrl, + svgLink, + pngLink + } + }; + const renderProps = { + onRender: this.handleSvgMarkup, + syntax, + expr + }; + + return <> + { loading && } @@ -71,9 +183,15 @@ class App extends React.PureComponent { Retry } - { Render && } - ; + { Render && } + ; } } +App.propTypes = { + syntax: PropTypes.string, + expr: PropTypes.string, + permalinkUrl: PropTypes.string +}; + export default App; diff --git a/src/components/AppContext/index.js b/src/components/AppContext/index.js deleted file mode 100644 index 6743015..0000000 --- a/src/components/AppContext/index.js +++ /dev/null @@ -1,149 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import URLSearchParams from '@ungap/url-search-params'; - -const AppContext = React.createContext(); - -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 AppContextProvider extends React.PureComponent { - state = {} - - mutations = { - setSvgMarkup: async ({ svg, width, height }) => { - if (svg !== this.state.svg) { - this.setState({ - svg, - svgLink: await createSvgLink({ svg }), - pngLink: await createPngLink({ svg, width, height }) - }); - } - }, - - renderExpr: ({ syntax, expr }) => { - if (expr) { - document.location.hash = toUrl({ syntax, expr }); - } - } - } - - componentDidMount() { - // Gatsby doesn't have document.location, so readURLHash can't be called - // until here - this.setState(readURLHash()); - - 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() - }); - - if (this.props.onExpressionChange) { - this.props.onExpressionChange({ syntax, expr }); - } - } - - render() { - const { children } = this.props; - const context = { - ...this.state, - ...this.mutations - }; - - return - { children } - ; - } -} - -AppContextProvider.propTypes = { - onExpressionChange: PropTypes.func, - children: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.node), - PropTypes.node - ]).isRequired -}; - -export { AppContextProvider }; -export default AppContext; diff --git a/src/components/Form/index.js b/src/components/Form/index.js index c8b03fc..b95950c 100644 --- a/src/components/Form/index.js +++ b/src/components/Form/index.js @@ -1,10 +1,10 @@ import React from 'react'; +import PropTypes from 'prop-types'; import ExpandIcon from 'react-feather/dist/icons/chevrons-down'; import style from './style.module.css'; -import AppContext from 'components/AppContext'; import FormActions from 'components/FormActions'; const syntaxList = [ @@ -13,20 +13,9 @@ const syntaxList = [ ]; class Form extends React.PureComponent { - static contextType = AppContext - state = { - expr: this.context.expr, - syntax: this.context.syntax - } - - componentDidUpdate() { - if (this.state.expr === undefined && this.state.syntax === undefined) { - this.setState({ - syntax: this.context.syntax, - expr: this.context.expr - }); - } + expr: this.props.expr, + syntax: this.props.syntax } handleSubmit = event => { @@ -34,7 +23,7 @@ class Form extends React.PureComponent { const { expr, syntax } = this.state; - this.context.renderExpr({ expr, syntax }); + this.props.onSubmit({ expr, syntax }); } handleKeyPress = event => { @@ -48,6 +37,9 @@ class Form extends React.PureComponent { }) render() { + const { + actions + } = this.props; const { expr, syntax } = this.state; return
@@ -71,10 +63,17 @@ class Form extends React.PureComponent {
- + ; } } +Form.propTypes = { + expr: PropTypes.string, + syntax: PropTypes.string, + actions: PropTypes.object, + onSubmit: PropTypes.func.isRequired +}; + export default Form; diff --git a/src/components/FormActions/index.js b/src/components/FormActions/index.js index 33a3319..2616188 100644 --- a/src/components/FormActions/index.js +++ b/src/components/FormActions/index.js @@ -1,15 +1,12 @@ import React from 'react'; +import PropTypes from 'prop-types'; 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/AppContext'; - class FormActions extends React.PureComponent { - static contextType = AppContext - downloadLink({ url, filename, type, label }) { return
  • @@ -23,7 +20,7 @@ class FormActions extends React.PureComponent { permalinkUrl, svgLink, pngLink - } = this.context; + } = this.props; return
      { pngLink && this.downloadLink(pngLink) } @@ -35,4 +32,10 @@ class FormActions extends React.PureComponent { } } +FormActions.propTypes = { + permalinkUrl: PropTypes.string, + svgLink: PropTypes.object, + pngLink: PropTypes.object +}; + export default FormActions; diff --git a/src/components/Render/index.js b/src/components/Render/index.js index d76f67a..39e5184 100644 --- a/src/components/Render/index.js +++ b/src/components/Render/index.js @@ -1,14 +1,11 @@ import React from 'react'; +import PropTypes from 'prop-types'; import PlaceholderIcon from 'react-feather/dist/icons/file-text'; import style from './style.module.css'; -import AppContext from 'components/AppContext'; - class Render extends React.PureComponent { - static contextType = AppContext - svgContainer = React.createRef() provideSVGData() { @@ -17,7 +14,7 @@ class Render extends React.PureComponent { } const svg = this.svgContainer.current.querySelector('svg'); - this.context.setSvgMarkup({ + this.props.onRender({ svg: svg.outerHTML, width: svg.getAttribute('width'), height: svg.getAttribute('height') @@ -33,7 +30,7 @@ class Render extends React.PureComponent { } render() { - const { syntax, expr } = this.context; + const { syntax, expr } = this.props; console.log('Render:', syntax, expr); // eslint-disable-line no-console @@ -44,4 +41,10 @@ class Render extends React.PureComponent { } } +Render.propTypes = { + syntax: PropTypes.string, + expr: PropTypes.string, + onRender: PropTypes.func.isRequired +}; + export default Render; diff --git a/src/pages/__snapshots__/index.test.js.snap b/src/pages/__snapshots__/index.test.js.snap index 29e6f63..1efe07c 100644 --- a/src/pages/__snapshots__/index.test.js.snap +++ b/src/pages/__snapshots__/index.test.js.snap @@ -22,6 +22,40 @@ exports[`Index Page rendering 1`] = `

      - + + +`; + +exports[`Index Page rendering with an expression on the URL 1`] = ` + + + + `; diff --git a/src/pages/index.js b/src/pages/index.js index a5d22ec..73b0d7a 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -1,11 +1,34 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { Link } from 'gatsby'; +import URLSearchParams from '@ungap/url-search-params'; import Metadata from 'components/Metadata'; import Message from 'components/Message'; import App from 'components/App'; -export const IndexPage = () => <> +const readURLHash = location => { + const query = location.hash.slice(1); + const params = new URLSearchParams(query); + const permalinkUrl = query ? location.href : null; + + if (params.get('syntax')) { + return { + syntax: params.get('syntax'), + expr: params.get('expr'), + permalinkUrl + }; + } else { + // Assuming old-style URL + return { + syntax: 'js', + expr: query, + permalinkUrl + }; + } +}; + +export const IndexPage = ({ location }) => <> - + ; +IndexPage.propTypes = { + location: PropTypes.object +}; + export default IndexPage; diff --git a/src/pages/index.test.js b/src/pages/index.test.js index c7e0144..b2e5179 100644 --- a/src/pages/index.test.js +++ b/src/pages/index.test.js @@ -6,7 +6,17 @@ import { IndexPage } from 'pages/index'; describe('Index Page', () => { test('rendering', () => { const component = shallow( - + + ); + expect(component).toMatchSnapshot(); + }); + + test('rendering with an expression on the URL', () => { + const component = shallow( + ); expect(component).toMatchSnapshot(); });