The CSS in the project is relatively difficult to change. While it is very DRY, it relies heavily on inheritance. It's difficult to make changes in the markup modal without it also affecting styles elsewhere. [styled-components](https://www.styled-components.com/) is one of the leading CSS-in-JS libraries. By reducing dependency on global state and CSS inheritance, styles become explicit and are easier to inspect and change. It's also convenient that styles can be embedded with the components they modify. At runtime, the library creates CSS classes, so it's pretty efficient. We were using a little bit of [styled-jsx](https://github.com/zeit/styled-jsx) before, which ships with Next.js, though styled-components is more widely used and I've had good experiences with it all around. In a few cases I've duplicated styles where it feels more natural to do that: for example, `text-align: center` is duplicated in `Main` and `MarkupModal`. Much of this is a refactor, though there are a few visual changes, particularly in the markup modal and the style examples.
117 lines
2.9 KiB
JavaScript
117 lines
2.9 KiB
JavaScript
import React from 'react'
|
|
import PropTypes from 'prop-types'
|
|
import fetchPonyfill from 'fetch-ponyfill'
|
|
import debounce from 'lodash.debounce'
|
|
import resolveUrl from '../lib/resolve-url'
|
|
import BadgeExamples from './badge-examples'
|
|
import { BlockInput } from './common'
|
|
|
|
export default class SuggestionAndSearch extends React.Component {
|
|
static propTypes = {
|
|
queryChanged: PropTypes.func.isRequired,
|
|
onBadgeClick: PropTypes.func.isRequired,
|
|
baseUrl: PropTypes.string.isRequired,
|
|
longCache: PropTypes.bool.isRequired,
|
|
}
|
|
|
|
constructor(props) {
|
|
super(props)
|
|
this.queryChangedDebounced = debounce(props.queryChanged, 50, {
|
|
leading: true,
|
|
})
|
|
}
|
|
|
|
state = {
|
|
isUrl: false,
|
|
inProgress: false,
|
|
projectUrl: null,
|
|
suggestions: [],
|
|
}
|
|
|
|
queryChanged(query) {
|
|
const isUrl = query.startsWith('https://') || query.startsWith('http://')
|
|
this.setState({
|
|
isUrl,
|
|
projectUrl: isUrl ? query : null,
|
|
})
|
|
|
|
this.queryChangedDebounced(query)
|
|
}
|
|
|
|
getSuggestions() {
|
|
this.setState({ inProgress: true }, async () => {
|
|
const { baseUrl } = this.props
|
|
const { projectUrl } = this.state
|
|
|
|
const url = resolveUrl('/$suggest/v1', baseUrl, { url: projectUrl })
|
|
|
|
const fetch = window.fetch || fetchPonyfill
|
|
const res = await fetch(url)
|
|
let suggestions
|
|
try {
|
|
const json = await res.json()
|
|
// This doesn't validate the response. The default value here prevents
|
|
// a crash if the server returns {"err":"Disallowed"}.
|
|
suggestions = json.suggestions || []
|
|
} catch (e) {
|
|
suggestions = []
|
|
}
|
|
|
|
this.setState({ inProgress: false, suggestions })
|
|
})
|
|
}
|
|
|
|
renderSuggestions() {
|
|
const { baseUrl, longCache } = this.props
|
|
const { suggestions } = this.state
|
|
|
|
if (suggestions.length === 0) {
|
|
return null
|
|
}
|
|
|
|
const transformed = [
|
|
{
|
|
examples: suggestions.map(({ title, path, link, queryParams }) => ({
|
|
title,
|
|
preview: { path, queryParams },
|
|
example: { path, queryParams },
|
|
link,
|
|
})),
|
|
},
|
|
]
|
|
|
|
return (
|
|
<BadgeExamples
|
|
definitions={transformed}
|
|
baseUrl={baseUrl}
|
|
longCache={longCache}
|
|
onClick={this.props.onBadgeClick}
|
|
/>
|
|
)
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<section>
|
|
<form action="javascript:void 0" autoComplete="off">
|
|
<BlockInput
|
|
onChange={event => this.queryChanged(event.target.value)}
|
|
autofill="off"
|
|
autoFocus
|
|
placeholder="search / project URL"
|
|
/>
|
|
<br />
|
|
<button
|
|
onClick={event => this.getSuggestions(event.target.value)}
|
|
disabled={this.state.inProgress}
|
|
hidden={!this.state.isUrl}
|
|
>
|
|
Suggest badges
|
|
</button>
|
|
</form>
|
|
{this.renderSuggestions()}
|
|
</section>
|
|
)
|
|
}
|
|
}
|