mirror of
https://github.com/badges/shields.git
synced 2025-04-18 19:44:04 +03:00
migrate frontend to docusaurus (#9014)
* delete loads of really important stuff that we definitely need * v basic MVP smoosh docusaurus PoC into repo * TODO * delete more really important stuff * TODO * tidyup: use run-s * don't redirect images used in frontend to raster proxy * fix routing * preserve the /endpoint link * delete the blog (for now) I would quite like to re-add this at some point but its not really the top priority thing right now * content edits * appease the lint gods * update danger rules * remove placeholder * cypress tests * dockerhub --> ghcr * Revert "dockerhub --> ghcr" This reverts commit ef74cbb26b1c24ce987a8975e60313682f9161f8. * downgrade lockfile format * implement defs/BASE_URL * fix e2e build * actually fix cypress tests * always run cypress tests on build * this never worked * add command for docusaurus:clear * delete more code we don't need any more * update ESLint/prettier config * delete unsused exports * documentation updates * delete a fairly large chunk of our dependency tree * allow base_url as build arg to Dockerfile * fixup dockerfile * work out base url at runtime if not set doing this at image build time is not the right approach * remove gatsby monorepo from closebot * rename HomepageFeatures to homepage-features
This commit is contained in:
parent
67d935492d
commit
50ea7068a8
@ -2,7 +2,6 @@ extends:
|
||||
- standard
|
||||
- standard-jsx
|
||||
- standard-react
|
||||
- plugin:@typescript-eslint/recommended
|
||||
- prettier
|
||||
- eslint:recommended
|
||||
|
||||
@ -18,7 +17,7 @@ settings:
|
||||
react:
|
||||
version: '16.8'
|
||||
jsdoc:
|
||||
mode: jsdoc
|
||||
mode: typescript
|
||||
|
||||
plugins:
|
||||
- chai-friendly
|
||||
@ -37,39 +36,33 @@ overrides:
|
||||
# rules listed here are only ones which conflict.
|
||||
|
||||
- files:
|
||||
- '**/*.js'
|
||||
- '!frontend/**/*.js'
|
||||
- 'badge-maker/**/*.js'
|
||||
- '**/*.cjs'
|
||||
env:
|
||||
node: true
|
||||
es6: true
|
||||
|
||||
- files:
|
||||
- '**/*.js'
|
||||
- '!frontend/**/*.js'
|
||||
- '!badge-maker/**/*.js'
|
||||
env:
|
||||
node: true
|
||||
es6: true
|
||||
parserOptions:
|
||||
sourceType: 'module'
|
||||
parser: '@typescript-eslint/parser'
|
||||
rules:
|
||||
no-console: 'off'
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off'
|
||||
|
||||
- files:
|
||||
- '**/*.@(ts|tsx)'
|
||||
- '**/*.ts'
|
||||
parserOptions:
|
||||
sourceType: 'module'
|
||||
parser: '@typescript-eslint/parser'
|
||||
rules:
|
||||
# Argh.
|
||||
'@typescript-eslint/explicit-function-return-type':
|
||||
['error', { 'allowExpressions': true }]
|
||||
'@typescript-eslint/no-empty-function': 'error'
|
||||
'@typescript-eslint/no-var-requires': 'error'
|
||||
'@typescript-eslint/no-object-literal-type-assertion': 'off'
|
||||
'@typescript-eslint/no-explicit-any': 'error'
|
||||
'@typescript-eslint/ban-ts-ignore': 'off'
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off'
|
||||
|
||||
- files:
|
||||
- core/**/*.ts
|
||||
parserOptions:
|
||||
sourceType: 'module'
|
||||
parser: '@typescript-eslint/parser'
|
||||
- files:
|
||||
- gatsby-browser.js
|
||||
- 'frontend/**/*.@(js|ts|tsx)'
|
||||
- 'frontend/**/*.js'
|
||||
parserOptions:
|
||||
sourceType: 'module'
|
||||
env:
|
||||
@ -128,14 +121,6 @@ rules:
|
||||
# Disable some rules from eslint:recommended.
|
||||
no-empty: ['error', { 'allowEmptyCatch': true }]
|
||||
|
||||
# Allow unused parameters. In callbacks, removing them seems to obscure
|
||||
# what the functions are doing.
|
||||
'@typescript-eslint/no-unused-vars': ['error', { 'args': 'none' }]
|
||||
no-unused-vars: 'off'
|
||||
|
||||
'@typescript-eslint/no-var-requires': 'off'
|
||||
|
||||
'@typescript-eslint/no-use-before-define': 'error'
|
||||
no-use-before-define: 'off'
|
||||
|
||||
# These should be disabled by eslint-config-prettier, but are not.
|
||||
@ -197,11 +182,7 @@ rules:
|
||||
jsdoc/require-returns-type: 'error'
|
||||
jsdoc/valid-types: 'error'
|
||||
|
||||
# Disable some from TypeScript.
|
||||
'@typescript-eslint/camelcase': off
|
||||
'@typescript-eslint/explicit-function-return-type': 'off'
|
||||
'@typescript-eslint/no-empty-function': 'off'
|
||||
|
||||
react/prop-types: 'off'
|
||||
react/jsx-sort-props: 'error'
|
||||
react-hooks/rules-of-hooks: 'error'
|
||||
react-hooks/exhaustive-deps: 'error'
|
||||
|
1
.github/actions/close-bot/helpers.js
vendored
1
.github/actions/close-bot/helpers.js
vendored
@ -35,7 +35,6 @@ function allChangelogLinesAreVersionBump(changelogLines) {
|
||||
|
||||
function isPointlessVersionBump(body) {
|
||||
const pointlessBumpLinks = [
|
||||
'https://github.com/gatsbyjs/gatsby',
|
||||
'https://github.com/typescript-eslint/typescript-eslint',
|
||||
]
|
||||
|
||||
|
31
.github/actions/frontend-tests/action.yml
vendored
31
.github/actions/frontend-tests/action.yml
vendored
@ -1,31 +0,0 @@
|
||||
name: 'Frontend tests'
|
||||
description: 'Run frontend tests and check types'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Prepare frontend tests
|
||||
if: always()
|
||||
run: npm run defs && npm run features
|
||||
shell: bash
|
||||
|
||||
- name: Tests
|
||||
if: always()
|
||||
run: npm run test:frontend -- --reporter json --reporter-option 'output=reports/frontend-tests.json'
|
||||
shell: bash
|
||||
|
||||
- name: Type Checks
|
||||
if: always()
|
||||
run: |
|
||||
set -o pipefail
|
||||
npm run check-types:frontend 2>&1 | tee reports/frontend-types.txt
|
||||
shell: bash
|
||||
|
||||
- name: Write Markdown Summary
|
||||
if: always()
|
||||
run: |
|
||||
node scripts/mocha2md.js 'Frontend Tests' reports/frontend-tests.json >> $GITHUB_STEP_SUMMARY
|
||||
echo '# Frontend Types' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
cat reports/frontend-types.txt >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
shell: bash
|
5
.github/workflows/test-e2e.yml
vendored
5
.github/workflows/test-e2e.yml
vendored
@ -29,13 +29,10 @@ jobs:
|
||||
node-version: 16
|
||||
cypress: true
|
||||
|
||||
- name: Frontend build
|
||||
run: GATSBY_BASE_URL=http://localhost:8080 npm run build
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: npm run e2e-on-build
|
||||
run: npm run e2e
|
||||
|
||||
- name: Archive videos
|
||||
if: always()
|
||||
|
26
.github/workflows/test-frontend.yml
vendored
26
.github/workflows/test-frontend.yml
vendored
@ -1,26 +0,0 @@
|
||||
name: Frontend
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
jobs:
|
||||
test-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Frontend tests
|
||||
uses: ./.github/actions/frontend-tests
|
||||
|
||||
- name: Frontend build
|
||||
run: npm run build
|
14
.gitignore
vendored
14
.gitignore
vendored
@ -92,10 +92,6 @@ typings/
|
||||
|
||||
# Temporary build artifacts.
|
||||
/build
|
||||
.next
|
||||
badge-examples.json
|
||||
supported-features.json
|
||||
service-definitions.yml
|
||||
frontend/categories/*.yaml
|
||||
|
||||
# Local runtime configuration.
|
||||
@ -104,11 +100,6 @@ frontend/categories/*.yaml
|
||||
# Template for the local runtime configuration.
|
||||
!/config/local*.template.yml
|
||||
|
||||
# Gatsby
|
||||
/frontend/.cache
|
||||
/frontend/public
|
||||
/public
|
||||
|
||||
# Cypress
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
@ -121,3 +112,8 @@ flamegraph.html
|
||||
|
||||
# config file for node-pg-migrate
|
||||
migrations-config.json
|
||||
|
||||
# Frontend/Docusaurus
|
||||
frontend/.docusaurus
|
||||
frontend/.cache-loader
|
||||
/public
|
||||
|
@ -1,5 +0,0 @@
|
||||
reporter: mocha-env-reporter
|
||||
require:
|
||||
- '@babel/polyfill'
|
||||
- '@babel/register'
|
||||
- mocha-yaml-loader
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"reporter": ["lcov"],
|
||||
"all": false,
|
||||
"silent": true,
|
||||
"clean": false,
|
||||
"sourceMap": false,
|
||||
"instrument": false,
|
||||
"include": ["frontend/**/*.js"],
|
||||
"exclude": ["**/*.spec.js", "**/mocha-*.js"]
|
||||
}
|
@ -10,5 +10,5 @@ public
|
||||
private/*.json
|
||||
/.nyc_output
|
||||
analytics.json
|
||||
supported-features.json
|
||||
service-definitions.yml
|
||||
frontend/.docusaurus
|
||||
frontend/categories
|
||||
|
@ -107,7 +107,7 @@ You can read a [tutorial on how to add a badge][tutorial].
|
||||
|
||||
When server source files change, the badge server should automatically restart
|
||||
itself (using [nodemon][]). When the frontend files change, the frontend dev
|
||||
server (`gatsby dev`) should also automatically reload. However the badge
|
||||
server (`docusaurus start`) should also automatically reload. However the badge
|
||||
definitions are built only before the server first starts. To regenerate those,
|
||||
either run `npm run defs` or manually restart the server.
|
||||
|
||||
|
94
core/badge-urls/make-badge-url.d.ts
vendored
94
core/badge-urls/make-badge-url.d.ts
vendored
@ -1,94 +0,0 @@
|
||||
export function badgeUrlFromPath({
|
||||
baseUrl,
|
||||
path,
|
||||
queryParams,
|
||||
style,
|
||||
format,
|
||||
longCache,
|
||||
}: {
|
||||
baseUrl?: string
|
||||
path: string
|
||||
queryParams: { [k: string]: string | number | boolean }
|
||||
style?: string
|
||||
format?: string
|
||||
longCache?: boolean
|
||||
}): string
|
||||
|
||||
export function encodeField(s: string): string
|
||||
|
||||
export function staticBadgeUrl({
|
||||
baseUrl,
|
||||
label,
|
||||
message,
|
||||
labelColor,
|
||||
color,
|
||||
style,
|
||||
namedLogo,
|
||||
format,
|
||||
links,
|
||||
}: {
|
||||
baseUrl?: string
|
||||
label: string
|
||||
message: string
|
||||
labelColor?: string
|
||||
color?: string
|
||||
style?: string
|
||||
namedLogo?: string
|
||||
format?: string
|
||||
links?: string[]
|
||||
}): string
|
||||
|
||||
export function queryStringStaticBadgeUrl({
|
||||
baseUrl,
|
||||
label,
|
||||
message,
|
||||
color,
|
||||
labelColor,
|
||||
style,
|
||||
namedLogo,
|
||||
logoColor,
|
||||
logoWidth,
|
||||
logoPosition,
|
||||
format,
|
||||
}: {
|
||||
baseUrl?: string
|
||||
label: string
|
||||
message: string
|
||||
color?: string
|
||||
labelColor?: string
|
||||
style?: string
|
||||
namedLogo?: string
|
||||
logoColor?: string
|
||||
logoWidth?: number
|
||||
logoPosition?: number
|
||||
format?: string
|
||||
}): string
|
||||
|
||||
export function dynamicBadgeUrl({
|
||||
baseUrl,
|
||||
datatype,
|
||||
label,
|
||||
dataUrl,
|
||||
query,
|
||||
prefix,
|
||||
suffix,
|
||||
color,
|
||||
style,
|
||||
format,
|
||||
}: {
|
||||
baseUrl?: string
|
||||
datatype: string
|
||||
label: string
|
||||
dataUrl: string
|
||||
query: string
|
||||
prefix: string
|
||||
suffix: string
|
||||
color?: string
|
||||
style?: string
|
||||
format?: string
|
||||
}): string
|
||||
|
||||
export function rasterRedirectUrl(
|
||||
{ rasterUrl }: { rasterUrl: string },
|
||||
badgeUrl: string
|
||||
): string
|
@ -1,119 +1,5 @@
|
||||
// Avoid "Attempted import error: 'URL' is not exported from 'url'" in frontend.
|
||||
import url from 'url'
|
||||
import queryString from 'query-string'
|
||||
|
||||
function badgeUrlFromPath({
|
||||
baseUrl = '',
|
||||
path,
|
||||
queryParams,
|
||||
style,
|
||||
format = '',
|
||||
longCache = false,
|
||||
}) {
|
||||
const outExt = format.length ? `.${format}` : ''
|
||||
|
||||
const outQueryString = queryString.stringify({
|
||||
cacheSeconds: longCache ? '2592000' : undefined,
|
||||
style,
|
||||
...queryParams,
|
||||
})
|
||||
const suffix = outQueryString ? `?${outQueryString}` : ''
|
||||
|
||||
return `${baseUrl}${path}${outExt}${suffix}`
|
||||
}
|
||||
|
||||
function encodeField(s) {
|
||||
return encodeURIComponent(s.replace(/-/g, '--').replace(/_/g, '__'))
|
||||
}
|
||||
|
||||
function staticBadgeUrl({
|
||||
baseUrl = '',
|
||||
label,
|
||||
message,
|
||||
labelColor,
|
||||
color = 'lightgray',
|
||||
style,
|
||||
namedLogo,
|
||||
format = '',
|
||||
links = [],
|
||||
}) {
|
||||
const path = [label, message, color].map(encodeField).join('-')
|
||||
const outQueryString = queryString.stringify({
|
||||
labelColor,
|
||||
style,
|
||||
logo: namedLogo,
|
||||
link: links,
|
||||
})
|
||||
const outExt = format.length ? `.${format}` : ''
|
||||
const suffix = outQueryString ? `?${outQueryString}` : ''
|
||||
return `${baseUrl}/badge/${path}${outExt}${suffix}`
|
||||
}
|
||||
|
||||
function queryStringStaticBadgeUrl({
|
||||
baseUrl = '',
|
||||
label,
|
||||
message,
|
||||
color,
|
||||
labelColor,
|
||||
style,
|
||||
namedLogo,
|
||||
logoColor,
|
||||
logoWidth,
|
||||
logoPosition,
|
||||
format = '',
|
||||
}) {
|
||||
// schemaVersion could be a parameter if we iterate on it,
|
||||
// for now it's hardcoded to the only supported version.
|
||||
const schemaVersion = '1'
|
||||
const suffix = `?${queryString.stringify({
|
||||
label,
|
||||
message,
|
||||
color,
|
||||
labelColor,
|
||||
style,
|
||||
logo: namedLogo,
|
||||
logoColor,
|
||||
logoWidth,
|
||||
logoPosition,
|
||||
})}`
|
||||
const outExt = format.length ? `.${format}` : ''
|
||||
return `${baseUrl}/static/v${schemaVersion}${outExt}${suffix}`
|
||||
}
|
||||
|
||||
function dynamicBadgeUrl({
|
||||
baseUrl,
|
||||
datatype,
|
||||
label,
|
||||
dataUrl,
|
||||
query,
|
||||
prefix,
|
||||
suffix,
|
||||
color,
|
||||
style,
|
||||
format = '',
|
||||
}) {
|
||||
const outExt = format.length ? `.${format}` : ''
|
||||
|
||||
const queryParams = {
|
||||
label,
|
||||
url: dataUrl,
|
||||
query,
|
||||
style,
|
||||
}
|
||||
|
||||
if (color) {
|
||||
queryParams.color = color
|
||||
}
|
||||
if (prefix) {
|
||||
queryParams.prefix = prefix
|
||||
}
|
||||
if (suffix) {
|
||||
queryParams.suffix = suffix
|
||||
}
|
||||
|
||||
const outQueryString = queryString.stringify(queryParams)
|
||||
return `${baseUrl}/badge/dynamic/${datatype}${outExt}?${outQueryString}`
|
||||
}
|
||||
|
||||
function rasterRedirectUrl({ rasterUrl }, badgeUrl) {
|
||||
// Ensure we're always using the `rasterUrl` by using just the path from
|
||||
@ -124,11 +10,4 @@ function rasterRedirectUrl({ rasterUrl }, badgeUrl) {
|
||||
return result
|
||||
}
|
||||
|
||||
export {
|
||||
badgeUrlFromPath,
|
||||
encodeField,
|
||||
staticBadgeUrl,
|
||||
queryStringStaticBadgeUrl,
|
||||
dynamicBadgeUrl,
|
||||
rasterRedirectUrl,
|
||||
}
|
||||
export { rasterRedirectUrl }
|
||||
|
@ -1,142 +0,0 @@
|
||||
import { test, given } from 'sazerac'
|
||||
import {
|
||||
badgeUrlFromPath,
|
||||
encodeField,
|
||||
staticBadgeUrl,
|
||||
queryStringStaticBadgeUrl,
|
||||
dynamicBadgeUrl,
|
||||
} from './make-badge-url.js'
|
||||
|
||||
describe('Badge URL generation functions', function () {
|
||||
test(badgeUrlFromPath, () => {
|
||||
given({
|
||||
baseUrl: 'http://example.com',
|
||||
path: '/npm/v/gh-badges',
|
||||
style: 'flat-square',
|
||||
longCache: true,
|
||||
}).expect(
|
||||
'http://example.com/npm/v/gh-badges?cacheSeconds=2592000&style=flat-square'
|
||||
)
|
||||
})
|
||||
|
||||
test(encodeField, () => {
|
||||
given('foo').expect('foo')
|
||||
given('').expect('')
|
||||
given('happy go lucky').expect('happy%20go%20lucky')
|
||||
given('do-right').expect('do--right')
|
||||
given('it_is_a_snake').expect('it__is__a__snake')
|
||||
})
|
||||
|
||||
test(staticBadgeUrl, () => {
|
||||
given({
|
||||
label: 'foo',
|
||||
message: 'bar',
|
||||
color: 'blue',
|
||||
style: 'flat-square',
|
||||
}).expect('/badge/foo-bar-blue?style=flat-square')
|
||||
given({
|
||||
label: 'foo',
|
||||
message: 'bar',
|
||||
color: 'blue',
|
||||
style: 'flat-square',
|
||||
format: 'png',
|
||||
namedLogo: 'github',
|
||||
}).expect('/badge/foo-bar-blue.png?logo=github&style=flat-square')
|
||||
given({
|
||||
label: 'Hello World',
|
||||
message: 'Привет Мир',
|
||||
color: '#aabbcc',
|
||||
}).expect(
|
||||
'/badge/Hello%20World-%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80-%23aabbcc'
|
||||
)
|
||||
given({
|
||||
label: '123-123',
|
||||
message: 'abc-abc',
|
||||
color: 'blue',
|
||||
}).expect('/badge/123--123-abc--abc-blue')
|
||||
given({
|
||||
label: '123-123',
|
||||
message: '',
|
||||
color: 'blue',
|
||||
style: 'social',
|
||||
}).expect('/badge/123--123--blue?style=social')
|
||||
given({
|
||||
label: '',
|
||||
message: 'blue',
|
||||
color: 'blue',
|
||||
}).expect('/badge/-blue-blue')
|
||||
})
|
||||
|
||||
test(queryStringStaticBadgeUrl, () => {
|
||||
// the query-string library sorts parameters by name
|
||||
given({
|
||||
label: 'foo',
|
||||
message: 'bar',
|
||||
color: 'blue',
|
||||
style: 'flat-square',
|
||||
}).expect('/static/v1?color=blue&label=foo&message=bar&style=flat-square')
|
||||
given({
|
||||
label: 'foo Bar',
|
||||
message: 'bar Baz',
|
||||
color: 'blue',
|
||||
style: 'flat-square',
|
||||
format: 'png',
|
||||
namedLogo: 'github',
|
||||
}).expect(
|
||||
'/static/v1.png?color=blue&label=foo%20Bar&logo=github&message=bar%20Baz&style=flat-square'
|
||||
)
|
||||
given({
|
||||
label: 'Hello World',
|
||||
message: 'Привет Мир',
|
||||
color: '#aabbcc',
|
||||
}).expect(
|
||||
'/static/v1?color=%23aabbcc&label=Hello%20World&message=%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80'
|
||||
)
|
||||
})
|
||||
|
||||
test(dynamicBadgeUrl, () => {
|
||||
const dataUrl = 'http://example.com/foo.json'
|
||||
const query = '$.bar'
|
||||
const prefix = 'value: '
|
||||
given({
|
||||
baseUrl: 'http://img.example.com',
|
||||
datatype: 'json',
|
||||
label: 'foo',
|
||||
dataUrl,
|
||||
query,
|
||||
prefix,
|
||||
style: 'plastic',
|
||||
}).expect(
|
||||
[
|
||||
'http://img.example.com/badge/dynamic/json',
|
||||
'?label=foo',
|
||||
`&prefix=${encodeURIComponent(prefix)}`,
|
||||
`&query=${encodeURIComponent(query)}`,
|
||||
'&style=plastic',
|
||||
`&url=${encodeURIComponent(dataUrl)}`,
|
||||
].join('')
|
||||
)
|
||||
const suffix = '<- value'
|
||||
const color = 'blue'
|
||||
given({
|
||||
baseUrl: 'http://img.example.com',
|
||||
datatype: 'json',
|
||||
label: 'foo',
|
||||
dataUrl,
|
||||
query,
|
||||
suffix,
|
||||
color,
|
||||
style: 'plastic',
|
||||
}).expect(
|
||||
[
|
||||
'http://img.example.com/badge/dynamic/json',
|
||||
'?color=blue',
|
||||
'&label=foo',
|
||||
`&query=${encodeURIComponent(query)}`,
|
||||
'&style=plastic',
|
||||
`&suffix=${encodeURIComponent(suffix)}`,
|
||||
`&url=${encodeURIComponent(dataUrl)}`,
|
||||
].join('')
|
||||
)
|
||||
})
|
||||
})
|
@ -65,7 +65,7 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
|
||||
*/
|
||||
if (match[0] === '/endpoint' && Object.keys(queryParams).length === 0) {
|
||||
ask.res.statusCode = 301
|
||||
ask.res.setHeader('Location', '/endpoint/')
|
||||
ask.res.setHeader('Location', '/badges/endpoint-badge')
|
||||
ask.res.end()
|
||||
return
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
const baseUrl = process.env.BASE_URL || 'https://img.shields.io'
|
||||
const baseUrl = process.env.BASE_URL
|
||||
const globalParamRefs = [
|
||||
{ $ref: '#/components/parameters/style' },
|
||||
{ $ref: '#/components/parameters/logo' },
|
||||
@ -228,7 +228,7 @@ function category2openapi(category, services) {
|
||||
name: 'CC0',
|
||||
},
|
||||
},
|
||||
servers: [{ url: baseUrl }],
|
||||
servers: baseUrl ? [{ url: baseUrl }] : undefined,
|
||||
components: {
|
||||
parameters: {
|
||||
style: {
|
||||
|
@ -76,7 +76,6 @@ class LegacyService extends BaseJsonService {
|
||||
const expected = {
|
||||
openapi: '3.0.0',
|
||||
info: { version: '1.0.0', title: 'build', license: { name: 'CC0' } },
|
||||
servers: [{ url: 'https://img.shields.io' }],
|
||||
components: {
|
||||
parameters: {
|
||||
style: {
|
||||
|
@ -1,8 +1,5 @@
|
||||
import Joi from 'joi'
|
||||
|
||||
// This should be kept in sync with the schema in
|
||||
// `frontend/lib/service-definitions/index.ts`.
|
||||
|
||||
const arrayOfStrings = Joi.array().items(Joi.string()).min(0).required()
|
||||
|
||||
const objectOfKeyValues = Joi.object()
|
||||
@ -92,9 +89,4 @@ function assertValidServiceDefinitionExport(examples, message = undefined) {
|
||||
Joi.assert(examples, serviceDefinitionExport, message)
|
||||
}
|
||||
|
||||
export {
|
||||
serviceDefinition,
|
||||
assertValidServiceDefinition,
|
||||
serviceDefinitionExport,
|
||||
assertValidServiceDefinitionExport,
|
||||
}
|
||||
export { assertValidServiceDefinition, assertValidServiceDefinitionExport }
|
||||
|
@ -362,7 +362,7 @@ class Server {
|
||||
})
|
||||
|
||||
if (!rasterUrl) {
|
||||
camp.route(/\.png$/, (query, match, end, request) => {
|
||||
camp.route(/^\/((?!img\/)).*\.png$/, (query, match, end, request) => {
|
||||
makeSend(
|
||||
'svg',
|
||||
request.res,
|
||||
@ -412,7 +412,7 @@ class Server {
|
||||
|
||||
if (rasterUrl) {
|
||||
// Redirect to the raster server for raster versions of modern badges.
|
||||
camp.route(/\.png$/, (queryParams, match, end, ask) => {
|
||||
camp.route(/^\/((?!img\/)).*\.png$/, (queryParams, match, end, ask) => {
|
||||
ask.res.statusCode = 301
|
||||
ask.res.setHeader(
|
||||
'Location',
|
||||
|
@ -98,6 +98,11 @@ describe('The server', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('should not redirect for PNG requests in /img', async function () {
|
||||
const { statusCode } = await got(`${baseUrl}img/frontend-image.png`)
|
||||
expect(statusCode).to.equal(200)
|
||||
})
|
||||
|
||||
it('should produce SVG badges with expected headers', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}:fruit-apple-green.svg`
|
||||
|
BIN
core/server/test-public/img/frontend-image.png
Normal file
BIN
core/server/test-public/img/frontend-image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
@ -2,16 +2,9 @@ import { registerCommand } from 'cypress-wait-for-stable-dom'
|
||||
|
||||
registerCommand()
|
||||
|
||||
describe('Main page', function () {
|
||||
describe('Frontend', function () {
|
||||
const backendUrl = Cypress.env('backend_url')
|
||||
const SEARCH_INPUT = 'input[placeholder="search"]'
|
||||
|
||||
function expectBadgeExample(title, previewUrl, pattern) {
|
||||
cy.contains('tr', `${title}:`).find('code').should('have.text', pattern)
|
||||
cy.contains('tr', `${title}:`)
|
||||
.find('img')
|
||||
.should('have.attr', 'src', previewUrl)
|
||||
}
|
||||
const SEARCH_INPUT = 'input[placeholder="Search"]'
|
||||
|
||||
function visitAndWait(page) {
|
||||
cy.visit(page)
|
||||
@ -26,36 +19,37 @@ describe('Main page', function () {
|
||||
cy.contains('PyPI - License')
|
||||
})
|
||||
|
||||
it('Shows badge from category', function () {
|
||||
visitAndWait('/category/chat')
|
||||
it('Shows badges from category', function () {
|
||||
visitAndWait('/badges')
|
||||
|
||||
expectBadgeExample(
|
||||
'Discourse status',
|
||||
'http://localhost:8080/badge/discourse-online-brightgreen',
|
||||
'/discourse/status?server=https%3A%2F%2Fmeta.discourse.org'
|
||||
)
|
||||
cy.contains('Build')
|
||||
cy.contains('Chat').click()
|
||||
|
||||
cy.contains('Discourse status')
|
||||
cy.contains('Stack Exchange questions')
|
||||
})
|
||||
|
||||
it('Customizate badges', function () {
|
||||
visitAndWait('/')
|
||||
it('Shows expected code examples', function () {
|
||||
visitAndWait('/badges/static-badge')
|
||||
|
||||
cy.get(SEARCH_INPUT).type('issues')
|
||||
|
||||
cy.contains('/github/issues/:user/:repo').click()
|
||||
|
||||
cy.get('input[name="user"]').type('badges')
|
||||
cy.get('input[name="repo"]').type('shields')
|
||||
cy.get('table input[name="color"]').type('orange')
|
||||
|
||||
cy.get(`img[src='${backendUrl}/github/issues/badges/shields?color=orange']`)
|
||||
cy.contains('button', 'URL').should('have.class', 'api-code-tab')
|
||||
cy.contains('button', 'Markdown').should('have.class', 'api-code-tab')
|
||||
cy.contains('button', 'rSt').should('have.class', 'api-code-tab')
|
||||
cy.contains('button', 'AsciiDoc').should('have.class', 'api-code-tab')
|
||||
cy.contains('button', 'HTML').should('have.class', 'api-code-tab')
|
||||
})
|
||||
|
||||
it('Do not duplicate example parameters', function () {
|
||||
visitAndWait('/category/funding')
|
||||
it('Build a badge', function () {
|
||||
visitAndWait('/badges/git-hub-issues')
|
||||
|
||||
cy.contains('GitHub Sponsors').click()
|
||||
cy.get('[name="style"]').should($style => {
|
||||
expect($style).to.have.length(1)
|
||||
})
|
||||
cy.contains('/github/issues/:user/:repo')
|
||||
|
||||
cy.get('input[placeholder="user"]').type('badges')
|
||||
cy.get('input[placeholder="repo"]').type('shields')
|
||||
|
||||
cy.intercept('GET', `${backendUrl}/github/issues/badges/shields`).as('get')
|
||||
cy.contains('Execute').click()
|
||||
cy.wait('@get').its('response.statusCode').should('eq', 200)
|
||||
cy.get('img[id="badge-preview"]')
|
||||
})
|
||||
})
|
||||
|
@ -15,8 +15,8 @@ const { fileMatch } = danger.git
|
||||
|
||||
const documentation = fileMatch(
|
||||
'**/*.md',
|
||||
'frontend/components/usage.tsx',
|
||||
'frontend/pages/endpoint.tsx'
|
||||
'frontend/docs/**',
|
||||
'frontend/src/**'
|
||||
)
|
||||
const server = fileMatch('core/server/**.js', '!*.spec.js')
|
||||
const serverTests = fileMatch('core/server/**.spec.js')
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
The Shields codebase is divided into several parts:
|
||||
|
||||
1. The frontend (about 7% of the code)
|
||||
1. The frontend
|
||||
1. [`frontend`][frontend]
|
||||
2. The badge renderer (which is available as an npm package)
|
||||
1. [`badge-maker`][badge-maker]
|
||||
@ -30,16 +30,16 @@ The Shields codebase is divided into several parts:
|
||||
|
||||
The tests are also divided into several parts:
|
||||
|
||||
1. Unit and functional tests of the frontend
|
||||
1. `frontend/**/*.spec.js`
|
||||
2. Unit and functional tests of the badge renderer
|
||||
1. Unit and functional tests of the badge renderer
|
||||
1. `badge-maker/**/*.spec.js`
|
||||
3. Unit and functional tests of the core code
|
||||
2. Unit and functional tests of the core code
|
||||
1. `core/**/*.spec.js`
|
||||
4. Unit and functional tests of the service helper functions
|
||||
3. Unit and functional tests of the service helper functions
|
||||
1. `services/*.spec.js`
|
||||
5. Unit and functional tests of the service code (we have only a few of these)
|
||||
4. Unit and functional tests of the service code (we have only a few of these)
|
||||
1. `services/*/**/*.spec.js`
|
||||
5. End-to-end tests for the frontend
|
||||
1. `cypress/e2e/*.cy.js`
|
||||
6. The service tester and service test runner
|
||||
1. [`core/service-test-runner`][service-test-runner]
|
||||
7. [The service tests themselves][service tests] live integration tests of the
|
||||
|
@ -155,13 +155,13 @@ These are documented in [server-secrets.md](./server-secrets.md)
|
||||
If you want to host the frontend on a separate server, such as cloud storage
|
||||
or a CDN, you can do that.
|
||||
|
||||
First, build the frontend, pointing `GATSBY_BASE_URL` to your server.
|
||||
First, build the frontend, pointing `BASE_URL` to your server.
|
||||
|
||||
```sh
|
||||
GATSBY_BASE_URL=https://your-server.example.com npm run build
|
||||
BASE_URL=https://your-server.example.com npm run build
|
||||
```
|
||||
|
||||
Then copy the contents of the `build/` folder to your static hosting / CDN.
|
||||
Then copy the contents of the `public/` folder to your static hosting / CDN.
|
||||
|
||||
There are also a couple settings you should configure on the server.
|
||||
|
||||
|
3
frontend/babel.config.cjs
Normal file
3
frontend/babel.config.cjs
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {
|
||||
badgeUrlFromPath,
|
||||
staticBadgeUrl,
|
||||
} from '../../core/badge-urls/make-badge-url'
|
||||
import { removeRegexpFromPattern } from '../lib/pattern-helpers'
|
||||
import {
|
||||
Example as ExampleData,
|
||||
RenderableExample,
|
||||
} from '../lib/service-definitions'
|
||||
import { Badge } from './common'
|
||||
import { StyledCode } from './snippet'
|
||||
|
||||
const ExampleTable = styled.table`
|
||||
min-width: 50%;
|
||||
margin: auto;
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
}
|
||||
`
|
||||
|
||||
const ClickableTh = styled.th`
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const ClickableCode = styled(StyledCode)`
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
function Example({
|
||||
baseUrl,
|
||||
onClick,
|
||||
exampleData,
|
||||
}: {
|
||||
baseUrl?: string
|
||||
onClick: (example: RenderableExample) => void
|
||||
exampleData: RenderableExample
|
||||
}): JSX.Element {
|
||||
const handleClick = React.useCallback(
|
||||
function (): void {
|
||||
onClick(exampleData)
|
||||
},
|
||||
[exampleData, onClick]
|
||||
)
|
||||
|
||||
const {
|
||||
example: { pattern, queryParams },
|
||||
preview: { label, message, color, style, namedLogo },
|
||||
} = exampleData as ExampleData
|
||||
const previewUrl = staticBadgeUrl({
|
||||
baseUrl,
|
||||
label: label || '',
|
||||
message,
|
||||
color,
|
||||
style,
|
||||
namedLogo,
|
||||
})
|
||||
const exampleUrl = badgeUrlFromPath({
|
||||
path: removeRegexpFromPattern(pattern),
|
||||
queryParams,
|
||||
})
|
||||
|
||||
const { title } = exampleData
|
||||
return (
|
||||
<tr>
|
||||
<ClickableTh onClick={handleClick}>{title}:</ClickableTh>
|
||||
<td>
|
||||
<Badge
|
||||
alt={`${title} badge`}
|
||||
clickable
|
||||
onClick={handleClick}
|
||||
src={previewUrl}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<ClickableCode onClick={handleClick}>{exampleUrl}</ClickableCode>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
export function BadgeExamples({
|
||||
examples,
|
||||
baseUrl,
|
||||
onClick,
|
||||
}: {
|
||||
examples: RenderableExample[]
|
||||
baseUrl?: string
|
||||
onClick: (exampleData: RenderableExample) => void
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<ExampleTable>
|
||||
<tbody>
|
||||
{examples.map(exampleData => (
|
||||
<Example
|
||||
baseUrl={baseUrl}
|
||||
exampleData={exampleData}
|
||||
key={`${exampleData.title} ${exampleData.example.pattern}`}
|
||||
onClick={onClick}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</ExampleTable>
|
||||
)
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Link } from 'gatsby'
|
||||
import { H3 } from './common'
|
||||
|
||||
export interface Category {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export function CategoryHeading({
|
||||
category: { id, name },
|
||||
}: {
|
||||
category: Category
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<Link to={`/category/${id}`}>
|
||||
<H3 id={id}>{name}</H3>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function CategoryHeadings({
|
||||
categories,
|
||||
}: {
|
||||
categories: Category[]
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
{categories.map(category => (
|
||||
<CategoryHeading category={category} key={category.id} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledNav = styled.nav`
|
||||
ul {
|
||||
display: flex;
|
||||
|
||||
min-width: 50%;
|
||||
max-width: 500px;
|
||||
|
||||
margin: 0 auto 20px;
|
||||
padding-inline-start: 0;
|
||||
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
ul {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 4px 10px;
|
||||
}
|
||||
|
||||
.active {
|
||||
font-weight: 900;
|
||||
}
|
||||
`
|
||||
|
||||
export function CategoryNav({
|
||||
categories,
|
||||
}: {
|
||||
categories: Category[]
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<StyledNav>
|
||||
<ul>
|
||||
{categories.map(({ id, name }) => (
|
||||
<li key={id}>
|
||||
<Link to={`/category/${id}`}>{name}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</StyledNav>
|
||||
)
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled, { css, createGlobalStyle } from 'styled-components'
|
||||
|
||||
export const noAutocorrect = Object.freeze({
|
||||
autoComplete: 'off',
|
||||
autoCorrect: 'off',
|
||||
autoCapitalize: 'off',
|
||||
spellcheck: 'false',
|
||||
})
|
||||
|
||||
export const nonBreakingSpace = '\u00a0'
|
||||
|
||||
export const GlobalStyle = createGlobalStyle`
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`
|
||||
|
||||
export const BaseFont = styled.div`
|
||||
font-family: Lekton, sans-serif;
|
||||
color: #534;
|
||||
`
|
||||
|
||||
export const H2 = styled.h2`
|
||||
font-style: italic;
|
||||
|
||||
margin-top: 12mm;
|
||||
font-variant: small-caps;
|
||||
|
||||
::before {
|
||||
content: '☙ ';
|
||||
}
|
||||
|
||||
::after {
|
||||
content: ' ❧';
|
||||
}
|
||||
`
|
||||
|
||||
export const H3 = styled.h3`
|
||||
font-style: italic;
|
||||
`
|
||||
|
||||
interface BadgeWrapperProps {
|
||||
height: string
|
||||
display: string
|
||||
clickable: boolean
|
||||
}
|
||||
|
||||
const BadgeWrapper = styled.span<BadgeWrapperProps>`
|
||||
padding: 2px;
|
||||
height: ${({ height }) => height};
|
||||
vertical-align: middle;
|
||||
display: ${({ display }) => display};
|
||||
|
||||
${({ clickable }) =>
|
||||
clickable &&
|
||||
css`
|
||||
cursor: pointer;
|
||||
`};
|
||||
`
|
||||
|
||||
interface BadgeProps extends React.HTMLAttributes<HTMLImageElement> {
|
||||
src: string
|
||||
alt?: string
|
||||
display?: 'inline' | 'block' | 'inline-block'
|
||||
height?: string
|
||||
clickable?: boolean
|
||||
object?: boolean
|
||||
}
|
||||
|
||||
export function Badge({
|
||||
src,
|
||||
alt = '',
|
||||
display = 'inline',
|
||||
height = '20px',
|
||||
clickable = false,
|
||||
object = false,
|
||||
...rest
|
||||
}: BadgeProps): JSX.Element {
|
||||
return (
|
||||
<BadgeWrapper clickable={clickable} display={display} height={height}>
|
||||
{src ? (
|
||||
object ? (
|
||||
<object data={src}>alt</object>
|
||||
) : (
|
||||
<img alt={alt} src={src} {...rest} />
|
||||
)
|
||||
) : (
|
||||
nonBreakingSpace
|
||||
)}
|
||||
</BadgeWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export const StyledInput = styled.input`
|
||||
height: 15px;
|
||||
border: solid #b9a;
|
||||
border-width: 0 0 1px 0;
|
||||
padding: 0;
|
||||
|
||||
text-align: center;
|
||||
|
||||
color: #534;
|
||||
|
||||
:focus {
|
||||
outline: 0;
|
||||
}
|
||||
`
|
||||
|
||||
export const InlineInput = styled(StyledInput)`
|
||||
width: 70px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
`
|
||||
|
||||
export const BlockInput = styled(StyledInput)`
|
||||
width: 40%;
|
||||
background-color: transparent;
|
||||
`
|
||||
|
||||
export const VerticalSpace = styled.hr`
|
||||
border: 0;
|
||||
display: block;
|
||||
height: 3mm;
|
||||
`
|
@ -1,46 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const BuilderOuterContainer = styled.div`
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
|
||||
// The inner container is inline-block so that its width matches its columns.
|
||||
const BuilderInnerContainer = styled.div`
|
||||
display: inline-block;
|
||||
|
||||
padding: 1px 14px 10px;
|
||||
|
||||
border-radius: 4px;
|
||||
background: #eef;
|
||||
`
|
||||
|
||||
export function BuilderContainer({
|
||||
children,
|
||||
}: {
|
||||
children: JSX.Element[] | JSX.Element
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<BuilderOuterContainer>
|
||||
<BuilderInnerContainer>{children}</BuilderInnerContainer>
|
||||
</BuilderOuterContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const labelFont = `
|
||||
font-family: system-ui;
|
||||
font-size: 11px;
|
||||
`
|
||||
|
||||
export const BuilderLabel = styled.label`
|
||||
${labelFont}
|
||||
|
||||
text-transform: lowercase;
|
||||
`
|
||||
|
||||
export const BuilderCaption = styled.span`
|
||||
${labelFont}
|
||||
|
||||
color: #999;
|
||||
`
|
@ -1,73 +0,0 @@
|
||||
import React, { useState, useImperativeHandle, forwardRef } from 'react'
|
||||
import posed from 'react-pose'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const ContentAnchor = styled.span`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
`
|
||||
|
||||
// 100vw allows providing styled content which is wider than its container.
|
||||
const ContentContainer = styled.span`
|
||||
width: 100vw;
|
||||
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
will-change: opacity, top;
|
||||
|
||||
pointer-events: none;
|
||||
`
|
||||
|
||||
const PosedContentContainer = posed(ContentContainer)({
|
||||
hidden: { opacity: 0, transition: { duration: 100 } },
|
||||
effectStart: { top: '-10px', opacity: 1.0, transition: { duration: 0 } },
|
||||
effectEnd: { top: '-75px', opacity: 0.5 },
|
||||
})
|
||||
|
||||
export interface CopiedContentIndicatorHandle {
|
||||
trigger: () => void
|
||||
}
|
||||
|
||||
// When `trigger()` is called, render copied content that floats up, then
|
||||
// disappears.
|
||||
function _CopiedContentIndicator(
|
||||
{
|
||||
copiedContent,
|
||||
children,
|
||||
}: {
|
||||
copiedContent: JSX.Element | string
|
||||
children: JSX.Element | JSX.Element[]
|
||||
},
|
||||
ref: React.Ref<CopiedContentIndicatorHandle>
|
||||
): JSX.Element {
|
||||
const [pose, setPose] = useState('hidden')
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
trigger() {
|
||||
setPose('effectStart')
|
||||
},
|
||||
}))
|
||||
|
||||
const handlePoseComplete = React.useCallback(
|
||||
function (): void {
|
||||
if (pose === 'effectStart') {
|
||||
setPose('effectEnd')
|
||||
} else {
|
||||
setPose('hidden')
|
||||
}
|
||||
},
|
||||
[pose, setPose]
|
||||
)
|
||||
|
||||
return (
|
||||
<ContentAnchor>
|
||||
<PosedContentContainer onPoseComplete={handlePoseComplete} pose={pose}>
|
||||
{copiedContent}
|
||||
</PosedContentContainer>
|
||||
{children}
|
||||
</ContentAnchor>
|
||||
)
|
||||
}
|
||||
export const CopiedContentIndicator = forwardRef(_CopiedContentIndicator)
|
@ -1,156 +0,0 @@
|
||||
import React, { useRef, useState } from 'react'
|
||||
import clipboardCopy from 'clipboard-copy'
|
||||
import { staticBadgeUrl } from '../../../core/badge-urls/make-badge-url'
|
||||
import { generateMarkup, MarkupFormat } from '../../lib/generate-image-markup'
|
||||
import { Badge } from '../common'
|
||||
import PathBuilder from './path-builder'
|
||||
import QueryStringBuilder from './query-string-builder'
|
||||
import RequestMarkupButtom from './request-markup-button'
|
||||
import {
|
||||
CopiedContentIndicator,
|
||||
CopiedContentIndicatorHandle,
|
||||
} from './copied-content-indicator'
|
||||
|
||||
export default function Customizer({
|
||||
baseUrl,
|
||||
title,
|
||||
pattern,
|
||||
exampleNamedParams,
|
||||
exampleQueryParams,
|
||||
initialStyle,
|
||||
}: {
|
||||
baseUrl: string
|
||||
title: string
|
||||
pattern: string
|
||||
exampleNamedParams: { [k: string]: string }
|
||||
exampleQueryParams: { [k: string]: string }
|
||||
initialStyle?: string
|
||||
}): JSX.Element {
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/35572
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/28884#issuecomment-471341041
|
||||
const indicatorRef =
|
||||
useRef<CopiedContentIndicatorHandle>() as React.MutableRefObject<CopiedContentIndicatorHandle>
|
||||
const [path, setPath] = useState('')
|
||||
const [queryString, setQueryString] = useState<string>()
|
||||
const [pathIsComplete, setPathIsComplete] = useState<boolean>()
|
||||
const [markup, setMarkup] = useState<string>()
|
||||
const [message, setMessage] = useState<string>()
|
||||
|
||||
const generateBuiltBadgeUrl = React.useCallback(
|
||||
function (): string {
|
||||
const suffix = queryString ? `?${queryString}` : ''
|
||||
return `${baseUrl}${path}${suffix}`
|
||||
},
|
||||
[baseUrl, path, queryString]
|
||||
)
|
||||
|
||||
function renderLivePreview(): JSX.Element {
|
||||
// There are some usability issues here. It would be better if the message
|
||||
// changed from a validation error to a loading message once the
|
||||
// parameters were filled in, and also switched back to loading when the
|
||||
// parameters changed.
|
||||
let src
|
||||
if (pathIsComplete) {
|
||||
src = generateBuiltBadgeUrl()
|
||||
} else {
|
||||
src = staticBadgeUrl({
|
||||
baseUrl,
|
||||
label: 'preview',
|
||||
message: 'some parameters missing',
|
||||
})
|
||||
}
|
||||
return (
|
||||
<p>
|
||||
<Badge alt="preview badge" display="block" src={src} />
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
const copyMarkup = React.useCallback(
|
||||
async function (markupFormat: MarkupFormat): Promise<void> {
|
||||
const builtBadgeUrl = generateBuiltBadgeUrl()
|
||||
const markup = generateMarkup({
|
||||
badgeUrl: builtBadgeUrl,
|
||||
title,
|
||||
markupFormat,
|
||||
})
|
||||
|
||||
try {
|
||||
await clipboardCopy(markup)
|
||||
} catch (e) {
|
||||
setMessage('Copy failed')
|
||||
setMarkup(markup)
|
||||
return
|
||||
}
|
||||
|
||||
setMarkup(markup)
|
||||
if (indicatorRef.current) {
|
||||
indicatorRef.current.trigger()
|
||||
}
|
||||
},
|
||||
[generateBuiltBadgeUrl, title, setMessage, setMarkup]
|
||||
)
|
||||
|
||||
function renderMarkupAndLivePreview(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
{renderLivePreview()}
|
||||
<CopiedContentIndicator copiedContent="Copied" ref={indicatorRef}>
|
||||
<RequestMarkupButtom
|
||||
isDisabled={!pathIsComplete}
|
||||
onMarkupRequested={copyMarkup}
|
||||
/>
|
||||
</CopiedContentIndicator>
|
||||
{message && (
|
||||
<div>
|
||||
<p>{message}</p>
|
||||
<p>Markup: {markup}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const handlePathChange = React.useCallback(
|
||||
function ({
|
||||
path,
|
||||
isComplete,
|
||||
}: {
|
||||
path: string
|
||||
isComplete: boolean
|
||||
}): void {
|
||||
setPath(path)
|
||||
setPathIsComplete(isComplete)
|
||||
},
|
||||
[setPath, setPathIsComplete]
|
||||
)
|
||||
|
||||
const handleQueryStringChange = React.useCallback(
|
||||
function ({
|
||||
queryString,
|
||||
isComplete,
|
||||
}: {
|
||||
queryString: string
|
||||
isComplete: boolean
|
||||
}): void {
|
||||
setQueryString(queryString)
|
||||
},
|
||||
[setQueryString]
|
||||
)
|
||||
|
||||
return (
|
||||
<form action="">
|
||||
<PathBuilder
|
||||
exampleParams={exampleNamedParams}
|
||||
onChange={handlePathChange}
|
||||
pattern={pattern}
|
||||
/>
|
||||
<QueryStringBuilder
|
||||
exampleParams={exampleQueryParams}
|
||||
initialStyle={initialStyle}
|
||||
onChange={handleQueryStringChange}
|
||||
/>
|
||||
<div>{renderMarkupAndLivePreview()}</div>
|
||||
</form>
|
||||
)
|
||||
}
|
@ -1,258 +0,0 @@
|
||||
import React, { useState, useEffect, ChangeEvent } from 'react'
|
||||
import styled, { css } from 'styled-components'
|
||||
import { Token, Key, parse } from 'path-to-regexp'
|
||||
import humanizeString from 'humanize-string'
|
||||
import { patternToOptions } from '../../lib/pattern-helpers'
|
||||
import { noAutocorrect, StyledInput } from '../common'
|
||||
import {
|
||||
BuilderContainer,
|
||||
BuilderLabel,
|
||||
BuilderCaption,
|
||||
} from './builder-common'
|
||||
|
||||
interface PathBuilderColumnProps {
|
||||
pathContainsOnlyLiterals: boolean
|
||||
withHorizPadding?: boolean
|
||||
}
|
||||
|
||||
const PathBuilderColumn = styled.span<PathBuilderColumnProps>`
|
||||
height: ${({ pathContainsOnlyLiterals }) =>
|
||||
pathContainsOnlyLiterals ? '18px' : '78px'};
|
||||
|
||||
float: left;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
margin: 0;
|
||||
|
||||
${({ withHorizPadding }) =>
|
||||
withHorizPadding &&
|
||||
css`
|
||||
padding: 0 8px;
|
||||
`};
|
||||
`
|
||||
|
||||
interface PathLiteralProps {
|
||||
isFirstToken: boolean
|
||||
pathContainsOnlyLiterals: boolean
|
||||
}
|
||||
|
||||
const PathLiteral = styled.div<PathLiteralProps>`
|
||||
margin-top: ${({ pathContainsOnlyLiterals }) =>
|
||||
pathContainsOnlyLiterals ? '0px' : '39px'};
|
||||
${({ isFirstToken }) =>
|
||||
isFirstToken &&
|
||||
css`
|
||||
margin-left: 3px;
|
||||
`};
|
||||
`
|
||||
|
||||
const NamedParamLabelContainer = styled.span`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 37px;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
const inputStyling = `
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
// 2px to align with input boxes alongside.
|
||||
const NamedParamInput = styled(StyledInput)`
|
||||
${inputStyling}
|
||||
margin-top: 2px;
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
|
||||
const NamedParamSelect = styled.select`
|
||||
${inputStyling}
|
||||
margin-bottom: 9px;
|
||||
font-size: 10px;
|
||||
`
|
||||
|
||||
const NamedParamCaption = styled(BuilderCaption)`
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
export function constructPath({
|
||||
tokens,
|
||||
namedParams,
|
||||
}: {
|
||||
tokens: Token[]
|
||||
namedParams: { [k: string]: string }
|
||||
}): { path: string; isComplete: boolean } {
|
||||
let isComplete = true
|
||||
let path = tokens
|
||||
.map(token => {
|
||||
if (typeof token === 'string') {
|
||||
return token.trim()
|
||||
} else {
|
||||
const { prefix, name, modifier } = token
|
||||
const value = namedParams[name]
|
||||
if (value) {
|
||||
return `${prefix}${value.trim()}`
|
||||
} else if (modifier === '?' || modifier === '*') {
|
||||
return ''
|
||||
} else {
|
||||
isComplete = false
|
||||
return `${prefix}:${name}`
|
||||
}
|
||||
}
|
||||
})
|
||||
.join('')
|
||||
path = encodeURI(path)
|
||||
return { path, isComplete }
|
||||
}
|
||||
|
||||
export default function PathBuilder({
|
||||
pattern,
|
||||
exampleParams,
|
||||
onChange,
|
||||
}: {
|
||||
pattern: string
|
||||
exampleParams: { [k: string]: string }
|
||||
onChange: ({
|
||||
path,
|
||||
isComplete,
|
||||
}: {
|
||||
path: string
|
||||
isComplete: boolean
|
||||
}) => void
|
||||
}): JSX.Element {
|
||||
const [tokens] = useState(() => parse(pattern))
|
||||
const [namedParams, setNamedParams] = useState(() =>
|
||||
// `pathToRegexp.parse()` returns a mixed array of strings for literals
|
||||
// and objects for parameters. Filter out the literals and work with the
|
||||
// objects.
|
||||
tokens
|
||||
.filter(t => typeof t !== 'string')
|
||||
.map(t => t as Key)
|
||||
.reduce((accum, { name }) => {
|
||||
accum[name] = ''
|
||||
return accum
|
||||
}, {} as { [k: string]: string })
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// Ensure the default style is applied right away.
|
||||
if (onChange) {
|
||||
const { path, isComplete } = constructPath({ tokens, namedParams })
|
||||
onChange({ path, isComplete })
|
||||
}
|
||||
}, [tokens, namedParams, onChange])
|
||||
|
||||
const handleTokenChange = React.useCallback(
|
||||
function ({
|
||||
target: { name, value },
|
||||
}: ChangeEvent<HTMLInputElement | HTMLSelectElement>): void {
|
||||
setNamedParams({
|
||||
...namedParams,
|
||||
[name]: value,
|
||||
})
|
||||
},
|
||||
[setNamedParams, namedParams]
|
||||
)
|
||||
|
||||
function renderLiteral(
|
||||
literal: string,
|
||||
tokenIndex: number,
|
||||
pathContainsOnlyLiterals: boolean
|
||||
): JSX.Element {
|
||||
return (
|
||||
<PathBuilderColumn
|
||||
key={`${tokenIndex}-${literal}`}
|
||||
pathContainsOnlyLiterals={pathContainsOnlyLiterals}
|
||||
>
|
||||
<PathLiteral
|
||||
isFirstToken={tokenIndex === 0}
|
||||
pathContainsOnlyLiterals={pathContainsOnlyLiterals}
|
||||
>
|
||||
{literal}
|
||||
</PathLiteral>
|
||||
</PathBuilderColumn>
|
||||
)
|
||||
}
|
||||
|
||||
function renderNamedParamInput(token: Key): JSX.Element {
|
||||
const { pattern } = token
|
||||
const name = `${token.name}`
|
||||
const options = patternToOptions(pattern)
|
||||
|
||||
const value = namedParams[name]
|
||||
|
||||
if (options) {
|
||||
return (
|
||||
<NamedParamSelect
|
||||
name={name}
|
||||
onChange={handleTokenChange}
|
||||
value={value}
|
||||
>
|
||||
<option key="empty" value="">
|
||||
{' '}
|
||||
</option>
|
||||
{options.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</NamedParamSelect>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<NamedParamInput
|
||||
name={name}
|
||||
onChange={handleTokenChange}
|
||||
type="text"
|
||||
value={value}
|
||||
{...noAutocorrect}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function renderNamedParam(
|
||||
token: Key,
|
||||
tokenIndex: number,
|
||||
namedParamIndex: number
|
||||
): JSX.Element {
|
||||
const { prefix, modifier } = token
|
||||
const optional = modifier === '?' || modifier === '*'
|
||||
const name = `${token.name}`
|
||||
|
||||
const exampleValue = exampleParams[name] || '(not set)'
|
||||
|
||||
return (
|
||||
<React.Fragment key={token.name}>
|
||||
{renderLiteral(prefix, tokenIndex, false)}
|
||||
<PathBuilderColumn pathContainsOnlyLiterals={false} withHorizPadding>
|
||||
<NamedParamLabelContainer>
|
||||
<BuilderLabel htmlFor={name}>{humanizeString(name)}</BuilderLabel>
|
||||
{optional ? <BuilderLabel>(optional)</BuilderLabel> : null}
|
||||
</NamedParamLabelContainer>
|
||||
{renderNamedParamInput(token)}
|
||||
<NamedParamCaption>
|
||||
{namedParamIndex === 0 ? `e.g. ${exampleValue}` : exampleValue}
|
||||
</NamedParamCaption>
|
||||
</PathBuilderColumn>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
let namedParamIndex = 0
|
||||
const pathContainsOnlyLiterals = tokens.every(
|
||||
token => typeof token === 'string'
|
||||
)
|
||||
return (
|
||||
<BuilderContainer>
|
||||
{tokens.map((token, tokenIndex) =>
|
||||
typeof token === 'string'
|
||||
? renderLiteral(token, tokenIndex, pathContainsOnlyLiterals)
|
||||
: renderNamedParam(token, tokenIndex, namedParamIndex++)
|
||||
)}
|
||||
</BuilderContainer>
|
||||
)
|
||||
}
|
@ -1,348 +0,0 @@
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
ChangeEvent,
|
||||
ChangeEventHandler,
|
||||
} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import humanizeString from 'humanize-string'
|
||||
import qs from 'query-string'
|
||||
import { advertisedStyles } from '../../lib/supported-features'
|
||||
import { noAutocorrect, StyledInput } from '../common'
|
||||
import {
|
||||
BuilderContainer,
|
||||
BuilderLabel,
|
||||
BuilderCaption,
|
||||
} from './builder-common'
|
||||
|
||||
const QueryParamLabel = styled(BuilderLabel)`
|
||||
margin: 5px;
|
||||
`
|
||||
|
||||
const QueryParamInput = styled(StyledInput)`
|
||||
margin: 5px 10px;
|
||||
`
|
||||
|
||||
const QueryParamCaption = styled(BuilderCaption)`
|
||||
margin: 5px;
|
||||
`
|
||||
|
||||
type BadgeOptionName = 'style' | 'label' | 'color' | 'logo' | 'logoColor'
|
||||
|
||||
interface BadgeOptionInfo {
|
||||
name: BadgeOptionName
|
||||
label?: string
|
||||
shieldsDefaultValue?: string
|
||||
}
|
||||
|
||||
const supportedBadgeOptions = [
|
||||
{ name: 'style', shieldsDefaultValue: 'flat' },
|
||||
{ name: 'label', label: 'override label' },
|
||||
{ name: 'color', label: 'override color' },
|
||||
{ name: 'logo', label: 'named logo' },
|
||||
{ name: 'logoColor', label: 'override logo color' },
|
||||
] as BadgeOptionInfo[]
|
||||
|
||||
function getBadgeOption(name: BadgeOptionName): BadgeOptionInfo {
|
||||
const result = supportedBadgeOptions.find(opt => opt.name === name)
|
||||
if (!result) {
|
||||
throw Error(`Unknown badge option: ${name}`)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function getQueryString({
|
||||
queryParams,
|
||||
badgeOptions,
|
||||
}: {
|
||||
queryParams: Record<string, string | boolean>
|
||||
badgeOptions: Record<BadgeOptionName, string | undefined>
|
||||
}): {
|
||||
queryString: string
|
||||
isComplete: boolean
|
||||
} {
|
||||
// Use `string | null`, because `query-string` renders e.g.
|
||||
// `{ compact_message: null }` as `?compact_message`. This is
|
||||
// what we want for boolean params that are true (see below).
|
||||
const outQuery = {} as Record<string, string | null>
|
||||
let isComplete = true
|
||||
|
||||
Object.entries(queryParams).forEach(([name, value]) => {
|
||||
// As above, there are two types of supported params: strings and
|
||||
// booleans.
|
||||
if (typeof value === 'string') {
|
||||
if (value) {
|
||||
outQuery[name] = value.trim()
|
||||
} else {
|
||||
// Skip empty params.
|
||||
isComplete = false
|
||||
}
|
||||
} else {
|
||||
// Generate empty query params for boolean parameters by translating
|
||||
// `{ compact_message: true }` to `?compact_message`. When values are
|
||||
// false, skip the param.
|
||||
if (value) {
|
||||
outQuery[name] = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Object.entries(badgeOptions).forEach(([name, value]) => {
|
||||
const { shieldsDefaultValue } = getBadgeOption(name as BadgeOptionName)
|
||||
if (value && value !== shieldsDefaultValue) {
|
||||
outQuery[name] = value
|
||||
}
|
||||
})
|
||||
|
||||
const queryString = qs.stringify(outQuery)
|
||||
|
||||
return { queryString, isComplete }
|
||||
}
|
||||
|
||||
function ServiceQueryParam({
|
||||
name,
|
||||
value,
|
||||
exampleValue,
|
||||
isStringParam,
|
||||
stringParamCount,
|
||||
handleServiceQueryParamChange,
|
||||
}: {
|
||||
name: string
|
||||
value: string | boolean
|
||||
exampleValue: string
|
||||
isStringParam: boolean
|
||||
stringParamCount?: number
|
||||
handleServiceQueryParamChange: ChangeEventHandler<HTMLInputElement>
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<QueryParamLabel htmlFor={name}>
|
||||
{humanizeString(name).toLowerCase()}
|
||||
</QueryParamLabel>
|
||||
</td>
|
||||
<td>
|
||||
{isStringParam && (
|
||||
<QueryParamCaption>
|
||||
{stringParamCount === 0 ? `e.g. ${exampleValue}` : exampleValue}
|
||||
</QueryParamCaption>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{isStringParam ? (
|
||||
<QueryParamInput
|
||||
name={name}
|
||||
onChange={handleServiceQueryParamChange}
|
||||
type="text"
|
||||
value={value as string}
|
||||
{...noAutocorrect}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
checked={value as boolean}
|
||||
name={name}
|
||||
onChange={handleServiceQueryParamChange}
|
||||
type="checkbox"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
function BadgeOptionInput({
|
||||
name,
|
||||
value,
|
||||
handleBadgeOptionChange,
|
||||
}: {
|
||||
name: BadgeOptionName
|
||||
value: string
|
||||
handleBadgeOptionChange: ChangeEventHandler<
|
||||
HTMLSelectElement | HTMLInputElement
|
||||
>
|
||||
}): JSX.Element {
|
||||
if (name === 'style') {
|
||||
return (
|
||||
<select name="style" onChange={handleBadgeOptionChange} value={value}>
|
||||
{advertisedStyles.map(style => (
|
||||
<option key={style} value={style}>
|
||||
{style}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<QueryParamInput
|
||||
name={name}
|
||||
onChange={handleBadgeOptionChange}
|
||||
type="text"
|
||||
value={value}
|
||||
{...noAutocorrect}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function BadgeOption({
|
||||
name,
|
||||
value,
|
||||
handleBadgeOptionChange,
|
||||
}: {
|
||||
name: BadgeOptionName
|
||||
value: string
|
||||
handleBadgeOptionChange: ChangeEventHandler<HTMLInputElement>
|
||||
}): JSX.Element {
|
||||
const {
|
||||
label = humanizeString(name),
|
||||
shieldsDefaultValue: hasShieldsDefaultValue,
|
||||
} = getBadgeOption(name)
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<QueryParamLabel htmlFor={name}>{label}</QueryParamLabel>
|
||||
</td>
|
||||
<td>
|
||||
{!hasShieldsDefaultValue && (
|
||||
<QueryParamCaption>optional</QueryParamCaption>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<BadgeOptionInput
|
||||
handleBadgeOptionChange={handleBadgeOptionChange}
|
||||
name={name}
|
||||
value={value}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
// The UI for building the query string, which includes two kinds of settings:
|
||||
// 1. Custom query params defined by the service, stored in
|
||||
// `this.state.queryParams`
|
||||
// 2. The standard badge options which apply to all badges, stored in
|
||||
// `this.state.badgeOptions`
|
||||
export default function QueryStringBuilder({
|
||||
exampleParams,
|
||||
initialStyle = 'flat',
|
||||
onChange,
|
||||
}: {
|
||||
exampleParams: { [k: string]: string }
|
||||
initialStyle?: string
|
||||
onChange: ({
|
||||
queryString,
|
||||
isComplete,
|
||||
}: {
|
||||
queryString: string
|
||||
isComplete: boolean
|
||||
}) => void
|
||||
}): JSX.Element {
|
||||
const [queryParams, setQueryParams] = useState(() =>
|
||||
// For each of the custom query params defined in `exampleParams`,
|
||||
// create empty values in `queryParams`.
|
||||
Object.entries(exampleParams)
|
||||
.filter(
|
||||
// If the example defines a value for one of the standard supported
|
||||
// options, do not duplicate the corresponding parameter.
|
||||
([name]) => !supportedBadgeOptions.some(option => name === option.name)
|
||||
)
|
||||
.reduce((accum, [name, value]) => {
|
||||
// Custom query params are either string or boolean. Inspect the example
|
||||
// value to infer which one, and set empty values accordingly.
|
||||
// Throughout the component, these two types are supported in the same
|
||||
// manner: by inspecting this value type.
|
||||
const isStringParam = typeof value === 'string'
|
||||
accum[name] = isStringParam ? '' : true
|
||||
return accum
|
||||
}, {} as { [k: string]: string | boolean })
|
||||
)
|
||||
// For each of the standard badge options, create empty values in
|
||||
// `badgeOptions`. When `initialStyle` has been provided, use it.
|
||||
const [badgeOptions, setBadgeOptions] = useState(() =>
|
||||
supportedBadgeOptions.reduce((accum, { name }) => {
|
||||
if (name === 'style') {
|
||||
accum[name] = initialStyle
|
||||
} else {
|
||||
accum[name] = ''
|
||||
}
|
||||
return accum
|
||||
}, {} as Record<BadgeOptionName, string>)
|
||||
)
|
||||
|
||||
const handleServiceQueryParamChange = React.useCallback(
|
||||
function ({
|
||||
target: { name, type: targetType, checked, value },
|
||||
}: ChangeEvent<HTMLInputElement>): void {
|
||||
const outValue = targetType === 'checkbox' ? checked : value
|
||||
setQueryParams({ ...queryParams, [name]: outValue })
|
||||
},
|
||||
[setQueryParams, queryParams]
|
||||
)
|
||||
|
||||
const handleBadgeOptionChange = React.useCallback(
|
||||
function ({
|
||||
target: { name, value },
|
||||
}: ChangeEvent<HTMLInputElement>): void {
|
||||
setBadgeOptions({ ...badgeOptions, [name]: value })
|
||||
},
|
||||
[setBadgeOptions, badgeOptions]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange) {
|
||||
const { queryString, isComplete } = getQueryString({
|
||||
queryParams,
|
||||
badgeOptions,
|
||||
})
|
||||
onChange({ queryString, isComplete })
|
||||
}
|
||||
}, [onChange, queryParams, badgeOptions])
|
||||
|
||||
const hasQueryParams = Boolean(Object.keys(queryParams).length)
|
||||
let stringParamCount = 0
|
||||
return (
|
||||
<>
|
||||
{hasQueryParams && (
|
||||
<BuilderContainer>
|
||||
<table>
|
||||
<tbody>
|
||||
{Object.entries(queryParams).map(([name, value]) => {
|
||||
const isStringParam = typeof value === 'string'
|
||||
return (
|
||||
<ServiceQueryParam
|
||||
exampleValue={exampleParams[name]}
|
||||
handleServiceQueryParamChange={
|
||||
handleServiceQueryParamChange
|
||||
}
|
||||
isStringParam={isStringParam}
|
||||
key={name}
|
||||
name={name}
|
||||
stringParamCount={
|
||||
isStringParam ? stringParamCount++ : undefined
|
||||
}
|
||||
value={value}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</BuilderContainer>
|
||||
)}
|
||||
<BuilderContainer>
|
||||
<table>
|
||||
<tbody>
|
||||
{Object.entries(badgeOptions).map(([name, value]) => (
|
||||
<BadgeOption
|
||||
handleBadgeOptionChange={handleBadgeOptionChange}
|
||||
key={name}
|
||||
name={name as BadgeOptionName}
|
||||
value={value}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</BuilderContainer>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
import React, { useRef } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Select, { components } from 'react-select'
|
||||
import { MarkupFormat } from '../../lib/generate-image-markup'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function ClickableControl(props: any): JSX.Element {
|
||||
return (
|
||||
<components.Control
|
||||
{...props}
|
||||
innerProps={{
|
||||
onMouseDown: props.selectProps.onControlMouseDown,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface Option {
|
||||
value: MarkupFormat
|
||||
label: string
|
||||
}
|
||||
|
||||
const MarkupFormatSelect = styled(Select)`
|
||||
width: 200px;
|
||||
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
font-family: 'Lato', sans-serif;
|
||||
font-size: 12px;
|
||||
|
||||
.markup-format__control {
|
||||
background-image: linear-gradient(-180deg, #00aeff 0%, #0076ff 100%);
|
||||
border: 1px solid rgba(238, 239, 241, 0.8);
|
||||
border-width: 0;
|
||||
box-shadow: unset;
|
||||
cursor: copy;
|
||||
}
|
||||
|
||||
.markup-format__control--is-disabled {
|
||||
background: rgba(0, 118, 255, 0.3);
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
.markup-format__placeholder {
|
||||
color: #eeeff1;
|
||||
}
|
||||
|
||||
.markup-format__indicator {
|
||||
color: rgba(238, 239, 241, 0.81);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.markup-format__indicator:hover {
|
||||
color: #eeeff1;
|
||||
}
|
||||
|
||||
.markup-format__control--is-focused .markup-format__indicator,
|
||||
.markup-format__control--is-focused .markup-format__indicator:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.markup-format__option {
|
||||
text-align: left;
|
||||
cursor: copy;
|
||||
}
|
||||
`
|
||||
|
||||
const markupOptions: Option[] = [
|
||||
{ value: 'markdown', label: 'Copy Markdown' },
|
||||
{ value: 'rst', label: 'Copy reStructuredText' },
|
||||
{ value: 'asciidoc', label: 'Copy AsciiDoc' },
|
||||
{ value: 'html', label: 'Copy HTML' },
|
||||
]
|
||||
|
||||
export default function GetMarkupButton({
|
||||
onMarkupRequested,
|
||||
isDisabled,
|
||||
}: {
|
||||
onMarkupRequested: (markupFormat: MarkupFormat) => Promise<void>
|
||||
isDisabled: boolean
|
||||
}): JSX.Element {
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/35572
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/28884#issuecomment-471341041
|
||||
const selectRef = useRef<Select<Option>>() as React.MutableRefObject<
|
||||
Select<Option>
|
||||
>
|
||||
|
||||
const onControlMouseDown = React.useCallback(
|
||||
async function (event: MouseEvent): Promise<void> {
|
||||
if (onMarkupRequested) {
|
||||
await onMarkupRequested('link')
|
||||
}
|
||||
if (selectRef.current) {
|
||||
selectRef.current.blur()
|
||||
}
|
||||
},
|
||||
[onMarkupRequested, selectRef]
|
||||
)
|
||||
|
||||
const onOptionClick = React.useCallback(
|
||||
async function onOptionClick(
|
||||
// Eeesh.
|
||||
value: Option | readonly Option[] | null | undefined
|
||||
): Promise<void> {
|
||||
const { value: markupFormat } = value as Option
|
||||
if (onMarkupRequested) {
|
||||
await onMarkupRequested(markupFormat)
|
||||
}
|
||||
},
|
||||
[onMarkupRequested]
|
||||
)
|
||||
|
||||
return (
|
||||
// TODO It doesn't seem to be possible to check the types and wrap with
|
||||
// styled-components at the same time. To check the types, replace
|
||||
// `MarkupFormatSelect` with `Select<Option>`.
|
||||
<MarkupFormatSelect
|
||||
blurInputOnSelect
|
||||
classNamePrefix="markup-format"
|
||||
closeMenuOnScroll
|
||||
components={{ Control: ClickableControl }}
|
||||
isDisabled={isDisabled}
|
||||
isSearchable={false}
|
||||
menuPlacement="auto"
|
||||
onChange={onOptionClick}
|
||||
onControlMouseDown={onControlMouseDown}
|
||||
options={markupOptions}
|
||||
placeholder="Copy Badge URL"
|
||||
ref={selectRef}
|
||||
value={null}
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { staticBadgeUrl } from '../../../core/badge-urls/make-badge-url'
|
||||
import { getBaseUrl } from '../../constants'
|
||||
import { shieldsLogos, simpleIcons } from '../../lib/supported-features'
|
||||
import Meta from '../meta'
|
||||
import Header from '../header'
|
||||
import { H3, Badge } from '../common'
|
||||
|
||||
const StyledTable = styled.table`
|
||||
border: 1px solid #ccc;
|
||||
border-collapse: collapse;
|
||||
|
||||
td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 3px;
|
||||
text-align: left;
|
||||
}
|
||||
`
|
||||
|
||||
function NamedLogoTable({ logoNames }: { logoNames: string[] }): JSX.Element {
|
||||
const baseUrl = getBaseUrl()
|
||||
return (
|
||||
<StyledTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Flat</td>
|
||||
<td>Social</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{logoNames.map(name => (
|
||||
<tr key={name}>
|
||||
<td>
|
||||
<Badge
|
||||
alt={`logo: ${name}`}
|
||||
src={staticBadgeUrl({
|
||||
baseUrl,
|
||||
label: 'named logo',
|
||||
message: name,
|
||||
color: 'blue',
|
||||
namedLogo: name,
|
||||
})}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Badge
|
||||
alt={`logo: ${name}`}
|
||||
src={staticBadgeUrl({
|
||||
baseUrl,
|
||||
label: 'Named Logo',
|
||||
message: name,
|
||||
color: 'blue',
|
||||
namedLogo: name,
|
||||
style: 'social',
|
||||
})}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</StyledTable>
|
||||
)
|
||||
}
|
||||
|
||||
export default function LogoPage(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<Meta />
|
||||
<Header />
|
||||
<H3>Named logos</H3>
|
||||
<NamedLogoTable logoNames={shieldsLogos} />
|
||||
<H3>Simple-icons</H3>
|
||||
<NamedLogoTable logoNames={simpleIcons} />
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,168 +0,0 @@
|
||||
import React, { Fragment } from 'react'
|
||||
import styled from 'styled-components'
|
||||
// FIXME: is this needed?
|
||||
// @ts-ingnore
|
||||
import { staticBadgeUrl } from '../../../core/badge-urls/make-badge-url'
|
||||
import { getBaseUrl } from '../../constants'
|
||||
import Meta from '../meta'
|
||||
// ts-expect-error: because reasons?
|
||||
import Header from '../header'
|
||||
import { H3, Badge } from '../common'
|
||||
|
||||
const StyledTable = styled.table`
|
||||
border: 1px solid #ccc;
|
||||
border-collapse: collapse;
|
||||
|
||||
td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 3px;
|
||||
text-align: left;
|
||||
}
|
||||
`
|
||||
|
||||
interface BadgeData {
|
||||
label: string
|
||||
message: string
|
||||
labelColor?: string
|
||||
color: string
|
||||
namedLogo?: string
|
||||
links?: string[]
|
||||
}
|
||||
|
||||
function Badges({
|
||||
baseUrl,
|
||||
style,
|
||||
badges,
|
||||
}: {
|
||||
baseUrl: string
|
||||
style: string
|
||||
badges: BadgeData[]
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{badges.map(({ label, message, labelColor, color, namedLogo, links }) => (
|
||||
<Fragment key={`${label}-${message}-${color}-${namedLogo}`}>
|
||||
<Badge
|
||||
alt="build"
|
||||
object={Boolean(links)}
|
||||
src={staticBadgeUrl({
|
||||
baseUrl,
|
||||
label,
|
||||
message,
|
||||
labelColor,
|
||||
color,
|
||||
namedLogo,
|
||||
style,
|
||||
links,
|
||||
})}
|
||||
/>
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const examples = [
|
||||
{
|
||||
title: 'Basic examples',
|
||||
badges: [
|
||||
{ label: 'build', message: 'passing', color: 'brightgreen' },
|
||||
{ label: 'tests', message: '5 passing, 1 failed', color: 'red' },
|
||||
{ label: 'python', message: '3.5 | 3.6 | 3.7', color: 'blue' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Logo',
|
||||
badges: [
|
||||
{
|
||||
label: 'build',
|
||||
message: 'passing',
|
||||
color: 'brightgreen',
|
||||
namedLogo: 'appveyor',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'No left text',
|
||||
badges: [
|
||||
{ label: '', message: 'blueviolet', color: 'blueviolet' },
|
||||
{
|
||||
label: '',
|
||||
message: 'passing',
|
||||
color: 'brightgreen',
|
||||
namedLogo: 'appveyor',
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
message: 'passing',
|
||||
color: 'brightgreen',
|
||||
labelColor: 'grey',
|
||||
namedLogo: 'appveyor',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Links',
|
||||
badges: [
|
||||
{
|
||||
label: 'badges',
|
||||
message: 'shields',
|
||||
color: 'blue',
|
||||
links: [
|
||||
'https://github.com/badges/',
|
||||
'https://github.com/badges/shields/',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
function StyleTable({ style }: { style: string }): JSX.Element {
|
||||
const baseUrl = getBaseUrl()
|
||||
return (
|
||||
<StyledTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Badges (new)</td>
|
||||
<td>Badges (img.shields.io)</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{examples.map(({ title, badges }) => (
|
||||
<tr key={title}>
|
||||
<td>{title}</td>
|
||||
<td>
|
||||
<Badges badges={badges} baseUrl={baseUrl} style={style} />
|
||||
</td>
|
||||
<td>
|
||||
<Badges
|
||||
badges={badges}
|
||||
baseUrl="https://img.shields.io"
|
||||
style={style}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</StyledTable>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = ['flat', 'flat-square', 'for-the-badge', 'social', 'plastic']
|
||||
|
||||
export default function StylePage(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<Meta />
|
||||
<Header />
|
||||
{styles.map(style => (
|
||||
<Fragment key={style}>
|
||||
<H3>{style}</H3>
|
||||
<StyleTable style={style} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const Donate = styled.div`
|
||||
padding: 25px 50px;
|
||||
`
|
||||
|
||||
export default function DonateBox(): JSX.Element {
|
||||
return (
|
||||
<Donate>
|
||||
Love Shields? Please consider{' '}
|
||||
<a href="https://opencollective.com/shields">donating</a> to sustain our
|
||||
activities
|
||||
</Donate>
|
||||
)
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
import React, { useState, ChangeEvent } from 'react'
|
||||
import { dynamicBadgeUrl } from '../../core/badge-urls/make-badge-url'
|
||||
import { InlineInput } from './common'
|
||||
|
||||
type StateKey =
|
||||
| 'datatype'
|
||||
| 'label'
|
||||
| 'dataUrl'
|
||||
| 'query'
|
||||
| 'color'
|
||||
| 'prefix'
|
||||
| 'suffix'
|
||||
type State = Record<StateKey, string>
|
||||
|
||||
interface InputDef {
|
||||
name: StateKey
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
const inputs = [
|
||||
{ name: 'label' },
|
||||
{ name: 'dataUrl', placeholder: 'data url' },
|
||||
{ name: 'query' },
|
||||
{ name: 'color' },
|
||||
{ name: 'prefix' },
|
||||
{ name: 'suffix' },
|
||||
] as InputDef[]
|
||||
|
||||
export default function DynamicBadgeMaker({
|
||||
baseUrl = document.location.href,
|
||||
}: {
|
||||
baseUrl: string
|
||||
}): JSX.Element {
|
||||
const [values, setValues] = useState<State>({
|
||||
datatype: '',
|
||||
label: '',
|
||||
dataUrl: '',
|
||||
query: '',
|
||||
color: '',
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
})
|
||||
|
||||
const isValid =
|
||||
values.datatype && values.label && values.dataUrl && values.query
|
||||
|
||||
const onChange = React.useCallback(
|
||||
function ({
|
||||
target: { name, value },
|
||||
}: ChangeEvent<HTMLInputElement | HTMLSelectElement>): void {
|
||||
setValues({
|
||||
...values,
|
||||
[name]: value,
|
||||
})
|
||||
},
|
||||
[values]
|
||||
)
|
||||
|
||||
const onSubmit = React.useCallback(
|
||||
function onSubmit(e: React.FormEvent): void {
|
||||
e.preventDefault()
|
||||
|
||||
const { datatype, label, dataUrl, query, color, prefix, suffix } = values
|
||||
window.open(
|
||||
dynamicBadgeUrl({
|
||||
baseUrl,
|
||||
datatype,
|
||||
label,
|
||||
dataUrl,
|
||||
query,
|
||||
color,
|
||||
prefix,
|
||||
suffix,
|
||||
}),
|
||||
'_blank'
|
||||
)
|
||||
},
|
||||
[baseUrl, values]
|
||||
)
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<select name="datatype" onChange={onChange} value={values.datatype}>
|
||||
<option disabled value="">
|
||||
data type
|
||||
</option>
|
||||
<option value="json">json</option>
|
||||
<option value="xml">xml</option>
|
||||
<option value="yaml">yaml</option>
|
||||
</select>{' '}
|
||||
{inputs.map(({ name, placeholder = name }) => (
|
||||
<InlineInput
|
||||
key={name}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
value={values[name]}
|
||||
/>
|
||||
))}
|
||||
<button disabled={!isValid}>Make Badge</button>
|
||||
</form>
|
||||
)
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { badgeUrlFromPath } from '../../core/badge-urls/make-badge-url'
|
||||
import { H2 } from './common'
|
||||
|
||||
const SpacedA = styled.a`
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
`
|
||||
|
||||
export default function Footer({ baseUrl }: { baseUrl: string }): JSX.Element {
|
||||
return (
|
||||
<section>
|
||||
<H2 id="like-this">Like This?</H2>
|
||||
|
||||
<p>
|
||||
<object
|
||||
data={badgeUrlFromPath({
|
||||
baseUrl,
|
||||
path: '/twitter/follow/shields_io',
|
||||
queryParams: { label: 'Follow' },
|
||||
style: 'social',
|
||||
})}
|
||||
/>{' '}
|
||||
{}
|
||||
<object
|
||||
data={badgeUrlFromPath({
|
||||
baseUrl,
|
||||
path: '/opencollective/backers/shields',
|
||||
queryParams: { link: 'https://opencollective.com/shields' },
|
||||
style: 'social',
|
||||
})}
|
||||
/>{' '}
|
||||
{}
|
||||
<object
|
||||
data={badgeUrlFromPath({
|
||||
baseUrl,
|
||||
path: '/opencollective/sponsors/shields',
|
||||
queryParams: { link: 'https://opencollective.com/shields' },
|
||||
style: 'social',
|
||||
})}
|
||||
/>{' '}
|
||||
{}
|
||||
<object
|
||||
data={badgeUrlFromPath({
|
||||
baseUrl,
|
||||
path: '/github/forks/badges/shields',
|
||||
queryParams: { label: 'Fork' },
|
||||
style: 'social',
|
||||
})}
|
||||
/>{' '}
|
||||
{}
|
||||
<object
|
||||
data={badgeUrlFromPath({
|
||||
baseUrl,
|
||||
path: '/discord/308323056592486420',
|
||||
queryParams: {
|
||||
label: 'Chat',
|
||||
link: 'https://discord.gg/HjJCwm5',
|
||||
},
|
||||
style: 'social',
|
||||
})}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Have an idea for an awesome new badge?
|
||||
<br />
|
||||
<a href="https://github.com/badges/shields/issues/new?labels=service-badge&template=3_Badge_request.yml">
|
||||
Tell us about it
|
||||
</a>{' '}
|
||||
and we might bring it to you!
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<SpacedA href="/community">Community</SpacedA>
|
||||
<SpacedA href="https://stats.uptimerobot.com/PjXogHB5p">Status</SpacedA>
|
||||
<SpacedA href="https://metrics.shields.io">Metrics</SpacedA>
|
||||
<SpacedA href="https://github.com/badges/shields">GitHub</SpacedA>
|
||||
</p>
|
||||
</section>
|
||||
)
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import { Link } from 'gatsby'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Logo from '../images/logo.svg'
|
||||
import { VerticalSpace } from './common'
|
||||
|
||||
const Highlights = styled.p`
|
||||
font-style: italic;
|
||||
`
|
||||
|
||||
export default function Header(): JSX.Element {
|
||||
return (
|
||||
<section>
|
||||
<Link to="/">
|
||||
<Logo />
|
||||
</Link>
|
||||
|
||||
<VerticalSpace />
|
||||
|
||||
<Highlights>
|
||||
Pixel-perfect Retina-ready Fast Consistent
|
||||
Hackable No tracking
|
||||
</Highlights>
|
||||
</section>
|
||||
)
|
||||
}
|
@ -1,185 +0,0 @@
|
||||
import React, { useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import groupBy from 'lodash.groupby'
|
||||
import {
|
||||
ServiceDefinition,
|
||||
Example,
|
||||
categories,
|
||||
findCategory,
|
||||
services,
|
||||
getDefinitionsForCategory,
|
||||
RenderableExample,
|
||||
} from '../lib/service-definitions'
|
||||
import ServiceDefinitionSetHelper from '../lib/service-definitions/service-definition-set-helper'
|
||||
import { getBaseUrl } from '../constants'
|
||||
import Meta from './meta'
|
||||
import Header from './header'
|
||||
import Search from './search'
|
||||
import DonateBox from './donate'
|
||||
import { MarkupModal } from './markup-modal'
|
||||
import Usage from './usage'
|
||||
import Footer from './footer'
|
||||
import {
|
||||
Category,
|
||||
CategoryHeading,
|
||||
CategoryHeadings,
|
||||
CategoryNav,
|
||||
} from './category-headings'
|
||||
import { BadgeExamples } from './badge-examples'
|
||||
import { BaseFont, GlobalStyle } from './common'
|
||||
|
||||
const AppContainer = styled(BaseFont)`
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
// `pageContext` is the `context` passed to `createPage()` in
|
||||
// `gatsby-node.js`. In the case of the index page, `pageContext` is empty.
|
||||
interface PageContext {
|
||||
category?: Category
|
||||
}
|
||||
|
||||
export default function Main({
|
||||
pageContext,
|
||||
}: {
|
||||
pageContext: PageContext
|
||||
}): JSX.Element {
|
||||
const [searchIsInProgress, setSearchIsInProgress] = useState(false)
|
||||
const [queryIsTooShort, setQueryIsTooShort] = useState(false)
|
||||
const [searchResults, setSearchResults] = useState<{
|
||||
[k: string]: ServiceDefinition[]
|
||||
}>()
|
||||
const [selectedExample, setSelectedExample] = useState<RenderableExample>()
|
||||
const searchTimeout = useRef(0)
|
||||
const baseUrl = getBaseUrl()
|
||||
|
||||
const performSearch = React.useCallback(
|
||||
function (query: string): void {
|
||||
setSearchIsInProgress(false)
|
||||
|
||||
setQueryIsTooShort(query.length === 1)
|
||||
|
||||
if (query.length >= 2) {
|
||||
const flat = ServiceDefinitionSetHelper.create(services)
|
||||
.notDeprecated()
|
||||
.search(query)
|
||||
.toArray()
|
||||
setSearchResults(groupBy(flat, 'category'))
|
||||
} else {
|
||||
setSearchResults(undefined)
|
||||
}
|
||||
},
|
||||
[setSearchIsInProgress, setQueryIsTooShort, setSearchResults]
|
||||
)
|
||||
|
||||
const searchQueryChanged = React.useCallback(
|
||||
function (query: string): void {
|
||||
/*
|
||||
Add a small delay before showing search results
|
||||
so that we wait until the user has stopped typing
|
||||
before we start loading stuff.
|
||||
|
||||
This
|
||||
a) reduces the amount of badges we will load and
|
||||
b) stops the page from 'flashing' as the user types, like this:
|
||||
https://user-images.githubusercontent.com/7288322/42600206-9b278470-85b5-11e8-9f63-eb4a0c31cb4a.gif
|
||||
*/
|
||||
setSearchIsInProgress(true)
|
||||
window.clearTimeout(searchTimeout.current)
|
||||
searchTimeout.current = window.setTimeout(() => performSearch(query), 500)
|
||||
},
|
||||
[setSearchIsInProgress, performSearch]
|
||||
)
|
||||
|
||||
const dismissMarkupModal = React.useCallback(
|
||||
function (): void {
|
||||
setSelectedExample(undefined)
|
||||
},
|
||||
[setSelectedExample]
|
||||
)
|
||||
|
||||
function Category({
|
||||
category,
|
||||
definitions,
|
||||
}: {
|
||||
category: Category
|
||||
definitions: ServiceDefinition[]
|
||||
}): JSX.Element {
|
||||
const flattened = definitions.reduce((accum, current) => {
|
||||
const { examples } = current
|
||||
return accum.concat(examples)
|
||||
}, [] as Example[])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CategoryHeading category={category} />
|
||||
<BadgeExamples
|
||||
baseUrl={baseUrl}
|
||||
examples={flattened}
|
||||
onClick={setSelectedExample}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function renderMain(): JSX.Element | JSX.Element[] {
|
||||
const { category } = pageContext
|
||||
|
||||
if (searchIsInProgress) {
|
||||
return <div>searching...</div>
|
||||
} else if (queryIsTooShort) {
|
||||
return <div>Search term must have 2 or more characters</div>
|
||||
} else if (searchResults) {
|
||||
return Object.entries(searchResults).map(([categoryId, definitions]) => {
|
||||
const category = findCategory(categoryId)
|
||||
if (category === undefined) {
|
||||
throw Error(`Couldn't find category: ${categoryId}`)
|
||||
}
|
||||
return (
|
||||
<Category
|
||||
category={category}
|
||||
definitions={definitions}
|
||||
key={categoryId}
|
||||
/>
|
||||
)
|
||||
})
|
||||
} else if (category) {
|
||||
const definitions = ServiceDefinitionSetHelper.create(
|
||||
getDefinitionsForCategory(category.id)
|
||||
)
|
||||
.notDeprecated()
|
||||
.toArray()
|
||||
return (
|
||||
<div>
|
||||
<CategoryNav categories={categories} />
|
||||
<Category
|
||||
category={category}
|
||||
definitions={definitions}
|
||||
key={category.id}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return <CategoryHeadings categories={categories} />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AppContainer id="app">
|
||||
<GlobalStyle />
|
||||
<Meta />
|
||||
<Header />
|
||||
<MarkupModal
|
||||
baseUrl={baseUrl}
|
||||
example={selectedExample}
|
||||
onRequestClose={dismissMarkupModal}
|
||||
/>
|
||||
<section>
|
||||
<Search queryChanged={searchQueryChanged} />
|
||||
<DonateBox />
|
||||
</section>
|
||||
{renderMain()}
|
||||
<Usage baseUrl={baseUrl} />
|
||||
<Footer baseUrl={baseUrl} />
|
||||
</AppContainer>
|
||||
)
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
import React from 'react'
|
||||
import Modal from 'react-modal'
|
||||
import styled from 'styled-components'
|
||||
import { BaseFont } from '../common'
|
||||
import { RenderableExample } from '../../lib/service-definitions'
|
||||
import { MarkupModalContent } from './markup-modal-content'
|
||||
|
||||
const ContentContainer = styled(BaseFont)`
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
export function MarkupModal({
|
||||
example,
|
||||
baseUrl,
|
||||
onRequestClose,
|
||||
}: {
|
||||
example: RenderableExample | undefined
|
||||
baseUrl: string
|
||||
onRequestClose: () => void
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<Modal
|
||||
ariaHideApp={false}
|
||||
contentLabel="Example Modal"
|
||||
isOpen={example !== undefined}
|
||||
onRequestClose={onRequestClose}
|
||||
>
|
||||
{example !== undefined && (
|
||||
<ContentContainer>
|
||||
<MarkupModalContent baseUrl={baseUrl} example={example} />
|
||||
</ContentContainer>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Example, RenderableExample } from '../../lib/service-definitions'
|
||||
import { H3 } from '../common'
|
||||
import Customizer from '../customizer/customizer'
|
||||
|
||||
const Documentation = styled.div`
|
||||
max-width: 800px;
|
||||
margin: 35px auto 20px;
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
export function MarkupModalContent({
|
||||
example,
|
||||
baseUrl,
|
||||
}: {
|
||||
example: RenderableExample
|
||||
baseUrl: string
|
||||
}): JSX.Element {
|
||||
const { documentation } = example as Example
|
||||
|
||||
const {
|
||||
title,
|
||||
example: { pattern, namedParams, queryParams },
|
||||
preview: { style: initialStyle },
|
||||
} = example
|
||||
|
||||
return (
|
||||
<>
|
||||
<H3>{title}</H3>
|
||||
{documentation ? (
|
||||
<Documentation dangerouslySetInnerHTML={documentation} />
|
||||
) : null}
|
||||
<Customizer
|
||||
baseUrl={baseUrl}
|
||||
exampleNamedParams={namedParams}
|
||||
exampleQueryParams={queryParams}
|
||||
initialStyle={initialStyle}
|
||||
pattern={pattern}
|
||||
title={title}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
import favicon from '../images/favicon.png'
|
||||
import '@fontsource/lato'
|
||||
import '@fontsource/lekton'
|
||||
|
||||
const description = `We serve fast and scalable informational images as badges
|
||||
for GitHub, Travis CI, Jenkins, WordPress and many more services. Use them to
|
||||
track the state of your projects, or for promotional purposes.`
|
||||
|
||||
export default function Meta(): JSX.Element {
|
||||
return (
|
||||
<Helmet>
|
||||
<title>
|
||||
Shields.io: Quality metadata badges for open source projects
|
||||
</title>
|
||||
<meta charSet="utf-8" />
|
||||
<meta content="width=device-width,initial-scale=1" name="viewport" />
|
||||
<meta content={description} name="description" />
|
||||
<link href={favicon} rel="icon" type="image/png" />
|
||||
</Helmet>
|
||||
)
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
import React, { useRef, ChangeEvent } from 'react'
|
||||
import debounce from 'lodash.debounce'
|
||||
import { BlockInput } from './common'
|
||||
|
||||
export default function Search({
|
||||
queryChanged,
|
||||
}: {
|
||||
queryChanged: (query: string) => void
|
||||
}): JSX.Element {
|
||||
const queryChangedDebounced = useRef(
|
||||
debounce(queryChanged, 50, { leading: true })
|
||||
)
|
||||
|
||||
const onQueryChanged = React.useCallback(
|
||||
function ({
|
||||
target: { value: query },
|
||||
}: ChangeEvent<HTMLInputElement>): void {
|
||||
queryChangedDebounced.current(query)
|
||||
},
|
||||
[queryChangedDebounced]
|
||||
)
|
||||
|
||||
// TODO: Warning: A future version of React will block javascript: URLs as a security precaution
|
||||
// how else to do this?
|
||||
return (
|
||||
<section>
|
||||
<form action="javascript:void 0" autoComplete="off">
|
||||
<BlockInput
|
||||
autoComplete="off"
|
||||
onChange={onQueryChanged}
|
||||
placeholder="search"
|
||||
/>
|
||||
</form>
|
||||
</section>
|
||||
)
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
import React from 'react'
|
||||
import ClickToSelect from '@mapbox/react-click-to-select'
|
||||
import styled, { css } from 'styled-components'
|
||||
|
||||
interface CodeContainerProps {
|
||||
truncate?: boolean
|
||||
}
|
||||
|
||||
const CodeContainer = styled.span<CodeContainerProps>`
|
||||
position: relative;
|
||||
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
|
||||
${({ truncate }) =>
|
||||
truncate &&
|
||||
css`
|
||||
max-width: 40%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`}
|
||||
`
|
||||
|
||||
export interface StyledCodeProps {
|
||||
fontSize?: string
|
||||
}
|
||||
|
||||
export const StyledCode = styled.code<StyledCodeProps>`
|
||||
line-height: 1.2em;
|
||||
padding: 0.1em 0.3em;
|
||||
|
||||
border-radius: 4px;
|
||||
background: #eef;
|
||||
font-family: Lekton;
|
||||
|
||||
${({ fontSize }) =>
|
||||
fontSize &&
|
||||
css`
|
||||
font-size: ${fontSize};
|
||||
`}
|
||||
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
export function Snippet({
|
||||
snippet,
|
||||
truncate = false,
|
||||
fontSize,
|
||||
}: {
|
||||
snippet: string
|
||||
truncate?: boolean
|
||||
fontSize?: string
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<CodeContainer truncate={truncate}>
|
||||
<ClickToSelect>
|
||||
<StyledCode fontSize={fontSize}>{snippet}</StyledCode>
|
||||
</ClickToSelect>
|
||||
</CodeContainer>
|
||||
)
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
import React, { useState, ChangeEvent } from 'react'
|
||||
import { staticBadgeUrl } from '../../core/badge-urls/make-badge-url'
|
||||
import { InlineInput } from './common'
|
||||
|
||||
type StateKey = 'label' | 'message' | 'color'
|
||||
type State = Record<StateKey, string>
|
||||
|
||||
export default function StaticBadgeMaker({
|
||||
baseUrl = document.location.href,
|
||||
}: {
|
||||
baseUrl: string
|
||||
}): JSX.Element {
|
||||
const [values, setValues] = useState<State>({
|
||||
label: '',
|
||||
message: '',
|
||||
color: '',
|
||||
})
|
||||
|
||||
const isValid = values.message && values.color
|
||||
|
||||
const onChange = React.useCallback(
|
||||
function onChange({
|
||||
target: { name, value },
|
||||
}: ChangeEvent<HTMLInputElement | HTMLSelectElement>): void {
|
||||
setValues({
|
||||
...values,
|
||||
[name]: value,
|
||||
})
|
||||
},
|
||||
[setValues, values]
|
||||
)
|
||||
|
||||
const onSubmit = React.useCallback(
|
||||
function (e: React.FormEvent): void {
|
||||
e.preventDefault()
|
||||
|
||||
const { label, message, color } = values
|
||||
window.open(staticBadgeUrl({ baseUrl, label, message, color }), '_blank')
|
||||
},
|
||||
[baseUrl, values]
|
||||
)
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<InlineInput
|
||||
name="label"
|
||||
onChange={onChange}
|
||||
placeholder="label"
|
||||
value={values.label}
|
||||
/>
|
||||
<InlineInput
|
||||
name="message"
|
||||
onChange={onChange}
|
||||
placeholder="message"
|
||||
value={values.message}
|
||||
/>
|
||||
<InlineInput
|
||||
list="default-colors"
|
||||
name="color"
|
||||
onChange={onChange}
|
||||
placeholder="color"
|
||||
value={values.color}
|
||||
/>
|
||||
<datalist id="default-colors">
|
||||
<option value="brightgreen" />
|
||||
<option value="green" />
|
||||
<option value="yellowgreen" />
|
||||
<option value="yellow" />
|
||||
<option value="orange" />
|
||||
<option value="red" />
|
||||
<option value="lightgrey" />
|
||||
<option value="blue" />
|
||||
</datalist>
|
||||
<button disabled={!isValid}>Make Badge</button>
|
||||
</form>
|
||||
)
|
||||
}
|
@ -1,448 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'gatsby'
|
||||
import styled from 'styled-components'
|
||||
import { staticBadgeUrl } from '../../core/badge-urls/make-badge-url'
|
||||
import { advertisedStyles, shieldsLogos } from '../lib/supported-features'
|
||||
// ts-expect-error: because reasons?
|
||||
import StaticBadgeMaker from './static-badge-maker'
|
||||
import DynamicBadgeMaker from './dynamic-badge-maker'
|
||||
import { H2, H3, Badge, VerticalSpace } from './common'
|
||||
import { Snippet, StyledCode } from './snippet'
|
||||
|
||||
const LogoName = styled.span`
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const Lhs = styled.td`
|
||||
text-align: right;
|
||||
`
|
||||
|
||||
const EscapingRuleTable = styled.table`
|
||||
margin: auto;
|
||||
`
|
||||
|
||||
const QueryParamTable = styled.table`
|
||||
min-width: 50%;
|
||||
margin: auto;
|
||||
table-layout: fixed;
|
||||
border-spacing: 20px 10px;
|
||||
`
|
||||
|
||||
const QueryParamSyntax = styled.td`
|
||||
max-width: 300px;
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
const QueryParamDocumentation = styled.td`
|
||||
max-width: 600px;
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
function QueryParam({
|
||||
snippet,
|
||||
documentation,
|
||||
}: {
|
||||
snippet: string
|
||||
documentation: JSX.Element | JSX.Element[]
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<tr>
|
||||
<QueryParamSyntax>
|
||||
<Snippet snippet={snippet} />
|
||||
</QueryParamSyntax>
|
||||
<QueryParamDocumentation>{documentation}</QueryParamDocumentation>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
function EscapingConversion({
|
||||
lhs,
|
||||
rhs,
|
||||
}: {
|
||||
lhs: JSX.Element
|
||||
rhs: JSX.Element
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<tr>
|
||||
<Lhs>{lhs}</Lhs>
|
||||
<td>→</td>
|
||||
<td>{rhs}</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
function ColorExamples({
|
||||
baseUrl,
|
||||
colors,
|
||||
}: {
|
||||
baseUrl: string
|
||||
colors: string[]
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<span>
|
||||
{colors.map((color, i) => (
|
||||
<Badge
|
||||
alt={color}
|
||||
key={color}
|
||||
src={staticBadgeUrl({ baseUrl, label: '', message: color, color })}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function StyleExamples({ baseUrl }: { baseUrl: string }): JSX.Element {
|
||||
return (
|
||||
<QueryParamTable>
|
||||
<tbody>
|
||||
{advertisedStyles.map(style => {
|
||||
const snippet = `?style=${style}&logo=appveyor`
|
||||
const badgeUrl = staticBadgeUrl({
|
||||
baseUrl,
|
||||
label: 'style',
|
||||
message: style,
|
||||
color: 'green',
|
||||
namedLogo: 'appveyor',
|
||||
style,
|
||||
})
|
||||
return (
|
||||
<QueryParam
|
||||
documentation={<Badge alt={style} src={badgeUrl} />}
|
||||
key={style}
|
||||
snippet={snippet}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</QueryParamTable>
|
||||
)
|
||||
}
|
||||
|
||||
function NamedLogos(): JSX.Element {
|
||||
const renderLogo = (logo: string): JSX.Element => (
|
||||
<LogoName key={logo}>{logo}</LogoName>
|
||||
)
|
||||
const [first, ...rest] = shieldsLogos
|
||||
const result = ([renderLogo(first)] as (JSX.Element | string)[]).concat(
|
||||
rest.reduce(
|
||||
(result, logo) => result.concat([', ', renderLogo(logo)]),
|
||||
[] as (JSX.Element | string)[]
|
||||
)
|
||||
)
|
||||
return <>{result}</>
|
||||
}
|
||||
|
||||
function StaticBadgeEscapingRules(): JSX.Element {
|
||||
return (
|
||||
<EscapingRuleTable>
|
||||
<tbody>
|
||||
<EscapingConversion
|
||||
key="dashes"
|
||||
lhs={
|
||||
<span>
|
||||
Dashes <code>--</code>
|
||||
</span>
|
||||
}
|
||||
rhs={
|
||||
<span>
|
||||
<code>-</code> Dash
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<EscapingConversion
|
||||
key="underscores"
|
||||
lhs={
|
||||
<span>
|
||||
Underscores <code>__</code>
|
||||
</span>
|
||||
}
|
||||
rhs={
|
||||
<span>
|
||||
<code>_</code> Underscore
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<EscapingConversion
|
||||
key="spaces"
|
||||
lhs={
|
||||
<span>
|
||||
<code>_</code> or Space <code> </code>
|
||||
</span>
|
||||
}
|
||||
rhs={
|
||||
<span>
|
||||
<code> </code> Space
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</tbody>
|
||||
</EscapingRuleTable>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Usage({ baseUrl }: { baseUrl: string }): JSX.Element {
|
||||
return (
|
||||
<section>
|
||||
<H2 id="your-badge">Your Badge</H2>
|
||||
|
||||
<H3>Static</H3>
|
||||
<StaticBadgeMaker baseUrl={baseUrl} />
|
||||
|
||||
<VerticalSpace />
|
||||
|
||||
<p>Using dash "-" separator</p>
|
||||
<p>
|
||||
<Snippet snippet={`${baseUrl}/badge/<LABEL>-<MESSAGE>-<COLOR>`} />
|
||||
</p>
|
||||
<StaticBadgeEscapingRules />
|
||||
<p>Using query string parameters</p>
|
||||
<p>
|
||||
<Snippet
|
||||
snippet={`${baseUrl}/static/v1?label=<LABEL>&message=<MESSAGE>&color=<COLOR>`}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<H3 id="colors">Colors</H3>
|
||||
<p>
|
||||
<ColorExamples
|
||||
baseUrl={baseUrl}
|
||||
colors={[
|
||||
'brightgreen',
|
||||
'green',
|
||||
'yellowgreen',
|
||||
'yellow',
|
||||
'orange',
|
||||
'red',
|
||||
'blue',
|
||||
'lightgrey',
|
||||
]}
|
||||
/>
|
||||
<br />
|
||||
<ColorExamples
|
||||
baseUrl={baseUrl}
|
||||
colors={[
|
||||
'success',
|
||||
'important',
|
||||
'critical',
|
||||
'informational',
|
||||
'inactive',
|
||||
]}
|
||||
/>
|
||||
<br />
|
||||
<ColorExamples
|
||||
baseUrl={baseUrl}
|
||||
colors={['blueviolet', 'ff69b4', '9cf']}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<H3>Endpoint</H3>
|
||||
|
||||
<p>
|
||||
<Snippet snippet={`${baseUrl}/endpoint?url=<URL>&style<STYLE>`} />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Create badges from <Link to="/endpoint">your own JSON endpoint</Link>.
|
||||
</p>
|
||||
|
||||
<H3 id="dynamic-badge">Dynamic</H3>
|
||||
|
||||
<DynamicBadgeMaker baseUrl={baseUrl} />
|
||||
|
||||
<p>
|
||||
<StyledCode>
|
||||
{baseUrl}
|
||||
/badge/dynamic/json?url=<URL>&label=<LABEL>&query=<
|
||||
<a
|
||||
href="https://jsonpath.com"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
title="JSONPath syntax"
|
||||
>
|
||||
$.DATA.SUBDATA
|
||||
</a>
|
||||
>&color=<COLOR>&prefix=<PREFIX>&suffix=<SUFFIX>
|
||||
</StyledCode>
|
||||
</p>
|
||||
<p>
|
||||
<StyledCode>
|
||||
{baseUrl}
|
||||
/badge/dynamic/xml?url=<URL>&label=<LABEL>&query=<
|
||||
<a
|
||||
href="http://xpather.com"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
title="XPath syntax"
|
||||
>
|
||||
//data/subdata
|
||||
</a>
|
||||
>&color=<COLOR>&prefix=<PREFIX>&suffix=<SUFFIX>
|
||||
</StyledCode>
|
||||
</p>
|
||||
<p>
|
||||
<StyledCode>
|
||||
{baseUrl}
|
||||
/badge/dynamic/yaml?url=<URL>&label=<LABEL>&query=<
|
||||
<a
|
||||
href="https://jsonpath.com"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
title="YAML (JSONPath) syntax"
|
||||
>
|
||||
$.DATA.SUBDATA
|
||||
</a>
|
||||
>&color=<COLOR>&prefix=<PREFIX>&suffix=<SUFFIX>
|
||||
</StyledCode>
|
||||
</p>
|
||||
|
||||
<VerticalSpace />
|
||||
|
||||
<H2 id="styles">Styles</H2>
|
||||
|
||||
<p>
|
||||
The following styles are available. Flat is the default. Examples are
|
||||
shown with an optional logo:
|
||||
</p>
|
||||
<StyleExamples baseUrl={baseUrl} />
|
||||
|
||||
<p>
|
||||
Here are a few other parameters you can use: (connecting several with
|
||||
"&" is possible)
|
||||
</p>
|
||||
<QueryParamTable>
|
||||
<tbody>
|
||||
<QueryParam
|
||||
documentation={
|
||||
<span>
|
||||
Override the default left-hand-side text (
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding">
|
||||
URL-Encoding
|
||||
</a>
|
||||
{} needed for spaces or special characters!)
|
||||
</span>
|
||||
}
|
||||
key="label"
|
||||
snippet="?label=healthinesses"
|
||||
/>
|
||||
<QueryParam
|
||||
documentation={
|
||||
<span>
|
||||
Insert one of the named logos from (<NamedLogos />) or
|
||||
simple-icons. All simple-icons are referenced using icon slugs.
|
||||
You can click the icon title on{' '}
|
||||
<a
|
||||
href="https://simpleicons.org/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
simple-icons
|
||||
</a>{' '}
|
||||
to copy the slug or they can be found in the{' '}
|
||||
<a href="https://github.com/simple-icons/simple-icons/blob/master/slugs.md">
|
||||
slugs.md file
|
||||
</a>{' '}
|
||||
in the simple-icons repository.
|
||||
</span>
|
||||
}
|
||||
key="logo"
|
||||
snippet="?logo=appveyor"
|
||||
/>
|
||||
<QueryParam
|
||||
documentation={
|
||||
<span>
|
||||
Insert custom logo image (≥ 14px high). There is a limit on the
|
||||
total size of request headers we can accept (8192 bytes). From a
|
||||
practical perspective, this means the base64-encoded image text
|
||||
is limited to somewhere slightly under 8192 bytes depending on
|
||||
the rest of the request header.
|
||||
</span>
|
||||
}
|
||||
key="logoSvg"
|
||||
snippet="?logo=data:image/png;base64,…"
|
||||
/>
|
||||
<QueryParam
|
||||
documentation={
|
||||
<span>
|
||||
Set the color of the logo (hex, rgb, rgba, hsl, hsla and css
|
||||
named colors supported). Supported for named logos and Shields
|
||||
logos but not for custom logos. For multicolor Shields logos,
|
||||
the corresponding named logo will be used and colored.
|
||||
</span>
|
||||
}
|
||||
key="logoColor"
|
||||
snippet="?logoColor=violet"
|
||||
/>
|
||||
<QueryParam
|
||||
documentation={
|
||||
<span>Set the horizontal space to give to the logo</span>
|
||||
}
|
||||
key="logoWidth"
|
||||
snippet="?logoWidth=40"
|
||||
/>
|
||||
<QueryParam
|
||||
documentation={
|
||||
<span>
|
||||
Specify what clicking on the left/right of a badge should do.
|
||||
Note that this only works when integrating your badge in an
|
||||
<StyledCode><object></StyledCode> HTML tag, but not an
|
||||
<StyledCode><img></StyledCode> tag or a markup language.
|
||||
</span>
|
||||
}
|
||||
key="link"
|
||||
snippet="?link=http://left&link=http://right"
|
||||
/>
|
||||
<QueryParam
|
||||
documentation={
|
||||
<span>
|
||||
Set background of the left part (hex, rgb, rgba, hsl, hsla and
|
||||
css named colors supported). The legacy name "colorA" is also
|
||||
supported.
|
||||
</span>
|
||||
}
|
||||
key="labelColor"
|
||||
snippet="?labelColor=abcdef"
|
||||
/>
|
||||
<QueryParam
|
||||
documentation={
|
||||
<span>
|
||||
Set background of the right part (hex, rgb, rgba, hsl, hsla and
|
||||
css named colors supported). The legacy name "colorB" is also
|
||||
supported.
|
||||
</span>
|
||||
}
|
||||
key="color"
|
||||
snippet="?color=fedcba"
|
||||
/>
|
||||
<QueryParam
|
||||
documentation={
|
||||
<span>
|
||||
Set the HTTP cache lifetime (rules are applied to infer a
|
||||
default value on a per-badge basis, any values specified below
|
||||
the default will be ignored). The legacy name "maxAge" is also
|
||||
supported.
|
||||
</span>
|
||||
}
|
||||
key="cacheSeconds"
|
||||
snippet="?cacheSeconds=3600"
|
||||
/>
|
||||
</tbody>
|
||||
</QueryParamTable>
|
||||
|
||||
<p>
|
||||
We support <code>.svg</code> and <code>.json</code>. The default is{' '}
|
||||
<code>.svg</code>, which can be omitted from the URL.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
While we highly recommend using SVG, we also support <code>.png</code>{' '}
|
||||
for use cases where SVG will not work. These requests should be made to
|
||||
our raster server <code>https://raster.shields.io</code>. For example,
|
||||
the raster equivalent of{' '}
|
||||
<code>https://img.shields.io/npm/v/express</code> is{' '}
|
||||
<code>https://raster.shields.io/npm/v/express</code>. For backward
|
||||
compatibility, the badge server will redirect <code>.png</code> badges
|
||||
to the raster server.
|
||||
</p>
|
||||
</section>
|
||||
)
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
const baseUrl = process.env.GATSBY_BASE_URL
|
||||
|
||||
export function getBaseUrl(): string {
|
||||
if (baseUrl) {
|
||||
return baseUrl
|
||||
}
|
||||
|
||||
/*
|
||||
This is a special case for production.
|
||||
|
||||
We want to be able to build the front end with no value set for
|
||||
`GATSBY_BASE_URL` so that we can deploy a build to staging
|
||||
and then promote the exact same build to production.
|
||||
|
||||
When deployed to staging, we want the frontend on
|
||||
https://staging.shields.io/ to generate badges with the base
|
||||
https://staging.shields.io/
|
||||
|
||||
When we promote to production we want https://shields.io/ and
|
||||
https://www.shields.io/ to both generate badges with the base
|
||||
https://img.shields.io/
|
||||
*/
|
||||
try {
|
||||
const { protocol, hostname, port } = window.location
|
||||
if (['shields.io', 'www.shields.io'].includes(hostname)) {
|
||||
return 'https://img.shields.io'
|
||||
}
|
||||
if (!port) {
|
||||
return `${protocol}//${hostname}`
|
||||
}
|
||||
return `${protocol}//${hostname}:${port}`
|
||||
} catch (e) {
|
||||
// server-side rendering
|
||||
return ''
|
||||
}
|
||||
}
|
5
frontend/docs/intro.md
Normal file
5
frontend/docs/intro.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# TODO
|
122
frontend/docusaurus.config.cjs
Normal file
122
frontend/docusaurus.config.cjs
Normal file
@ -0,0 +1,122 @@
|
||||
const lightCodeTheme = require('prism-react-renderer/themes/github')
|
||||
const darkCodeTheme = require('prism-react-renderer/themes/dracula')
|
||||
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
title: 'Shields.io',
|
||||
tagline: 'Concise, consistent, and legible badges',
|
||||
url: 'https://shields.io',
|
||||
baseUrl: '/',
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
favicon: 'img/favicon.ico',
|
||||
organizationName: 'badges',
|
||||
projectName: 'shields',
|
||||
|
||||
themes: [
|
||||
[
|
||||
require.resolve('@easyops-cn/docusaurus-search-local'),
|
||||
/** @type {import("@easyops-cn/docusaurus-search-local").PluginOptions} */
|
||||
({
|
||||
hashed: true,
|
||||
indexPages: true,
|
||||
}),
|
||||
],
|
||||
],
|
||||
|
||||
presets: [
|
||||
[
|
||||
'docusaurus-preset-openapi',
|
||||
/** @type {import('docusaurus-preset-openapi').Options} */
|
||||
({
|
||||
docs: {
|
||||
sidebarPath: require.resolve('./sidebars.cjs'),
|
||||
editUrl: 'https://github.com/badges/shields/',
|
||||
},
|
||||
blog: {
|
||||
showReadingTime: true,
|
||||
editUrl: 'https://github.com/badges/shields/',
|
||||
},
|
||||
theme: {
|
||||
customCss: require.resolve('./src/css/custom.css'),
|
||||
},
|
||||
api: {
|
||||
path: 'categories',
|
||||
routeBasePath: 'badges',
|
||||
},
|
||||
}),
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig:
|
||||
/** @type {import('docusaurus-preset-openapi').ThemeConfig} */
|
||||
({
|
||||
languageTabs: [],
|
||||
navbar: {
|
||||
title: 'Shields.io',
|
||||
logo: {
|
||||
alt: 'Shields Logo',
|
||||
src: 'img/logo.png',
|
||||
},
|
||||
items: [
|
||||
{ to: '/badges', label: 'Badges', position: 'left' },
|
||||
{ to: '/community', label: 'Community', position: 'left' },
|
||||
{
|
||||
href: 'https://github.com/badges/shields',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
style: 'dark',
|
||||
links: [
|
||||
{
|
||||
title: 'Community',
|
||||
items: [
|
||||
{
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/badges/shields',
|
||||
},
|
||||
{
|
||||
label: 'Open Collective',
|
||||
href: 'https://opencollective.com/shields',
|
||||
},
|
||||
{
|
||||
label: 'Discord',
|
||||
href: 'https://discord.gg/HjJCwm5',
|
||||
},
|
||||
{
|
||||
label: 'Twitter',
|
||||
href: 'https://twitter.com/shields_io',
|
||||
},
|
||||
{
|
||||
label: 'Awesome Badges',
|
||||
href: 'https://github.com/badges/awesome-badges',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Stats',
|
||||
items: [
|
||||
{
|
||||
label: 'Service Status',
|
||||
href: 'https://stats.uptimerobot.com/PjXogHB5p',
|
||||
},
|
||||
{
|
||||
label: 'Metrics dashboard',
|
||||
href: 'https://metrics.shields.io/',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
copyright: `Copyright © ${new Date().getFullYear()} Shields.io. Built with Docusaurus.`,
|
||||
},
|
||||
prism: {
|
||||
theme: lightCodeTheme,
|
||||
darkTheme: darkCodeTheme,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
module.exports = config
|
@ -1,18 +0,0 @@
|
||||
import redirectLegacyRoutes from './lib/redirect-legacy-routes'
|
||||
|
||||
// Adapted from https://github.com/gatsbyjs/gatsby/issues/8413
|
||||
function scrollToElementId(id) {
|
||||
const el = document.querySelector(id)
|
||||
if (el) {
|
||||
return window.scrollTo(0, el.offsetTop - 20)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function onRouteUpdate({ location: { hash } }) {
|
||||
if (hash) {
|
||||
redirectLegacyRoutes()
|
||||
window.setTimeout(() => scrollToElementId(hash), 10)
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
siteMetadata: {
|
||||
title: 'Shields.io: Quality metadata badges for open source projects',
|
||||
description:
|
||||
'We serve fast and scalable informational images as badges for GitHub, Travis CI, Jenkins, WordPress and many more services. Use them to track the state of your projects, or for promotional purposes.',
|
||||
author: '@shields_io',
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
resolve: 'gatsby-plugin-page-creator',
|
||||
options: {
|
||||
path: path.join(__dirname, 'pages'),
|
||||
},
|
||||
},
|
||||
'gatsby-plugin-react-helmet',
|
||||
'gatsby-plugin-catch-links',
|
||||
'gatsby-plugin-styled-components',
|
||||
'gatsby-plugin-remove-trailing-slashes',
|
||||
'gatsby-plugin-typescript',
|
||||
// This currently is not being used.
|
||||
// {
|
||||
// resolve: 'gatsby-source-filesystem',
|
||||
// options: {
|
||||
// name: 'static',
|
||||
// path: `${__dirname}/frontend/static`,
|
||||
// },
|
||||
// },
|
||||
],
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
/*
|
||||
* Implement Gatsby's Node APIs in this file.
|
||||
*
|
||||
* See: https://www.gatsbyjs.org/docs/node-apis/
|
||||
*/
|
||||
|
||||
const fs = require('fs')
|
||||
const yaml = require('js-yaml')
|
||||
const envFlag = require('node-env-flag')
|
||||
|
||||
const includeDevPages = envFlag(process.env.INCLUDE_DEV_PAGES, true)
|
||||
|
||||
const { categories } = yaml.load(
|
||||
fs.readFileSync('./service-definitions.yml', 'utf8')
|
||||
)
|
||||
|
||||
// Often in Gatsby context gets piped through GraphQL, but GraphQL adds
|
||||
// unnecessary complexity here, so this uses the programmatic API.
|
||||
// https://www.gatsbyjs.org/docs/using-gatsby-without-graphql/#the-approach-fetch-data-and-use-gatsbys-createpages-api
|
||||
async function createPages({ actions: { createPage } }) {
|
||||
if (includeDevPages) {
|
||||
createPage({
|
||||
path: '/dev/styles',
|
||||
component: require.resolve('./components/development/style-page.tsx'),
|
||||
})
|
||||
createPage({
|
||||
path: '/dev/logos',
|
||||
component: require.resolve('./components/development/logo-page.tsx'),
|
||||
})
|
||||
}
|
||||
|
||||
categories.forEach(category => {
|
||||
const { id } = category
|
||||
createPage({
|
||||
path: `/category/${id}`,
|
||||
component: require.resolve('./components/main.tsx'),
|
||||
// `context` provided here becomes `props.pageContext` on the page.
|
||||
context: { category },
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { createPages }
|
Binary file not shown.
Before Width: | Height: | Size: 332 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="198" height="58"><rect rx="8" x="140" width="55" height="58"/><g stroke="#000" stroke-width="8"><path d="M135.5 54a8 8 0 0 0 8.5 -8.5"/><rect x="4" y="4" rx="8" width="190" height="50" fill="none"/></g><path d="m23.906 33.641c.953-.083 1.906-.167 2.859-.25.108 2.099 1.511 4.139 3.578 4.722 2.438.895 5.357.799 7.559-.658 1.49-1.129 1.861-3.674.324-4.925-1.557-1.322-3.685-1.504-5.576-2.057-2.343-.565-4.912-1.133-6.611-2.979-1.805-2.088-1.627-5.485.292-7.443 2.041-2.113 5.222-2.55 8.02-2.274 2.46.244 5.058 1.343 6.252 3.635.426.908 1.095 2.241.656 3.108-.888.173-1.81.148-2.715.245-.077-2.084-1.727-4.073-3.863-4.234-1.902-.317-4.02-.252-5.691.802-1.398.989-1.849 3.363-.381 4.494 1.281 1.01 2.962 1.199 4.482 1.642 2.66.627 5.602 1.118 7.596 3.158 2 2.188 1.893 5.84-.088 8.01-2.01 2.32-5.304 2.972-8.237 2.713-2.585-.147-5.319-1.024-6.916-3.184-.987-1.288-1.517-2.905-1.542-4.523"/><path d="m45.953 41c0-7.635 0-15.271 0-22.906.938 0 1.875 0 2.813 0 0 2.74 0 5.479 0 8.219 1.391-1.721 3.69-2.523 5.86-2.236 1.975.154 4.03 1.371 4.513 3.402.504 1.973.278 4.02.33 6.04 0 2.495 0 4.989 0 7.484-.938 0-1.875 0-2.813 0-.009-3.675.018-7.351-.014-11.03-.026-1.342-.627-2.835-2-3.282-2.187-.802-5.077.393-5.609 2.773-.417 1.764-.216 3.586-.264 5.381 0 2.051 0 4.102 0 6.153-.938 0-1.875 0-2.813 0"/><path d="m63.781 21.328v-3.234h2.813v3.234zm0 19.672v-16.594h2.813v16.594z"/><path d="m82.25 35.656c.969.12 1.938.24 2.906.359-.702 3.464-4.348 5.767-7.781 5.386-3.235-.066-6.43-2.328-7.06-5.598-.843-3.307-.404-7.285 2.101-9.784 3.082-3 8.699-2.618 11.235.892 1.374 1.85 1.676 4.267 1.578 6.51-4.125 0-8.25 0-12.375 0-.142 2.889 2.267 6 5.346 5.658 1.881-.162 3.613-1.566 4.045-3.423m-9.234-4.547c3.089 0 6.177 0 9.266 0 .129-2.774-2.616-5.422-5.419-4.713-2.174.427-3.912 2.474-3.846 4.713"/><path d="m88.64 41v-22.906h2.813v22.906z"/><path d="m106.59 41c0-.698 0-1.396 0-2.094-1.412 2.442-4.776 3.067-7.233 1.949-2.378-1.02-3.971-3.403-4.345-5.924-.507-2.761-.123-5.768 1.389-8.167 1.863-2.705 5.968-3.642 8.711-1.741.422.228 1.028 1.144 1.294 1.018-.006-2.649-.0001-5.298-.003-7.948.932 0 1.865 0 2.797 0 0 7.635 0 15.271 0 22.906-.87 0-1.74 0-2.61 0m-8.89-8.281c-.075 2.246.637 4.861 2.79 5.952 2 1.023 4.682-.047 5.488-2.134.897-1.996.746-4.278.388-6.382-.425-1.95-2.046-3.804-4.158-3.805-1.903-.065-3.633 1.363-4.099 3.181-.327 1.028-.394 2.116-.408 3.188"/><path d="m112.52 36.05c.927-.146 1.854-.292 2.781-.438.126 1.69 1.513 3.244 3.239 3.365 1.398.212 3.01.12 4.12-.851.807-.749 1.1-2.243.159-3.01-.908-.723-2.115-.812-3.182-1.172-1.797-.485-3.713-.848-5.243-1.97-1.83-1.551-1.868-4.679-.099-6.293 1.577-1.507 3.918-1.784 6-1.594 1.685.176 3.54.749 4.535 2.217.464.715.708 1.549.844 2.384-.917.125-1.833.25-2.75.375-.121-1.569-1.653-2.762-3.19-2.695-1.246-.082-2.702.012-3.608.982-.624.724-.543 1.971.314 2.481.998.706 2.269.757 3.389 1.173 1.754.512 3.647.848 5.141 1.965 1.686 1.476 1.728 4.244.396 5.966-1.298 1.788-3.597 2.417-5.709 2.448-1.466-.007-2.984-.214-4.299-.893-1.599-.909-2.585-2.655-2.84-4.444"/><g fill="#fff"><path d="m151.11 41v-22.906h3.03v22.906z"/><path d="m158.55 29.844c-.277-4.765 2.335-9.977 7.05-11.551 4.902-1.757 11.226.197 13.477 5.098 2.266 4.706 1.89 10.92-1.767 14.833-4.554 4.948-13.81 3.976-17.08-1.954-1.111-1.946-1.679-4.188-1.68-6.426m3.125.047c-.377 4.273 2.892 8.844 7.375 8.951 3.791.221 7.557-2.653 7.997-6.497.794-3.731.139-8.292-3.107-10.696-3.788-2.814-10.05-1.104-11.591 3.444-.54 1.539-.642 3.181-.675 4.798"/></g></svg>
|
Before Width: | Height: | Size: 3.4 KiB |
@ -1,72 +0,0 @@
|
||||
import { test, given } from 'sazerac'
|
||||
import {
|
||||
bareLink,
|
||||
html,
|
||||
markdown,
|
||||
reStructuredText,
|
||||
renderAsciiDocAttributes,
|
||||
asciiDoc,
|
||||
generateMarkup,
|
||||
} from './generate-image-markup'
|
||||
|
||||
test(bareLink, () => {
|
||||
given(
|
||||
'https://img.shields.io/badge',
|
||||
'https://example.com/example',
|
||||
'Example'
|
||||
).expect('https://img.shields.io/badge')
|
||||
})
|
||||
|
||||
test(html, () => {
|
||||
given('https://img.shields.io/badge', 'Example').expect(
|
||||
'<img alt="Example" src="https://img.shields.io/badge">'
|
||||
)
|
||||
given('https://img.shields.io/badge', undefined).expect(
|
||||
'<img src="https://img.shields.io/badge">'
|
||||
)
|
||||
})
|
||||
|
||||
test(markdown, () => {
|
||||
given('https://img.shields.io/badge', 'Example').expect(
|
||||
''
|
||||
)
|
||||
given('https://img.shields.io/badge', undefined).expect(
|
||||
''
|
||||
)
|
||||
})
|
||||
|
||||
test(reStructuredText, () => {
|
||||
given('https://img.shields.io/badge', undefined).expect(
|
||||
'.. image:: https://img.shields.io/badge'
|
||||
)
|
||||
given('https://img.shields.io/badge', 'Example').expect(
|
||||
'.. image:: https://img.shields.io/badge\n :alt: Example'
|
||||
)
|
||||
})
|
||||
|
||||
test(renderAsciiDocAttributes, () => {
|
||||
given(['abc', '123'], {}).expect('[abc,123]')
|
||||
given(['abc', '123', null], { foo: 'def', bar: 'hello, world!' }).expect(
|
||||
'["abc","123",None,foo="def",bar="hello, world!"]'
|
||||
)
|
||||
})
|
||||
|
||||
test(asciiDoc, () => {
|
||||
given('https://img.shields.io/badge', undefined).expect(
|
||||
'image:https://img.shields.io/badge[]'
|
||||
)
|
||||
given('https://img.shields.io/badge', 'Example').expect(
|
||||
'image:https://img.shields.io/badge[Example]'
|
||||
)
|
||||
given('https://img.shields.io/badge', 'Example, with comma').expect(
|
||||
'image:https://img.shields.io/badge["Example, with comma"]'
|
||||
)
|
||||
})
|
||||
|
||||
test(generateMarkup, () => {
|
||||
given({
|
||||
badgeUrl: 'https://img.shields.io/badge',
|
||||
title: 'Example',
|
||||
markupFormat: 'markdown',
|
||||
}).expect('')
|
||||
})
|
@ -1,99 +0,0 @@
|
||||
export function bareLink(badgeUrl: string, link?: string, title = ''): string {
|
||||
return badgeUrl
|
||||
}
|
||||
|
||||
export function html(badgeUrl: string, title?: string): string {
|
||||
// To be more robust, this should escape the title.
|
||||
const alt = title ? ` alt="${title}"` : ''
|
||||
return `<img${alt} src="${badgeUrl}">`
|
||||
}
|
||||
|
||||
export function markdown(badgeUrl: string, title?: string): string {
|
||||
return ``
|
||||
}
|
||||
|
||||
export function reStructuredText(badgeUrl: string, title?: string): string {
|
||||
let result = `.. image:: ${badgeUrl}`
|
||||
if (title) {
|
||||
result += `\n :alt: ${title}`
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function quoteAsciiDocAttribute(attr: string | null): string {
|
||||
if (attr == null) {
|
||||
return 'None'
|
||||
} else {
|
||||
// String values are prepared and returned to users who want to include their badge
|
||||
// in an AsciiDoc document. We're not using the value in any actual processing, so
|
||||
// no need to perform proper sanitization. We simply escape quotes, as mandated by
|
||||
// http://asciidoc.org/userguide.html#X21
|
||||
const withQuotesEscaped = attr.replace(/"/g, '\\"') // lgtm [js/incomplete-sanitization]
|
||||
return `"${withQuotesEscaped}"`
|
||||
}
|
||||
}
|
||||
|
||||
// lodash.mapvalues is huge!
|
||||
function mapValues(
|
||||
obj: { [k: string]: string | null },
|
||||
iteratee: (value: string | null) => string
|
||||
): { [k: string]: string } {
|
||||
const result = {} as { [k: string]: string }
|
||||
for (const k in obj) {
|
||||
result[k] = iteratee(obj[k])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function renderAsciiDocAttributes(
|
||||
positional: string[],
|
||||
named: { [k: string]: string | null }
|
||||
): string {
|
||||
// http://asciidoc.org/userguide.html#X21
|
||||
const needsQuoting =
|
||||
positional.some(attr => attr && attr.includes(',')) ||
|
||||
Object.keys(named).length > 0
|
||||
|
||||
if (needsQuoting) {
|
||||
positional = positional.map(attr => quoteAsciiDocAttribute(attr))
|
||||
named = mapValues(named, attr => quoteAsciiDocAttribute(attr))
|
||||
}
|
||||
|
||||
const items = positional.concat(
|
||||
Object.entries(named).map(([k, v]) => `${k}=${v}`)
|
||||
)
|
||||
|
||||
if (items.length) {
|
||||
return `[${items.join(',')}]`
|
||||
} else {
|
||||
return '[]'
|
||||
}
|
||||
}
|
||||
|
||||
export function asciiDoc(badgeUrl: string, title?: string): string {
|
||||
const positional = title ? [title] : []
|
||||
const named = {} as { [k: string]: string }
|
||||
const attrs = renderAsciiDocAttributes(positional, named)
|
||||
return `image:${badgeUrl}${attrs}`
|
||||
}
|
||||
|
||||
export type MarkupFormat = 'markdown' | 'rst' | 'asciidoc' | 'link' | 'html'
|
||||
|
||||
export function generateMarkup({
|
||||
badgeUrl,
|
||||
title,
|
||||
markupFormat,
|
||||
}: {
|
||||
badgeUrl: string
|
||||
title?: string
|
||||
markupFormat: MarkupFormat
|
||||
}): string {
|
||||
const generatorFn = {
|
||||
markdown,
|
||||
rst: reStructuredText,
|
||||
asciidoc: asciiDoc,
|
||||
link: bareLink,
|
||||
html,
|
||||
}[markupFormat]
|
||||
return generatorFn(badgeUrl, title)
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import { test, given } from 'sazerac'
|
||||
import { patternToOptions, removeRegexpFromPattern } from './pattern-helpers'
|
||||
|
||||
describe('Badge URL functions', function () {
|
||||
test(patternToOptions, () => {
|
||||
given('[^\\/]+?').expect(undefined)
|
||||
given('abc|[^\\/]+').expect(undefined)
|
||||
given('abc|def|ghi').expect(['abc', 'def', 'ghi'])
|
||||
})
|
||||
|
||||
test(removeRegexpFromPattern, () => {
|
||||
given('/appveyor/ci/:user/:repo').expect('/appveyor/ci/:user/:repo')
|
||||
given('/discourse/:scheme(http|https)/:host/topics').expect(
|
||||
'/discourse/:scheme/:host/topics'
|
||||
)
|
||||
given('/github/size/:user/:repo/:path*').expect(
|
||||
'/github/size/:user/:repo/:path*'
|
||||
)
|
||||
given('/microbadger/image-size/image-size/:imageId+').expect(
|
||||
'/microbadger/image-size/image-size/:imageId+'
|
||||
)
|
||||
given('/node/v/@:scope/:packageName').expect('/node/v/@:scope/:packageName')
|
||||
given('/ubuntu/v/:packageName/:series?').expect(
|
||||
'/ubuntu/v/:packageName/:series?'
|
||||
)
|
||||
given('/:foo/(.*)').expect('/:foo/(.*)')
|
||||
})
|
||||
})
|
@ -1,34 +0,0 @@
|
||||
import { parse } from 'path-to-regexp'
|
||||
|
||||
// Given a patternToRegex `pattern` with multiple-choice options like
|
||||
// `foo|bar|baz`, return an array with the options. If it can't be described
|
||||
// as multiple-choice options, return `undefined`.
|
||||
const basicChars = /^[A-za-z0-9-]+$/
|
||||
export function patternToOptions(pattern: string): string[] | undefined {
|
||||
const split = pattern.split('|')
|
||||
if (split.some(part => !part.match(basicChars))) {
|
||||
return undefined
|
||||
} else {
|
||||
return split
|
||||
}
|
||||
}
|
||||
|
||||
// Removes regexp for named parameters.
|
||||
export function removeRegexpFromPattern(pattern: string): string {
|
||||
const tokens = parse(pattern)
|
||||
const simplePattern = tokens
|
||||
.map(token => {
|
||||
if (typeof token === 'string') {
|
||||
return token
|
||||
} else {
|
||||
const { prefix, modifier, name, pattern } = token
|
||||
if (typeof name === 'number') {
|
||||
return `${prefix}(${pattern})`
|
||||
} else {
|
||||
return `${prefix}:${name}${modifier}`
|
||||
}
|
||||
}
|
||||
})
|
||||
.join('')
|
||||
return simplePattern
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import { navigate } from 'gatsby'
|
||||
|
||||
export default function redirectLegacyRoutes(): void {
|
||||
const { hash } = window.location
|
||||
if (hash && hash.startsWith('#/examples/')) {
|
||||
const category = hash.replace('#/examples/', '')
|
||||
navigate(`category/${category}`, {
|
||||
replace: true,
|
||||
})
|
||||
} else if (hash === '#/endpoint') {
|
||||
navigate('endpoint', {
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { expect } from 'chai'
|
||||
import { test, given } from 'sazerac'
|
||||
import { findCategory, getDefinitionsForCategory } from '.'
|
||||
|
||||
describe('Service definition helpers', function () {
|
||||
test(findCategory, () => {
|
||||
given('build').expect({ id: 'build', name: 'Build', keywords: ['build'] })
|
||||
given('foo').expect(undefined)
|
||||
})
|
||||
|
||||
it('getDefinitionsForCategory', function () {
|
||||
expect(getDefinitionsForCategory('build'))
|
||||
.to.have.length.greaterThan(10)
|
||||
.and.lessThan(75)
|
||||
})
|
||||
})
|
@ -1,67 +0,0 @@
|
||||
import groupBy from 'lodash.groupby'
|
||||
// load using js-yaml-loader
|
||||
import definitions from '../../service-definitions.yml'
|
||||
|
||||
export interface Category {
|
||||
id: string
|
||||
name: string
|
||||
keywords: string[]
|
||||
}
|
||||
|
||||
export interface ExampleSignature {
|
||||
pattern: string
|
||||
namedParams: { [k: string]: string }
|
||||
queryParams: { [k: string]: string }
|
||||
}
|
||||
|
||||
export interface Preview {
|
||||
label?: string
|
||||
message: string
|
||||
color: string
|
||||
style?: string
|
||||
namedLogo?: string
|
||||
}
|
||||
|
||||
export interface Example {
|
||||
title: string
|
||||
example: ExampleSignature
|
||||
preview: Preview
|
||||
keywords: string[]
|
||||
documentation?: {
|
||||
__html: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface Route {
|
||||
pattern: string
|
||||
queryParams: string[]
|
||||
}
|
||||
|
||||
export interface LegacyRoute {
|
||||
format: string
|
||||
queryParams: string[]
|
||||
}
|
||||
|
||||
export interface ServiceDefinition {
|
||||
category: string
|
||||
name: string
|
||||
isDeprecated: boolean
|
||||
route: Route | LegacyRoute
|
||||
examples: Example[]
|
||||
}
|
||||
|
||||
export const services = definitions.services as ServiceDefinition[]
|
||||
export const categories = definitions.categories as Category[]
|
||||
|
||||
export function findCategory(category: string): Category | undefined {
|
||||
return categories.find(({ id }) => id === category)
|
||||
}
|
||||
|
||||
const byCategory = groupBy(services, 'category')
|
||||
export function getDefinitionsForCategory(
|
||||
category: string
|
||||
): ServiceDefinition[] {
|
||||
return byCategory[category] || []
|
||||
}
|
||||
|
||||
export type RenderableExample = Example
|
@ -1,31 +0,0 @@
|
||||
import { test, given, forCases } from 'sazerac'
|
||||
import { predicateFromQuery } from './service-definition-set-helper'
|
||||
import { Example } from '.'
|
||||
|
||||
describe('Badge example functions', function () {
|
||||
function exampleMatchesQuery(
|
||||
{ examples }: { examples: Example[] },
|
||||
query: string
|
||||
): boolean {
|
||||
return predicateFromQuery(query)({ examples })
|
||||
}
|
||||
|
||||
test(exampleMatchesQuery, () => {
|
||||
forCases([given({ examples: [{ title: 'node version' }] }, 'npm')]).expect(
|
||||
false
|
||||
)
|
||||
|
||||
forCases([
|
||||
given(
|
||||
{ examples: [{ title: 'node version', keywords: ['npm'] }] },
|
||||
'node'
|
||||
),
|
||||
given(
|
||||
{ examples: [{ title: 'node version', keywords: ['npm'] }] },
|
||||
'npm'
|
||||
),
|
||||
// https://github.com/badges/shields/issues/1578
|
||||
given({ examples: [{ title: 'c++ is the best language' }] }, 'c++'),
|
||||
]).expect(true)
|
||||
})
|
||||
})
|
@ -1,54 +0,0 @@
|
||||
import escapeStringRegexp from 'escape-string-regexp'
|
||||
import { Example, ServiceDefinition } from '.'
|
||||
|
||||
export function exampleMatchesRegex(example: Example, regex: RegExp): boolean {
|
||||
const { title, keywords } = example
|
||||
const haystack = [title].concat(keywords).join(' ')
|
||||
return regex.test(haystack)
|
||||
}
|
||||
|
||||
export function predicateFromQuery(
|
||||
query: string
|
||||
): ({ examples }: { examples: Example[] }) => boolean {
|
||||
const escaped = escapeStringRegexp(query)
|
||||
const regex = new RegExp(escaped, 'i') // Case-insensitive.
|
||||
return ({ examples }: { examples: Example[] }) =>
|
||||
examples.some(example => exampleMatchesRegex(example, regex))
|
||||
}
|
||||
|
||||
export default class ServiceDefinitionSetHelper {
|
||||
private readonly definitionData: ServiceDefinition[]
|
||||
|
||||
public constructor(definitionData: ServiceDefinition[]) {
|
||||
this.definitionData = definitionData
|
||||
}
|
||||
|
||||
public static create(
|
||||
definitionData: ServiceDefinition[]
|
||||
): ServiceDefinitionSetHelper {
|
||||
return new ServiceDefinitionSetHelper(definitionData)
|
||||
}
|
||||
|
||||
public getCategory(wantedCategory: string): ServiceDefinitionSetHelper {
|
||||
return ServiceDefinitionSetHelper.create(
|
||||
this.definitionData.filter(({ category }) => category === wantedCategory)
|
||||
)
|
||||
}
|
||||
|
||||
public search(query: string): ServiceDefinitionSetHelper {
|
||||
const predicate = predicateFromQuery(query)
|
||||
return ServiceDefinitionSetHelper.create(
|
||||
this.definitionData.filter(predicate)
|
||||
)
|
||||
}
|
||||
|
||||
public notDeprecated(): ServiceDefinitionSetHelper {
|
||||
return ServiceDefinitionSetHelper.create(
|
||||
this.definitionData.filter(({ isDeprecated }) => !isDeprecated)
|
||||
)
|
||||
}
|
||||
|
||||
public toArray(): ServiceDefinition[] {
|
||||
return this.definitionData
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import supportedFeatures from '../supported-features.json'
|
||||
|
||||
export const shieldsLogos = supportedFeatures.shieldsLogos as string[]
|
||||
export const simpleIcons = supportedFeatures.simpleIcons as string[]
|
||||
export const advertisedStyles = supportedFeatures.advertisedStyles as string[]
|
@ -12,21 +12,5 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo 'Run tests from parent dir'; false"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gatsby": "*"
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
[
|
||||
"inline-react-svg",
|
||||
{
|
||||
"svgo": false
|
||||
}
|
||||
]
|
||||
],
|
||||
"presets": [
|
||||
"babel-preset-gatsby"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -1,118 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { getBaseUrl } from '../constants'
|
||||
import Meta from '../components/meta'
|
||||
import Header from '../components/header'
|
||||
import Footer from '../components/footer'
|
||||
import { BaseFont, GlobalStyle, H3 } from '../components/common'
|
||||
import NodePing from '../../static/images/nodeping.svg'
|
||||
import Sentry from '../../static/images/sentry-logo-black.svg'
|
||||
const MainContainer = styled(BaseFont)`
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const SponsorContainer = styled.div`
|
||||
display: block;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
padding-top: 20px;
|
||||
`
|
||||
|
||||
const SponsorItems = styled.div`
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
export default function SponsorsPage(): JSX.Element {
|
||||
const baseUrl = getBaseUrl()
|
||||
return (
|
||||
<MainContainer>
|
||||
<GlobalStyle />
|
||||
<Meta />
|
||||
<Header />
|
||||
<H3>Community</H3>
|
||||
|
||||
<SponsorContainer>
|
||||
Shields.io is possible thanks to the people and companies who donate
|
||||
money, services or time to keep the project running.
|
||||
</SponsorContainer>
|
||||
|
||||
<SponsorContainer>
|
||||
<h4>Sponsors</h4>
|
||||
❤️ These companies help us by donating their services to shields:
|
||||
<ul style={{ listStyleType: 'none' }}>
|
||||
<SponsorItems>
|
||||
<li>
|
||||
<a href="https://nodeping.com/">
|
||||
<NodePing alt="nodeping_logo" height={60} />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://sentry.io/">
|
||||
<Sentry alt="sentry_logo" height={100} />
|
||||
</a>
|
||||
</li>
|
||||
</SponsorItems>
|
||||
</ul>
|
||||
💵 These organisations help keep shields running by donating on
|
||||
OpenCollective. Your organisation can support this project by{' '}
|
||||
<a href="https://opencollective.com/shields#sponsor">
|
||||
becoming a sponsor
|
||||
</a>
|
||||
. Your logo will show up here with a link to your website.
|
||||
<p>
|
||||
<object data="https://opencollective.com/shields/sponsors.svg?avatarHeight=80&width=600" />
|
||||
</p>
|
||||
</SponsorContainer>
|
||||
|
||||
<SponsorContainer>
|
||||
<h4>Backers</h4>
|
||||
💵 Thank you to all our backers who help keep shields running by
|
||||
donating on OpenCollective. You can support this project by{' '}
|
||||
<a href="https://opencollective.com/shields#backer">
|
||||
becoming a backer
|
||||
</a>
|
||||
.
|
||||
<p>
|
||||
<object data="https://opencollective.com/shields/backers.svg?width=600" />
|
||||
</p>
|
||||
</SponsorContainer>
|
||||
|
||||
<SponsorContainer>
|
||||
<h4>Contributors</h4>
|
||||
🙏 This project exists thanks to all the nice people who contribute
|
||||
their time to work on the project.
|
||||
<p>
|
||||
<object data="https://opencollective.com/shields/contributors.svg?width=600" />
|
||||
</p>
|
||||
</SponsorContainer>
|
||||
|
||||
<SponsorContainer>
|
||||
✨ Shields is helped by these companies which provide a free plan for
|
||||
their product or service:
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://coveralls.io/">Coveralls</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://circleci.com/">CircleCI</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.cloudflare.com/">Cloudflare</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://discord.com/">Discord</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/">GitHub</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://uptimerobot.com/">Uptime Robot</a>
|
||||
</li>
|
||||
</ul>
|
||||
</SponsorContainer>
|
||||
|
||||
<Footer baseUrl={baseUrl} />
|
||||
</MainContainer>
|
||||
)
|
||||
}
|
@ -1,255 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled, { css } from 'styled-components'
|
||||
import { staticBadgeUrl } from '../../core/badge-urls/make-badge-url'
|
||||
import { getBaseUrl } from '../constants'
|
||||
import Meta from '../components/meta'
|
||||
import Header from '../components/header'
|
||||
import Footer from '../components/footer'
|
||||
import { BaseFont, GlobalStyle, H3, Badge } from '../components/common'
|
||||
import { Snippet } from '../components/snippet'
|
||||
import Customizer from '../components/customizer/customizer'
|
||||
|
||||
const MainContainer = styled(BaseFont)`
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const Explanation = styled.div`
|
||||
display: block;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
interface JsonExampleBlockProps {
|
||||
fontSize?: string
|
||||
}
|
||||
|
||||
const JsonExampleBlock = styled.code<JsonExampleBlockProps>`
|
||||
display: inline-block;
|
||||
|
||||
text-align: left;
|
||||
line-height: 1.2em;
|
||||
padding: 16px 18px;
|
||||
|
||||
border-radius: 4px;
|
||||
background: #eef;
|
||||
|
||||
font-family: Lekton;
|
||||
${({ fontSize }) =>
|
||||
css`
|
||||
font-size: ${fontSize};
|
||||
`};
|
||||
|
||||
white-space: pre;
|
||||
`
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, react/prop-types
|
||||
function JsonExample({ data }: { [k: string]: any }): JSX.Element {
|
||||
return (
|
||||
<JsonExampleBlock>{JSON.stringify(data, undefined, 2)}</JsonExampleBlock>
|
||||
)
|
||||
}
|
||||
|
||||
const Schema = styled.dl`
|
||||
display: inline-block;
|
||||
max-width: 800px;
|
||||
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
|
||||
background: #efefef;
|
||||
|
||||
clear: both;
|
||||
overflow: hidden;
|
||||
|
||||
dt,
|
||||
dd {
|
||||
padding: 0 1%;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
dt {
|
||||
width: 100px;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left: 20px;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.data_table {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function EndpointPage(): JSX.Element {
|
||||
const baseUrl = getBaseUrl()
|
||||
return (
|
||||
<MainContainer>
|
||||
<GlobalStyle />
|
||||
<Meta />
|
||||
<Header />
|
||||
<H3>Endpoint</H3>
|
||||
<Snippet snippet={`${baseUrl}/endpoint?url=...&style=...`} />
|
||||
<p>Endpoint response:</p>
|
||||
<JsonExample
|
||||
data={{
|
||||
schemaVersion: 1,
|
||||
label: 'hello',
|
||||
message: 'sweet world',
|
||||
color: 'orange',
|
||||
}}
|
||||
/>
|
||||
<p>Shields response:</p>
|
||||
<Badge
|
||||
alt="hello | sweet world"
|
||||
src={staticBadgeUrl({
|
||||
baseUrl,
|
||||
label: 'hello',
|
||||
message: 'sweet world',
|
||||
color: 'orange',
|
||||
})}
|
||||
/>
|
||||
<Explanation>
|
||||
<p>
|
||||
Developers rely on Shields for visual consistency and powerful
|
||||
customization options. As a service provider or data provider, you can
|
||||
use the endpoint badge to provide content while giving users the full
|
||||
power of Shields' badge customization.
|
||||
</p>
|
||||
<p>
|
||||
Using the endpoint badge, you can provide content for a badge through
|
||||
a JSON endpoint. The content can be prerendered, or generated on the
|
||||
fly. To strike a balance between responsiveness and bandwidth
|
||||
utilization on one hand, and freshness on the other, cache behavior is
|
||||
configurable, subject to the Shields minimum. The endpoint URL is
|
||||
provided to Shields through the query string. Shields fetches it and
|
||||
formats the badge.
|
||||
</p>
|
||||
<p>
|
||||
The endpoint badge is a better alternative than redirecting to the
|
||||
static badge endpoint or generating SVG on your server:
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
<a href="https://en.wikipedia.org/wiki/Separation_of_content_and_presentation">
|
||||
Content and presentation are separate.
|
||||
</a>{' '}
|
||||
The service provider authors the badge, and Shields takes input from
|
||||
the user to format it. As a service provider, you author the badge
|
||||
but don't have to concern yourself with styling. You don't even have
|
||||
to pass the formatting options through to Shields.
|
||||
</li>
|
||||
<li>
|
||||
Badge formatting is always 100% up to date. There's no need to track
|
||||
updates to the npm package, badge templates, or options.
|
||||
</li>
|
||||
<li>
|
||||
A JSON response is easy to implement; easier than an HTTP redirect.
|
||||
It is trivial in almost any framework and is more compatible with
|
||||
hosting environments such as{' '}
|
||||
<a href="https://runkit.com/docs/endpoint">RunKit endpoints</a>.
|
||||
</li>
|
||||
<li>
|
||||
As a service provider, you can rely on the Shields CDN. There's no
|
||||
need to study the HTTP headers. Adjusting cache behavior is as
|
||||
simple as setting a property in the JSON response.
|
||||
</li>
|
||||
</ol>
|
||||
</Explanation>
|
||||
<h4>Schema</h4>
|
||||
<Explanation>
|
||||
<p>
|
||||
Breaking changes to the schema will trigger an increment to the
|
||||
`schemaVersion`.
|
||||
</p>
|
||||
</Explanation>
|
||||
<Schema>
|
||||
<dt>schemaVersion</dt>
|
||||
<dd>
|
||||
Required. Always the number <code>1</code>.
|
||||
</dd>
|
||||
<dt>label</dt>
|
||||
<dd>
|
||||
Required. The left text, or the empty string to omit the left side of
|
||||
the badge. This can be overridden by the query string.
|
||||
</dd>
|
||||
<dt>message</dt>
|
||||
<dd>Required. Can't be empty. The right text.</dd>
|
||||
<dt>color</dt>
|
||||
<dd>
|
||||
Default: <code>lightgrey</code>. The right color. Supports the eight
|
||||
named colors above, as well as hex, rgb, rgba, hsl, hsla and css named
|
||||
colors. This can be overridden by the query string.
|
||||
</dd>
|
||||
<dt>labelColor</dt>
|
||||
<dd>
|
||||
Default: <code>grey</code>. The left color. This can be overridden by
|
||||
the query string.
|
||||
</dd>
|
||||
<dt>isError</dt>
|
||||
<dd>
|
||||
Default: <code>false</code>. <code>true</code> to treat this as an
|
||||
error badge. This prevents the user from overriding the color. In the
|
||||
future, it may affect cache behavior.
|
||||
</dd>
|
||||
<dt>namedLogo</dt>
|
||||
<dd>
|
||||
Default: none. One of the named logos supported by Shields or {}
|
||||
<a href="https://simpleicons.org/">simple-icons</a>. Can be overridden
|
||||
by the query string.
|
||||
</dd>
|
||||
<dt>logoSvg</dt>
|
||||
<dd>Default: none. An SVG string containing a custom logo.</dd>
|
||||
<dt>logoColor</dt>
|
||||
<dd>
|
||||
Default: none. Same meaning as the query string. Can be overridden by
|
||||
the query string. Only works for named logos and Shields logos. If you
|
||||
override the color of a multicolor Shield logo, the corresponding
|
||||
named logo will be used and colored.
|
||||
</dd>
|
||||
<dt>logoWidth</dt>
|
||||
<dd>
|
||||
Default: none. Same meaning as the query string. Can be overridden by
|
||||
the query string.
|
||||
</dd>
|
||||
<dt>logoPosition</dt>
|
||||
<dd>
|
||||
Default: none. Same meaning as the query string. Can be overridden by
|
||||
the query string.
|
||||
</dd>
|
||||
<dt>style</dt>
|
||||
<dd>
|
||||
Default: <code>flat</code>. The default template to use. Can be
|
||||
overridden by the query string.
|
||||
</dd>
|
||||
<dt>cacheSeconds</dt>
|
||||
<dd>
|
||||
Default: <code>300</code>, min <code>300</code>. Set the HTTP cache
|
||||
lifetime in seconds, which should be respected by the Shields' CDN and
|
||||
downstream users. Values below 300 will be ignored. This lets you tune
|
||||
performance and traffic vs. responsiveness. The value you specify can
|
||||
be overridden by the user via the query string, but only to a longer
|
||||
value.
|
||||
</dd>
|
||||
</Schema>
|
||||
<h4>Customize and test</h4>
|
||||
<Customizer
|
||||
baseUrl={baseUrl}
|
||||
exampleNamedParams={{}}
|
||||
exampleQueryParams={{
|
||||
url: 'https://shields.redsparr0w.com/2473/monday',
|
||||
}}
|
||||
pattern="/endpoint"
|
||||
title="Custom badge"
|
||||
/>
|
||||
<Footer baseUrl={baseUrl} />
|
||||
</MainContainer>
|
||||
)
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
import Main from '../components/main'
|
||||
export default Main
|
31
frontend/sidebars.cjs
Normal file
31
frontend/sidebars.cjs
Normal file
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Creating a sidebar enables you to:
|
||||
* - create an ordered group of docs
|
||||
* - render a sidebar for each doc of that group
|
||||
* - provide next/previous navigation
|
||||
*
|
||||
* The sidebars can be generated from the filesystem, or explicitly defined here.
|
||||
*
|
||||
* Create as many sidebars as you want.
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
|
||||
const sidebars = {
|
||||
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||
tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }],
|
||||
|
||||
// But you can create a sidebar manually
|
||||
/*
|
||||
tutorialSidebar: [
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Tutorial',
|
||||
items: ['hello'],
|
||||
},
|
||||
],
|
||||
*/
|
||||
}
|
||||
|
||||
module.exports = sidebars
|
92
frontend/src/components/homepage-features.js
Normal file
92
frontend/src/components/homepage-features.js
Normal file
@ -0,0 +1,92 @@
|
||||
import React from 'react'
|
||||
import clsx from 'clsx'
|
||||
import styles from './homepage-features.module.css'
|
||||
|
||||
const FeatureList = [
|
||||
{
|
||||
title: 'Dynamic badges',
|
||||
description: (
|
||||
<>
|
||||
<img
|
||||
alt="build:passing"
|
||||
src="https://img.shields.io/badge/build-passing-brightgreen"
|
||||
/>
|
||||
<br />
|
||||
Show metrics for your project. We've got badges for hundreds of
|
||||
services.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Static Badges',
|
||||
description: (
|
||||
<>
|
||||
Create a badge with
|
||||
<br />
|
||||
<img
|
||||
alt="any text you like"
|
||||
src="https://img.shields.io/badge/any%20text-you%20like-blue"
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Badge-Maker NPM library',
|
||||
description: (
|
||||
<>
|
||||
Render badges in your own application using our{' '}
|
||||
<a href="https://www.npmjs.com/package/badge-maker">NPM library</a>
|
||||
<br />
|
||||
<code>npm install badge-maker</code>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Host your own instance',
|
||||
description: (
|
||||
<>
|
||||
Host a shields instance behind your firewall with our{' '}
|
||||
<a href="https://registry.hub.docker.com/r/shieldsio/shields/">
|
||||
docker image
|
||||
</a>
|
||||
<br />
|
||||
<code>docker pull shieldsio/shields</code>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Love Shields?',
|
||||
description: (
|
||||
<>
|
||||
Please consider{' '}
|
||||
<a href="https://opencollective.com/shields">donating</a> to sustain our
|
||||
activities
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
function Feature({ title, description }) {
|
||||
return (
|
||||
<div className={clsx('col col--6')}>
|
||||
<div className="text--center padding-horiz--md padding-vert--lg">
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function HomepageFeatures() {
|
||||
return (
|
||||
<section className={styles.features}>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
{FeatureList.map((props, idx) => (
|
||||
<Feature key={idx} {...props} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
11
frontend/src/components/homepage-features.module.css
Normal file
11
frontend/src/components/homepage-features.module.css
Normal file
@ -0,0 +1,11 @@
|
||||
.features {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.featureSvg {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
}
|
28
frontend/src/css/custom.css
Normal file
28
frontend/src/css/custom.css
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Any CSS included here will be global. The classic template
|
||||
* bundles Infima by default. Infima is a CSS framework designed to
|
||||
* work well for content-centric websites.
|
||||
*/
|
||||
|
||||
/* You can override the default Infima variables here. */
|
||||
:root {
|
||||
--ifm-color-primary: #25c2a0;
|
||||
--ifm-color-primary-dark: rgb(33, 175, 144);
|
||||
--ifm-color-primary-darker: rgb(31, 165, 136);
|
||||
--ifm-color-primary-darkest: rgb(26, 136, 112);
|
||||
--ifm-color-primary-light: rgb(70, 203, 174);
|
||||
--ifm-color-primary-lighter: rgb(102, 212, 189);
|
||||
--ifm-color-primary-lightest: rgb(146, 224, 208);
|
||||
--ifm-code-font-size: 95%;
|
||||
}
|
||||
|
||||
.docusaurus-highlight-code-line {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
display: block;
|
||||
margin: 0 calc(-1 * var(--ifm-pre-padding));
|
||||
padding: 0 var(--ifm-pre-padding);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .docusaurus-highlight-code-line {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
62
frontend/src/pages/community.md
Normal file
62
frontend/src/pages/community.md
Normal file
@ -0,0 +1,62 @@
|
||||
# Community
|
||||
|
||||
Shields.io is possible thanks to the people and companies who donate money, services or time to keep the project running.
|
||||
|
||||
## Sponsors
|
||||
|
||||
❤️ These companies help us by donating their services to shields:
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://nodeping.com/">
|
||||
NodePing
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://sentry.io/">
|
||||
Sentry
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
💵 These organisations help keep shields running by donating on OpenCollective. Your organisation can support this project by <a href="https://opencollective.com/shields#sponsor">becoming a sponsor </a>. Your logo will show up here with a link to your website.
|
||||
|
||||
<p>
|
||||
<object data="https://opencollective.com/shields/sponsors.svg?avatarHeight=80&width=600" />
|
||||
</p>
|
||||
|
||||
## Backers
|
||||
|
||||
💵 Thank you to all our backers who help keep shields running by donating on OpenCollective. You can support this project by <a href="https://opencollective.com/shields#backer">becoming a backer</a>.
|
||||
|
||||
<p>
|
||||
<object data="https://opencollective.com/shields/backers.svg?width=600" />
|
||||
</p>
|
||||
|
||||
## Contributors
|
||||
|
||||
🙏 This project exists thanks to all the nice people who contribute their time to work on the project.
|
||||
|
||||
<p>
|
||||
<object data="https://opencollective.com/shields/contributors.svg?width=600" />
|
||||
</p>
|
||||
|
||||
✨ Shields is helped by these companies which provide a free plan for their product or service:
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://coveralls.io/">Coveralls</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.cloudflare.com/">Cloudflare</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://discord.com/">Discord</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/">GitHub</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://uptimerobot.com/">Uptime Robot</a>
|
||||
</li>
|
||||
</ul>
|
39
frontend/src/pages/index.js
Normal file
39
frontend/src/pages/index.js
Normal file
@ -0,0 +1,39 @@
|
||||
import React from 'react'
|
||||
import clsx from 'clsx'
|
||||
import Layout from '@theme/Layout'
|
||||
import Link from '@docusaurus/Link'
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
|
||||
import HomepageFeatures from '../components/homepage-features'
|
||||
import styles from './index.module.css'
|
||||
|
||||
function HomepageHeader() {
|
||||
const { siteConfig } = useDocusaurusContext()
|
||||
return (
|
||||
<header className={clsx('hero hero--primary', styles.heroBanner)}>
|
||||
<div className="container">
|
||||
<h1 className="hero__title">{siteConfig.title}</h1>
|
||||
<p className="hero__subtitle">{siteConfig.tagline}</p>
|
||||
<div className={styles.buttons}>
|
||||
<Link className="button button--secondary button--lg" to="/badges">
|
||||
Get started
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const { siteConfig } = useDocusaurusContext()
|
||||
return (
|
||||
<Layout
|
||||
description="Description will go into a meta tag in <head />"
|
||||
title={`${siteConfig.title}`}
|
||||
>
|
||||
<HomepageHeader />
|
||||
<main>
|
||||
<HomepageFeatures />
|
||||
</main>
|
||||
</Layout>
|
||||
)
|
||||
}
|
23
frontend/src/pages/index.module.css
Normal file
23
frontend/src/pages/index.module.css
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* CSS files with the .module.css suffix will be treated as CSS modules
|
||||
* and scoped locally.
|
||||
*/
|
||||
|
||||
.heroBanner {
|
||||
padding: 4rem 0;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 966px) {
|
||||
.heroBanner {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
300
frontend/src/theme/ApiDemoPanel/Curl/index.js
Normal file
300
frontend/src/theme/ApiDemoPanel/Curl/index.js
Normal file
@ -0,0 +1,300 @@
|
||||
import React, { useRef, useState, useEffect } from 'react'
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
|
||||
import clsx from 'clsx'
|
||||
import codegen from 'postman-code-generators'
|
||||
import Highlight, { defaultProps } from 'prism-react-renderer'
|
||||
import { useTypedSelector } from '@theme/ApiDemoPanel/hooks'
|
||||
import buildPostmanRequest from '@theme/ApiDemoPanel/buildPostmanRequest'
|
||||
import FloatingButton from '@theme/ApiDemoPanel/FloatingButton'
|
||||
import styles from 'docusaurus-theme-openapi/lib/theme/ApiDemoPanel/Curl/styles.module.css'
|
||||
|
||||
const languageSet = [
|
||||
{
|
||||
tabName: 'cURL',
|
||||
highlight: 'bash',
|
||||
language: 'curl',
|
||||
variant: 'curl',
|
||||
options: {
|
||||
longFormat: false,
|
||||
followRedirect: true,
|
||||
trimRequestBody: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
tabName: 'Node',
|
||||
highlight: 'javascript',
|
||||
language: 'nodejs',
|
||||
variant: 'axios',
|
||||
options: {
|
||||
ES6_enabled: true,
|
||||
followRedirect: true,
|
||||
trimRequestBody: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
tabName: 'Go',
|
||||
highlight: 'go',
|
||||
language: 'go',
|
||||
variant: 'native',
|
||||
options: {
|
||||
followRedirect: true,
|
||||
trimRequestBody: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
tabName: 'Python',
|
||||
highlight: 'python',
|
||||
language: 'python',
|
||||
variant: 'requests',
|
||||
options: {
|
||||
followRedirect: true,
|
||||
trimRequestBody: true,
|
||||
},
|
||||
},
|
||||
]
|
||||
const languageTheme = {
|
||||
plain: {
|
||||
color: 'var(--ifm-code-color)',
|
||||
},
|
||||
styles: [
|
||||
{
|
||||
types: ['inserted', 'attr-name'],
|
||||
style: {
|
||||
color: 'var(--openapi-code-green)',
|
||||
},
|
||||
},
|
||||
{
|
||||
types: ['string', 'url'],
|
||||
style: {
|
||||
color: 'var(--openapi-code-green)',
|
||||
},
|
||||
},
|
||||
{
|
||||
types: ['builtin', 'char', 'constant', 'function'],
|
||||
style: {
|
||||
color: 'var(--openapi-code-blue)',
|
||||
},
|
||||
},
|
||||
{
|
||||
types: ['punctuation', 'operator'],
|
||||
style: {
|
||||
color: 'var(--openapi-code-dim)',
|
||||
},
|
||||
},
|
||||
{
|
||||
types: ['class-name'],
|
||||
style: {
|
||||
color: 'var(--openapi-code-orange)',
|
||||
},
|
||||
},
|
||||
{
|
||||
types: ['tag', 'arrow', 'keyword'],
|
||||
style: {
|
||||
color: 'var(--openapi-code-purple)',
|
||||
},
|
||||
},
|
||||
{
|
||||
types: ['boolean'],
|
||||
style: {
|
||||
color: 'var(--openapi-code-red)',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
function getBaseUrl() {
|
||||
/*
|
||||
This is a special case for production.
|
||||
|
||||
We want to be able to build the front end with no value set for
|
||||
`BASE_URL` so that staging, prod and self hosting users
|
||||
can all use the same docker image.
|
||||
|
||||
When deployed to staging, we want the frontend on
|
||||
https://staging.shields.io/ to generate badges with the base
|
||||
https://staging.shields.io/
|
||||
(and we want similar behaviour for users hosting their own instance)
|
||||
|
||||
When we promote to production we want https://shields.io/ and
|
||||
https://www.shields.io/ to both generate badges with the base
|
||||
https://img.shields.io/
|
||||
|
||||
For local dev, we can deal with setting the api and front-end
|
||||
being on different ports using the BASE_URL env var
|
||||
*/
|
||||
const { protocol, hostname, port } = window.location
|
||||
if (['shields.io', 'www.shields.io'].includes(hostname)) {
|
||||
return 'https://img.shields.io'
|
||||
}
|
||||
if (!port) {
|
||||
return `${protocol}//${hostname}`
|
||||
}
|
||||
return `${protocol}//${hostname}:${port}`
|
||||
}
|
||||
|
||||
function getServer() {
|
||||
return {
|
||||
url: getBaseUrl(),
|
||||
variables: {},
|
||||
}
|
||||
}
|
||||
|
||||
function Curl({ postman, codeSamples }) {
|
||||
// TODO: match theme for vscode.
|
||||
const { siteConfig } = useDocusaurusContext()
|
||||
const [copyText, setCopyText] = useState('Copy')
|
||||
const contentType = useTypedSelector(state => state.contentType.value)
|
||||
const accept = useTypedSelector(state => state.accept.value)
|
||||
const server = useTypedSelector(state => state.server.value) || getServer()
|
||||
const body = useTypedSelector(state => state.body)
|
||||
const pathParams = useTypedSelector(state => state.params.path)
|
||||
const queryParams = useTypedSelector(state => state.params.query)
|
||||
const cookieParams = useTypedSelector(state => state.params.cookie)
|
||||
const headerParams = useTypedSelector(state => state.params.header)
|
||||
const auth = useTypedSelector(state => state.auth)
|
||||
|
||||
const langs = [
|
||||
...(siteConfig?.themeConfig?.languageTabs ?? languageSet),
|
||||
...codeSamples,
|
||||
]
|
||||
const [language, setLanguage] = useState(langs[0])
|
||||
const [codeText, setCodeText] = useState('')
|
||||
useEffect(() => {
|
||||
const postmanRequest = buildPostmanRequest(postman, {
|
||||
queryParams,
|
||||
pathParams,
|
||||
cookieParams,
|
||||
contentType,
|
||||
accept,
|
||||
headerParams,
|
||||
body,
|
||||
server,
|
||||
auth,
|
||||
})
|
||||
if (language && !!language.options) {
|
||||
codegen.convert(
|
||||
language.language,
|
||||
language.variant,
|
||||
postmanRequest,
|
||||
language.options,
|
||||
(error, snippet) => {
|
||||
if (error) {
|
||||
return
|
||||
}
|
||||
|
||||
setCodeText(snippet)
|
||||
}
|
||||
)
|
||||
} else if (language && !!language.source) {
|
||||
setCodeText(
|
||||
language.source.replace('$url', postmanRequest.url.toString())
|
||||
)
|
||||
} else {
|
||||
setCodeText('')
|
||||
}
|
||||
}, [
|
||||
accept,
|
||||
body,
|
||||
contentType,
|
||||
cookieParams,
|
||||
headerParams,
|
||||
language,
|
||||
pathParams,
|
||||
postman,
|
||||
queryParams,
|
||||
server,
|
||||
auth,
|
||||
])
|
||||
const ref = useRef(null)
|
||||
|
||||
const handleCurlCopy = () => {
|
||||
setCopyText('Copied')
|
||||
setTimeout(() => {
|
||||
setCopyText('Copy')
|
||||
}, 2000)
|
||||
|
||||
if (ref.current?.innerText) {
|
||||
navigator.clipboard.writeText(ref.current.innerText)
|
||||
}
|
||||
}
|
||||
|
||||
if (language === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={clsx(styles.buttonGroup, 'api-code-tab-group')}>
|
||||
{langs.map(lang => (
|
||||
<button
|
||||
className={clsx(
|
||||
language === lang ? styles.selected : undefined,
|
||||
language === lang ? 'api-code-tab--active' : undefined,
|
||||
'api-code-tab'
|
||||
)}
|
||||
key={lang.tabName || lang.label}
|
||||
onClick={() => setLanguage(lang)}
|
||||
>
|
||||
{lang.tabName || lang.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Highlight
|
||||
{...defaultProps}
|
||||
code={codeText}
|
||||
language={language.highlight || language.lang}
|
||||
theme={languageTheme}
|
||||
>
|
||||
{({ className, tokens, getLineProps, getTokenProps }) => (
|
||||
<FloatingButton label={copyText} onClick={handleCurlCopy}>
|
||||
<pre
|
||||
className={className}
|
||||
style={{
|
||||
background: 'var(--openapi-card-background-color)',
|
||||
paddingRight: '60px',
|
||||
borderRadius:
|
||||
'2px 2px var(--openapi-card-border-radius) var(--openapi-card-border-radius)',
|
||||
}}
|
||||
>
|
||||
<code ref={ref}>
|
||||
{tokens.map((line, i) => (
|
||||
// this <span> does have a key but eslint fails
|
||||
// to detect it because it is an arg to getLineProps()
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<span
|
||||
{...getLineProps({
|
||||
line,
|
||||
key: i,
|
||||
})}
|
||||
>
|
||||
{line.map((token, key) => {
|
||||
if (token.types.includes('arrow')) {
|
||||
token.types = ['arrow']
|
||||
}
|
||||
|
||||
return (
|
||||
// this <span> does have a key but eslint fails
|
||||
// to detect it because it is an arg to getLineProps()
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<span
|
||||
{...getTokenProps({
|
||||
token,
|
||||
key,
|
||||
})}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{'\n'}
|
||||
</span>
|
||||
))}
|
||||
</code>
|
||||
</pre>
|
||||
</FloatingButton>
|
||||
)}
|
||||
</Highlight>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Curl
|
66
frontend/src/theme/ApiDemoPanel/Response/index.js
Normal file
66
frontend/src/theme/ApiDemoPanel/Response/index.js
Normal file
@ -0,0 +1,66 @@
|
||||
import React from 'react'
|
||||
import { useTypedDispatch, useTypedSelector } from '@theme/ApiDemoPanel/hooks'
|
||||
import FloatingButton from '@theme/ApiDemoPanel/FloatingButton'
|
||||
import { clearResponse } from 'docusaurus-theme-openapi/lib/theme/ApiDemoPanel/Response/slice'
|
||||
|
||||
function formatXml(xml) {
|
||||
const tab = ' '
|
||||
let formatted = ''
|
||||
let indent = ''
|
||||
xml.split(/>\s*</).forEach(node => {
|
||||
if (node.match(/^\/\w/)) {
|
||||
// decrease indent by one 'tab'
|
||||
indent = indent.substring(tab.length)
|
||||
}
|
||||
|
||||
formatted += `${indent}<${node}>\r\n`
|
||||
|
||||
if (node.match(/^<?\w[^>]*[^/]$/)) {
|
||||
// increase indent
|
||||
indent += tab
|
||||
}
|
||||
})
|
||||
return formatted.substring(1, formatted.length - 3)
|
||||
}
|
||||
|
||||
function Response() {
|
||||
const response = useTypedSelector(state => state.response.value)
|
||||
const dispatch = useTypedDispatch()
|
||||
|
||||
if (response === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
let prettyResponse = response
|
||||
|
||||
try {
|
||||
prettyResponse = JSON.stringify(JSON.parse(response), null, 2)
|
||||
} catch {
|
||||
if (response.startsWith('<?xml ')) {
|
||||
prettyResponse = formatXml(response)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FloatingButton label="Clear" onClick={() => dispatch(clearResponse())}>
|
||||
{(response.startsWith('<svg ') && (
|
||||
<img
|
||||
id="badge-preview"
|
||||
src={`data:image/svg+xml;utf8,${encodeURIComponent(response)}`}
|
||||
/>
|
||||
)) || (
|
||||
<pre
|
||||
style={{
|
||||
background: 'var(--openapi-card-background-color)',
|
||||
borderRadius: 'var(--openapi-card-border-radius)',
|
||||
paddingRight: '60px',
|
||||
}}
|
||||
>
|
||||
<code>{prettyResponse || 'No Response'}</code>
|
||||
</pre>
|
||||
)}
|
||||
</FloatingButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default Response
|
3
frontend/src/theme/DocPaginator/index.js
Normal file
3
frontend/src/theme/DocPaginator/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
export default function DocPaginator(props) {
|
||||
return ''
|
||||
}
|
0
frontend/static/.nojekyll
Normal file
0
frontend/static/.nojekyll
Normal file
BIN
frontend/static/img/favicon.ico
Normal file
BIN
frontend/static/img/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
frontend/static/img/logo.png
Normal file
BIN
frontend/static/img/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
5
frontend/types/assets.d.ts
vendored
5
frontend/types/assets.d.ts
vendored
@ -1,5 +0,0 @@
|
||||
declare module '*.svg'
|
||||
declare module '*.json'
|
||||
|
||||
// Handled by js-yaml-loader.
|
||||
declare module '*.yml'
|
@ -1,11 +0,0 @@
|
||||
declare module '@mapbox/react-click-to-select' {
|
||||
import * as React from 'react'
|
||||
|
||||
export type ContainerElementType = 'span' | 'div'
|
||||
|
||||
export interface Props {
|
||||
containerElement?: ContainerElementType
|
||||
}
|
||||
|
||||
export default class ClickToSelect extends React.Component<Props> {}
|
||||
}
|
49223
package-lock.json
generated
49223
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
83
package.json
83
package.json
@ -21,8 +21,6 @@
|
||||
"url": "https://github.com/badges/shields"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/lato": "^5.0.2",
|
||||
"@fontsource/lekton": "^5.0.2",
|
||||
"@renovate/pep440": "^1.0.0",
|
||||
"@renovatebot/ruby-semver": "^2.1.11",
|
||||
"@sentry/node": "^7.54.0",
|
||||
@ -70,22 +68,20 @@
|
||||
},
|
||||
"scripts": {
|
||||
"coverage:test:core": "c8 npm run test:core",
|
||||
"coverage:test:frontend": "c8 -c .nycrc-frontend.json npm run test:frontend",
|
||||
"coverage:test:package": "c8 npm run test:package",
|
||||
"coverage:test:entrypoint": "c8 npm run test:entrypoint",
|
||||
"coverage:test:integration": "c8 npm run test:integration",
|
||||
"coverage:test:services": "c8 npm run test:services",
|
||||
"coverage:clean": "rimraf .nyc_output coverage",
|
||||
"precoverage:test": "run-s --silent coverage:clean defs features",
|
||||
"coverage:test": "run-s --silent --continue-on-error coverage:test:core coverage:test:package coverage:test:entrypoint coverage:test:frontend coverage:test:integration",
|
||||
"precoverage:test": "cross-env BASE_URL=http://localhost:8080 run-s --silent coverage:clean defs",
|
||||
"coverage:test": "run-s --silent --continue-on-error coverage:test:core coverage:test:package coverage:test:entrypoint coverage:test:integration",
|
||||
"coverage:report:generate": "c8 report",
|
||||
"coverage:report:open": "open-cli coverage/lcov-report/index.html",
|
||||
"coverage:report": "run-s --silent coverage:report:generate coverage:report:open",
|
||||
"lint": "eslint \"**/*.@(js|ts|tsx)\"",
|
||||
"prettier": "prettier --write \"**/*.@(js|ts|tsx|md|json|yml)\"",
|
||||
"prettier:check": "prettier --check \"**/*.@(js|ts|tsx|md|json|yml)\"",
|
||||
"lint": "eslint \"**/*.@(cjs|js|ts|tsx)\"",
|
||||
"prettier": "prettier --write \"**/*.@(cjs|js|ts|tsx|md|json|yml)\"",
|
||||
"prettier:check": "prettier --check \"**/*.@(cjs|js|ts|tsx|md|json|yml)\"",
|
||||
"danger": "danger",
|
||||
"test:frontend": "cross-env NODE_ENV=test ts-mocha --config .mocharc-frontend.yml \"frontend/**/*.spec.@(js|ts|tsx)\"",
|
||||
"test:e2e": "cypress run",
|
||||
"test:core": "cross-env NODE_CONFIG_ENV=test mocha \"core/**/*.spec.js\" \"lib/**/*.spec.js\" \"services/**/*.spec.js\"",
|
||||
"test:package": "mocha \"badge-maker/**/*.spec.js\"",
|
||||
@ -96,15 +92,13 @@
|
||||
"test:services:pr:prepare": "node core/service-test-runner/pull-request-services-cli.js > pull-request-services.log",
|
||||
"test:services:pr:run": "cross-env NODE_CONFIG_ENV=test mocha core/service-test-runner/cli.js --stdin < pull-request-services.log",
|
||||
"test:services:pr": "run-s --silent test:services:pr:prepare test:services:pr:run",
|
||||
"pretest": "run-s --silent defs features",
|
||||
"test": "run-s --silent --continue-on-error lint test:frontend test:package test:core test:entrypoint check-types:package check-types:frontend prettier:check",
|
||||
"pretest": "cross-env BASE_URL=http://localhost:8080 run-s --silent defs",
|
||||
"test": "run-s --silent --continue-on-error lint test:package test:core test:entrypoint check-types:package prettier:check",
|
||||
"check-types:package": "tsd badge-maker",
|
||||
"check-types:frontend": "tsc --noEmit --project .",
|
||||
"depcheck": "check-node-version --node \">= 16.0\"",
|
||||
"prebuild": "run-s --silent depcheck",
|
||||
"features": "node scripts/export-supported-features-cli.js > ./frontend/supported-features.json",
|
||||
"defs": "node scripts/export-service-definitions-cli.js > ./frontend/service-definitions.yml",
|
||||
"build": "run-s defs features && cd frontend && gatsby build && mv public ..",
|
||||
"defs": "node scripts/export-openapi-cli.js",
|
||||
"build": "rimraf public && run-s defs docusaurus:build",
|
||||
"heroku-postbuild": "run-s --silent build",
|
||||
"start:server:prod": "node server",
|
||||
"now-start": "npm run start:server:prod",
|
||||
@ -113,13 +107,17 @@
|
||||
"debug:server": "cross-env NODE_CONFIG_ENV=development nodemon --inspect server.js 8080",
|
||||
"profile:server": "cross-env NODE_CONFIG_ENV=development node --prof server 8080",
|
||||
"benchmark:badge": "cross-env NODE_CONFIG_ENV=test node scripts/benchmark-performance.js --iterations 10100 | node scripts/capture-timings.js --warmup-iterations 100",
|
||||
"prestart": "run-s --silent depcheck defs features",
|
||||
"start": "concurrently --names server,frontend \"npm run start:server\" \"cd frontend && cross-env GATSBY_BASE_URL=http://localhost:8080 gatsby develop --port 3000\"",
|
||||
"e2e": "start-server-and-test start http://localhost:3000 test:e2e",
|
||||
"e2e-on-build": "cross-env CYPRESS_baseUrl=http://localhost:8080 start-server-and-test start:server:e2e-on-build http://localhost:8080 test:e2e",
|
||||
"prestart": "cross-env BASE_URL=http://localhost:8080 run-s --silent depcheck defs",
|
||||
"start": "concurrently --names server,frontend \"npm run start:server\" \"npm run docusaurus:start\"",
|
||||
"e2e": "cross-env BASE_URL=http://localhost:8080 run-s build e2e-on-build",
|
||||
"e2e-on-build": "cross-env CYPRESS_BASE_URL=http://localhost:8080 start-server-and-test start:server:e2e-on-build http://localhost:8080 test:e2e",
|
||||
"badge": "cross-env NODE_CONFIG_ENV=test TRACE_SERVICES=true node scripts/badge-cli.js",
|
||||
"build-docs": "rimraf api-docs/ && jsdoc --pedantic -c ./jsdoc.json . && echo 'contributing.shields.io' > api-docs/CNAME",
|
||||
"migrate": "node scripts/write-migrations-config.js > migrations-config.json && node-pg-migrate --config-file=migrations-config.json"
|
||||
"migrate": "node scripts/write-migrations-config.js > migrations-config.json && node-pg-migrate --config-file=migrations-config.json",
|
||||
"docusaurus:start": "docusaurus start frontend",
|
||||
"docusaurus:build": "docusaurus build frontend --out-dir ../public",
|
||||
"docusaurus:swizzle": "cd frontend && docusaurus swizzle",
|
||||
"docusaurus:clear": "docusaurus clear frontend"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.@(js|ts|tsx)": [
|
||||
@ -144,37 +142,24 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.5",
|
||||
"@babel/polyfill": "^7.12.1",
|
||||
"@babel/register": "7.22.5",
|
||||
"@istanbuljs/schema": "^0.1.3",
|
||||
"@mapbox/react-click-to-select": "^2.2.1",
|
||||
"@types/chai": "^4.3.5",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/lodash.groupby": "^4.6.7",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/node": "^16.7.10",
|
||||
"@types/react-helmet": "^6.1.6",
|
||||
"@types/react-modal": "^3.16.0",
|
||||
"@types/react-select": "^4.0.17",
|
||||
"@types/styled-components": "5.1.26",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
||||
"@docusaurus/core": "^2.0.0",
|
||||
"@easyops-cn/docusaurus-search-local": "^0.33.6",
|
||||
"@mdx-js/react": "^1.6.21",
|
||||
"@typescript-eslint/parser": "^5.58.0",
|
||||
"babel-plugin-inline-react-svg": "^2.0.2",
|
||||
"babel-preset-gatsby": "^2.22.0",
|
||||
"c8": "^7.14.0",
|
||||
"c8": "^7.13.0",
|
||||
"caller": "^1.1.0",
|
||||
"chai": "^4.3.7",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-datetime": "^1.8.0",
|
||||
"chai-string": "^1.4.0",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"clsx": "^1.1.1",
|
||||
"concurrently": "^8.2.0",
|
||||
"cypress": "^12.14.0",
|
||||
"cypress-wait-for-stable-dom": "^0.1.0",
|
||||
"danger": "^11.2.6",
|
||||
"deepmerge": "^4.3.1",
|
||||
"docusaurus-preset-openapi": "0.6.4",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
@ -192,23 +177,12 @@
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-sort-class-members": "^1.18.0",
|
||||
"fetch-ponyfill": "^7.1.0",
|
||||
"form-data": "^4.0.0",
|
||||
"gatsby": "4.23.1",
|
||||
"gatsby-plugin-catch-links": "^4.25.0",
|
||||
"gatsby-plugin-page-creator": "^4.25.0",
|
||||
"gatsby-plugin-react-helmet": "^5.25.0",
|
||||
"gatsby-plugin-remove-trailing-slashes": "^4.9.0",
|
||||
"gatsby-plugin-styled-components": "^5.24.0",
|
||||
"gatsby-plugin-typescript": "^4.25.0",
|
||||
"humanize-string": "^2.1.0",
|
||||
"icedfrisby": "4.0.0",
|
||||
"icedfrisby-nock": "^2.1.0",
|
||||
"is-svg": "^5.0.0",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"jsdoc": "^4.0.2",
|
||||
"lint-staged": "^13.2.2",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"minimist": "^1.2.8",
|
||||
"mocha": "^10.2.0",
|
||||
@ -222,13 +196,9 @@
|
||||
"open-cli": "^7.2.0",
|
||||
"portfinder": "^1.0.32",
|
||||
"prettier": "2.8.8",
|
||||
"prism-react-renderer": "^1.2.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-error-overlay": "^6.0.11",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-pose": "^4.0.10",
|
||||
"react-select": "^4.3.1",
|
||||
"read-all-stdin-sync": "^1.0.5",
|
||||
"rimraf": "^5.0.1",
|
||||
"sazerac": "^2.0.0",
|
||||
@ -237,10 +207,7 @@
|
||||
"sinon-chai": "^3.7.0",
|
||||
"snap-shot-it": "^7.9.10",
|
||||
"start-server-and-test": "2.0.0",
|
||||
"styled-components": "^5.3.11",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"tsd": "^0.28.1",
|
||||
"typescript": "^5.1.3",
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -1,24 +0,0 @@
|
||||
import yaml from 'js-yaml'
|
||||
import { collectDefinitions } from '../core/base-service/loader.js'
|
||||
;(async () => {
|
||||
const definitions = await collectDefinitions()
|
||||
|
||||
// filter out static, dynamic and debug badge examples
|
||||
const publicCategories = definitions.categories.map(c => c.id)
|
||||
definitions.services = definitions.services.filter(s =>
|
||||
publicCategories.includes(s.category)
|
||||
)
|
||||
|
||||
// drop the openApi property for the "legacy" frontend
|
||||
for (const service of definitions.services) {
|
||||
if (service.openApi) {
|
||||
service.openApi = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// Omit undefined
|
||||
// https://github.com/nodeca/js-yaml/issues/356#issuecomment-312430599
|
||||
const cleaned = JSON.parse(JSON.stringify(definitions))
|
||||
|
||||
process.stdout.write(yaml.dump(cleaned, { flowLevel: 5 }))
|
||||
})()
|
@ -1,22 +0,0 @@
|
||||
import loadLogos from '../lib/load-logos.js'
|
||||
import loadSimpleIcons from '../lib/load-simple-icons.js'
|
||||
const shieldsLogos = Object.keys(loadLogos())
|
||||
const simpleIcons = loadSimpleIcons()
|
||||
|
||||
const simpleIconSet = new Set(Object.keys(simpleIcons))
|
||||
shieldsLogos.forEach(logo => simpleIconSet.delete(logo))
|
||||
const simpleIconNames = Array.from(simpleIconSet)
|
||||
|
||||
const supportedFeatures = {
|
||||
shieldsLogos,
|
||||
simpleIcons: simpleIconNames,
|
||||
advertisedStyles: [
|
||||
'plastic',
|
||||
'flat',
|
||||
'flat-square',
|
||||
'for-the-badge',
|
||||
'social',
|
||||
],
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(supportedFeatures, null, 2))
|
@ -3,7 +3,7 @@ import { fetch } from '../../../core/base-service/got.js'
|
||||
import log from '../../../core/server/log.js'
|
||||
|
||||
function setRoutes({ server, authHelper, onTokenAccepted }) {
|
||||
const baseUrl = process.env.GATSBY_BASE_URL || 'https://img.shields.io'
|
||||
const baseUrl = 'https://img.shields.io'
|
||||
|
||||
server.route(/^\/github-auth$/, (data, match, end, ask) => {
|
||||
ask.res.statusCode = 302 // Found.
|
||||
|
@ -39,7 +39,6 @@ describe('Text formatters', function () {
|
||||
|
||||
test(metric, () => {
|
||||
/* eslint-disable no-loss-of-precision */
|
||||
/* eslint-disable @typescript-eslint/no-loss-of-precision */
|
||||
given(0).expect('0')
|
||||
given(999).expect('999')
|
||||
given(1000).expect('1k')
|
||||
|
@ -1,16 +0,0 @@
|
||||
{
|
||||
"include": ["frontend/**/*"],
|
||||
"exclude": [],
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"outDir": "unused_ts_output",
|
||||
"strict": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react",
|
||||
"typeRoots": ["node_modules/@types", "frontend/types"],
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user