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:
@@ -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,
|
||||
|
||||
104
frontend/components/examples-page.js
Normal file
104
frontend/components/examples-page.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
56
frontend/components/search-results.js
Normal file
56
frontend/components/search-results.js
Normal 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
9
frontend/constants.js
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user