Skip to content

Commit

Permalink
Rewrite frontend in React (#1273)
Browse files Browse the repository at this point in the history
I rewrote the frontend in React using a module bundler. It's matched feature-for-feature with the current frontend, with only slight changes in the styling. I did not fuss about making the styling identical; the badge popup looks particularly different.

This makes the front end much easier to develop. I'm really looking forward to implementing #701, to which this paves the way.

This makes light use of Next.js, which provides webpack config and dev/build tooling. We’ll probably replace it with create-react-app or our own webpack setup because unfortunately it comes with a lot of runtime overhead (the build is 400k).

Let’s open new issues for bugs and features, and track other follow-ups here: https://github.com/badges/shields/projects/1
  • Loading branch information
paulmelnikow authored Nov 28, 2017
1 parent a0cd930 commit 4b5bf03
Show file tree
Hide file tree
Showing 32 changed files with 5,925 additions and 1,049 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/build
/coverage
12 changes: 12 additions & 0 deletions .eslintrc-frontend.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
env:
browser: true

parser: "babel-eslint"

parserOptions:
sourceType: "module"

extends:
- "standard-jsx"
- "standard-react"
- "./.eslintrc-preferred.yml"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,4 @@ typings/

# Temporary build artifacts.
/build
.next
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ before_script:
script:
- npm run lint
- npm run test:js
- npm run build:production
- if node_modules/.bin/check-node-version --node '< 8.0' > /dev/null; then echo "Skipping build."; else make website; fi

jobs:
include:
Expand Down
34 changes: 6 additions & 28 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,63 +1,41 @@
all: website favicon test

UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
SED=sed -r
NEWLINE=$\n
endif
ifeq ($(UNAME_S),Darwin)
SED=sed -E
NEWLINE=$$'\n'
endif

favicon:
node lib/badge-cli.js '' '' '#bada55' .png > favicon.png

footer-production-transform:
@$(SED) "s,(<img src=\")(/[^\"\?]+)\",\1https://img.shields.io\2?maxAge=2592000\"," \
frontend/fragments/try-footer.html \
| $(SED) "s,(<img src=\")(/[^\"\?]+\?[^\"]+)\",\1https://img.shields.io\2\&maxAge=2592000\"," \
| $(SED) "s,<span id='imgUrlPrefix'>,&https://img.shields.io," \
| $(SED) "s,var origin = '';,var origin = 'https://img.shields.io';," \
> build/try-footer.html

website:
BASE_URL=https://img.shields.io npm run build:production
LONG_CACHE=false BASE_URL=https://img.shields.io npm run build

deploy: deploy-s0 deploy-s1 deploy-s2 deploy-gh-pages

deploy-s0:
# index.html on each server gets a dev build.
# Ship a copy of the front end to each server for debugging.
# https://github.com/badges/shields/issues/1220
npm run build
LONG_CACHE=false BASE_URL=https://s0.shields-server.com npm run build
git add -f Verdana.ttf private/secret.json index.html
git commit -m'MUST NOT BE ON GITHUB'
git push -f s0 HEAD:master
git reset HEAD~1
git checkout master

deploy-s1:
# index.html on each server gets a dev build.
# https://github.com/badges/shields/issues/1220
npm run build
LONG_CACHE=false BASE_URL=https://s1.shields-server.com npm run build
git add -f Verdana.ttf private/secret.json index.html
git commit -m'MUST NOT BE ON GITHUB'
git push -f s1 HEAD:master
git reset HEAD~1
git checkout master

deploy-s2:
# index.html on each server gets a dev build.
# https://github.com/badges/shields/issues/1220
npm run build
LONG_CACHE=false BASE_URL=https://s2.shields-server.com npm run build
git add -f Verdana.ttf private/secret.json index.html
git commit -m'MUST NOT BE ON GITHUB'
git push -f s2 HEAD:master
git reset HEAD~1
git checkout master

deploy-gh-pages:
(BASE_URL=https://img.shields.io npm run build:production && \
(LONG_CACHE=true BASE_URL=https://img.shields.io npm run build && \
git checkout -B gh-pages master && \
git add -f index.html && \
git commit -m '[DEPLOY] Build index.html' && \
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,10 @@ View the [documentation for gh-badges][gh-badges doc].
Development
-----------

1. Install Node 6 or later. You can use the [package manager][] of your choice.
1. Install Node 8 or later. You can use the [package manager][] of your choice.
Node 8 is required for building or developing the front end. Node 6 or 8 will
work to run the server, and we'll transition to Node 8 everywhere once the
production server is upgraded. Server tests need to pass in both.
2. Clone this repository.
3. Run `npm install` to install the dependencies.
4. Run `npm run build` to build the frontend.
Expand All @@ -125,6 +128,9 @@ Development
To generate the frontend using production cache settings &ndash; that is,
badge preview URIs with `maxAge` &ndash; run `npm run build:production`.

To analyze the frontend bundle, run `npm install webpack-bundle-analyzer` and
then `ANALYZE=true npm start`.

[package manager]: https://nodejs.org/en/download/package-manager/


Expand Down
8 changes: 1 addition & 7 deletions frontend/.eslintrc.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1 @@
extends:
- 'standard-jsx'
- '../.eslintrc-preferred.yml'

parserOptions:
ecmaFeatures:
jsx: true
extends: '../.eslintrc-frontend.yml'
57 changes: 0 additions & 57 deletions frontend/badge-examples.js

This file was deleted.

119 changes: 119 additions & 0 deletions frontend/components/badge-examples.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { URL } from '../lib/url-api';
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

function resolveUri (uri, baseUri, options) {
const { longCache } = options || {};
const result = new URL(uri, baseUri);
if (longCache) {
result.searchParams.maxAge = '2592000';
}
return result.href;
}

const Badge = ({ title, previewUri, exampleUri, documentation, baseUri, longCache, onClick }) => {
const handleClick = onClick ?
() => onClick({ title, previewUri, exampleUri, documentation })
: undefined;

const previewImage = previewUri
? (<img
className={classNames('badge-img', { clickable: onClick })}
onClick={handleClick}
src={resolveUri(previewUri, baseUri, { longCache } )}
alt="" />
) : '\u00a0'; // non-breaking space
const resolvedExampleUri = resolveUri(
exampleUri || previewUri,
baseUri || 'https://img.shields.io/',
{ longCache: false });

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>
);
};
Badge.propTypes = {
title: PropTypes.string.isRequired,
previewUri: PropTypes.string,
exampleUri: PropTypes.string,
documentation: PropTypes.string,
baseUri: PropTypes.string.isRequired,
longCache: PropTypes.bool.isRequired,
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, i) => (
<Badge
key={i}
{...badgeData}
baseUri={baseUri}
longCache={longCache}
onClick={onClick} />
))
}
</tbody>
</table>
</div>
);
Category.propTypes = {
category: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
}).isRequired,
examples: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string.isRequired,
previewUri: PropTypes.string,
exampleUri: PropTypes.string,
documentation: PropTypes.string,
})).isRequired,
baseUri: PropTypes.string.isRequired,
longCache: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
};

const BadgeExamples = ({ examples, baseUri, longCache, onClick }) => (
<div>
{
examples.map((categoryData, i) => (
<Category
key={i}
{...categoryData}
baseUri={baseUri}
longCache={longCache}
onClick={onClick} />
))
}
</div>
);
BadgeExamples.propTypes = {
examples: PropTypes.arrayOf(PropTypes.shape({
category: Category.propTypes.category,
examples: Category.propTypes.examples,
})),
baseUri: PropTypes.string.isRequired,
longCache: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
};

module.exports = {
Badge,
BadgeExamples,
resolveUri,
};
90 changes: 90 additions & 0 deletions frontend/components/dynamic-badge-maker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from 'react';
import PropTypes from 'prop-types';

export default class DynamicBadgeMaker extends React.Component {
static propTypes = {
baseUri: PropTypes.string.isRequired,
};

state = {
type: 'json',
label: '',
uri: '',
colorB: '',
prefix: '',
suffix: '',
query: '',
};

makeBadgeUri () {
const result = new URL(`/dynamic/${this.state.type}.svg`, this.props.baseUri);
const searchParams = [
'label',
'uri',
'colorB',
'prefix',
'suffix',
'query',
];
searchParams.forEach(k => {
result.searchParams.set(k, this.state[k]);
});
return result.href;
}

handleSubmit(e) {
e.preventDefault();
document.location = this.makeBadgeUri();
}

get isValid() {
const { label, uri, query } = this.state;
return label && uri && query;
}

render() {
return (
<form onSubmit={e => this.handleSubmit(e)}>
<input
className="short"
value={this.state.type}
readOnly
list="dynamic-type" />
<datalist id="dynamic-type">
<option value="json" />
</datalist>
<input
className="short"
value={this.state.label}
onChange={event => this.setState({ label: event.target.value })}
placeholder="label" />
<input
className="short"
value={this.state.uri}
onChange={event => this.setState({ uri: event.target.value })}
placeholder="uri" />
<input
className="short"
value={this.state.query}
onChange={event => this.setState({ query: event.target.value })}
placeholder="$.data.subdata" />
<input
className="short"
value={this.state.color}
onChange={event => this.setState({ color: event.target.value })}
placeholder="hex color" />
<input
className="short"
value={this.state.prefix}
onChange={event => this.setState({ prefix: event.target.value })}
placeholder="prefix" />
<input
className="short"
value={this.state.suffix}
onChange={event => this.setState({ suffix: event.target.value })}
placeholder="suffix" />
<button disabled={! this.isValid}>Make Badge</button>
</form>
);
}
}
Loading

0 comments on commit 4b5bf03

Please sign in to comment.