Split front end into one page per category (#1808)

- Present 'downloads', 'version', etc as pages
- Don't show any badges on the index page,
  just links to categories.
- Tweak search so we can search all badges
  from the index page, but without rendering
  every badge as soon as we press a key.
This commit is contained in:
chris48s
2018-08-01 21:02:55 +01:00
committed by GitHub
parent ca01b99e9f
commit 901a7b8a43
9 changed files with 2850 additions and 2602 deletions

View File

@@ -1,4 +1,5 @@
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';
@@ -29,19 +30,22 @@ const Badge = ({
baseUri,
{ longCache: false });
return (
<tr className={classNames({ excluded: !shouldDisplay() })}>
<th className={classNames({ clickable: onClick })} onClick={handleClick}>
{ title }:
</th>
<td>{ previewImage }</td>
<td>
<code className={classNames({ clickable: onClick })} onClick={handleClick}>
{ resolvedExampleUri }
</code>
</td>
</tr>
);
if (shouldDisplay()) {
return (
<tr>
<th className={classNames({ clickable: onClick })} onClick={handleClick}>
{ title }:
</th>
<td>{ previewImage }</td>
<td>
<code className={classNames({ clickable: onClick })} onClick={handleClick}>
{ resolvedExampleUri }
</code>
</td>
</tr>
);
}
return null;
};
Badge.propTypes = {
title: PropTypes.string.isRequired,
@@ -54,25 +58,32 @@ Badge.propTypes = {
onClick: PropTypes.func.isRequired,
};
const Category = ({ category, examples, baseUri, longCache, onClick }) => (
<div>
<h3 id={category.id}>{ category.name }</h3>
<table className="badge">
<tbody>
{
examples.map(badgeData => (
<Badge
key={badgeData.key}
{...badgeData}
baseUri={baseUri}
longCache={longCache}
onClick={onClick} />
))
}
</tbody>
</table>
</div>
);
const Category = ({ category, examples, baseUri, longCache, onClick }) => {
if (examples.filter(example => example.shouldDisplay()).length === 0){
return null;
}
return (
<div>
<Link to={'/examples/' + category.id}>
<h3 id={category.id}>{ category.name }</h3>
</Link>
<table className="badge">
<tbody>
{
examples.map(badgeData => (
<Badge
key={badgeData.key}
{...badgeData}
baseUri={baseUri}
longCache={longCache}
onClick={onClick} />
))
}
</tbody>
</table>
</div>
);
};
Category.propTypes = {
category: PropTypes.shape({
id: PropTypes.string.isRequired,

View File

@@ -0,0 +1,104 @@
import React from 'react';
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 { baseUri, longCache } from '../constants';
export default class ExamplesPage extends React.Component {
constructor(props) {
super(props);
this.state = {
category: props.match.params.id,
query: null,
example: null,
searchReady: true,
};
this.searchTimeout = 0;
this.renderSearchResults = this.renderSearchResults.bind(this);
this.searchQueryChanged = this.searchQueryChanged.bind(this);
}
static propTypes = {
match: PropTypes.object.isRequired,
}
searchQueryChanged(query) {
this.setState({searchReady: false});
/*
Add a small delay before showing search results
so that we wait until the user has stipped typing
before we start loading stuff.
This
a) reduces the amount of badges we will load and
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
*/
window.clearTimeout(this.searchTimeout);
this.searchTimeout = window.setTimeout(() => {
this.setState({
searchReady: true,
query: query
});
}, 500);
}
renderSearchResults() {
if (this.state.searchReady) {
if ((this.state.query != null) && (this.state.query.length === 1)) {
return (<div>Search term must have 2 or more characters</div>);
} else {
return (
<SearchResults
category={this.state.category}
query={this.state.query}
clickHandler={example => { this.setState({ example }); }} />
);
}
} else {
return (<div>searching...</div>);
}
}
render() {
return (
<div>
<Meta />
<Header />
<MarkupModal
example={this.state.example}
onRequestClose={() => { this.setState({ example: null }); }}
baseUri={baseUri} />
<section>
<SuggestionAndSearch
queryChanged={this.searchQueryChanged}
onBadgeClick={example => { this.setState({ example }); }}
baseUri={baseUri}
longCache={longCache} />
<a
className="donate"
href="https://opencollective.com/shields">
donate
</a>
</section>
{ this.renderSearchResults() }
<Usage
baseUri={baseUri}
longCache={longCache} />
<Footer baseUri={baseUri} />
<style jsx>{`
.donate {
text-decoration: none;
color: rgba(0,0,0,0.1);
}
`}</style>
</div>
);
}
}

View File

@@ -1,8 +1,11 @@
import { Link } from "react-router-dom";
import React from 'react';
export default () => (
<section>
<img alt="Shields.io" src="static/logo.svg" />
<Link to="/">
<img alt="Shields.io" src="/static/logo.svg" />
</Link>
<hr className="spacing" />

View File

@@ -12,7 +12,7 @@ export default () => (
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="description" content={description} />
<link rel="icon" type="image/png" href="favicon.png" />
<link href="static/main.css" rel="stylesheet" />
<link href="/static/main.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css?family=Lekton" rel="stylesheet" />
</Head>
);

View File

@@ -0,0 +1,56 @@
import React from 'react';
import { Link } from "react-router-dom";
import PropTypes from 'prop-types';
import { BadgeExamples } from './badge-examples';
import badgeExampleData from '../../badge-examples.json';
import { prepareExamples, predicateFromQuery } from '../lib/prepare-examples';
import { baseUri, longCache } from '../constants';
export default class SearchResults extends React.Component {
static propTypes = {
category: PropTypes.string,
query: PropTypes.string,
clickHandler: PropTypes.func.isRequired,
}
prepareExamples(category) {
const examples = category ? badgeExampleData.filter(example => example.category.id === category) : badgeExampleData;
return prepareExamples(examples, () => predicateFromQuery(this.props.query));
}
renderExamples() {
return (
<BadgeExamples
categories={this.preparedExamples}
onClick={this.props.clickHandler}
baseUri={baseUri}
longCache={longCache} />
);
}
renderCategoryHeadings() {
return this.preparedExamples.map(function(category, i) {
return (
<Link to={'/examples/' + category.category.id} key={category.category.id}>
<h3 id={category.category.id}>{ category.category.name }</h3>
</Link>
)
});
}
render() {
this.preparedExamples = this.prepareExamples(this.props.category);
if (this.props.category) {
return this.renderExamples();
} else if ((this.props.query == null) || (this.props.query.length === 0)) {
return this.renderCategoryHeadings();
} else {
return this.renderExamples();
}
}
}

9
frontend/constants.js Normal file
View File

@@ -0,0 +1,9 @@
import envFlag from 'node-env-flag';
const baseUri = process.env.BASE_URL;
const longCache = envFlag(process.env.LONG_CACHE, false);
export {
baseUri,
longCache
}