Merge pull request #1019 from 30-seconds/graphql_custom_schemas

Create custom 'Snippet' schema in GraphQL
This commit is contained in:
Angelos Chalaris
2019-09-21 09:13:59 +03:00
committed by GitHub
9 changed files with 371 additions and 329 deletions

View File

@ -21,4 +21,9 @@ module.exports = {
moduleName: `_30s`,
rollupInputFile: `imports.temp.js`,
testModuleFile: `test/_30s.js`,
// Requirable JSONs
requirables: [
`snippets.json`,
`archivedSnippets.json`
]
};

View File

@ -2,6 +2,14 @@ const path = require(`path`);
const { createFilePath } = require(`gatsby-source-filesystem`);
const config = require('./config');
const { getTextualContent, getCodeBlocks, optimizeAllNodes } = require(`./src/docs/util`);
const requirables = [];
config.requirables.forEach(fileName => {
requirables.push(require(`./snippet_data/${fileName}`));
});
const toKebabCase = str =>
str &&
str
@ -9,97 +17,6 @@ const toKebabCase = str =>
.map(x => x.toLowerCase())
.join('-');
exports.createPages = ({ graphql, actions }) => {
const { createPage } = actions;
const snippetPage = path.resolve(`./src/docs/templates/SnippetPage.js`);
const tagPage = path.resolve(`./src/docs/templates/TagPage.js`);
return graphql(
`
{
allMarkdownRemark(
sort: { fields: [frontmatter___title], order: ASC }
limit: 1000
) {
edges {
node {
fields {
slug
}
frontmatter {
tags
}
fileAbsolutePath
}
}
}
}
`,
).then(result => {
if (result.errors) {
throw result.errors;
}
// Create individual snippet pages.
const snippets = result.data.allMarkdownRemark.edges;
snippets.forEach((post, index) => {
if(post.node.fileAbsolutePath.indexOf('README') !== -1)
return;
if (post.node.fileAbsolutePath.indexOf(config.snippetArchivePath) === -1)
createPage({
path: `/snippet${post.node.fields.slug}`,
component: snippetPage,
context: {
slug: post.node.fields.slug,
scope: `./snippets`,
},
});
else
createPage({
path: `/archive${post.node.fields.slug}`,
component: snippetPage,
context: {
slug: post.node.fields.slug,
scope: `./snippets_archive`,
},
});
});
// Create tag pages.
const tags = snippets.reduce((acc, post) => {
if (!post.node.frontmatter || !post.node.frontmatter.tags) return acc;
const primaryTag = post.node.frontmatter.tags.split(',')[0];
if (!acc.includes(primaryTag)) acc.push(primaryTag);
return acc;
}, []);
tags.forEach(tag => {
const tagPath = `/tag/${toKebabCase(tag)}/`;
const tagRegex = `/^\\s*${tag}/`;
createPage({
path: tagPath,
component: tagPage,
context: {
tag,
tagRegex,
},
});
});
createPage({
path: `/beginner`,
component: tagPage,
context: {
tag: `beginner snippets`,
tagRegex: `/beginner/`,
},
});
return null;
});
};
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions;
@ -112,3 +29,231 @@ exports.onCreateNode = ({ node, actions, getNode }) => {
});
}
};
exports.sourceNodes = ({ actions, createNodeId, createContentDigest, getNodesByType }) => {
const { createTypes, createNode } = actions;
const typeDefs = `
type Snippet implements Node {
html: HtmlData
tags: TagData
title: String
code: CodeData
id: String
slug: String
path: String
text: TextData
archived: Boolean
}
type HtmlData @infer {
full: String
text: String
code: String
example: String
}
type CodeData @infer {
src: String
example: String
}
type TextData @infer {
full: String
short: String
}
type TagData @infer {
primary: String
all: [String]
}
`;
createTypes(typeDefs);
const markdownNodes = getNodesByType('MarkdownRemark');
const snippetNodes = requirables
.reduce((acc, sArr) => {
const archivedScope = sArr.meta.scope.indexOf('archive') !== -1;
return ({
...acc,
...sArr.data.reduce((snippets, snippet) => {
return ({
...snippets,
[snippet.id]: { ...snippet, archived: archivedScope}
});
}, {})
});
}, {});
Object.entries(snippetNodes).forEach(([id, sNode]) => {
let mNode = markdownNodes.find(mN => mN.frontmatter.title === id);
let nodeContent = {
id,
tags: {
all: sNode.attributes.tags,
primary: sNode.attributes.tags[0]
},
title: mNode.frontmatter.title,
code: {
src: sNode.attributes.codeBlocks.es6,
example: sNode.attributes.codeBlocks.example
},
slug: mNode.fields.slug,
path: mNode.fileAbsolutePath,
text: {
full: sNode.attributes.text,
short: sNode.attributes.text.slice(0, sNode.attributes.text.indexOf('\n\n'))
},
archived: sNode.archived
};
createNode({
id: createNodeId(`snippet-${sNode.meta.hash}`),
parent: null,
children: [],
internal: {
type: 'Snippet',
content: JSON.stringify(nodeContent),
contentDigest: createContentDigest(nodeContent)
},
...nodeContent
});
});
};
exports.createResolvers = ({ createResolvers }) => createResolvers({
Snippet: {
html: {
resolve: async (source, _, context, info) => {
const resolver = info.schema.getType("MarkdownRemark").getFields()["html"].resolve;
const node = await context.nodeModel.nodeStore.getNodesByType('MarkdownRemark').filter(v => v.frontmatter.title === source.title)[0];
const args = {}; // arguments passed to the resolver
const html = await resolver(node, args);
return {
full: `${html}`,
text: `${getTextualContent(html, true)}`,
code: `${optimizeAllNodes(getCodeBlocks(html).code)}`,
example: `${optimizeAllNodes(getCodeBlocks(html).example)}`
};
}
}
}
});
exports.createPages = ({ graphql, actions }) => {
const { createPage } = actions;
const snippetPage = path.resolve(`./src/docs/templates/SnippetPage.js`);
const tagPage = path.resolve(`./src/docs/templates/TagPage.js`);
return graphql(
`
{
allSnippet(sort: {fields: id}) {
edges {
node {
id
slug
tags {
all
primary
}
text {
full
short
}
title
html {
code
example
full
text
}
code {
src
example
}
archived
}
}
}
}
`,
).then(result => {
if (result.errors) {
throw result.errors;
}
// Create individual snippet pages.
const snippets = result.data.allSnippet.edges;
snippets.forEach(snippet => {
if (!snippet.node.archived) {
createPage({
path: `/snippet${snippet.node.slug}`,
component: snippetPage,
context: {
snippet: snippet.node
}
});
} else {
createPage({
path: `/archive${snippet.node.slug}`,
component: snippetPage,
context: {
snippet: snippet.node
}
});
}
});
// Create tag pages.
const tags = [...new Set(
snippets.map(snippet => (snippet.node.tags || {primary: null}).primary)
)]
.filter(Boolean)
.sort((a, b) => a.localeCompare(b));
tags.forEach(tag => {
const tagPath = `/tag/${toKebabCase(tag)}/`;
const taggedSnippets = snippets
.filter(snippet => snippet.node.tags.primary === tag)
.filter(snippet => !snippet.node.archived)
.map(({node}) => ({
title: node.title,
html: node.html.text,
tags: node.tags.all,
id: node.slug.slice(1)
}));
createPage({
path: tagPath,
component: tagPage,
context: {
tag,
snippets: taggedSnippets
},
});
});
const beginnerSnippets = snippets
.filter(({ node }) => node.tags.all.includes('beginner'))
.filter(snippet => !snippet.node.archived)
.map(({ node }) => ({
title: node.title,
html: node.html.text,
tags: node.tags.all,
id: node.slug.slice(1)
}));
createPage({
path: `/beginner`,
component: tagPage,
context: {
tag: `beginner snippets`,
snippets: beginnerSnippets
},
});
return null;
});
};

View File

@ -42,24 +42,18 @@ const CardCorner = ({ difficulty = 'intermediate' }) => (
// ===================================================
const FullCard = ({ snippetData, difficulty, isDarkMode }) => {
const [examplesOpen, setExamplesOpen] = React.useState(false);
const tags = snippetData.tags;
let cardCodeHtml = `${optimizeAllNodes(
getCodeBlocks(snippetData.html).code,
)}`;
let cardExamplesHtml = `${optimizeAllNodes(
getCodeBlocks(snippetData.html).example,
)}`;
return (
<div className='card'>
<CardCorner difficulty={difficulty} />
<h4 className='card-title'>{snippetData.title}</h4>
{tags.map(tag => (
{snippetData.tags.map(tag => (
<span className='tag' key={`tag_${tag}`}>{tag}</span>
))}
<div
className='card-description'
dangerouslySetInnerHTML={{
__html: `${getTextualContent(snippetData.html)}`,
__html: snippetData.textHtml,
}}
/>
<div className='card-bottom'>
@ -83,12 +77,9 @@ const FullCard = ({ snippetData, difficulty, isDarkMode }) => {
aria-label='Copy to clipboard'
/>
</CopyToClipboard>
{/* <button className="button button-b button-social-sh" aria-label="Share">
<ShareIcon />
</button> */}
<pre
className={`card-code language-${config.language}`}
dangerouslySetInnerHTML={{ __html: cardCodeHtml }}
dangerouslySetInnerHTML={{ __html: snippetData.codeHtml }}
/>
<button
className='button button-example-toggler'
@ -99,7 +90,7 @@ const FullCard = ({ snippetData, difficulty, isDarkMode }) => {
{examplesOpen && (
<pre
className='section card-examples language-js'
dangerouslySetInnerHTML={{ __html: cardExamplesHtml }}
dangerouslySetInnerHTML={{ __html: snippetData.exampleHtml }}
/>
)}
</div>
@ -124,13 +115,12 @@ const ShortCard = ({
<div className='card short'>
<CardCorner difficulty={difficulty} />
<h4 className='card-title'>
{snippetData.title}
</h4>
<div
className='card-description'
dangerouslySetInnerHTML={{
__html: `${getTextualContent(snippetData.html, true)}`,
__html: snippetData.html,
}}
/>
</div>

View File

@ -11,7 +11,7 @@ import SnippetCard from '../components/SnippetCard'
// Individual snippet category/tag page
// ===================================================
const ArchivePage = props => {
const posts = props.data.allMarkdownRemark.edges;
const snippets = props.data.allSnippet.edges;
React.useEffect(() => {
props.dispatch(pushNewPage('Archived', props.path));
@ -24,17 +24,17 @@ const ArchivePage = props => {
<h2 className='page-title'>Archived snippets</h2>
<p className='page-sub-title'>These snippets, while useful and interesting, didn't quite make it into the repository due to either having very specific use-cases or being outdated. However we felt like they might still be useful to some readers, so here they are.</p>
<p className='light-sub'>Click on a snippet card to view the snippet.</p>
{posts &&
posts.map(({ node }) => (
{snippets &&
snippets.map(({ node }) => (
<SnippetCard
key={`snippet_${node.id}`}
short
archived
snippetData={{
title: node.frontmatter.title,
html: node.html,
tags: node.frontmatter.tags.split(',').map(v => v.trim()),
id: node.fields.slug.slice(1),
title: node.title,
html: node.html.text,
tags: node.tags.all,
id: node.id
}}
isDarkMode={props.isDarkMode}
/>
@ -55,25 +55,19 @@ export default connect(
)(ArchivePage);
export const archivePageQuery = graphql`
query ArchivePage {
allMarkdownRemark(
limit: 1000
sort: { fields: [frontmatter___title], order: ASC }
filter: { fileAbsolutePath: { regex: "/snippets_archive/" }, frontmatter: {title: { ne: "" } } }
) {
totalCount
query archiveListing {
allSnippet(filter: {archived: {eq: true}}) {
edges {
node {
id
html
rawMarkdownBody
fields {
slug
}
frontmatter {
title
tags
html {
text
}
tags {
all
primary
}
id
}
}
}

View File

@ -13,14 +13,7 @@ import SimpleCard from '../components/SimpleCard';
// Home page (splash and search)
// ===================================================
const IndexPage = props => {
const snippets = props.data.snippetDataJson.data.map(snippet => ({
title: snippet.title,
html: props.data.allMarkdownRemark.edges.find(
v => v.node.frontmatter.title === snippet.title,
).node.html,
tags: snippet.attributes.tags,
id: snippet.id
}));
const snippets = props.data.allSnippet.edges;
const [searchQuery, setSearchQuery] = React.useState(props.searchQuery);
const [searchResults, setSearchResults] = React.useState(snippets);
@ -31,9 +24,9 @@ const IndexPage = props => {
let results = snippets;
if (q.trim().length)
results = snippets.filter(
v =>
v.tags.filter(t => t.indexOf(q) !== -1).length ||
v.title.toLowerCase().indexOf(q) !== -1,
({node}) =>
node.tags.all.filter(t => t.indexOf(q) !== -1).length ||
node.title.toLowerCase().indexOf(q) !== -1,
);
setSearchResults(results);
}, [searchQuery]);
@ -80,11 +73,16 @@ const IndexPage = props => {
Click on a snippet card to view the snippet.
</p>
<h2 className='page-sub-title'>Search results</h2>
{searchResults.map(snippet => (
{searchResults.map(({node}) => (
<SnippetCard
short
key={`snippet_${snippet.id}`}
snippetData={snippet}
key={`snippet_${node.id}`}
snippetData={{
title: node.title,
html: node.html.text,
tags: node.tags.all,
id: node.id
}}
isDarkMode={props.isDarkMode}
/>
))}
@ -139,22 +137,17 @@ export const indexPageQuery = graphql`
}
}
}
snippetDataJson(meta: { type: { eq: "snippetListingArray" }, scope: {eq: "./snippets"} }) {
data {
id
title
attributes {
tags
}
}
}
allMarkdownRemark {
allSnippet {
edges {
node {
html
frontmatter {
title
html {
text
}
tags {
all
}
id
}
}
}

View File

@ -15,28 +15,15 @@ import SimpleCard from '../components/SimpleCard';
// Snippet list page
// ===================================================
const ListPage = props => {
const snippets = props.data.snippetDataJson.data.map(snippet => ({
title: snippet.title,
html: props.data.allMarkdownRemark.edges.find(
v => v.node.frontmatter.title === snippet.title,
).node.html,
tags: snippet.attributes.tags,
id: snippet.id,
}));
const archivedSnippets = props.data.snippetsArchiveDataJson.data.map(snippet => ({
title: snippet.title,
html: props.data.allMarkdownRemark.edges.find(
v => v.node.frontmatter.title === snippet.title,
).node.html,
tags: snippet.attributes.tags,
id: snippet.id,
}));
const tags = snippets.reduce((acc, snippet) => {
if (!snippet.tags) return acc;
const primaryTag = snippet.tags[0];
if (!acc.includes(primaryTag)) acc.push(primaryTag);
return acc;
}, []);
const snippets = props.data.allSnippet.edges;
const archivedSnippets = props.data.allArchivedSnippet.edges;
const tags = [...new Set(
snippets.map(snippet => (snippet.node.tags || { primary: null }).primary)
)]
.filter(Boolean)
.sort((a, b) => a.localeCompare(b));
const staticPages = [
{
url: 'beginner',
@ -79,12 +66,17 @@ const ListPage = props => {
</Link>
</h3>
{snippets
.filter(snippet => snippet.tags[0] === tag)
.map(snippet => (
.filter(({node}) => node.tags.primary === tag)
.map(({node}) => (
<SnippetCard
key={`snippet_${snippet.id}`}
key={`snippet_${node.id}`}
short
snippetData={snippet}
snippetData={{
title: node.title,
html: node.html.text,
tags: node.tags.all,
id: node.id
}}
/>
))}
</>
@ -95,12 +87,17 @@ const ListPage = props => {
Archived snippets
</Link></h3>
{archivedSnippets
.map(snippet => (
.map(({node}) => (
<SnippetCard
key={`a_snippet_${snippet.id}`}
key={`a_snippet_${node.id}`}
short
archived
snippetData={snippet}
snippetData={{
title: node.title,
html: node.html.text,
tags: node.tags.all,
id: node.id
}}
/>
))}
<br/>
@ -131,31 +128,32 @@ export default connect(
export const listPageQuery = graphql`
query snippetListing {
snippetDataJson(meta: { type: { eq: "snippetListingArray" }, scope: {eq: "./snippets"} }) {
data {
id
title
attributes {
tags
}
}
}
snippetsArchiveDataJson : snippetDataJson(meta: { type: { eq: "snippetListingArray" }, scope: {eq: "./snippets_archive"} }) {
data {
id
title
attributes {
tags
}
}
}
allMarkdownRemark {
allSnippet(filter: {archived: {eq: false}}) {
edges {
node {
html
frontmatter {
title
html {
text
}
tags {
all
primary
}
id
}
}
}
allArchivedSnippet: allSnippet(filter: {archived: {eq: true}}) {
edges {
node {
title
html {
text
}
tags {
all
}
id
}
}
}

View File

@ -12,14 +12,7 @@ import SnippetCard from '../components/SnippetCard';
// Search page
// ===================================================
const SearchPage = props => {
const snippets = props.data.snippetDataJson.data.map(snippet => ({
title: snippet.title,
html: props.data.allMarkdownRemark.edges.find(
v => v.node.frontmatter.title === snippet.title,
).node.html,
tags: snippet.attributes.tags,
id: snippet.id,
}));
const snippets = props.data.allSnippet.edges;
const [searchQuery, setSearchQuery] = React.useState(props.searchQuery);
const [searchResults, setSearchResults] = React.useState(snippets);
@ -30,9 +23,9 @@ const SearchPage = props => {
let results = snippets;
if (q.trim().length)
results = snippets.filter(
v =>
v.tags.filter(t => t.indexOf(q) !== -1).length ||
v.title.toLowerCase().indexOf(q) !== -1,
({ node }) =>
node.tags.all.filter(t => t.indexOf(q) !== -1).length ||
node.title.toLowerCase().indexOf(q) !== -1,
);
setSearchResults(results);
}, [searchQuery]);
@ -75,11 +68,16 @@ const SearchPage = props => {
) : (
<>
<h2 className='page-sub-title'>Search results</h2>
{searchResults.map(snippet => (
{searchResults.map(({node}) => (
<SnippetCard
key={`snippet_${snippet.id}`}
key={`snippet_${node.id}`}
short
snippetData={snippet}
snippetData={{
title: node.title,
html: node.html.text,
tags: node.tags.all,
id: node.id
}}
isDarkMode={props.isDarkMode}
/>
))}
@ -102,22 +100,17 @@ export default connect(
export const searchPageQuery = graphql`
query searchSnippetList {
snippetDataJson(meta: { type: { eq: "snippetListingArray" }, scope: {eq: "./snippets"} }) {
data {
id
title
attributes {
tags
}
}
}
allMarkdownRemark {
allSnippet {
edges {
node {
html
frontmatter {
title
html {
text
}
tags {
all
}
id
}
}
}

View File

@ -11,14 +11,11 @@ import BackArrowIcon from '../components/SVGs/BackArrowIcon';
// Individual snippet page template
// ===================================================
const SnippetPage = props => {
const post = props.data.markdownRemark;
const postData = props.data.snippetDataJson.data.find(
v => v.title === post.frontmatter.title,
);
const snippet = props.pageContext.snippet;
return (
<>
<Meta title={post.frontmatter.title} description={post.excerpt} />
<Meta title={snippet.title} description={snippet.text.short} />
<Shell>
<Link
className='link-back'
@ -29,10 +26,13 @@ const SnippetPage = props => {
</Link>
<SnippetCard
snippetData={{
title: postData.title,
html: post.html,
code: postData.attributes.codeBlocks.code,
tags: postData.attributes.tags,
title: snippet.title,
html: snippet.html.full,
codeHtml: snippet.html.code,
exampleHtml: snippet.html.example,
textHtml: snippet.html.text,
code: snippet.code.src,
tags: snippet.tags.all,
}}
isDarkMode={props.isDarkMode}
/>
@ -50,54 +50,3 @@ export default connect(
}),
null,
)(SnippetPage);
export const pageQuery = graphql`
query BlogPostBySlug($slug: String!, $scope: String!) {
logo: file(absolutePath: { regex: "/logo_reverse_md.png/" }) {
id
childImageSharp {
fixed(height: 45, width: 45) {
src
}
}
}
allMarkdownRemark {
edges {
node {
fields {
slug
}
fileAbsolutePath
frontmatter {
title
}
}
}
}
markdownRemark(fields: { slug: { eq: $slug } }) {
id
fields {
slug
}
excerpt(pruneLength: 160)
html
frontmatter {
title
}
}
snippetDataJson(meta: { type: { eq: "snippetArray" }, scope: {eq: $scope} }) {
data {
title
id
attributes {
text
codeBlocks {
es6
example
}
tags
}
}
}
}
`;

View File

@ -13,8 +13,8 @@ import { capitalize, getRawCodeBlocks as getCodeBlocks } from '../util';
// Individual snippet category/tag page
// ===================================================
const TagRoute = props => {
const posts = props.data.allMarkdownRemark.edges;
const tag = props.pageContext.tag;
const snippets = props.pageContext.snippets;
React.useEffect(() => {
props.dispatch(pushNewPage(capitalize(tag), props.path));
@ -26,16 +26,16 @@ const TagRoute = props => {
<Shell>
<h2 className='page-title'>{capitalize(tag)}</h2>
<p className='light-sub'>Click on a snippet card to view the snippet.</p>
{posts &&
posts.map(({ node }) => (
{snippets &&
snippets.map(snippet => (
<SnippetCard
key={`snippet_${node.id}`}
key={`snippet_${snippet.id}`}
short
snippetData={{
title: node.frontmatter.title,
html: node.html,
tags: node.frontmatter.tags.split(',').map(v => v.trim()),
id: node.fields.slug.slice(1),
title: snippet.title,
html: snippet.html,
tags: snippet.tags,
id: snippet.id,
}}
isDarkMode={props.isDarkMode}
/>
@ -54,28 +54,3 @@ export default connect(
}),
null,
)(TagRoute);
export const tagPageQuery = graphql`
query TagPage($tagRegex: String) {
allMarkdownRemark(
limit: 1000
sort: { fields: [frontmatter___title], order: ASC }
filter: { fileAbsolutePath: { regex: "/snippets(?!_archive)/" }, frontmatter: { tags: { regex: $tagRegex } } }
) {
totalCount
edges {
node {
id
html
fields {
slug
}
frontmatter {
title
tags
}
}
}
}
}
`;