From bcfbaec6f82f1334ee32567925b68fbc60d5b02d Mon Sep 17 00:00:00 2001 From: chris48s Date: Sat, 17 Jun 2023 10:59:07 +0100 Subject: [PATCH] migrate frontend to docusaurus (#9014) * delete loads of really important stuff that we definitely need * v basic MVP smoosh docusaurus PoC into repo * TODO * delete more really important stuff * TODO * tidyup: use run-s * don't redirect images used in frontend to raster proxy * fix routing * preserve the /endpoint link * delete the blog (for now) I would quite like to re-add this at some point but its not really the top priority thing right now * content edits * appease the lint gods * update danger rules * remove placeholder * cypress tests * dockerhub --> ghcr * Revert "dockerhub --> ghcr" This reverts commit ef74cbb26b1c24ce987a8975e60313682f9161f8. * downgrade lockfile format * implement defs/BASE_URL * fix e2e build * actually fix cypress tests * always run cypress tests on build * this never worked * add command for docusaurus:clear * delete more code we don't need any more * update ESLint/prettier config * delete unsused exports * documentation updates * delete a fairly large chunk of our dependency tree * allow base_url as build arg to Dockerfile * fixup dockerfile * work out base url at runtime if not set doing this at image build time is not the right approach * remove gatsby monorepo from closebot * rename HomepageFeatures to homepage-features --- .eslintrc.yml | 47 +- .github/actions/close-bot/helpers.js | 1 - .github/actions/frontend-tests/action.yml | 31 - .github/workflows/test-e2e.yml | 5 +- .github/workflows/test-frontend.yml | 26 - .gitignore | 14 +- .mocharc-frontend.yml | 5 - .nycrc-frontend.json | 10 - .prettierignore | 4 +- README.md | 2 +- core/badge-urls/make-badge-url.d.ts | 94 - core/badge-urls/make-badge-url.js | 123 +- core/badge-urls/make-badge-url.spec.js | 142 - core/base-service/legacy-request-handler.js | 2 +- core/base-service/openapi.js | 4 +- core/base-service/openapi.spec.js | 1 - core/base-service/service-definitions.js | 10 +- core/server/server.js | 4 +- core/server/server.spec.js | 5 + .../server/test-public/img/frontend-image.png | Bin 0 -> 16615 bytes cypress/e2e/main-page.cy.js | 58 +- dangerfile.js | 4 +- doc/code-walkthrough.md | 14 +- doc/self-hosting.md | 6 +- frontend/babel.config.cjs | 3 + frontend/components/badge-examples.tsx | 108 - frontend/components/category-headings.tsx | 84 - frontend/components/common.tsx | 125 - .../components/customizer/builder-common.tsx | 46 - .../customizer/copied-content-indicator.tsx | 73 - frontend/components/customizer/customizer.tsx | 156 - .../components/customizer/path-builder.tsx | 258 - .../customizer/query-string-builder.tsx | 348 - .../customizer/request-markup-button.tsx | 134 - frontend/components/development/logo-page.tsx | 77 - .../components/development/style-page.tsx | 168 - frontend/components/donate.tsx | 16 - frontend/components/dynamic-badge-maker.tsx | 103 - frontend/components/footer.tsx | 83 - frontend/components/header.tsx | 26 - frontend/components/main.tsx | 185 - frontend/components/markup-modal/index.tsx | 35 - .../markup-modal/markup-modal-content.tsx | 44 - frontend/components/meta.tsx | 25 - frontend/components/search.tsx | 36 - frontend/components/snippet.tsx | 61 - frontend/components/static-badge-maker.tsx | 77 - frontend/components/usage.tsx | 448 - frontend/constants.ts | 36 - frontend/docs/intro.md | 5 + frontend/docusaurus.config.cjs | 122 + frontend/gatsby-browser.js | 18 - frontend/gatsby-config.js | 33 - frontend/gatsby-node.js | 45 - frontend/images/favicon.png | Bin 332 -> 0 bytes frontend/images/logo.svg | 1 - frontend/lib/generate-image-markup.spec.ts | 72 - frontend/lib/generate-image-markup.ts | 99 - frontend/lib/pattern-helpers.spec.ts | 28 - frontend/lib/pattern-helpers.ts | 34 - frontend/lib/redirect-legacy-routes.ts | 15 - .../lib/service-definitions/index.spec.ts | 16 - frontend/lib/service-definitions/index.ts | 67 - .../service-definition-set-helper.spec.ts | 31 - .../service-definition-set-helper.ts | 54 - frontend/lib/supported-features.ts | 5 - frontend/package.json | 16 - frontend/pages/community.tsx | 118 - frontend/pages/endpoint.tsx | 255 - frontend/pages/index.tsx | 2 - frontend/sidebars.cjs | 31 + frontend/src/components/homepage-features.js | 92 + .../components/homepage-features.module.css | 11 + frontend/src/css/custom.css | 28 + frontend/src/pages/community.md | 62 + frontend/src/pages/index.js | 39 + frontend/src/pages/index.module.css | 23 + frontend/src/theme/ApiDemoPanel/Curl/index.js | 300 + .../src/theme/ApiDemoPanel/Response/index.js | 66 + frontend/src/theme/DocPaginator/index.js | 3 + frontend/static/.nojekyll | 0 frontend/static/img/favicon.ico | Bin 0 -> 15086 bytes frontend/static/img/logo.png | Bin 0 -> 16615 bytes frontend/types/assets.d.ts | 5 - .../mapbox__react-click-to-select/index.d.ts | 11 - package-lock.json | 67825 +++++++--------- package.json | 83 +- scripts/export-service-definitions-cli.js | 24 - scripts/export-supported-features-cli.js | 22 - services/amo/amo-downloads.service.js | 2 +- services/endpoint/endpoint.service.js | 2 +- services/github/auth/acceptor.js | 2 +- services/text-formatters.spec.js | 1 - tsconfig.json | 16 - 94 files changed, 32221 insertions(+), 40830 deletions(-) delete mode 100644 .github/actions/frontend-tests/action.yml delete mode 100644 .github/workflows/test-frontend.yml delete mode 100644 .mocharc-frontend.yml delete mode 100644 .nycrc-frontend.json delete mode 100644 core/badge-urls/make-badge-url.d.ts delete mode 100644 core/badge-urls/make-badge-url.spec.js create mode 100644 core/server/test-public/img/frontend-image.png create mode 100644 frontend/babel.config.cjs delete mode 100644 frontend/components/badge-examples.tsx delete mode 100644 frontend/components/category-headings.tsx delete mode 100644 frontend/components/common.tsx delete mode 100644 frontend/components/customizer/builder-common.tsx delete mode 100644 frontend/components/customizer/copied-content-indicator.tsx delete mode 100644 frontend/components/customizer/customizer.tsx delete mode 100644 frontend/components/customizer/path-builder.tsx delete mode 100644 frontend/components/customizer/query-string-builder.tsx delete mode 100644 frontend/components/customizer/request-markup-button.tsx delete mode 100644 frontend/components/development/logo-page.tsx delete mode 100644 frontend/components/development/style-page.tsx delete mode 100644 frontend/components/donate.tsx delete mode 100644 frontend/components/dynamic-badge-maker.tsx delete mode 100644 frontend/components/footer.tsx delete mode 100644 frontend/components/header.tsx delete mode 100644 frontend/components/main.tsx delete mode 100644 frontend/components/markup-modal/index.tsx delete mode 100644 frontend/components/markup-modal/markup-modal-content.tsx delete mode 100644 frontend/components/meta.tsx delete mode 100644 frontend/components/search.tsx delete mode 100644 frontend/components/snippet.tsx delete mode 100644 frontend/components/static-badge-maker.tsx delete mode 100644 frontend/components/usage.tsx delete mode 100644 frontend/constants.ts create mode 100644 frontend/docs/intro.md create mode 100644 frontend/docusaurus.config.cjs delete mode 100644 frontend/gatsby-browser.js delete mode 100644 frontend/gatsby-config.js delete mode 100644 frontend/gatsby-node.js delete mode 100644 frontend/images/favicon.png delete mode 100644 frontend/images/logo.svg delete mode 100644 frontend/lib/generate-image-markup.spec.ts delete mode 100644 frontend/lib/generate-image-markup.ts delete mode 100644 frontend/lib/pattern-helpers.spec.ts delete mode 100644 frontend/lib/pattern-helpers.ts delete mode 100644 frontend/lib/redirect-legacy-routes.ts delete mode 100644 frontend/lib/service-definitions/index.spec.ts delete mode 100644 frontend/lib/service-definitions/index.ts delete mode 100644 frontend/lib/service-definitions/service-definition-set-helper.spec.ts delete mode 100644 frontend/lib/service-definitions/service-definition-set-helper.ts delete mode 100644 frontend/lib/supported-features.ts delete mode 100644 frontend/pages/community.tsx delete mode 100644 frontend/pages/endpoint.tsx delete mode 100644 frontend/pages/index.tsx create mode 100644 frontend/sidebars.cjs create mode 100644 frontend/src/components/homepage-features.js create mode 100644 frontend/src/components/homepage-features.module.css create mode 100644 frontend/src/css/custom.css create mode 100644 frontend/src/pages/community.md create mode 100644 frontend/src/pages/index.js create mode 100644 frontend/src/pages/index.module.css create mode 100644 frontend/src/theme/ApiDemoPanel/Curl/index.js create mode 100644 frontend/src/theme/ApiDemoPanel/Response/index.js create mode 100644 frontend/src/theme/DocPaginator/index.js create mode 100644 frontend/static/.nojekyll create mode 100644 frontend/static/img/favicon.ico create mode 100644 frontend/static/img/logo.png delete mode 100644 frontend/types/assets.d.ts delete mode 100644 frontend/types/mapbox__react-click-to-select/index.d.ts delete mode 100644 scripts/export-service-definitions-cli.js delete mode 100644 scripts/export-supported-features-cli.js delete mode 100644 tsconfig.json diff --git a/.eslintrc.yml b/.eslintrc.yml index cda0674cbf8125..c8232bb936057c 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 - rules: - no-console: 'off' - '@typescript-eslint/explicit-module-boundary-types': 'off' - files: - - '**/*.@(ts|tsx)' + - '**/*.js' + - '!frontend/**/*.js' + - '!badge-maker/**/*.js' + env: + node: true + es6: true 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' + no-console: 'off' - files: - - core/**/*.ts + - '**/*.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 4d766cf70ecc93..aa0854846f85c0 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 2ef870586dd9a6..00000000000000 --- 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 aa750533967149..dcc6b9506fc4bc 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 e5f2a2e7a7da21..00000000000000 --- 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 5c8b82a5b47e6b..0d7154143a5d27 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 0e076ba900ada4..00000000000000 --- 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 447812c5504b5e..00000000000000 --- 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 9f913cee41ec1c..fed0f550dd0384 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 773d060ac3299c..b8c4f8f9706ecd 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 bf925cf72970ee..00000000000000 --- 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 8870c9acea44ca..13953a4add1027 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 c8c3fcbb1bd87f..00000000000000 --- 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 9ef3d4efeb4b5c..f7cbb94001f3c7 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 1cb463a76c5529..4ea3169a7d5ae1 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 8d6b3f09e46588..217cb37ac9424d 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 8718b58cbdf1a7..8c1c94a155fc4c 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 a1c0daa31578ca..98d8673c97b917 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 936033d2eedf74..6de51974ff9590 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 0000000000000000000000000000000000000000..311cb687cb4cf9bffd9c7d809a798d9b6523a22c GIT binary patch literal 16615 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 literal 0 HcmV?d00001 diff --git a/cypress/e2e/main-page.cy.js b/cypress/e2e/main-page.cy.js index 14a9c216137ad1..20870e22b16d7f 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() - it('Customizate badges', function () { - visitAndWait('/') + cy.contains('Discourse status') + cy.contains('Stack Exchange questions') + }) - cy.get(SEARCH_INPUT).type('issues') + it('Shows expected code examples', function () { + visitAndWait('/badges/static-badge') - cy.contains('/github/issues/:user/:repo').click() + 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') + }) - cy.get('input[name="user"]').type('badges') - cy.get('input[name="repo"]').type('shields') - cy.get('table input[name="color"]').type('orange') + it('Build a badge', function () { + visitAndWait('/badges/git-hub-issues') - cy.get(`img[src='${backendUrl}/github/issues/badges/shields?color=orange']`) - }) + cy.contains('/github/issues/:user/:repo') - it('Do not duplicate example parameters', function () { - visitAndWait('/category/funding') + cy.get('input[placeholder="user"]').type('badges') + cy.get('input[placeholder="repo"]').type('shields') - cy.contains('GitHub Sponsors').click() - cy.get('[name="style"]').should($style => { - expect($style).to.have.length(1) - }) + 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 2806b12b84528a..c54b5a4392ebf9 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 b1f94c496ab0f1..a1a4637dfdc635 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 096a6661916591..63d5acf1d31df0 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 00000000000000..67526481891509 --- /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 7d17b81827e2c2..00000000000000 --- 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 2f1d03c9a512ac..00000000000000 --- 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 e878e6792be5c0..00000000000000 --- 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 939184028728f3..00000000000000 --- 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 a45a0dfb9b147b..00000000000000 --- 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 503efc6741978b..00000000000000 --- 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 5e4d10f653f9e1..00000000000000 --- 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 bb369d0c6b8435..00000000000000 --- 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 8d1f93ffd5eea7..00000000000000 --- 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