Browse Source

Merge pull request #1205 from DSchau/gatsby-source-filesystem/home-example

feat: tweak plugin to use onCreateNode API, and get hot reloading working
main
Brian Vaughn 6 years ago
committed by GitHub
parent
commit
d0603c1b6c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      gatsby-config.js
  2. 49
      plugins/gatsby-transformer-home-example-code/gatsby-node.js
  3. 300
      src/components/CodeEditor/CodeEditor.js
  4. 73
      src/components/CodeExample/CodeExample.js
  5. 3
      src/components/CodeExample/index.js
  6. 122
      src/pages/index.js
  7. 36
      src/utils/mountCodeExample.js

4
gatsby-config.js

@ -33,14 +33,14 @@ module.exports = {
{
resolve: 'gatsby-source-filesystem',
options: {
path: `${__dirname}/src/pages`,
name: 'pages',
path: `${__dirname}/src/pages`,
},
},
{
resolve: 'gatsby-source-filesystem',
options: {
name: 'packages',
name: 'content',
path: `${__dirname}/content/`,
},
},

49
plugins/gatsby-transformer-home-example-code/gatsby-node.js

@ -1,28 +1,33 @@
const {readdirSync, readFileSync} = require('fs');
const {join, resolve} = require('path');
const crypto = require(`crypto`);
const createContentDigest = obj =>
crypto
.createHash(`md5`)
.update(obj)
.digest(`hex`);
// Store code snippets in GraphQL for the home page examples.
// Snippets will be matched with markdown templates of the same name.
exports.sourceNodes = ({graphql, actions}) => {
exports.onCreateNode = async ({actions, node, loadNodeContent}) => {
const {createNode} = actions;
const {absolutePath, ext, name, relativeDirectory, sourceInstanceName} = node;
const path = resolve(__dirname, '../../content/home/examples');
const files = readdirSync(path);
files.forEach(file => {
if (file.match(/\.js$/)) {
const code = readFileSync(join(path, file), 'utf8');
const id = file.replace(/\.js$/, '');
createNode({
id,
children: [],
parent: 'EXAMPLES',
internal: {
type: 'ExampleCode',
contentDigest: JSON.stringify(code),
},
});
}
});
if (
sourceInstanceName === 'content' &&
relativeDirectory === 'home/examples' &&
ext === '.js'
) {
const code = await loadNodeContent(node);
createNode({
id: name,
children: [],
parent: node.id,
code,
mdAbsolutePath: absolutePath.replace(/\.js$/, '.md'),
internal: {
type: 'ExampleCode',
contentDigest: createContentDigest(JSON.stringify(code)),
},
});
}
};

300
src/components/CodeEditor/CodeEditor.js

@ -38,6 +38,12 @@ class CodeEditor extends Component {
}
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.props.code !== nextProps.code) {
this.setState(this._updateState(nextProps.code));
}
}
render() {
const {children} = this.props;
const {
@ -68,211 +74,169 @@ class CodeEditor extends Component {
<LiveProvider code={showJSX ? code : compiledES6} mountStylesheet={false}>
<div
css={{
[media.greaterThan('xlarge')]: {
[media.greaterThan('medium')]: {
display: 'flex',
alignItems: 'stretch',
flexDirection: 'row',
},
[media.lessThan('large')]: {
[media.lessThan('small')]: {
display: 'block',
},
}}>
{children && (
<div
css={{
flex: '0 0 70%',
overflow: 'hidden',
borderRadius: '10px 0 0 10px',
[media.lessThan('medium')]: {
borderRadius: '10px 10px 0 0',
},
}}>
<div
css={{
flex: '0 0 33%',
[media.lessThan('xlarge')]: {
marginBottom: 20,
},
'& h3': {
color: colors.dark,
maxWidth: '11em',
paddingTop: 0,
padding: '0px 10px',
background: colors.darker,
color: colors.white,
}}>
<MetaTitle onDark={true}>
Live JSX Editor
<label
css={{
fontSize: 14,
float: 'right',
cursor: 'pointer',
}}>
<input
checked={this.state.showJSX}
onChange={event =>
this.setState({showJSX: event.target.checked})
}
type="checkbox"
/>{' '}
JSX?
</label>
</MetaTitle>
</div>
<div
css={{
height: '100%',
width: '100%',
borderRadius: '0',
maxHeight: '340px !important',
marginTop: '0 !important',
marginLeft: '0 !important',
paddingLeft: '0 !important',
marginRight: '0 !important',
paddingRight: '0 !important',
marginBottom: '0 !important',
paddingBottom: '20px !important',
[media.lessThan('medium')]: {
marginBottom: '0 !important',
},
'& p': {
marginTop: 15,
marginRight: 40,
lineHeight: 1.7,
[media.greaterThan('xlarge')]: {
marginTop: 25,
},
'& pre.prism-code[contenteditable]': {
outline: 0,
overflow: 'auto',
marginRight: '0 !important',
marginBottom: '0 !important',
},
}}>
{children}
}}
className="gatsby-highlight">
<LiveEditor ignoreTabKey={true} onChange={this._onChange} />
</div>
)}
<div
css={{
[media.greaterThan('medium')]: {
flex: '0 0 67%',
display: 'flex',
alignItems: 'stretch',
flexDirection: 'row',
},
[media.lessThan('small')]: {
display: 'block',
},
}}>
</div>
{error && (
<div
css={{
flex: '0 0 70%',
flex: '0 0 30%',
overflow: 'hidden',
borderRadius: '10px 0 0 10px',
border: `1px solid ${colors.error}`,
borderRadius: '0 10px 10px 0',
fontSize: 12,
lineHeight: 1.5,
[media.lessThan('medium')]: {
borderRadius: '10px 10px 0 0',
borderRadius: '0 0 10px 10px',
},
}}>
<div
css={{
padding: '0px 10px',
background: colors.darker,
background: colors.error,
color: colors.white,
}}>
<MetaTitle onDark={true}>
Live JSX Editor
<label
css={{
fontSize: 14,
float: 'right',
cursor: 'pointer',
}}>
<input
checked={this.state.showJSX}
onChange={event =>
this.setState({showJSX: event.target.checked})
}
type="checkbox"
/>{' '}
JSX?
</label>
<MetaTitle
cssProps={{
color: colors.white,
}}>
Error
</MetaTitle>
</div>
<div
<pre
css={{
height: '100%',
width: '100%',
borderRadius: '0',
maxHeight: '340px !important',
marginTop: '0 !important',
marginLeft: '0 !important',
paddingLeft: '0 !important',
marginRight: '0 !important',
paddingRight: '0 !important',
marginBottom: '0 !important',
paddingBottom: '20px !important',
[media.lessThan('medium')]: {
marginBottom: '0 !important',
},
'& pre.prism-code[contenteditable]': {
outline: 0,
overflow: 'auto',
marginRight: '0 !important',
marginBottom: '0 !important',
},
}}
className="gatsby-highlight">
<LiveEditor ignoreTabKey={true} onChange={this._onChange} />
</div>
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
color: colors.error,
padding: 10,
}}>
{errorMessage}
</pre>
</div>
{error && (
)}
{!error && (
<div
css={{
flex: '0 0 30%',
overflow: 'hidden',
border: `1px solid ${colors.divider}`,
borderRadius: '0 10px 10px 0',
[media.lessThan('medium')]: {
borderRadius: '0 0 10px 10px',
},
}}>
<div
css={{
flex: '0 0 30%',
overflow: 'hidden',
border: `1px solid ${colors.error}`,
borderRadius: '0 10px 10px 0',
fontSize: 12,
lineHeight: 1.5,
[media.lessThan('medium')]: {
borderRadius: '0 0 10px 10px',
},
padding: '0 10px',
backgroundColor: colors.divider,
}}>
<div
css={{
padding: '0px 10px',
background: colors.error,
color: colors.white,
}}>
<MetaTitle
cssProps={{
color: colors.white,
}}>
Error
</MetaTitle>
</div>
<pre
css={{
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
color: colors.error,
padding: 10,
}}>
{errorMessage}
</pre>
<MetaTitle>Result</MetaTitle>
</div>
)}
{!error && (
<div
css={{
flex: '0 0 30%',
overflow: 'hidden',
border: `1px solid ${colors.divider}`,
borderRadius: '0 10px 10px 0',
padding: 10,
maxHeight: '340px !important',
overflow: 'auto',
[media.lessThan('medium')]: {
borderRadius: '0 0 10px 10px',
'& input': {
width: '100%',
display: 'block',
border: '1px solid #ccc', // TODO
padding: 5,
},
}}>
<div
css={{
padding: '0 10px',
backgroundColor: colors.divider,
}}>
<MetaTitle>Result</MetaTitle>
</div>
<div
css={{
padding: 10,
maxHeight: '340px !important',
overflow: 'auto',
'& input': {
width: '100%',
display: 'block',
border: '1px solid #ccc', // TODO
padding: 5,
},
'& button': {
marginTop: 10,
padding: '5px 10px',
},
'& label': {
display: 'block',
marginTop: 10,
},
'& textarea': {
width: '100%',
height: 60,
padding: 5,
},
}}
ref={this._setMountRef}
/>
</div>
)}
</div>
'& button': {
marginTop: 10,
padding: '5px 10px',
},
'& label': {
display: 'block',
marginTop: 10,
},
'& textarea': {
width: '100%',
height: 60,
padding: 5,
},
}}
ref={this._setMountRef}
/>
</div>
)}
</div>
</LiveProvider>
);

73
src/components/CodeExample/CodeExample.js

@ -0,0 +1,73 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {colors, media} from 'theme';
import CodeEditor from '../CodeEditor/CodeEditor';
class CodeExample extends Component {
render() {
const {children, code, id, loaded} = this.props;
return (
<div
id={id}
css={{
marginTop: 40,
'&:first-child': {
marginTop: 0,
},
'& .react-live': {
width: '100%',
},
[media.greaterThan('xlarge')]: {
display: 'flex',
flexDirection: 'row',
marginTop: 80,
},
[media.lessThan('large')]: {
display: 'block',
},
}}>
{children && (
<div
css={{
flex: '0 0 33%',
[media.lessThan('xlarge')]: {
marginBottom: 20,
},
'& h3': {
color: colors.dark,
maxWidth: '11em',
paddingTop: 0,
},
'& p': {
marginTop: 15,
marginRight: 40,
lineHeight: 1.7,
[media.greaterThan('xlarge')]: {
marginTop: 25,
},
},
}}>
{children}
</div>
)}
{loaded ? <CodeEditor code={code} /> : <h4>Loading code example...</h4>}
</div>
);
}
}
CodeExample.propTypes = {
children: PropTypes.node,
code: PropTypes.string.isRequired,
loaded: PropTypes.bool.isRequired,
};
export default CodeExample;

3
src/components/CodeExample/index.js

@ -0,0 +1,3 @@
import CodeExample from './CodeExample';
export default CodeExample;

122
src/pages/index.js

@ -7,72 +7,46 @@
import ButtonLink from 'components/ButtonLink';
import Container from 'components/Container';
import Flex from 'components/Flex';
import mountCodeExample from 'utils/mountCodeExample';
import CodeExample from 'components/CodeExample';
import PropTypes from 'prop-types';
import React, {Component} from 'react';
import {graphql} from 'gatsby';
import TitleAndMetaTags from 'components/TitleAndMetaTags';
import Layout from 'components/Layout';
import {colors, media, sharedStyles} from 'theme';
import createOgUrl from 'utils/createOgUrl';
import loadScript from 'utils/loadScript';
import createOgUrl from 'utils/createOgUrl';
import {babelURL} from 'site-constants';
import ReactDOM from 'react-dom';
import logoWhiteSvg from 'icons/logo-white.svg';
class Home extends Component {
constructor(props, context) {
super(props, context);
const {data} = props;
const code = data.code.edges.reduce((map, {node}) => {
map[node.id] = JSON.parse(node.internal.contentDigest);
return map;
}, {});
const examples = data.examples.edges.map(({node}) => ({
content: node.html,
id: node.fields.slug.replace(/^.+\//, '').replace('.html', ''),
title: node.frontmatter.title,
}));
const marketing = data.marketing.edges.map(({node}) => ({
title: node.frontmatter.title,
content: node.html,
}));
this.state = {
code,
examples,
marketing,
};
}
state = {
babelLoaded: false,
};
componentDidMount() {
const {code, examples} = this.state;
examples.forEach(({id}) => {
renderExamplePlaceholder(id);
});
function mountCodeExamples() {
examples.forEach(({id}) => {
mountCodeExample(id, code[id]);
});
}
loadScript(babelURL).then(mountCodeExamples, error => {
console.error('Babel failed to load.');
mountCodeExamples();
});
loadScript(babelURL).then(
() => {
this.setState({
babelLoaded: true,
});
},
error => {
console.error('Babel failed to load.');
},
);
}
render() {
const {examples, marketing} = this.state;
const {location} = this.props;
const {babelLoaded} = this.state;
const {data, location} = this.props;
const {codeExamples, examples, marketing} = data;
const code = codeExamples.edges.reduce((lookup, {node}) => {
lookup[node.mdAbsolutePath] = node;
return lookup;
}, {});
return (
<Layout location={location}>
@ -217,7 +191,7 @@ class Home extends Component {
whiteSpace: 'nowrap',
},
}}>
{marketing.map((column, index) => (
{marketing.edges.map(({node: column}, index) => (
<div
key={index}
css={{
@ -266,9 +240,9 @@ class Home extends Component {
},
},
]}>
{column.title}
{column.frontmatter.title}
</h3>
<div dangerouslySetInnerHTML={{__html: column.content}} />
<div dangerouslySetInnerHTML={{__html: column.html}} />
</div>
))}
</div>
@ -283,27 +257,19 @@ class Home extends Component {
/>
<section css={sectionStyles}>
<div id="examples">
{examples.map((example, index) => (
<div
key={index}
css={{
marginTop: 40,
'&:first-child': {
marginTop: 0,
},
[media.greaterThan('xlarge')]: {
marginTop: 80,
},
}}>
<h3 css={headingStyles}>{example.title}</h3>
<div
dangerouslySetInnerHTML={{__html: example.content}}
/>
<div id={example.id} />
</div>
))}
{examples.edges.map(({node}, index) => {
const snippet = code[node.fileAbsolutePath];
return (
<CodeExample
key={index}
id={snippet.id}
code={snippet.code}
loaded={babelLoaded}>
<h3 css={headingStyles}>{node.frontmatter.title}</h3>
<div dangerouslySetInnerHTML={{__html: node.html}} />
</CodeExample>
);
})}
</div>
</section>
</div>
@ -339,7 +305,6 @@ class Home extends Component {
Home.propTypes = {
data: PropTypes.shape({
code: PropTypes.object.isRequired,
examples: PropTypes.object.isRequired,
marketing: PropTypes.object.isRequired,
}).isRequired,
@ -382,22 +347,23 @@ const CtaItem = ({children, primary = false}) => (
export const pageQuery = graphql`
query IndexMarkdown {
code: allExampleCode {
codeExamples: allExampleCode {
edges {
node {
id
internal {
contentDigest
}
code
mdAbsolutePath
}
}
}
examples: allMarkdownRemark(
filter: {fileAbsolutePath: {regex: "//home/examples//"}}
sort: {fields: [frontmatter___order], order: ASC}
) {
edges {
node {
fileAbsolutePath
fields {
slug
}

36
src/utils/mountCodeExample.js

@ -1,36 +0,0 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* @emails react-core
*/
import CodeEditor from '../components/CodeEditor';
import React from 'react';
import ReactDOM from 'react-dom';
// TODO This is a huge hack.
// Remark transform this template to split code examples and their targets apart.
const mountCodeExample = (containerId, code) => {
const container = document.getElementById(containerId);
const parent = container.parentElement;
const children = Array.prototype.filter.call(
parent.children,
child => child !== container,
);
children.forEach(child => parent.removeChild(child));
const description = children
.map(child => child.outerHTML)
.join('')
.replace(/`([^`]+)`/g, '<code>$1</code>');
ReactDOM.render(
<CodeEditor code={code}>
{<div dangerouslySetInnerHTML={{__html: description}} />}
</CodeEditor>,
container,
);
};
export default mountCodeExample;
Loading…
Cancel
Save