1
0
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:
chris48s 2023-06-17 10:59:07 +01:00 committed by GitHub
parent 67d935492d
commit 50ea7068a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 22922 additions and 31531 deletions

View File

@ -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'

View File

@ -35,7 +35,6 @@ function allChangelogLinesAreVersionBump(changelogLines) {
function isPointlessVersionBump(body) {
const pointlessBumpLinks = [
'https://github.com/gatsbyjs/gatsby',
'https://github.com/typescript-eslint/typescript-eslint',
]

View File

@ -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

View File

@ -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()

View File

@ -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
View File

@ -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

View File

@ -1,5 +0,0 @@
reporter: mocha-env-reporter
require:
- '@babel/polyfill'
- '@babel/register'
- mocha-yaml-loader

View File

@ -1,10 +0,0 @@
{
"reporter": ["lcov"],
"all": false,
"silent": true,
"clean": false,
"sourceMap": false,
"instrument": false,
"include": ["frontend/**/*.js"],
"exclude": ["**/*.spec.js", "**/mocha-*.js"]
}

View File

@ -10,5 +10,5 @@ public
private/*.json
/.nyc_output
analytics.json
supported-features.json
service-definitions.yml
frontend/.docusaurus
frontend/categories

View File

@ -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.

View File

@ -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

View File

@ -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 }

View File

@ -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('')
)
})
})

View File

@ -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
}

View File

@ -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: {

View File

@ -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: {

View File

@ -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 }

View File

@ -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',

View File

@ -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`

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -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"]')
})
})

View File

@ -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')

View File

@ -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

View File

@ -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.

View File

@ -0,0 +1,3 @@
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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;
`

View File

@ -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;
`

View File

@ -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)

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
</>
)
}

View File

@ -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}
/>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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 &nbsp; Retina-ready &nbsp; Fast &nbsp; Consistent &nbsp;
Hackable &nbsp; No tracking
</Highlights>
</section>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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}
/>
</>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>&nbsp;</code>
</span>
}
rhs={
<span>
<code>&nbsp;</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=&lt;URL&gt;&amp;label=&lt;LABEL&gt;&amp;query=&lt;
<a
href="https://jsonpath.com"
rel="noopener noreferrer"
target="_blank"
title="JSONPath syntax"
>
$.DATA.SUBDATA
</a>
&gt;&amp;color=&lt;COLOR&gt;&amp;prefix=&lt;PREFIX&gt;&amp;suffix=&lt;SUFFIX&gt;
</StyledCode>
</p>
<p>
<StyledCode>
{baseUrl}
/badge/dynamic/xml?url=&lt;URL&gt;&amp;label=&lt;LABEL&gt;&amp;query=&lt;
<a
href="http://xpather.com"
rel="noopener noreferrer"
target="_blank"
title="XPath syntax"
>
&#x2F;&#x2F;data/subdata
</a>
&gt;&amp;color=&lt;COLOR&gt;&amp;prefix=&lt;PREFIX&gt;&amp;suffix=&lt;SUFFIX&gt;
</StyledCode>
</p>
<p>
<StyledCode>
{baseUrl}
/badge/dynamic/yaml?url=&lt;URL&gt;&amp;label=&lt;LABEL&gt;&amp;query=&lt;
<a
href="https://jsonpath.com"
rel="noopener noreferrer"
target="_blank"
title="YAML (JSONPath) syntax"
>
$.DATA.SUBDATA
</a>
&gt;&amp;color=&lt;COLOR&gt;&amp;prefix=&lt;PREFIX&gt;&amp;suffix=&lt;SUFFIX&gt;
</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>&lt;object&gt;</StyledCode> HTML tag, but not an
<StyledCode>&lt;img&gt;</StyledCode> tag or a markup language.
</span>
}
key="link"
snippet="?link=http://left&amp;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>
)
}

View File

@ -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
View File

@ -0,0 +1,5 @@
---
sidebar_position: 1
---
# TODO

View 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

View File

@ -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)
}
}

View File

@ -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`,
// },
// },
],
}

View File

@ -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

View File

@ -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

View File

@ -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(
'![Example](https://img.shields.io/badge)'
)
given('https://img.shields.io/badge', undefined).expect(
'![](https://img.shields.io/badge)'
)
})
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('![Example](https://img.shields.io/badge)')
})

View File

@ -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 `![${title || ''}](${badgeUrl})`
}
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)
}

View File

@ -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/(.*)')
})
})

View File

@ -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
}

View File

@ -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,
})
}
}

View File

@ -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)
})
})

View File

@ -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

View File

@ -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)
})
})

View File

@ -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
}
}

View File

@ -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[]

View File

@ -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"
]
}
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -1,2 +0,0 @@
import Main from '../components/main'
export default Main

31
frontend/sidebars.cjs Normal file
View 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

View 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>
)
}

View File

@ -0,0 +1,11 @@
.features {
display: flex;
align-items: center;
padding: 2rem 0;
width: 100%;
}
.featureSvg {
height: 200px;
width: 200px;
}

View 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);
}

View 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>

View 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>
)
}

View 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;
}

View 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

View 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

View File

@ -0,0 +1,3 @@
export default function DocPaginator(props) {
return ''
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,5 +0,0 @@
declare module '*.svg'
declare module '*.json'
// Handled by js-yaml-loader.
declare module '*.yml'

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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": {

View File

@ -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 }))
})()

View File

@ -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))

View File

@ -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.

View File

@ -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')

View File

@ -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
}
}