diff --git a/.eslintrc.yml b/.eslintrc.yml
index cda0674cbf..c8232bb936 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -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'
diff --git a/.github/actions/close-bot/helpers.js b/.github/actions/close-bot/helpers.js
index 4d766cf70e..aa0854846f 100644
--- a/.github/actions/close-bot/helpers.js
+++ b/.github/actions/close-bot/helpers.js
@@ -35,7 +35,6 @@ function allChangelogLinesAreVersionBump(changelogLines) {
function isPointlessVersionBump(body) {
const pointlessBumpLinks = [
- 'https://github.com/gatsbyjs/gatsby',
'https://github.com/typescript-eslint/typescript-eslint',
]
diff --git a/.github/actions/frontend-tests/action.yml b/.github/actions/frontend-tests/action.yml
deleted file mode 100644
index 2ef870586d..0000000000
--- a/.github/actions/frontend-tests/action.yml
+++ /dev/null
@@ -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
diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml
index aa75053396..dcc6b9506f 100644
--- a/.github/workflows/test-e2e.yml
+++ b/.github/workflows/test-e2e.yml
@@ -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()
diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml
deleted file mode 100644
index e5f2a2e7a7..0000000000
--- a/.github/workflows/test-frontend.yml
+++ /dev/null
@@ -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
diff --git a/.gitignore b/.gitignore
index 5c8b82a5b4..0d7154143a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/.mocharc-frontend.yml b/.mocharc-frontend.yml
deleted file mode 100644
index 0e076ba900..0000000000
--- a/.mocharc-frontend.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-reporter: mocha-env-reporter
-require:
- - '@babel/polyfill'
- - '@babel/register'
- - mocha-yaml-loader
diff --git a/.nycrc-frontend.json b/.nycrc-frontend.json
deleted file mode 100644
index 447812c550..0000000000
--- a/.nycrc-frontend.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "reporter": ["lcov"],
- "all": false,
- "silent": true,
- "clean": false,
- "sourceMap": false,
- "instrument": false,
- "include": ["frontend/**/*.js"],
- "exclude": ["**/*.spec.js", "**/mocha-*.js"]
-}
diff --git a/.prettierignore b/.prettierignore
index 9f913cee41..fed0f550dd 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -10,5 +10,5 @@ public
private/*.json
/.nyc_output
analytics.json
-supported-features.json
-service-definitions.yml
+frontend/.docusaurus
+frontend/categories
diff --git a/README.md b/README.md
index 773d060ac3..b8c4f8f970 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/core/badge-urls/make-badge-url.d.ts b/core/badge-urls/make-badge-url.d.ts
deleted file mode 100644
index bf925cf729..0000000000
--- a/core/badge-urls/make-badge-url.d.ts
+++ /dev/null
@@ -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
diff --git a/core/badge-urls/make-badge-url.js b/core/badge-urls/make-badge-url.js
index 8870c9acea..13953a4add 100644
--- a/core/badge-urls/make-badge-url.js
+++ b/core/badge-urls/make-badge-url.js
@@ -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 }
diff --git a/core/badge-urls/make-badge-url.spec.js b/core/badge-urls/make-badge-url.spec.js
deleted file mode 100644
index c8c3fcbb1b..0000000000
--- a/core/badge-urls/make-badge-url.spec.js
+++ /dev/null
@@ -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('')
- )
- })
-})
diff --git a/core/base-service/legacy-request-handler.js b/core/base-service/legacy-request-handler.js
index 9ef3d4efeb..f7cbb94001 100644
--- a/core/base-service/legacy-request-handler.js
+++ b/core/base-service/legacy-request-handler.js
@@ -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
}
diff --git a/core/base-service/openapi.js b/core/base-service/openapi.js
index 1cb463a76c..4ea3169a7d 100644
--- a/core/base-service/openapi.js
+++ b/core/base-service/openapi.js
@@ -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: {
diff --git a/core/base-service/openapi.spec.js b/core/base-service/openapi.spec.js
index 8d6b3f09e4..217cb37ac9 100644
--- a/core/base-service/openapi.spec.js
+++ b/core/base-service/openapi.spec.js
@@ -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: {
diff --git a/core/base-service/service-definitions.js b/core/base-service/service-definitions.js
index 8718b58cbd..8c1c94a155 100644
--- a/core/base-service/service-definitions.js
+++ b/core/base-service/service-definitions.js
@@ -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 }
diff --git a/core/server/server.js b/core/server/server.js
index a1c0daa315..98d8673c97 100644
--- a/core/server/server.js
+++ b/core/server/server.js
@@ -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',
diff --git a/core/server/server.spec.js b/core/server/server.spec.js
index 936033d2ee..6de51974ff 100644
--- a/core/server/server.spec.js
+++ b/core/server/server.spec.js
@@ -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`
diff --git a/core/server/test-public/img/frontend-image.png b/core/server/test-public/img/frontend-image.png
new file mode 100644
index 0000000000..311cb687cb
Binary files /dev/null and b/core/server/test-public/img/frontend-image.png differ
diff --git a/cypress/e2e/main-page.cy.js b/cypress/e2e/main-page.cy.js
index 14a9c21613..20870e22b1 100644
--- a/cypress/e2e/main-page.cy.js
+++ b/cypress/e2e/main-page.cy.js
@@ -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"]')
})
})
diff --git a/dangerfile.js b/dangerfile.js
index 2806b12b84..c54b5a4392 100644
--- a/dangerfile.js
+++ b/dangerfile.js
@@ -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')
diff --git a/doc/code-walkthrough.md b/doc/code-walkthrough.md
index b1f94c496a..a1a4637dfd 100644
--- a/doc/code-walkthrough.md
+++ b/doc/code-walkthrough.md
@@ -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
diff --git a/doc/self-hosting.md b/doc/self-hosting.md
index 096a666191..63d5acf1d3 100644
--- a/doc/self-hosting.md
+++ b/doc/self-hosting.md
@@ -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.
diff --git a/frontend/babel.config.cjs b/frontend/babel.config.cjs
new file mode 100644
index 0000000000..6752648189
--- /dev/null
+++ b/frontend/babel.config.cjs
@@ -0,0 +1,3 @@
+module.exports = {
+ presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
+}
diff --git a/frontend/components/badge-examples.tsx b/frontend/components/badge-examples.tsx
deleted file mode 100644
index 7d17b81827..0000000000
--- a/frontend/components/badge-examples.tsx
+++ /dev/null
@@ -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 (
-
- {title}:
- |
-
- |
-
- {exampleUrl}
- |
-
- )
-}
-
-export function BadgeExamples({
- examples,
- baseUrl,
- onClick,
-}: {
- examples: RenderableExample[]
- baseUrl?: string
- onClick: (exampleData: RenderableExample) => void
-}): JSX.Element {
- return (
-
-
- {examples.map(exampleData => (
-
- ))}
-
-
- )
-}
diff --git a/frontend/components/category-headings.tsx b/frontend/components/category-headings.tsx
deleted file mode 100644
index 2f1d03c9a5..0000000000
--- a/frontend/components/category-headings.tsx
+++ /dev/null
@@ -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 (
-
- {name}
-
- )
-}
-
-export function CategoryHeadings({
- categories,
-}: {
- categories: Category[]
-}): JSX.Element {
- return (
-
- {categories.map(category => (
-
- ))}
-
- )
-}
-
-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 (
-
-
- {categories.map(({ id, name }) => (
- -
- {name}
-
- ))}
-
-
- )
-}
diff --git a/frontend/components/common.tsx b/frontend/components/common.tsx
deleted file mode 100644
index e878e6792b..0000000000
--- a/frontend/components/common.tsx
+++ /dev/null
@@ -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`
- padding: 2px;
- height: ${({ height }) => height};
- vertical-align: middle;
- display: ${({ display }) => display};
-
- ${({ clickable }) =>
- clickable &&
- css`
- cursor: pointer;
- `};
-`
-
-interface BadgeProps extends React.HTMLAttributes {
- 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 (
-
- {src ? (
- object ? (
-
- ) : (
-
- )
- ) : (
- nonBreakingSpace
- )}
-
- )
-}
-
-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;
-`
diff --git a/frontend/components/customizer/builder-common.tsx b/frontend/components/customizer/builder-common.tsx
deleted file mode 100644
index 9391840287..0000000000
--- a/frontend/components/customizer/builder-common.tsx
+++ /dev/null
@@ -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 (
-
- {children}
-
- )
-}
-
-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;
-`
diff --git a/frontend/components/customizer/copied-content-indicator.tsx b/frontend/components/customizer/copied-content-indicator.tsx
deleted file mode 100644
index a45a0dfb9b..0000000000
--- a/frontend/components/customizer/copied-content-indicator.tsx
+++ /dev/null
@@ -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
-): 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 (
-
-
- {copiedContent}
-
- {children}
-
- )
-}
-export const CopiedContentIndicator = forwardRef(_CopiedContentIndicator)
diff --git a/frontend/components/customizer/customizer.tsx b/frontend/components/customizer/customizer.tsx
deleted file mode 100644
index 503efc6741..0000000000
--- a/frontend/components/customizer/customizer.tsx
+++ /dev/null
@@ -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() as React.MutableRefObject
- const [path, setPath] = useState('')
- const [queryString, setQueryString] = useState()
- const [pathIsComplete, setPathIsComplete] = useState()
- const [markup, setMarkup] = useState()
- const [message, setMessage] = useState()
-
- 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 (
-
-
-
- )
- }
-
- const copyMarkup = React.useCallback(
- async function (markupFormat: MarkupFormat): Promise {
- 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 (
-
- {renderLivePreview()}
-
-
-
- {message && (
-
-
{message}
-
Markup: {markup}
-
- )}
-
- )
- }
-
- 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 (
-
- )
-}
diff --git a/frontend/components/customizer/path-builder.tsx b/frontend/components/customizer/path-builder.tsx
deleted file mode 100644
index 5e4d10f653..0000000000
--- a/frontend/components/customizer/path-builder.tsx
+++ /dev/null
@@ -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`
- 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`
- 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): void {
- setNamedParams({
- ...namedParams,
- [name]: value,
- })
- },
- [setNamedParams, namedParams]
- )
-
- function renderLiteral(
- literal: string,
- tokenIndex: number,
- pathContainsOnlyLiterals: boolean
- ): JSX.Element {
- return (
-
-
- {literal}
-
-
- )
- }
-
- function renderNamedParamInput(token: Key): JSX.Element {
- const { pattern } = token
- const name = `${token.name}`
- const options = patternToOptions(pattern)
-
- const value = namedParams[name]
-
- if (options) {
- return (
-
-
- {options.map(option => (
-
- ))}
-
- )
- } else {
- return (
-
- )
- }
- }
-
- 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 (
-
- {renderLiteral(prefix, tokenIndex, false)}
-
-
- {humanizeString(name)}
- {optional ? (optional) : null}
-
- {renderNamedParamInput(token)}
-
- {namedParamIndex === 0 ? `e.g. ${exampleValue}` : exampleValue}
-
-
-
- )
- }
-
- let namedParamIndex = 0
- const pathContainsOnlyLiterals = tokens.every(
- token => typeof token === 'string'
- )
- return (
-
- {tokens.map((token, tokenIndex) =>
- typeof token === 'string'
- ? renderLiteral(token, tokenIndex, pathContainsOnlyLiterals)
- : renderNamedParam(token, tokenIndex, namedParamIndex++)
- )}
-
- )
-}
diff --git a/frontend/components/customizer/query-string-builder.tsx b/frontend/components/customizer/query-string-builder.tsx
deleted file mode 100644
index bb369d0c6b..0000000000
--- a/frontend/components/customizer/query-string-builder.tsx
+++ /dev/null
@@ -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
- badgeOptions: Record
-}): {
- 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
- 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
-}): JSX.Element {
- return (
-
- |
-
- {humanizeString(name).toLowerCase()}
-
- |
-
- {isStringParam && (
-
- {stringParamCount === 0 ? `e.g. ${exampleValue}` : exampleValue}
-
- )}
- |
-
- {isStringParam ? (
-
- ) : (
-
- )}
- |
-
- )
-}
-
-function BadgeOptionInput({
- name,
- value,
- handleBadgeOptionChange,
-}: {
- name: BadgeOptionName
- value: string
- handleBadgeOptionChange: ChangeEventHandler<
- HTMLSelectElement | HTMLInputElement
- >
-}): JSX.Element {
- if (name === 'style') {
- return (
-
- )
- } else {
- return (
-
- )
- }
-}
-
-function BadgeOption({
- name,
- value,
- handleBadgeOptionChange,
-}: {
- name: BadgeOptionName
- value: string
- handleBadgeOptionChange: ChangeEventHandler
-}): JSX.Element {
- const {
- label = humanizeString(name),
- shieldsDefaultValue: hasShieldsDefaultValue,
- } = getBadgeOption(name)
- return (
-
- |
- {label}
- |
-
- {!hasShieldsDefaultValue && (
- optional
- )}
- |
-
-
- |
-
- )
-}
-
-// 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)
- )
-
- const handleServiceQueryParamChange = React.useCallback(
- function ({
- target: { name, type: targetType, checked, value },
- }: ChangeEvent): void {
- const outValue = targetType === 'checkbox' ? checked : value
- setQueryParams({ ...queryParams, [name]: outValue })
- },
- [setQueryParams, queryParams]
- )
-
- const handleBadgeOptionChange = React.useCallback(
- function ({
- target: { name, value },
- }: ChangeEvent): 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 && (
-
-
-
- {Object.entries(queryParams).map(([name, value]) => {
- const isStringParam = typeof value === 'string'
- return (
-
- )
- })}
-
-
-
- )}
-
-
-
- {Object.entries(badgeOptions).map(([name, value]) => (
-
- ))}
-
-
-
- >
- )
-}
diff --git a/frontend/components/customizer/request-markup-button.tsx b/frontend/components/customizer/request-markup-button.tsx
deleted file mode 100644
index 8d1f93ffd5..0000000000
--- a/frontend/components/customizer/request-markup-button.tsx
+++ /dev/null
@@ -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 (
-
- )
-}
-
-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
- isDisabled: boolean
-}): JSX.Element {
- // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/35572
- // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/28884#issuecomment-471341041
- const selectRef = useRef