From c9ba4ddaed62cc0743aeb20a9f897b6402a17b15 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Sun, 2 Dec 2018 15:30:09 -0500 Subject: [PATCH 01/27] Minor readability improvements; null -> undefined --- frontend/components/examples-page.js | 46 ++++++++++++++++++---------- frontend/components/markup-modal.js | 2 +- package-lock.json | 9 ++++++ package.json | 3 +- pages/index.js | 2 +- 5 files changed, 43 insertions(+), 19 deletions(-) diff --git a/frontend/components/examples-page.js b/frontend/components/examples-page.js index 1fe3bf4e2cdb9..4ee348292d651 100644 --- a/frontend/components/examples-page.js +++ b/frontend/components/examples-page.js @@ -12,13 +12,20 @@ import { baseUrl, longCache } from '../constants' export default class ExamplesPage extends React.Component { constructor(props) { super(props) + + const { category } = props.match.params + this.state = { - category: props.match.params.id, - query: null, - example: null, + category, + query: undefined, + selectedExample: undefined, searchReady: true, } + this.searchTimeout = 0 + + this.handleExampleSelected = this.handleExampleSelected.bind(this) + this.dismissMarkupModal = this.dismissMarkupModal.bind(this) this.renderSearchResults = this.renderSearchResults.bind(this) this.searchQueryChanged = this.searchQueryChanged.bind(this) } @@ -29,6 +36,7 @@ export default class ExamplesPage extends React.Component { searchQueryChanged(query) { this.setState({ searchReady: false }) + /* Add a small delay before showing search results so that we wait until the user has stipped typing @@ -48,18 +56,26 @@ export default class ExamplesPage extends React.Component { }, 500) } + handleExampleSelected(example) { + this.setState({ selectedExample: example }) + } + + dismissMarkupModal() { + this.setState({ selectedExample: undefined }) + } + renderSearchResults() { - if (this.state.searchReady) { - if (this.state.query != null && this.state.query.length === 1) { + const { searchReady, query, category } = this.state + + if (searchReady) { + if (query !== undefined && query.length === 1) { return
Search term must have 2 or more characters
} else { return ( { - this.setState({ example }) - }} + clickHandler={this.handleExampleSelected} /> ) } @@ -69,24 +85,22 @@ export default class ExamplesPage extends React.Component { } render() { + const { selectedExample } = this.state + return (
{ - this.setState({ example: null }) - }} + example={selectedExample} + onRequestClose={this.dismissMarkupModal} baseUrl={baseUrl} - key={this.state.example} + key={selectedExample} />
{ - this.setState({ example }) - }} + onBadgeClick={this.handleExampleSelected} baseUrl={baseUrl} longCache={longCache} /> diff --git a/frontend/components/markup-modal.js b/frontend/components/markup-modal.js index c89516a8941cd..1d477e5fed423 100644 --- a/frontend/components/markup-modal.js +++ b/frontend/components/markup-modal.js @@ -50,7 +50,7 @@ export default class MarkupModal extends React.Component { } get isOpen() { - return this.props.example !== null + return this.props.example !== undefined } generateCompleteBadgeUrl() { diff --git a/package-lock.json b/package-lock.json index d69417c7a8555..4efb378d24d10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15856,6 +15856,15 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" }, + "yaml-loader": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/yaml-loader/-/yaml-loader-0.5.0.tgz", + "integrity": "sha512-p9QIzcFSNm4mCw/m5NdyMfN4RE4aFZJWRRb01ERVNGCym8VNbKtw3OYZXnvUIkim6U/EjqE/2yIh9F/msShH9A==", + "dev": true, + "requires": { + "js-yaml": "^3.5.2" + } + }, "yargs": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", diff --git a/package.json b/package.json index 8de12d9d53287..48f02d11ebf81 100644 --- a/package.json +++ b/package.json @@ -176,7 +176,8 @@ "snap-shot-it": "^6.2.7", "tmp": "0.0.33", "url": "^0.11.0", - "wait-promise": "^0.4.1" + "wait-promise": "^0.4.1", + "yaml-loader": "^0.5.0" }, "engines": { "node": ">= 8", diff --git a/pages/index.js b/pages/index.js index c9444d2ce819f..9052ddb5ec09a 100644 --- a/pages/index.js +++ b/pages/index.js @@ -7,7 +7,7 @@ export default class Router extends React.Component { const router = (
- +
) From 8126a1fa4739688e23ff4245fb8053e4136d09a1 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Sun, 2 Dec 2018 18:04:56 -0500 Subject: [PATCH 02/27] Get badge listing working --- frontend/components/badge-examples.js | 208 +++++++----------- frontend/components/category-headings.js | 32 +++ frontend/components/examples-page.js | 100 ++++++--- frontend/components/search-results.js | 53 ----- frontend/lib/badge-url.js | 5 +- frontend/lib/service-definitions/index.js | 13 ++ .../service-definition-helper.js | 9 + .../service-definition-set-helper.js | 31 +++ lib/make-badge-url.js | 4 +- next.config.js | 8 +- package-lock.json | 32 ++- package.json | 5 +- 12 files changed, 268 insertions(+), 232 deletions(-) create mode 100644 frontend/components/category-headings.js delete mode 100644 frontend/components/search-results.js create mode 100644 frontend/lib/service-definitions/index.js create mode 100644 frontend/lib/service-definitions/service-definition-helper.js create mode 100644 frontend/lib/service-definitions/service-definition-set-helper.js diff --git a/frontend/components/badge-examples.js b/frontend/components/badge-examples.js index 049ea2b608ec5..48de87eb989c3 100644 --- a/frontend/components/badge-examples.js +++ b/frontend/components/badge-examples.js @@ -1,149 +1,99 @@ import React from 'react' -import { Link } from 'react-router-dom' import PropTypes from 'prop-types' import classNames from 'classnames' import resolveBadgeUrl from '../lib/badge-url' +import { staticBadgeUrl } from '../../lib/make-badge-url' -const Badge = ({ - title, - exampleUrl, - previewUrl, - urlPattern, - documentation, - baseUrl, - longCache, - shouldDisplay = () => true, - onClick, -}) => { - const handleClick = onClick - ? () => - onClick({ - title, - exampleUrl, - previewUrl, - urlPattern, - documentation, - }) - : undefined +const nonBreakingSpace = '\u00a0' - const previewImage = previewUrl ? ( - - ) : ( - '\u00a0' - ) // non-breaking space - const resolvedExampleUrl = resolveBadgeUrl( - urlPattern || previewUrl, - baseUrl, - { longCache: false } - ) +export default class BadgeExamples extends React.Component { + static propTypes = { + definitions: PropTypes.array.isRequired, + baseUrl: PropTypes.string, + longCache: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + } + + buildUrl({ path, pattern, namedParams, queryParams }, { longCache } = {}) { + const { baseUrl } = this.props + + let outPath + let outLongCache + if (pattern === undefined) { + outPath = path + outLongCache = longCache + } else { + outPath = pattern + outLongCache = false + } + + return resolveBadgeUrl(outPath, baseUrl, { + queryParams, + longCache: outLongCache, + format: 'svg', + }) + } + + renderExample(exampleData) { + const { baseUrl, longCache, onClick } = this.props + const { title, example, preview, keywords, documentation } = exampleData + + let previewUrl + if (preview.label !== undefined) { + const { label, message, color } = preview + previewUrl = staticBadgeUrl({ baseUrl, label, message, color }) + } else { + previewUrl = this.buildUrl(preview, { longCache: true }) + } + + const exampleUrl = this.buildUrl(example) + + const key = `${title} ${previewUrl} ${exampleUrl}` + + const handleClick = () => onClick(exampleData) - if (shouldDisplay()) { return ( - - + + {title}: - {previewImage} - - {resolvedExampleUrl} + src={previewUrl} + alt="" + /> + + + + {exampleUrl} ) } - return null -} -Badge.propTypes = { - title: PropTypes.string.isRequired, - exampleUrl: PropTypes.string, - previewUrl: PropTypes.string, - urlPattern: PropTypes.string, - documentation: PropTypes.string, - baseUrl: PropTypes.string, - longCache: PropTypes.bool.isRequired, - shouldDisplay: PropTypes.func, - onClick: PropTypes.func.isRequired, -} -const Category = ({ category, examples, baseUrl, longCache, onClick }) => { - if (examples.filter(example => example.shouldDisplay()).length === 0) { - return null - } - return ( -
- -

{category.name}

- - - - {examples.map(badgeData => ( - - ))} - -
-
- ) -} -Category.propTypes = { - category: PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - }).isRequired, - examples: PropTypes.arrayOf( - PropTypes.shape({ - title: PropTypes.string.isRequired, - exampleUrl: PropTypes.string, - previewUrl: PropTypes.string, - urlPattern: PropTypes.string, - documentation: PropTypes.string, - }) - ).isRequired, - baseUrl: PropTypes.string, - longCache: PropTypes.bool.isRequired, - onClick: PropTypes.func.isRequired, -} + render() { + const { definitions } = this.props -const BadgeExamples = ({ categories, baseUrl, longCache, onClick }) => ( -
- {categories.map((categoryData, i) => ( - - ))} -
-) -BadgeExamples.propTypes = { - categories: PropTypes.arrayOf( - PropTypes.shape({ - category: Category.propTypes.category, - examples: Category.propTypes.examples, - }) - ), - baseUrl: PropTypes.string, - longCache: PropTypes.bool.isRequired, - onClick: PropTypes.func.isRequired, -} + if (!definitions) { + return null + } + + const flattened = definitions.reduce((accum, current) => { + const { examples } = current + return accum.concat(examples) + }, []) -export { Badge, BadgeExamples } + return ( +
+ + + {flattened.map(exampleData => this.renderExample(exampleData))} + +
+
+ ) + } +} diff --git a/frontend/components/category-headings.js b/frontend/components/category-headings.js new file mode 100644 index 0000000000000..f0ffb1552ba5b --- /dev/null +++ b/frontend/components/category-headings.js @@ -0,0 +1,32 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Link } from 'react-router-dom' + +const CategoryHeading = ({ category }) => { + const { id, name } = category + + return ( + +

{name}

+ + ) +} +CategoryHeading.propTypes = { + category: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + }).isRequired, +} + +const CategoryHeadings = ({ categories }) => ( +
+ {categories.map(category => ( + + ))} +
+) +CategoryHeadings.propTypes = { + categories: PropTypes.arrayOf(CategoryHeading.propTypes.category).isRequired, +} + +export { CategoryHeading, CategoryHeadings } diff --git a/frontend/components/examples-page.js b/frontend/components/examples-page.js index 4ee348292d651..385d038bd9f94 100644 --- a/frontend/components/examples-page.js +++ b/frontend/components/examples-page.js @@ -3,30 +3,34 @@ import PropTypes from 'prop-types' import Meta from './meta' import Header from './header' import SuggestionAndSearch from './suggestion-and-search' -import SearchResults from './search-results' import MarkupModal from './markup-modal' import Usage from './usage' import Footer from './footer' +import { CategoryHeadings } from './category-headings' +import { + categories, + services, + getDefinitionsForCategory, +} from '../lib/service-definitions' +import BadgeExamples from './badge-examples' import { baseUrl, longCache } from '../constants' +import ServiceDefinitionSetHelper from '../lib/service-definitions/service-definition-set-helper' export default class ExamplesPage extends React.Component { constructor(props) { super(props) - const { category } = props.match.params - this.state = { - category, - query: undefined, + isSearchInProgress: false, + isQueryTooShort: false, + searchResults: undefined, selectedExample: undefined, - searchReady: true, } this.searchTimeout = 0 this.handleExampleSelected = this.handleExampleSelected.bind(this) this.dismissMarkupModal = this.dismissMarkupModal.bind(this) - this.renderSearchResults = this.renderSearchResults.bind(this) this.searchQueryChanged = this.searchQueryChanged.bind(this) } @@ -34,9 +38,29 @@ export default class ExamplesPage extends React.Component { match: PropTypes.object.isRequired, } - searchQueryChanged(query) { - this.setState({ searchReady: false }) + get category() { + return this.props.match.params.category + } + performSearch(query) { + const isQueryTooShort = query !== undefined && query.length === 1 + + let searchResults + if (!isQueryTooShort) { + searchResults = ServiceDefinitionSetHelper.create(services) + .notDeprecated() + .search(query) + .asNative() + } + + this.setState({ + isSearchInProgress: false, + isQueryTooShort, + searchResults, + }) + } + + searchQueryChanged(query) { /* Add a small delay before showing search results so that we wait until the user has stipped typing @@ -47,13 +71,9 @@ export default class ExamplesPage extends React.Component { b) stops the page from 'flashing' as the user types, like this: https://user-images.githubusercontent.com/7288322/42600206-9b278470-85b5-11e8-9f63-eb4a0c31cb4a.gif */ + this.setState({ isSearchInProgress: true }) window.clearTimeout(this.searchTimeout) - this.searchTimeout = window.setTimeout(() => { - this.setState({ - searchReady: true, - query, - }) - }, 500) + this.searchTimeout = window.setTimeout(() => this.performSearch(query), 500) } handleExampleSelected(example) { @@ -64,23 +84,39 @@ export default class ExamplesPage extends React.Component { this.setState({ selectedExample: undefined }) } - renderSearchResults() { - const { searchReady, query, category } = this.state - - if (searchReady) { - if (query !== undefined && query.length === 1) { - return
Search term must have 2 or more characters
- } else { - return ( - - ) - } - } else { + renderMain() { + const { category } = this + const { isSearchInProgress, isQueryTooShort, searchResults } = this.state + + if (isSearchInProgress) { return
searching...
+ } else if (isQueryTooShort) { + return
Search term must have 2 or more characters
+ } else if (searchResults) { + return ( + + ) + } else if (category) { + const definitions = ServiceDefinitionSetHelper.create( + getDefinitionsForCategory(category) + ) + .notDeprecated() + .asNative() + return ( + + ) + } else { + return } } @@ -108,7 +144,7 @@ export default class ExamplesPage extends React.Component { donate
- {this.renderSearchResults()} + {this.renderMain()}