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 ( - - - - ) -} 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 ? ( - alt - ) : ( - {alt} - ) - ) : ( - 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 ( -
- - -
{renderMarkupAndLivePreview()}
- - ) -} 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>() as React.MutableRefObject< - Select