diff --git a/www/gatsby-browser.js b/packages/gatsby-source-drupal/.Rhistory similarity index 100% rename from www/gatsby-browser.js rename to packages/gatsby-source-drupal/.Rhistory diff --git a/packages/gatsby-source-npm-package-search/.gitignore b/packages/gatsby-source-npm-package-search/.gitignore new file mode 100644 index 0000000000000..ab784e001feae --- /dev/null +++ b/packages/gatsby-source-npm-package-search/.gitignore @@ -0,0 +1,4 @@ +/*.js +!index.js +yarn.lock +gatsby-node.js \ No newline at end of file diff --git a/packages/gatsby-source-npm-package-search/.npmignore b/packages/gatsby-source-npm-package-search/.npmignore new file mode 100644 index 0000000000000..e771d2c9fa299 --- /dev/null +++ b/packages/gatsby-source-npm-package-search/.npmignore @@ -0,0 +1,34 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules +*.un~ +yarn.lock +src +flow-typed +coverage +decls +examples diff --git a/packages/gatsby-source-npm-package-search/README.md b/packages/gatsby-source-npm-package-search/README.md new file mode 100644 index 0000000000000..42f6c845622e0 --- /dev/null +++ b/packages/gatsby-source-npm-package-search/README.md @@ -0,0 +1,40 @@ +# gatsby-source-npm-package-search + +This plugin uses Yarn's Algolia search to import all gatsby-related package info (any package with the gatsby-component or gatsby-plugin keyword). Check back for updates to search for other npm packages based on keyword. + +## Install +`npm install --save gatsby-source-npm` + +## How to use + +```javascript +// In your gatsby-config.js +plugins: [ + resolve: `gatsby-source-npm`, + options: { + keywords: [`keyword1`, `keyword2`] + } +] +``` + +## How to query + +You can query npm nodes like the following + +```graphql +{ + allNpmPackages{ + edges{ + node{ + name + humanDownloadsLast30Days + readme{ + childMarkdownRemark{ + html + } + } + } + } + } +} +``` diff --git a/packages/gatsby-source-npm-package-search/index.js b/packages/gatsby-source-npm-package-search/index.js new file mode 100644 index 0000000000000..172f1ae6a468c --- /dev/null +++ b/packages/gatsby-source-npm-package-search/index.js @@ -0,0 +1 @@ +// noop diff --git a/packages/gatsby-source-npm-package-search/package.json b/packages/gatsby-source-npm-package-search/package.json new file mode 100644 index 0000000000000..291975724d7b3 --- /dev/null +++ b/packages/gatsby-source-npm-package-search/package.json @@ -0,0 +1,22 @@ +{ + "name": "gatsby-source-npm-package-search", + "version": "1.0.1", + "description": "Search gatsby plugins and pull metadata with algolia search", + "main": "index.js", + "scripts": { + "build": "babel src --out-dir . --ignore __tests__", + "watch": "babel -w src --out-dir . --ignore __tests__", + "prepublish": "cross-env NODE_ENV=production npm run build" + }, + "keywords": ["gatsby"], + "author": "james.a.stack@gmail.com", + "license": "MIT", + "dependencies": { + "algoliasearch": "^3.24.9", + "babel-runtime": "^6.26.0" + }, + "devDependencies": { + "babel-cli": "^6.26.0", + "cross-env": "^5.0.5" + } +} diff --git a/packages/gatsby-source-npm-package-search/src/.gitkeep b/packages/gatsby-source-npm-package-search/src/.gitkeep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/gatsby-source-npm-package-search/src/gatsby-node.js b/packages/gatsby-source-npm-package-search/src/gatsby-node.js new file mode 100644 index 0000000000000..4d9846f1418ba --- /dev/null +++ b/packages/gatsby-source-npm-package-search/src/gatsby-node.js @@ -0,0 +1,80 @@ +const algoliasearch = require(`algoliasearch`) +const crypto = require(`crypto`) + +const client = algoliasearch(`OFCNCOG2CU`, `f54e21fa3a2a0160595bb058179bfb1e`) +var index = client.initIndex(`npm-search`) + +const createContentDigest = obj => + crypto + .createHash(`md5`) + .update(JSON.stringify(obj)) + .digest(`hex`) + +exports.sourceNodes = async ( + { boundActionCreators, createNodeId }, + { keywords } +) => { + const { createNode } = boundActionCreators + + console.log(`Grabbing local NPM packages...`) + + let buildFilter = [] + + keywords.forEach(keyword => { + buildFilter.push(`keywords:${keyword}`) + }) + + const data = await index.search({ + query: ``, + filters: `(${buildFilter.join(` OR `)})`, + hitsPerPage: 1000, + }) + + data.hits.forEach(hit => { + // commented changed remove all badges and images from readme content to keep the creation of the node from failing below + // if (hit.readme.includes(`![`)) { + // hit.readme = hit.readme.replace(/[[]?!\[.*\b/gi, ``) + // console.log(hit.name) + // } + + const parentId = createNodeId(`plugin ${hit.objectID}`) + const readmeNode = { + id: createNodeId(`readme ${hit.objectID}`), + parent: parentId, + slug: `/packages/en/${hit.objectID}`, + children: [], + internal: { + type: `NPMPackageReadme`, + mediaType: `text/markdown`, + content: hit.readme !== undefined ? hit.readme : ``, + }, + } + readmeNode.internal.contentDigest = createContentDigest(readmeNode) + // Remove unneeded data + delete hit.readme + delete hit._highlightResult + delete hit.versions + + const node = { + ...hit, + deprecated: `${hit.deprecated}`, + created: new Date(hit.created), + modified: new Date(hit.modified), + id: parentId, + parent: null, + children: [], + slug: `/packages/${hit.objectID}/`, + readme___NODE: readmeNode.id, + title: `${hit.objectID}`, + internal: { + type: `NPMPackage`, + content: hit.readme !== undefined ? hit.readme : ``, + }, + } + node.internal.contentDigest = createContentDigest(node) + createNode(readmeNode) + createNode(node) + }) + + return +} diff --git a/www/gatsby-config.js b/www/gatsby-config.js index a417b06dc4c9f..0cdbe8958d83b 100644 --- a/www/gatsby-config.js +++ b/www/gatsby-config.js @@ -8,6 +8,12 @@ module.exports = { "MarkdownRemark.frontmatter.author": `AuthorYaml`, }, plugins: [ + { + resolve: `gatsby-source-npm-package-search`, + options: { + keywords: [`gatsby-plugin`, `gatsby-component`] + } + }, { resolve: `gatsby-source-filesystem`, options: { diff --git a/www/gatsby-node.js b/www/gatsby-node.js index 7064fed98cca7..002a8069c9d44 100644 --- a/www/gatsby-node.js +++ b/www/gatsby-node.js @@ -6,6 +6,11 @@ const fs = require(`fs-extra`) const slash = require(`slash`) const slugify = require(`limax`) +const localPackages = `../packages` +const localPackagesArr = [] +fs.readdirSync(localPackages).forEach(file => { + localPackagesArr.push(file) +}) // convert a string like `/some/long/path/name-of-docs/` to `name-of-docs` const slugToAnchor = slug => slug @@ -29,14 +34,17 @@ exports.createPages = ({ graphql, boundActionCreators }) => { const contributorPageTemplate = path.resolve( `src/templates/template-contributor-page.js` ) - const packageTemplate = path.resolve( - `src/templates/template-docs-packages.js` + const localPackageTemplate = path.resolve( + `src/templates/template-docs-local-packages.js` + ) + const remotePackageTemplate = path.resolve( + `src/templates/template-docs-remote-packages.js` ) // Query for markdown nodes to use in creating pages. resolve( graphql( ` - { + query { allMarkdownRemark( sort: { order: DESC, fields: [frontmatter___date] } limit: 1000 @@ -65,6 +73,22 @@ exports.createPages = ({ graphql, boundActionCreators }) => { } } } + allNpmPackage { + edges { + node { + id + title + slug + readme { + id + childMarkdownRemark { + id + html + } + } + } + } + } } ` ).then(result => { @@ -122,7 +146,7 @@ exports.createPages = ({ graphql, boundActionCreators }) => { createPage({ path: `${edge.node.fields.slug}`, // required component: slash( - edge.node.fields.package ? packageTemplate : docsTemplate + edge.node.fields.package ? localPackageTemplate : docsTemplate ), context: { slug: edge.node.fields.slug, @@ -131,6 +155,30 @@ exports.createPages = ({ graphql, boundActionCreators }) => { } }) + const allPackages = result.data.allNpmPackage.edges + // Create package readme + allPackages.forEach(edge => { + if (_.includes(localPackagesArr, edge.node.title)) { + createPage({ + path: edge.node.slug, + component: slash(localPackageTemplate), + context: { + slug: edge.node.slug, + id: edge.node.id, + }, + }) + } else { + createPage({ + path: edge.node.slug, + component: slash(remotePackageTemplate), + context: { + slug: edge.node.slug, + id: edge.node.id, + }, + }) + } + }) + return }) ) diff --git a/www/package.json b/www/package.json index 2de37bd82bec1..b17599d171310 100644 --- a/www/package.json +++ b/www/package.json @@ -5,6 +5,7 @@ "author": "Kyle Mathews ", "dependencies": { "bluebird": "^3.5.1", + "date-fns": "^1.29.0", "email-validator": "^1.1.1", "gatsby": "^1.9.210", "gatsby-image": "^1.0.39", @@ -47,6 +48,7 @@ "parse-filepath": "^1.0.2", "react-helmet": "^5.2.0", "react-icons": "^2.2.7", + "react-instantsearch": "^4.5.1", "slash": "^1.0.0", "typeface-space-mono": "^0.0.54", "typeface-spectral": "^0.0.54", diff --git a/www/src/components/markdown-page-footer.js b/www/src/components/markdown-page-footer.js index a0bf9c515f9b4..f6ac9b85db0a6 100644 --- a/www/src/components/markdown-page-footer.js +++ b/www/src/components/markdown-page-footer.js @@ -91,7 +91,7 @@ export default class MarkdownPageFooter extends React.Component { }} href={`https://github.com/gatsbyjs/gatsby/blob/master/${ this.props.packagePage ? `packages` : `docs` - }/${this.props.page.parent.relativePath}`} + }/${this.props.page ? this.props.page.parent.relativePath : ""}`} > {` `} diff --git a/www/src/components/package-readme.js b/www/src/components/package-readme.js new file mode 100644 index 0000000000000..2b9daa68eaa7e --- /dev/null +++ b/www/src/components/package-readme.js @@ -0,0 +1,114 @@ +import React from "react" +import PropTypes from "prop-types" +import Helmet from "react-helmet" +import distanceInWords from "date-fns/distance_in_words" + +import { rhythm, scale } from "../utils/typography" +import presets from "../utils/presets" +import Container from "../components/container" +import MarkdownPageFooter from "../components/markdown-page-footer" + +class PackageReadMe extends React.Component { + render() { + const { + lastPublisher, + page, + packageName, + excerpt, + modified, + html, + githubUrl, + keywords, + timeToRead, + } = this.props + + const lastUpdated = `${distanceInWords(new Date(modified), new Date())} ago` + const gatsbyKeywords = [`gatsby`, `gatsby-plugin`, `gatsby-component`] + const tags = keywords + .filter(keyword => !gatsbyKeywords.includes(keyword)) + .join(`, `) + + return ( + + + {packageName} + + + + + + + + + + + Browse source code for this package on GitHub + + + +
+
+ {tags} +
+ + {lastPublisher.name != "User Not Found" ? ( +
+ + + {lastPublisher.name} + + + {lastUpdated} + +
+ ) : null} +
+ +
+ + + ) + } +} + +PackageReadMe.propTypes = { + page: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), + packageName: PropTypes.string.isRequired, + excerpt: PropTypes.string, + html: PropTypes.string.isRequired, + githubUrl: PropTypes.string, + timeToRead: PropTypes.number, + modified: PropTypes.string, + keywords: PropTypes.array, + lastPublisher: PropTypes.object, +} + +export default PackageReadMe diff --git a/www/src/components/searchbar-body.js b/www/src/components/searchbar-body.js new file mode 100644 index 0000000000000..661b142f2f60e --- /dev/null +++ b/www/src/components/searchbar-body.js @@ -0,0 +1,258 @@ +import React, { Component } from "react" +import { + InstantSearch, + Hits, + SearchBox, + Stats, + RefinementList, + InfiniteHits, +} from "react-instantsearch/dom" +import distanceInWords from "date-fns/distance_in_words" +import presets, { colors } from "../utils/presets" +import Link from "gatsby-link" +import DownloadArrow from "react-icons/lib/go/arrow-small-down" +import { debounce } from "lodash" + +import typography, { rhythm } from "../utils/typography" + +// This is for the urlSync +const updateAfter = 700 +// + +const wideScreenSize = { + "@media (min-width: 1600px)": { + margin: rhythm(0.25), + fontSize: rhythm(0.5), + }, +} + +// Search shows a list of "hits", and is a child of the SearchBar component +class Search extends Component { + constructor(props) { + super(props) + this.state = { + searchState: this.props.searchState, + } + } + render() { + const emptySearchBox = this.props.searchState.length > 0 ? false : true + return ( +
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+ ( + + )} + /> +
+
+ +
+

+ Search by{" "} + + Algolia + +

+
+
+ ) + } +} + +// the result component is fed into the InfiniteHits component +const Result = ({ hit, pathname }) => { + const selected = pathname.slice(10) === hit.name + const lastUpdated = `${distanceInWords( + new Date(hit.modified), + new Date() + )} ago` + return ( + +
+
+ {hit.name} +
+ +
+ {hit.humanDownloadsLast30Days} + {selected ? ( + + ) : ( + + )} +
+
+ +
+ {hit.description} +
+ + ) +} + +// the search bar holds the Search component in the InstantSearch widget +class SearchBar extends Component { + constructor(props) { + super(props) + this.state = { searchState: { query: this.urlToSearch(), page: 1 } } + this.updateHistory = debounce(this.updateHistory, updateAfter) + } + + urlToSearch = () => { + return this.props.history.location.search.slice(2) + } + + updateHistory(value) { + this.props.history.replace(`/packages?=${value.query}`) + } + + onSearchStateChange(searchState) { + this.updateHistory(searchState) + this.setState({ searchState }) + } + + render() { + return ( +
+ + + +
+ ) + } +} + +export default SearchBar diff --git a/www/src/css/searchbox-style.css b/www/src/css/searchbox-style.css new file mode 100644 index 0000000000000..83c9b9363a33f --- /dev/null +++ b/www/src/css/searchbox-style.css @@ -0,0 +1,129 @@ +.ais-SearchBox__input:valid ~ .ais-SearchBox__reset { + display: block; +} + +.ais-SearchBox__root { + display: inline-block; + position: relative; + margin: 0; + width: 100%; + height: 46px; + white-space: nowrap; + box-sizing: border-box; + font-size: 14px; +} + +.ais-SearchBox__wrapper { + width: 100%; + height: 100%; +} + +.ais-SearchBox__input { + -webkit-appearance: none; + display: inline-block; + -webkit-transition: box-shadow 0.4s ease, background 0.4s ease; + transition: box-shadow 0.4s ease, background 0.4s ease; + border: 1px solid #d4d8e3; + border-radius: 4px; + background: #ffffff; + box-shadow: 0 1px 1px 0 rgba(85, 95, 110, 0.2); + padding: 0; + padding-right: 36px; + padding-left: 46px; + width: 100%; + height: 100%; + vertical-align: middle; + white-space: normal; + font-size: inherit; +} +.ais-SearchBox__input:hover, +.ais-SearchBox__input:active, +.ais-SearchBox__input:focus { + box-shadow: none; + outline: 0; +} +.ais-SearchBox__input::-webkit-input-placeholder, +.ais-SearchBox__input::-moz-placeholder, +.ais-SearchBox__input:-ms-input-placeholder, +.ais-SearchBox__input::placeholder { + color: #9faab2; +} + +.ais-SearchBox__submit { + position: absolute; + top: 0; + right: inherit; + left: 0; + margin: 0; + border: 0; + border-radius: 4px 0 0 4px; + background-color: rgba(255, 255, 255, 0); + padding: 0; + width: 46px; + height: 100%; + vertical-align: middle; + text-align: center; + font-size: inherit; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.ais-SearchBox__submit::before { + display: inline-block; + margin-right: -4px; + height: 100%; + vertical-align: middle; + content: "" 2; +} +.ais-SearchBox__submit:hover, +.ais-SearchBox__submit:active { + cursor: pointer; +} +.ais-SearchBox__submit:focus { + outline: 0; +} +.ais-SearchBox__submit svg { + width: 18px; + height: 18px; + vertical-align: middle; + fill: #bfc7d8; +} + +.ais-SearchBox__reset { + display: none; + position: absolute; + top: 13px; + right: 13px; + margin: 0; + border: 0; + background: none; + cursor: pointer; + padding: 0; + font-size: inherit; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + fill: #bfc7d8; +} +.ais-SearchBox__reset:focus { + outline: 0; +} +.ais-SearchBox__reset svg { + display: block; + margin: 4px; + width: 12px; + height: 12px; +} + +.ais-InfiniteHits__loadMore { + width: 100%; + height: 50px; + background-color: #663399; + color: #f5f3f7; + outline: none; +} +.ais-InfiniteHits__loadMore[disabled] { + display: none; +} diff --git a/www/src/html.js b/www/src/html.js index dd113122e3077..d72a008c8f63d 100644 --- a/www/src/html.js +++ b/www/src/html.js @@ -75,6 +75,7 @@ export default class HTML extends React.Component { />