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:
@@ -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,
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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)`
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user