Typescripterize BadgeExamples and SuggestionAndSearch (#3879)

The two different kinds of data that can be passed to `<BadgeExample />` were a bit less similar than I thought, so this includes a little refactor related to that which isn't perfect, but leaves things in a cleaner place than before.
This commit is contained in:
Paul Melnikow
2019-08-21 08:25:03 +01:00
committed by GitHub
parent af81095794
commit 35ef9be434
7 changed files with 133 additions and 64 deletions

View File

@@ -1,16 +1,29 @@
import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import {
badgeUrlFromPath,
badgeUrlFromPattern,
staticBadgeUrl,
} from '../../core/badge-urls/make-badge-url'
import { examplePropType } from '../lib/service-definitions/example-prop-types'
import { removeRegexpFromPattern } from '../lib/pattern-helpers'
import {
ExampleSignature,
Example as ExampleData,
} from '../lib/service-definitions'
import { Badge } from './common'
import { StyledCode } from './snippet'
export interface SuggestionData {
title: string
link: string
example: ExampleSignature
preview: {
style?: string
}
}
type RenderableExampleData = ExampleData | SuggestionData
const ExampleTable = styled.table`
min-width: 50%;
margin: auto;
@@ -29,25 +42,40 @@ const ClickableCode = styled(StyledCode)`
cursor: pointer;
`
function Example({ baseUrl, onClick, exampleData }) {
const { title, example, preview, isBadgeSuggestion } = exampleData
const { pattern, namedParams, queryParams } = example
let exampleUrl
let previewUrl
function Example({
baseUrl,
onClick,
exampleData,
isBadgeSuggestion,
}: {
baseUrl?: string
onClick: (exampleData: RenderableExampleData) => void
exampleData: RenderableExampleData
isBadgeSuggestion: boolean
}) {
function handleClick() {
onClick(exampleData)
}
let exampleUrl, previewUrl
if (isBadgeSuggestion) {
exampleUrl = badgeUrlFromPattern({
const {
example: { pattern, namedParams, queryParams },
} = exampleData as SuggestionData
exampleUrl = previewUrl = badgeUrlFromPattern({
baseUrl,
pattern,
namedParams,
queryParams,
})
previewUrl = exampleUrl
} else {
const { label, message, color, style, namedLogo } = preview
const {
example: { pattern, queryParams },
preview: { label, message, color, style, namedLogo },
} = exampleData as ExampleData
previewUrl = staticBadgeUrl({
baseUrl,
label,
label: label || '',
message,
color,
style,
@@ -55,13 +83,11 @@ function Example({ baseUrl, onClick, exampleData }) {
})
exampleUrl = badgeUrlFromPath({
path: removeRegexpFromPattern(pattern),
namedParams,
queryParams,
})
}
const handleClick = () => onClick(exampleData)
const { title } = exampleData
return (
<tr>
<ClickableTh onClick={handleClick}>{title}:</ClickableTh>
@@ -74,13 +100,18 @@ function Example({ baseUrl, onClick, exampleData }) {
</tr>
)
}
Example.propTypes = {
exampleData: examplePropType.isRequired,
baseUrl: PropTypes.string,
onClick: PropTypes.func.isRequired,
}
export default function BadgeExamples({ examples, baseUrl, onClick }) {
export function BadgeExamples({
examples,
areBadgeSuggestions,
baseUrl,
onClick,
}: {
examples: RenderableExampleData[]
areBadgeSuggestions: boolean
baseUrl?: string
onClick: (exampleData: RenderableExampleData) => void
}) {
return (
<ExampleTable>
<tbody>
@@ -88,6 +119,7 @@ export default function BadgeExamples({ examples, baseUrl, onClick }) {
<Example
baseUrl={baseUrl}
exampleData={exampleData}
isBadgeSuggestion={areBadgeSuggestions}
key={`${exampleData.title} ${exampleData.example.pattern}`}
onClick={onClick}
/>
@@ -96,8 +128,3 @@ export default function BadgeExamples({ examples, baseUrl, onClick }) {
</ExampleTable>
)
}
BadgeExamples.propTypes = {
examples: PropTypes.arrayOf(examplePropType).isRequired,
baseUrl: PropTypes.string,
onClick: PropTypes.func.isRequired,
}

View File

@@ -59,6 +59,14 @@ const BadgeWrapper = styled.span<BadgeWrapperProps>`
`};
`
interface BadgeProps extends React.HTMLAttributes<HTMLImageElement> {
src: string
alt?: string
display?: 'inline' | 'block' | 'inline-block'
height?: string
clickable?: boolean
}
export function Badge({
src,
alt = '',
@@ -66,13 +74,7 @@ export function Badge({
height = '20px',
clickable = false,
...rest
}: {
src: string
alt?: string
display?: 'inline' | 'block' | 'inline-block'
height?: string
clickable?: boolean
}) {
}: BadgeProps) {
return (
<BadgeWrapper clickable={clickable} display={display} height={height}>
{src ? <img alt={alt} src={src} {...rest} /> : nonBreakingSpace}

View File

@@ -22,7 +22,7 @@ import {
CategoryHeadings,
CategoryNav,
} from './category-headings'
import BadgeExamples from './badge-examples'
import { BadgeExamples } from './badge-examples'
import { BaseFont, GlobalStyle } from './common'
const AppContainer = styled(BaseFont)`

View File

@@ -1,25 +1,48 @@
import React, { useRef, useState } from 'react'
import PropTypes from 'prop-types'
import React, { useRef, useState, ChangeEvent } from 'react'
import fetchPonyfill from 'fetch-ponyfill'
import debounce from 'lodash.debounce'
import BadgeExamples from './badge-examples'
import { BadgeExamples } from './badge-examples'
import { BlockInput } from './common'
interface SuggestionItem {
title: string
link: string
example: {
pattern: string
namedParams: { [k: string]: string }
queryParams?: { [k: string]: string }
}
preview:
| {
style?: string
}
| undefined
}
interface SuggestionResponse {
suggestions: SuggestionItem[]
}
export default function SuggestionAndSearch({
queryChanged,
onBadgeClick,
baseUrl,
}: {
queryChanged: (query: string) => void
onBadgeClick: () => void
baseUrl: string
}) {
const queryChangedDebounced = useRef(
debounce(queryChanged, 50, { leading: true })
)
const [isUrl, setIsUrl] = useState(false)
const [inProgress, setInProgress] = useState(false)
const [projectUrl, setProjectUrl] = useState(undefined)
const [suggestions, setSuggestions] = useState([])
const [projectUrl, setProjectUrl] = useState<string>()
const [suggestions, setSuggestions] = useState<SuggestionItem[]>([])
function onQueryChanged(event) {
const query = event.target.value
function onQueryChanged({
target: { value: query },
}: ChangeEvent<HTMLInputElement>) {
const isUrl = query.startsWith('https://') || query.startsWith('http://')
setIsUrl(isUrl)
setProjectUrl(isUrl ? query : undefined)
@@ -28,15 +51,20 @@ export default function SuggestionAndSearch({
}
async function getSuggestions() {
if (!projectUrl) {
setSuggestions([])
return
}
setInProgress(true)
const fetch = window.fetch || fetchPonyfill
const res = await fetch(
`${baseUrl}/$suggest/v1?url=${encodeURIComponent(projectUrl)}`
)
let suggestions
let suggestions = [] as SuggestionItem[]
try {
const json = await res.json()
const json = (await res.json()) as SuggestionResponse
// This doesn't validate the response. The default value here prevents
// a crash if the server returns {"err":"Disallowed"}.
suggestions = json.suggestions || []
@@ -54,18 +82,21 @@ export default function SuggestionAndSearch({
}
const transformed = suggestions.map(
({ title, link, example, preview, documentation }) => ({
({ title, link, example, preview }) => ({
title,
link,
example,
preview,
example: {
...example,
queryParams: example.queryParams || {},
},
preview: preview || {},
isBadgeSuggestion: true,
documentation,
})
)
return (
<BadgeExamples
areBadgeSuggestions
baseUrl={baseUrl}
examples={transformed}
onClick={onBadgeClick}
@@ -77,8 +108,8 @@ export default function SuggestionAndSearch({
<section>
<form action="javascript:void 0" autoComplete="off">
<BlockInput
autoComplete="off"
autoFocus
autofill="off"
onChange={onQueryChanged}
placeholder="search / project URL"
/>
@@ -91,8 +122,3 @@ export default function SuggestionAndSearch({
</section>
)
}
SuggestionAndSearch.propTypes = {
queryChanged: PropTypes.func.isRequired,
onBadgeClick: PropTypes.func.isRequired,
baseUrl: PropTypes.string.isRequired,
}