Skip to content

Commit 58b2765

Browse files
authored
Refactor frontend main page and badge-example code (#2441)
- The goal of this PR is: - Consume the new service-definition format. (#2397) - Make the frontend more readable. - Behavior changes: - I changed the **Image** field in the markup modal to show only the path. - I added another click-to-select field below that shows the complete URL. - This made it easier to suppress the live badge preview while it contains placeholders like `:user` or `:gem`, a minor tweak discussed at #2427 (comment). - The search box now searches all categories, regardless of the current page. (This is an improvement, I would say.) - I did not deliberately address performance, though I ripped out a bunch of anonymous functions and avoided re-filtering all the examples by category on every render, which I expect will not hurt. I haven't really tested this on a mobile connection and it'd be worth doing that. - It would be great to have some tests of the components, though getting started with that seemed like a big project and I did not want to make this any larger than it already is. It's a medium-sized refactor: 1. Replace `BadgeExamples`, `Category` and `Badge` component with a completely rewritten `BadgeExamples` component which renders a table of badges, and `CategoryHeading` and `CategoryHeadings` components. 2. Refactor `ExamplesPage` and `SearchResults` components into a new `Main` component. 3. Rewrite the data flow for `MarkupModal`. Rather than rely on unmounting and remounting the component to copy the badge URL into state, employ the `getDerivedStateFromProps` lifecycle method. 4. Remove `prepareExamples` and `all-badge-examples`. 5. Rewrite the `$suggest` schema to harmonize with the service definition format. It's not backward-compatible which means at deploy time there probably will be 10–20 minutes of downtime on that feature, between the first server deploy and the final gh-pages deploy. 🤷‍♂️ (We could leave the old version in place if it seems worth it.) 6. Added two new functions in `make-badge-url` with tests. I removed _most_ of the uses of the old functions, but there are some in parts of the frontend I didn't touch like the static and dynamic badge generators, and again I didn't want to make this any larger than it already is. 7. Fix a couple bugs in the service-definition export.
1 parent 8a8311d commit 58b2765

33 files changed

+754
-996
lines changed

.eslintrc-frontend.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ settings:
2121
rules:
2222
no-console: "error"
2323

24-
import/extensions: ["error", "never", { "json": "always" }]
24+
import/extensions: ["error", "never", { "json": "always", "yml": "always" }]

doc/TUTORIAL.md

+3
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,9 @@ module.exports = class GemVersion extends BaseJsonService {
279279

280280
Save, run `npm start`, and you can see it [locally](http://127.0.0.1:3000/).
281281

282+
If you update `examples`, you don't have to restart the server. Run `npm run
283+
defs` in another terminal window and the frontend will update.
284+
282285
### (4.5) Write Tests <!-- Change the link below when you change the heading -->
283286
[write tests]: #45-write-tests
284287

frontend/components/badge-examples.js

+74-131
Original file line numberDiff line numberDiff line change
@@ -1,149 +1,92 @@
11
import React from 'react'
2-
import { Link } from 'react-router-dom'
32
import PropTypes from 'prop-types'
4-
import classNames from 'classnames'
5-
import resolveBadgeUrl from '../lib/badge-url'
3+
import { badgeUrlFromPath, staticBadgeUrl } from '../../lib/make-badge-url'
64

7-
const Badge = ({
8-
title,
9-
exampleUrl,
10-
previewUrl,
11-
urlPattern,
12-
documentation,
13-
baseUrl,
14-
longCache,
15-
shouldDisplay = () => true,
16-
onClick,
17-
}) => {
18-
const handleClick = onClick
19-
? () =>
20-
onClick({
21-
title,
22-
exampleUrl,
23-
previewUrl,
24-
urlPattern,
25-
documentation,
26-
})
27-
: undefined
5+
export default class BadgeExamples extends React.Component {
6+
static propTypes = {
7+
definitions: PropTypes.array.isRequired,
8+
baseUrl: PropTypes.string,
9+
longCache: PropTypes.bool.isRequired,
10+
onClick: PropTypes.func.isRequired,
11+
}
12+
13+
renderExample(exampleData) {
14+
const { baseUrl, longCache, onClick } = this.props
15+
const { title, example, preview } = exampleData
16+
17+
let previewUrl
18+
// There are two alternatives for `preview`. Refer to the schema in
19+
// `services/service-definitions.js`.
20+
if (preview.label !== undefined) {
21+
const { label, message, color } = preview
22+
previewUrl = staticBadgeUrl({ baseUrl, label, message, color })
23+
} else {
24+
const { path, queryParams } = preview
25+
previewUrl = badgeUrlFromPath({ baseUrl, path, queryParams, longCache })
26+
}
27+
28+
// There are two alternatives for `example`. Refer to the schema in
29+
// `services/service-definitions.js`.
30+
let exampleUrl
31+
if (example.pattern !== undefined) {
32+
const { pattern, namedParams, queryParams } = example
33+
exampleUrl = badgeUrlFromPath({
34+
baseUrl,
35+
path: pattern,
36+
namedParams,
37+
queryParams,
38+
})
39+
} else {
40+
const { path, queryParams } = example
41+
exampleUrl = badgeUrlFromPath({ baseUrl, path, queryParams })
42+
}
43+
44+
const key = `${title} ${previewUrl} ${exampleUrl}`
2845

29-
const previewImage = previewUrl ? (
30-
<img
31-
className={classNames('badge-img', { clickable: onClick })}
32-
onClick={handleClick}
33-
src={resolveBadgeUrl(previewUrl, baseUrl, { longCache })}
34-
alt=""
35-
/>
36-
) : (
37-
'\u00a0'
38-
) // non-breaking space
39-
const resolvedExampleUrl = resolveBadgeUrl(
40-
urlPattern || previewUrl,
41-
baseUrl,
42-
{ longCache: false }
43-
)
46+
const handleClick = () => onClick(exampleData)
4447

45-
if (shouldDisplay()) {
4648
return (
47-
<tr>
48-
<th
49-
className={classNames({ clickable: onClick })}
50-
onClick={handleClick}
51-
>
49+
<tr key={key}>
50+
<th className="clickable" onClick={handleClick}>
5251
{title}:
5352
</th>
54-
<td>{previewImage}</td>
5553
<td>
56-
<code
57-
className={classNames({ clickable: onClick })}
54+
<img
55+
className="badge-img clickable"
5856
onClick={handleClick}
59-
>
60-
{resolvedExampleUrl}
57+
src={previewUrl}
58+
alt=""
59+
/>
60+
</td>
61+
<td>
62+
<code className="clickable" onClick={handleClick}>
63+
{exampleUrl}
6164
</code>
6265
</td>
6366
</tr>
6467
)
6568
}
66-
return null
67-
}
68-
Badge.propTypes = {
69-
title: PropTypes.string.isRequired,
70-
exampleUrl: PropTypes.string,
71-
previewUrl: PropTypes.string,
72-
urlPattern: PropTypes.string,
73-
documentation: PropTypes.string,
74-
baseUrl: PropTypes.string,
75-
longCache: PropTypes.bool.isRequired,
76-
shouldDisplay: PropTypes.func,
77-
onClick: PropTypes.func.isRequired,
78-
}
7969

80-
const Category = ({ category, examples, baseUrl, longCache, onClick }) => {
81-
if (examples.filter(example => example.shouldDisplay()).length === 0) {
82-
return null
83-
}
84-
return (
85-
<div>
86-
<Link to={`/examples/${category.id}`}>
87-
<h3 id={category.id}>{category.name}</h3>
88-
</Link>
89-
<table className="badge">
90-
<tbody>
91-
{examples.map(badgeData => (
92-
<Badge
93-
key={badgeData.key}
94-
{...badgeData}
95-
baseUrl={baseUrl}
96-
longCache={longCache}
97-
onClick={onClick}
98-
/>
99-
))}
100-
</tbody>
101-
</table>
102-
</div>
103-
)
104-
}
105-
Category.propTypes = {
106-
category: PropTypes.shape({
107-
id: PropTypes.string.isRequired,
108-
name: PropTypes.string.isRequired,
109-
}).isRequired,
110-
examples: PropTypes.arrayOf(
111-
PropTypes.shape({
112-
title: PropTypes.string.isRequired,
113-
exampleUrl: PropTypes.string,
114-
previewUrl: PropTypes.string,
115-
urlPattern: PropTypes.string,
116-
documentation: PropTypes.string,
117-
})
118-
).isRequired,
119-
baseUrl: PropTypes.string,
120-
longCache: PropTypes.bool.isRequired,
121-
onClick: PropTypes.func.isRequired,
122-
}
70+
render() {
71+
const { definitions } = this.props
12372

124-
const BadgeExamples = ({ categories, baseUrl, longCache, onClick }) => (
125-
<div>
126-
{categories.map((categoryData, i) => (
127-
<Category
128-
key={i}
129-
{...categoryData}
130-
baseUrl={baseUrl}
131-
longCache={longCache}
132-
onClick={onClick}
133-
/>
134-
))}
135-
</div>
136-
)
137-
BadgeExamples.propTypes = {
138-
categories: PropTypes.arrayOf(
139-
PropTypes.shape({
140-
category: Category.propTypes.category,
141-
examples: Category.propTypes.examples,
142-
})
143-
),
144-
baseUrl: PropTypes.string,
145-
longCache: PropTypes.bool.isRequired,
146-
onClick: PropTypes.func.isRequired,
147-
}
73+
if (!definitions) {
74+
return null
75+
}
76+
77+
const flattened = definitions.reduce((accum, current) => {
78+
const { examples } = current
79+
return accum.concat(examples)
80+
}, [])
14881

149-
export { Badge, BadgeExamples }
82+
return (
83+
<div>
84+
<table className="badge">
85+
<tbody>
86+
{flattened.map(exampleData => this.renderExample(exampleData))}
87+
</tbody>
88+
</table>
89+
</div>
90+
)
91+
}
92+
}
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React from 'react'
2+
import PropTypes from 'prop-types'
3+
import { Link } from 'react-router-dom'
4+
5+
const CategoryHeading = ({ category }) => {
6+
const { id, name } = category
7+
8+
return (
9+
<Link to={`/examples/${id}`}>
10+
<h3 id={id}>{name}</h3>
11+
</Link>
12+
)
13+
}
14+
CategoryHeading.propTypes = {
15+
category: PropTypes.shape({
16+
id: PropTypes.string.isRequired,
17+
name: PropTypes.string.isRequired,
18+
}).isRequired,
19+
}
20+
21+
const CategoryHeadings = ({ categories }) => (
22+
<div>
23+
{categories.map(category => (
24+
<CategoryHeading category={category} key={category.id} />
25+
))}
26+
</div>
27+
)
28+
CategoryHeadings.propTypes = {
29+
categories: PropTypes.arrayOf(CategoryHeading.propTypes.category).isRequired,
30+
}
31+
32+
export { CategoryHeading, CategoryHeadings }

frontend/components/examples-page.js

-109
This file was deleted.

0 commit comments

Comments
 (0)