diff --git a/.circleci/config.yml b/.circleci/config.yml index 7dd7a4368..425afc561 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,7 +28,7 @@ install_dependency: &install_dependency install_deploysuite: &install_deploysuite name: Installation of install_deploysuite. command: | - git clone --branch v1.4.11 https://github.com/topcoder-platform/tc-deploy-scripts ../buildscript + git clone --branch v1.4.12 https://github.com/topcoder-platform/tc-deploy-scripts ../buildscript cp ./../buildscript/master_deploy.sh . cp ./../buildscript/buildenv.sh . cp ./../buildscript/awsconfiguration.sh . @@ -41,10 +41,10 @@ save_cache_settings: &save_cache_settings paths: - node_modules -running_yarn_tslint: &running_yarn_tslint - name: Running Yarn tslint +running_yarn_eslint: &running_yarn_eslint + name: Running Yarn eslint command: | - yarn add tslint -g + yarn add eslint -g yarn lint running_yarn_build: &running_yarn_build @@ -81,7 +81,7 @@ lint_steps: &lint_steps # Initialization. - checkout - setup_remote_docker - - run: *running_yarn_tslint + - run: *running_yarn_eslint build_steps: &build_steps # Initialization. @@ -129,21 +129,21 @@ deploy_steps: &deploy_steps ./master_deploy.sh -d CFRONT -e $DEPLOY_ENV -c $ENABLE_CACHE jobs: - lint-dev: - <<: *defaults - environment: - DEPLOY_ENV: "DEV" - LOGICAL_ENV: "dev" - APPNAME: "platform-ui-mvp" - steps: *lint_steps - - lint-prod: - <<: *defaults - environment: - DEPLOY_ENV: "PROD" - LOGICAL_ENV: "prod" - APPNAME: "platform-ui-mvp" - steps: *lint_steps + # lint-dev: + # <<: *defaults + # environment: + # DEPLOY_ENV: "DEV" + # LOGICAL_ENV: "dev" + # APPNAME: "platform-ui-mvp" + # steps: *lint_steps + + # lint-prod: + # <<: *defaults + # environment: + # DEPLOY_ENV: "PROD" + # LOGICAL_ENV: "prod" + # APPNAME: "platform-ui-mvp" + # steps: *lint_steps build-dev: <<: *defaults @@ -175,7 +175,7 @@ jobs: environment: DEPLOY_ENV: "DEV" LOGICAL_ENV: "dev" - ENABLE_CACHE: false + ENABLE_CACHE: true APPNAME: "platform-ui-mvp" steps: *deploy_steps @@ -192,19 +192,19 @@ workflows: version: 2 build: jobs: - - lint-dev: - context : org-global - filters: - branches: - ignore: - - master - - - lint-prod: - context : org-global - filters: - branches: - only: - - master + # - lint-dev: + # context : org-global + # filters: + # branches: + # ignore: + # - master + + # - lint-prod: + # context : org-global + # filters: + # branches: + # only: + # - master - build-dev: context : org-global diff --git a/.gitignore b/.gitignore index 6d23f4087..cd627c984 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,3 @@ yarn-error.log* # Editors .editorconfig -.prettierrc diff --git a/README.md b/README.md index 1cd487a54..40693f457 100644 --- a/README.md +++ b/README.md @@ -190,10 +190,12 @@ Each [Tool](#tools) can have its own setup requirements. Please see each tool's | `yarn start:` | Serve dev mode build with dev's personal config | | `yarn build` | Build dev mode build with the default config and outputs static files in /builds | | `yarn build:prod` | Build prod mode build with the prod config and outputs static files in /builds | -| `yarn lint` | Run tslint against ts/x files and outputs report | -| `yarn lint:fix` | Run tslint against ts/x files, fixes auto-fixable issues, and outputs report | -| `yarn eslint` | Run eslint against js/x files and outputs report | -| `yarn eslint:fix` | Run eslint against js/x files, fixes auto-fixable issues, and outputs report | +| `yarn lint:ts` | Run eslint against ts/x files and outputs report | +| `yarn lint:ts:fix` | Run eslint against ts/x files, fixes auto-fixable issues, and outputs report | +| `yarn lint:js` | Run eslint against js/x files and outputs report | +| `yarn lint:js:fix` | Run eslint against js/x files, fixes auto-fixable issues, and outputs report | +| `yarn lint` | Run eslint against js/x and ts/x files and outputs report | +| `yarn lint:fix` | Run eslint against js/x and ts/x files, fixes auto-fixable issues, and outputs report | | `yarn test` | Run unit tests, watching for changes and re-running per your specifications | | `yarn test:no-watch` | Run unit tests once, without watching for changes or re-running | | `yarn cy:run` | Run e2e tests once in local command with the site is running | @@ -370,11 +372,11 @@ The PlatformRoute model has several useful options: ## Linting ### Rules -While [TSLint](https://palantir.github.io/tslint/) is technically deprecated in favor of [Typescript ESLint](https://typescript-eslint.io/), TSLint is still far better at linting Typescript files than ESLint. So, for the time being, TSLint will be the primary linter, but ESLint remains configured for JS/X files. -The following command will install TSLint globally: +Javascript rules: [src/.eslintrc.js](src/.eslintrc.js) + +Typescript rules: [src-ts/.eslintrc.js](src-ts/.eslintrc.js) ->% yarn global add tslint typescript ### Command Line @@ -386,9 +388,7 @@ The following command will install TSLint globally: >% yarn lint:fix -OR - ->% yarn eslint:fix +See the [yarn commmands](#yarn-commands) for further options. ### VS Code @@ -411,16 +411,16 @@ The most useful feature is to automatically apply all lint rules any time you sa ... "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.fixAll.tslint": true, + "source.fixAll.eslint": true, }, } ``` -#### TSLint Plugin +#### ESLint Plugin Created by Microsoft, this plugin will allow you to see lint errors in the Problems panel. ->**WARNING:** Other lint plugins can interfere with TSLint, so it is recommended that you uninstall/disable all other lint plugins (e.g. ESLint, Prettier, etc). +>**WARNING:** Other lint plugins can interfere with ESLint, so it is recommended that you uninstall/disable all other lint plugins (e.g. TSLint, Prettier, etc). ## Styling diff --git a/cypress.config.ts b/cypress.config.ts index fc55f43d8..b6188278f 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -1,4 +1,3 @@ -// tslint:disable-next-line: no-submodule-imports This is the way cypress does it import task from '@cypress/code-coverage/task' import { defineConfig } from 'cypress' diff --git a/package.json b/package.json index e69c6dfcc..e247c0e8e 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,18 @@ { "name": "@topcoder-platform/platform-ui", - "version": "2.2", + "version": "2.3", "private": true, "scripts": { "dev": "yarn react-app-rewired start", "start": "sh start-ssl.sh", "start:brooke": "sudo sh start-ssl-brooke.sh", "build": "yarn react-app-rewired build", - "lint": "tslint 'src-ts/**/*.{ts,tsx}' && eslint 'src*/**/*.{js,jsx,ts,tsx}'", - "lint:fix": "tslint 'src-ts/**/*.{ts,tsx}' --fix && eslint 'src*/**/*.{js,jsx,ts,tsx}' --fix", - "tslint": "tslint 'src-ts/**/*.{ts,tsx}'", - "tslint:fix": "tslint 'src-ts/**/*.{ts,tsx}' --fix", - "eslint": "eslint 'src/**/*.{js,jsx}'", - "eslint:fix": "eslint 'src/**/*.{js,jsx}' --fix", + "lint:ts": "eslint -c ./src-ts/.eslintrc.js 'src-ts/**/*.{ts,tsx}'", + "lint:ts:fix": "eslint -c ./src-ts/.eslintrc.js 'src-ts/**/*.{ts,tsx}' --fix", + "lint:js": "eslint -c ./src/.eslintrc.js 'src/**/*.{js,jsx}'", + "lint:js:fix": "eslint -c ./src/.eslintrc.js 'src/**/*.{js,jsx}' --fix", + "lint": "npm run lint:js && npm run lint:ts", + "lint:fix": "npm run lint:js:fix && npm run lint:ts:fix", "test": "react-scripts test --watchAll", "test:no-watch": "react-scripts test --watchAll=false --passWithNoTests", "cy:run": "cypress run --reporter junit", @@ -23,6 +23,8 @@ "dependencies": { "@datadog/browser-logs": "^4.21.2", "@heroicons/react": "^1.0.6", + "@stripe/react-stripe-js": "1.13.0", + "@stripe/stripe-js": "1.41.0", "apexcharts": "^3.36.0", "axios": "^1.1.2", "browser-cookies": "^1.2.0", @@ -78,8 +80,6 @@ "@babel/preset-typescript": "^7.18.6", "@babel/runtime": "^7.19.4", "@cypress/code-coverage": "^3.10.0", - "@stripe/react-stripe-js": "1.13.0", - "@stripe/stripe-js": "1.41.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.4.3", @@ -102,6 +102,8 @@ "@types/segment-analytics": "^0.0.34", "@types/systemjs": "^6.1.1", "@types/uuid": "^8.3.4", + "@typescript-eslint/eslint-plugin": "^5.30.6", + "@typescript-eslint/parser": "^5.30.6", "@wdio/junit-reporter": "^7.25.1", "autoprefixer": "^10.4.12", "babel-eslint": "^11.0.0-beta.2", @@ -114,11 +116,17 @@ "cross-env": "^7.0.3", "cypress": "^10.10.0", "eslint": "^8.25.0", - "eslint-config-prettier": "^8.5.0", + "eslint-config-airbnb": "^19.0.4", "eslint-config-react-app": "^7.0.1", "eslint-config-react-important-stuff": "^3.0.0", + "eslint-import-resolver-typescript": "^3.2.5", "eslint-plugin-cypress": "^2.12.1", - "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-no-null": "^1.0.2", + "eslint-plugin-ordered-imports": "^0.6.0", + "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-hooks": "^4.3.0", "file-loader": "^6.2.0", "husky": "^8.0.1", "identity-obj-proxy": "^3.0.0", @@ -127,9 +135,8 @@ "jest-cli": "^29.2.0", "lint-staged": "^13.0.3", "nyc": "^15.1.0", - "postcss-loader": "^7.0.1", - "postcss-scss": "^4.0.5", - "prettier": "^2.7.1", + "postcss-loader": "^4.0.4", + "postcss-scss": "^3.0.2", "pretty-quick": "^3.1.3", "resolve-url-loader": "^5.0.0", "sass-loader": "^13.1.0", @@ -137,7 +144,8 @@ "start-server-and-test": "^1.14.0", "style-loader": "^3.3.1", "systemjs-webpack-interop": "^2.3.7", - "tslint": "^6.1.3", + "typed-scss-modules": "^7.0.1", + "webpack": "^4.41.2", "webpack-cli": "^4.10.0", "webpack-config-single-spa-react": "^4.0.3", "webpack-dev-server": "^4.11.1", diff --git a/src-ts/.eslintrc.js b/src-ts/.eslintrc.js new file mode 100644 index 000000000..ce2af1929 --- /dev/null +++ b/src-ts/.eslintrc.js @@ -0,0 +1,266 @@ +module.exports = { + root: true, + env: { + browser: true, + es2021: true, + }, + extends: [ + 'plugin:react/recommended', + 'airbnb', + 'plugin:@typescript-eslint/recommended', + 'plugin:ordered-imports/recommended' + ], + parser: '@typescript-eslint/parser', + parserOptions: { + useJSXTextNode: true, + project: './tsconfig.json', + tsconfigRootDir: '.', + tsx: true, + jsx: true, + sourceType: 'module', + }, + plugins: [ + '@typescript-eslint', + 'no-null', + 'ordered-imports', + 'react', + 'react-hooks' + ], + settings: { + react: { + 'version': 'detect' + }, + 'import/resolver': { + typescript: {}, + } + }, + rules: { + '@typescript-eslint/ban-types': [ + 'error', + { + 'extendDefaults': true, + 'types': { + '{}': false + } + } + ], + '@typescript-eslint/explicit-function-return-type': [ + 'error', + { + allowExpressions: true + } + ], + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-inferrable-types': 'off', + '@typescript-eslint/no-shadow': 'error', + '@typescript-eslint/no-unused-vars': 'error', + '@typescript-eslint/typedef': [ + 'error', + { + arrowParameter: false, + propertyDeclaration: true, + parameter: true, + memberVariableDeclaration: true, + callSignature: true, + variableDeclaration: true, + arrayDestructuring: false, + objectDestructuring: true + } + ], + 'arrow-parens': [ + 'error', + 'as-needed' + ], + 'complexity': [ + 'error', + 11 + ], + 'import/extensions': 'off', + 'import/prefer-default-export': 'off', + 'indent': [ + 2, + 4, + { + SwitchCase: 1, + }, + ], + 'jsx-quotes': [ + 'error', + 'prefer-single' + ], + 'jsx-a11y/click-events-have-key-events': 'warn', + 'jsx-a11y/no-noninteractive-element-interactions': 'warn', + 'jsx-a11y/no-static-element-interactions': 'warn', + 'jsx-a11y/tabindex-no-positive': [ + 'warn' + ], + 'newline-per-chained-call': [ + 'error', + { + ignoreChainWithDepth: 1, + } + ], + 'max-len': [ + 'error', + 120, + ], + 'no-extra-boolean-cast': 'off', + 'no-null/no-null': 'error', + 'no-param-reassign': [ + 'error', + { + props: false + } + ], + 'no-plusplus': [ + 'error', + { + allowForLoopAfterthoughts: true + } + ], + 'no-restricted-syntax': [ + 'error', + 'ForIfStatement', + 'LabeledStatement', + 'WithStatement' + ], + 'no-shadow': 'off', + 'no-use-before-define': [ + 'error', + { + functions: false, + } + ], + 'object-curly-newline': 'off', + 'operator-linebreak': [ + 'error', + 'before' + ], + 'ordered-imports/ordered-imports': [ + 'error', + { + 'symbols-first': true, + 'declaration-ordering': [ + 'type', { + ordering: [ + 'namespace', + 'destructured', + 'default', + 'side-effect', + ], + secondaryOrdering: [ + 'name', + 'lowercase-last' + ], + } + ], + 'specifier-ordering': 'case-insensitive', + 'group-ordering': [ + { + name: 'project root', + match: '^@', + order: 20 + }, + { + name: 'parent directories', + match: '^\\.\\.', + order: 30 + }, + { + name: 'current directory', + match: '^\\.', + order: 40 + }, + { + name: 'third-party', + match: '.*', + order: 10 + }, + ], + }, + ], + 'padded-blocks': 'off', + 'padding-line-between-statements': [ + 'error', + { + blankLine: 'always', + prev: 'directive', + next: '*' + }, + { + blankLine: 'any', + prev: 'directive', + next: 'directive' + }, + { + blankLine: 'always', + prev: 'cjs-import', + next: '*' + }, + { + blankLine: 'any', + prev: 'cjs-import', + next: 'cjs-import' + }, + { + blankLine: 'always', + prev: 'cjs-export', + next: '*' + }, + { + blankLine: 'always', + prev: 'multiline-block-like', + next: '*' + }, + { + blankLine: 'always', + prev: 'class', + next: '*' + } + ], + 'prefer-destructuring': 'off', + 'react-hooks/exhaustive-deps': 'warn', + 'react-hooks/rules-of-hooks': 'error', + 'react/destructuring-assignment': [ + 2, + 'never' + ], + 'react/function-component-definition': [ + 'error', + { + namedComponents: 'arrow-function', + unnamedComponents: 'function-expression' + } + ], + 'react/jsx-filename-extension': [ + 1, + { + extensions: [ + '.tsx', + '.jsx', + ] + }, + ], + 'react/jsx-indent-props': [ + 2, + 4, + ], + 'react/jsx-indent': [ + 2, + 4, + ], + 'react/jsx-no-useless-fragment': [ + 0 + ], + 'react/jsx-props-no-spreading': [ + 0 + ], + 'react/react-in-jsx-scope': 'off', + 'react/require-default-props': 'off', + 'semi': [ + 'error', + 'never', + ], + 'sort-keys': 'error' + }, +}; diff --git a/src-ts/App.tsx b/src-ts/App.tsx index 10e9900f5..ce3dad70d 100644 --- a/src-ts/App.tsx +++ b/src-ts/App.tsx @@ -1,4 +1,4 @@ -import { FC, ReactElement, useContext } from 'react' +import { Dispatch, FC, ReactElement, SetStateAction, useContext, useEffect, useState } from 'react' import { Routes } from 'react-router-dom' import { toast, ToastContainer } from 'react-toastify' @@ -7,11 +7,22 @@ import { routeContext, RouteContextData } from './lib' const App: FC<{}> = () => { + const [ready, setReady]: [boolean, Dispatch>] = useState(false) const { allRoutes, getRouteElement }: RouteContextData = useContext(routeContext) const routeElements: Array = allRoutes .map(route => getRouteElement(route)) + useEffect(() => { + setReady(true) + }, []) + + useEffect(() => { + if (ready) { + document.getElementById('root')?.classList.add('app-ready'); + } + }, [ready]); + return ( <>
diff --git a/src-ts/config/environments/environment.default.config.ts b/src-ts/config/environments/environment.default.config.ts index 8df336dc1..0c0b89f95 100644 --- a/src-ts/config/environments/environment.default.config.ts +++ b/src-ts/config/environments/environment.default.config.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ import { EnvironmentConfigModel } from './environment-config.model' const COMMUNITY_WEBSITE: string = 'https://www.topcoder-dev.com' diff --git a/src-ts/config/environments/environment.dev.config.ts b/src-ts/config/environments/environment.dev.config.ts index 61c8ec1e7..300bc2383 100644 --- a/src-ts/config/environments/environment.dev.config.ts +++ b/src-ts/config/environments/environment.dev.config.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ import { EnvironmentConfigModel } from './environment-config.model' import { EnvironmentConfigDefault } from './environment.default.config' diff --git a/src-ts/config/environments/environment.prod.config.ts b/src-ts/config/environments/environment.prod.config.ts index 4e4bd4e6b..c1b9d8caa 100644 --- a/src-ts/config/environments/environment.prod.config.ts +++ b/src-ts/config/environments/environment.prod.config.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ import { EnvironmentConfigModel } from './environment-config.model' import { EnvironmentConfigDefault } from './environment.default.config' diff --git a/src-ts/declarations.d.ts b/src-ts/declarations.d.ts index f132ccbb5..ff436d097 100644 --- a/src-ts/declarations.d.ts +++ b/src-ts/declarations.d.ts @@ -7,11 +7,6 @@ declare module '*.html' { declare module '*.pdf' -declare module '*.scss' { - const scssFile: { [style: string]: any } - export = scssFile -} - declare module '*.svg' { import * as React from 'react' diff --git a/src-ts/header/Header.module.scss b/src-ts/header/Header.module.scss index d65fe3953..798dbf383 100644 --- a/src-ts/header/Header.module.scss +++ b/src-ts/header/Header.module.scss @@ -3,6 +3,10 @@ .header-wrap { display: block; background-color: $tc-black; + + :global(#root:not(.app-ready)) & { + display: none; + } } .header { diff --git a/src-ts/header/Header.tsx b/src-ts/header/Header.tsx index 21d045913..6957a7c05 100644 --- a/src-ts/header/Header.tsx +++ b/src-ts/header/Header.tsx @@ -1,22 +1,20 @@ import { FC } from 'react' -import styles from './Header.module.scss' import { Logo } from './logo' import { ToolSelectors } from './tool-selectors' import { UtilitySelectors } from './utility-selectors' +import styles from './Header.module.scss' -const Header: FC<{}> = () => { - return ( -
-
- - - - -
-
-
- ) -} +const Header: FC<{}> = () => ( +
+
+ + + + +
+
+
+) export default Header diff --git a/src-ts/header/tool-selectors/ToolSelectors.tsx b/src-ts/header/tool-selectors/ToolSelectors.tsx index cdc9807cd..12280cf7f 100644 --- a/src-ts/header/tool-selectors/ToolSelectors.tsx +++ b/src-ts/header/tool-selectors/ToolSelectors.tsx @@ -7,8 +7,9 @@ interface ToolSelectorsProps { isWide: boolean } -const ToolSelectors: FC = (props: ToolSelectorsProps) => { - return props.isWide ? : -} +const ToolSelectors: FC + = (props: ToolSelectorsProps) => (props.isWide + ? + : ) export default ToolSelectors diff --git a/src-ts/header/tool-selectors/tool-selectors-narrow/ToolSelectorsNarrow.tsx b/src-ts/header/tool-selectors/tool-selectors-narrow/ToolSelectorsNarrow.tsx index 0e7c67021..318c6dd7e 100644 --- a/src-ts/header/tool-selectors/tool-selectors-narrow/ToolSelectorsNarrow.tsx +++ b/src-ts/header/tool-selectors/tool-selectors-narrow/ToolSelectorsNarrow.tsx @@ -1,5 +1,5 @@ -import classNames from 'classnames' import { Dispatch, FC, SetStateAction, useContext, useState } from 'react' +import classNames from 'classnames' import { IconOutline, routeContext, RouteContextData } from '../../../lib' diff --git a/src-ts/header/tool-selectors/tool-selectors-narrow/tool-selector-narrow/ToolSelectorNarrow.tsx b/src-ts/header/tool-selectors/tool-selectors-narrow/tool-selector-narrow/ToolSelectorNarrow.tsx index ea6454925..502af89cc 100644 --- a/src-ts/header/tool-selectors/tool-selectors-narrow/tool-selector-narrow/ToolSelectorNarrow.tsx +++ b/src-ts/header/tool-selectors/tool-selectors-narrow/tool-selector-narrow/ToolSelectorNarrow.tsx @@ -1,6 +1,6 @@ -import classNames from 'classnames' import { FC, useContext } from 'react' import { Link, useLocation } from 'react-router-dom' +import classNames from 'classnames' import { IconOutline, PlatformRoute, routeContext, RouteContextData, routeIsActiveTool } from '../../../../lib' diff --git a/src-ts/header/tool-selectors/tool-selectors-wide/tool-selector-wide/ToolSelectorWide.tsx b/src-ts/header/tool-selectors/tool-selectors-wide/tool-selector-wide/ToolSelectorWide.tsx index e30a3c5ba..33ec2ae48 100644 --- a/src-ts/header/tool-selectors/tool-selectors-wide/tool-selector-wide/ToolSelectorWide.tsx +++ b/src-ts/header/tool-selectors/tool-selectors-wide/tool-selector-wide/ToolSelectorWide.tsx @@ -1,6 +1,6 @@ -import classNames from 'classnames' import { FC, useContext } from 'react' import { Link, useLocation } from 'react-router-dom' +import classNames from 'classnames' import { PlatformRoute, @@ -37,8 +37,9 @@ const ToolSelectorWide: FC = (props: ToolSelectorWideProp
+ isLink ? styles['tool-selector-wide-is-link'] : undefined, + )} + > = (props: ToolSelectorWideProp > {toolRoute.title} -
+
) } diff --git a/src-ts/header/utility-selectors/UtilitySelector/ProfileSelector/profile-logged-in/profile-panel/ProfilePanel.tsx b/src-ts/header/utility-selectors/UtilitySelector/ProfileSelector/profile-logged-in/profile-panel/ProfilePanel.tsx index 3e7d1809e..a1a82951f 100644 --- a/src-ts/header/utility-selectors/UtilitySelector/ProfileSelector/profile-logged-in/profile-panel/ProfilePanel.tsx +++ b/src-ts/header/utility-selectors/UtilitySelector/ProfileSelector/profile-logged-in/profile-panel/ProfilePanel.tsx @@ -1,6 +1,6 @@ -import classNames from 'classnames' import { FC, useContext } from 'react' import { NavigateFunction, useNavigate } from 'react-router-dom' +import classNames from 'classnames' import { authUrlLogout, diff --git a/src-ts/header/utility-selectors/UtilitySelector/ProfileSelector/profile-not-logged-in/ProfileNotLoggedIn.tsx b/src-ts/header/utility-selectors/UtilitySelector/ProfileSelector/profile-not-logged-in/ProfileNotLoggedIn.tsx index 435f0e0f1..d0b577aee 100644 --- a/src-ts/header/utility-selectors/UtilitySelector/ProfileSelector/profile-not-logged-in/ProfileNotLoggedIn.tsx +++ b/src-ts/header/utility-selectors/UtilitySelector/ProfileSelector/profile-not-logged-in/ProfileNotLoggedIn.tsx @@ -1,4 +1,4 @@ -import { FC, useContext } from 'react' +import { FC, useCallback, useContext } from 'react' import { Location, useLocation } from 'react-router-dom' import { @@ -14,10 +14,13 @@ const ProfileNotLoggedIn: FC<{}> = () => { const routeData: RouteContextData = useContext(routeContext) const location: Location = useLocation() - function signUp(): void { + const signUpHandler: () => void = useCallback(() => { const signupUrl: string = routeData.getSignupUrl(location.pathname, routeData.toolsRoutes) window.location.href = signupUrl - } + }, [ + location.pathname, + routeData, + ]) return ( <> @@ -34,7 +37,7 @@ const ProfileNotLoggedIn: FC<{}> = () => { label='Sign Up' size='md' tabIndex={-1} - onClick={signUp} + onClick={signUpHandler} /> ) diff --git a/src-ts/header/utility-selectors/UtilitySelectors.tsx b/src-ts/header/utility-selectors/UtilitySelectors.tsx index 8d5b3136a..a909a7769 100644 --- a/src-ts/header/utility-selectors/UtilitySelectors.tsx +++ b/src-ts/header/utility-selectors/UtilitySelectors.tsx @@ -3,13 +3,11 @@ import { FC } from 'react' import ProfileSelector from './UtilitySelector/ProfileSelector/ProfileSelector' import styles from './UtilitySelectors.module.scss' -const UtilitySelectors: FC<{}> = () => { - return ( -
- {/* TODO: make this configurable */} - -
- ) -} +const UtilitySelectors: FC<{}> = () => ( +
+ {/* TODO: make this configurable */} + +
+) export default UtilitySelectors diff --git a/src-ts/index.ts b/src-ts/index.ts index a8d7d77fe..c402cb788 100644 --- a/src-ts/index.ts +++ b/src-ts/index.ts @@ -4,22 +4,22 @@ export { default as AppNextGen } from './App' export { EnvironmentConfig } from './config' export { - Analytics, - Breadcrumb, - ContactSupportModal, - logInitialize, - OrderContractModal, - PageFooter, - PrivacyPolicyModal, - profileContext, - ProfileProvider, - RouteProvider, - TabsNavbar, - TermsModal, - xhrGetAsync, - xhrGetBlobAsync, - xhrPatchAsync, - xhrPostAsync, + Analytics, + Breadcrumb, + ContactSupportModal, + logInitialize, + OrderContractModal, + PageFooter, + PrivacyPolicyModal, + profileContext, + ProfileProvider, + RouteProvider, + TabsNavbar, + TermsModal, + xhrGetAsync, + xhrGetBlobAsync, + xhrPatchAsync, + xhrPostAsync, } from './lib' export * from './tools' export * from './utils' diff --git a/src-ts/lib/analytics/Analytics.tsx b/src-ts/lib/analytics/Analytics.tsx index 91b842ab9..ace4d5256 100644 --- a/src-ts/lib/analytics/Analytics.tsx +++ b/src-ts/lib/analytics/Analytics.tsx @@ -3,14 +3,11 @@ import { FC } from 'react' import { GoogleTagManager } from './google-tag-manater' import { SegmentAnalytics } from './segment-analytics' -const Analytics: FC<{}> = () => { - - return ( - <> - - - - ) -} +const Analytics: FC<{}> = () => ( + <> + + + +) export default Analytics diff --git a/src-ts/lib/avatar/Avatar.tsx b/src-ts/lib/avatar/Avatar.tsx index daffe8126..1daa3e06b 100644 --- a/src-ts/lib/avatar/Avatar.tsx +++ b/src-ts/lib/avatar/Avatar.tsx @@ -1,5 +1,5 @@ -import classNames from 'classnames' import { FC } from 'react' +import classNames from 'classnames' import styles from './Avatar.module.scss' diff --git a/src-ts/lib/breadcrumb/Breadcrumb.tsx b/src-ts/lib/breadcrumb/Breadcrumb.tsx index abb4ade6a..62d93d0c9 100644 --- a/src-ts/lib/breadcrumb/Breadcrumb.tsx +++ b/src-ts/lib/breadcrumb/Breadcrumb.tsx @@ -15,58 +15,60 @@ const Breadcrumb: FC = (props: BreadcrumbProps) => { return <> } - return createPortal(( -
- +
+ ), portalRootEl, + ) } export default Breadcrumb diff --git a/src-ts/lib/breadcrumb/breadcrumb-item/BreadcrumbItem.tsx b/src-ts/lib/breadcrumb/breadcrumb-item/BreadcrumbItem.tsx index e9ab23f50..076b9ecae 100644 --- a/src-ts/lib/breadcrumb/breadcrumb-item/BreadcrumbItem.tsx +++ b/src-ts/lib/breadcrumb/breadcrumb-item/BreadcrumbItem.tsx @@ -1,7 +1,8 @@ import { FC } from 'react' import { Link } from 'react-router-dom' -import styles from './../Breadcrumb.module.scss' +import styles from '../Breadcrumb.module.scss' + import { BreadcrumbItemModel } from './breadcrumb-item.model' interface BreadcrumbItemProps { @@ -10,9 +11,20 @@ interface BreadcrumbItemProps { } const BreadcrumbItem: FC = (props: BreadcrumbItemProps) => { + + function onClick(): void { + props.item.onClick?.(props.item) + } + return ( -
  • props.item.onClick?.(props.item)}> - +
  • onClick()} + > + {props.item.name}
  • diff --git a/src-ts/lib/button/Button.tsx b/src-ts/lib/button/Button.tsx index 8703cd7d4..706edfb9f 100644 --- a/src-ts/lib/button/Button.tsx +++ b/src-ts/lib/button/Button.tsx @@ -1,9 +1,11 @@ -import classNames from 'classnames' +/* eslint-disable @typescript-eslint/no-explicit-any */ +// NEED to allow any in order to support intrinisic element types import { FC, SVGProps } from 'react' import { Link } from 'react-router-dom' +import classNames from 'classnames' -import '../styles/index.scss' import { IconOutline } from '../svgs' +import '../styles/index.scss' export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' export type ButtonStyle = 'icon' | 'icon-bordered' | 'link' | 'primary' | 'secondary' | 'tertiary' | 'text' @@ -32,9 +34,15 @@ export interface ButtonProps { const Button: FC = (props: ButtonProps) => { - const classes: string = getButtonClasses(props) - const clickHandler: (event?: any) => void = getClickHandler(props) - const content: JSX.Element = getButtonContent(props) + const classes: string = getButtonClasses( + props.className, + props.buttonStyle, + props.size, + props.disable, + props.hidden, + ) + const clickHandler: (event?: any) => void = getClickHandler(props.onClick) + const content: JSX.Element = getButtonContent(props.buttonStyle, props.icon, props.label) // if there is a url, this is a link button if (!!props.url) { @@ -43,7 +51,7 @@ const Button: FC = (props: ButtonProps) => { className={classes} href={props.url} onClick={clickHandler} - rel={props.rel ?? props.target === '_blank' ? 'noreferrer' : ''} + rel={props.rel || props.target === '_blank' ? 'noreferrer' : ''} role='button' tabIndex={props.tabIndex} title={props.title} @@ -79,6 +87,7 @@ const Button: FC = (props: ButtonProps) => { onClick={clickHandler} tabIndex={props.tabIndex} title={props.title} + // eslint-disable-next-line react/button-has-type type={props.type || 'button'} id={props.id} value={props.id} @@ -104,41 +113,52 @@ const Button: FC = (props: ButtonProps) => { ) } -function getButtonClasses(props: ButtonProps): string { +function getButtonClasses( + className?: string, + buttonStyle?: ButtonStyle, + size?: ButtonSize, + disable?: boolean, + hidden?: boolean, +): string { const classes: string = classNames( 'button', - props.className, - props.buttonStyle || 'primary', - `button-${props.size || 'md'}`, - !!props.disable ? 'disabled' : undefined, - props.hidden ? 'hidden' : undefined, + className, + buttonStyle || 'primary', + `button-${size || 'md'}`, + { disabled: disable }, + { hidden }, ) return classes } -function getButtonContent(props: ButtonProps): JSX.Element { +function getButtonContent( + buttonStyle?: ButtonStyle, + icon?: FC>, + label?: string, +): JSX.Element { // if this is a link, just add the label and the arrow icon - if (props.buttonStyle === 'link') { + if (buttonStyle === 'link') { return ( <> - {props.label} + {label} ) } - const Icon: FC> | undefined = props.icon + const Icon: FC> | undefined = icon return ( <> {!!Icon && } - {props.label} + {label} ) } -function getClickHandler(props: ButtonProps): (event?: any) => void { - return props.onClick || (() => undefined) +function getClickHandler(onClick?: (event?: any) => void): ((event?: any) => void) { + + return onClick || (() => undefined) } export default Button diff --git a/src-ts/lib/button/index.ts b/src-ts/lib/button/index.ts index 4d9c7c186..6d350516a 100644 --- a/src-ts/lib/button/index.ts +++ b/src-ts/lib/button/index.ts @@ -1,11 +1,5 @@ export { default as Button } from './Button' export { type ButtonProps } from './Button' -export -// tslint:disable-next-line: no-unused-expression -type { ButtonSize } from './Button' -export -// tslint:disable-next-line: no-unused-expression -type { ButtonStyle } from './Button' -export -// tslint:disable-next-line: no-unused-expression -type { ButtonType } from './Button' +export { type ButtonSize } from './Button' +export { type ButtonStyle } from './Button' +export { type ButtonType } from './Button' diff --git a/src-ts/lib/card/Card.tsx b/src-ts/lib/card/Card.tsx index b8333d8a3..c765dec0e 100644 --- a/src-ts/lib/card/Card.tsx +++ b/src-ts/lib/card/Card.tsx @@ -1,5 +1,5 @@ -import classNames from 'classnames' import { FC, ReactNode, SVGProps } from 'react' +import classNames from 'classnames' import { ButtonStyle } from '../button' import '../styles/index.scss' diff --git a/src-ts/lib/contact-support-form/ContactSupportForm.tsx b/src-ts/lib/contact-support-form/ContactSupportForm.tsx index 7315230af..57af39fb9 100644 --- a/src-ts/lib/contact-support-form/ContactSupportForm.tsx +++ b/src-ts/lib/contact-support-form/ContactSupportForm.tsx @@ -1,4 +1,4 @@ -import { Dispatch, FC, SetStateAction, useContext, useEffect, useState } from 'react' +import { Dispatch, FC, SetStateAction, useCallback, useContext, useEffect, useState } from 'react' import { Form, FormDefinition, formGetInputModel, FormInputModel } from '../form' import { LoadingSpinner } from '../loading-spinner' @@ -19,20 +19,28 @@ const ContactSupportForm: FC = (props: ContactSupportFo const { profile }: ProfileContextData = useContext(profileContext) - const [loading, setLoading]: [boolean, Dispatch>] = useState(false) - const [saveOnSuccess, setSaveOnSuccess]: [boolean, Dispatch>] = useState(false) + const [loading, setLoading]: [boolean, Dispatch>] + = useState(false) + const [saveOnSuccess, setSaveOnSuccess]: [boolean, Dispatch>] + = useState(false) useEffect(() => { - if (!loading && saveOnSuccess) { - props.onSave() - } - }, [loading, saveOnSuccess]) + if (!loading && saveOnSuccess) { + props.onSave() + } + }, [loading, saveOnSuccess, props.onSave]) - function generateRequest(inputs: ReadonlyArray): ContactSupportRequest { - const firstName: string = formGetInputModel(inputs, ContactSupportFormField.first).value as string - const lastName: string = formGetInputModel(inputs, ContactSupportFormField.last).value as string - const email: string = formGetInputModel(inputs, ContactSupportFormField.email).value as string - const question: string = formGetInputModel(inputs, ContactSupportFormField.question).value as string + const generateRequest = useCallback(( + inputs: ReadonlyArray, + ): ContactSupportRequest => { + const firstName: string + = formGetInputModel(inputs, ContactSupportFormField.first).value as string + const lastName: string + = formGetInputModel(inputs, ContactSupportFormField.last).value as string + const email: string + = formGetInputModel(inputs, ContactSupportFormField.email).value as string + const question: string + = formGetInputModel(inputs, ContactSupportFormField.question).value as string return { challengeId: props.workId, email, @@ -41,20 +49,23 @@ const ContactSupportForm: FC = (props: ContactSupportFo lastName, question, } - } + }, [props.workId]) - async function saveAsync(request: ContactSupportRequest): Promise { + const saveAsync = useCallback(async (request: ContactSupportRequest): Promise => { setLoading(true) return contactSupportSubmitRequestAsync(request) .then(() => { - setSaveOnSuccess(true) - }).finally(() => setLoading(false)) - } + setSaveOnSuccess(true) + }) + .finally(() => setLoading(false)) + }, []) const emailElement: JSX.Element | undefined = !!profile?.email ? ( <> -  at {profile.email} +  at + {' '} + {profile.email} ) : undefined @@ -64,10 +75,13 @@ const ContactSupportForm: FC = (props: ContactSupportFo

    - Hi {profile?.firstName || 'there'}, we're here to help. + Hi + {' '} + {profile?.firstName || 'there'} + , we're here to help.

    - Please describe what you'd like to discuss, and a + Please describe what you'd like to discuss, and a Topcoder Solutions Expert will email you back {emailElement}  within one business day. diff --git a/src-ts/lib/content-layout/ContentLayout.tsx b/src-ts/lib/content-layout/ContentLayout.tsx index a6bbbb6f0..2bedc0ac1 100644 --- a/src-ts/lib/content-layout/ContentLayout.tsx +++ b/src-ts/lib/content-layout/ContentLayout.tsx @@ -1,5 +1,5 @@ -import classNames from 'classnames' import { FC, ReactNode } from 'react' +import classNames from 'classnames' import { Button, ButtonProps } from '../button' import '../styles/index.scss' @@ -16,42 +16,40 @@ export interface ContentLayoutProps { titleClass?: string } -const ContentLayout: FC = (props: ContentLayoutProps) => { - return ( -

    - -
    +const ContentLayout: FC = (props: ContentLayoutProps) => ( +
    -
    +
    - {!!props.title && ( -
    +
    -

    - {props.title} -

    + {!!props.title && ( +
    - {!!props.buttonConfig && ( -
    -
    - )} +

    + {props.title} +

    -
    - )} + {!!props.buttonConfig && ( +
    +
    + )} - {props.children} +
    + )} -
    + {props.children}
    - ) -} + +
    +) export default ContentLayout diff --git a/src-ts/lib/form/Form.tsx b/src-ts/lib/form/Form.tsx index 4f0817a14..c5b954062 100644 --- a/src-ts/lib/form/Form.tsx +++ b/src-ts/lib/form/Form.tsx @@ -1,4 +1,3 @@ -import classNames from 'classnames' import { ChangeEvent, createRef, @@ -10,10 +9,11 @@ import { useEffect, useState, } from 'react' +import classNames from 'classnames' import { Button } from '../button' -import '../styles/index.scss' import { IconOutline } from '../svgs' +import '../styles/index.scss' import { FormAction, FormButton, FormDefinition, FormInputModel } from '.' import { @@ -23,8 +23,9 @@ import { formOnChange, formOnReset, formOnSubmitAsync, + formValidateForm, + FormValue, } from './form-functions' -import { validateForm } from './form-functions/form.functions' import { FormGroups } from './form-groups' import styles from './Form.module.scss' @@ -41,8 +42,8 @@ interface FormProps { readonly shouldDisableButton?: (isPrimaryGroup: boolean, index: number) => boolean } -const Form: (props: FormProps) => JSX.Element - = (props: FormProps) => { +const Form: (props: FormProps) => JSX.Element + = (props: FormProps) => { const [formDef, setFormDef]: [FormDefinition, Dispatch>] = useState({ ...props.formDef }) @@ -65,7 +66,7 @@ const Form: (props: FormProps(props: FormProps(props: FormProps { - return () => { - if (props.resetFormOnUnmount) { - onReset() - } + useEffect(() => () => { + if (props.resetFormOnUnmount) { + onReset() } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) function checkIfFormIsValid(formInputFields: Array): void { - setFormInvalid(formInputFields.filter(item => !!item.error).length > 0) + setFormInvalid(formInputFields.some(item => !!item.error)) } function onBlur(event: FocusEvent): void { @@ -147,34 +147,32 @@ const Form: (props: FormProps FormButton = (button) => { - // if this is a reset button, set its onclick to reset - if (!!button.isReset) { - button = { - ...button, - onClick: onReset, - } - } + const setOnClickOnReset: (button: FormButton) => FormButton = button => { + // if this is a reset button, set its onclick to reset + if (!!button.isReset) { + button = { + ...button, + onClick: onReset, + } + } - return button + return button } - const createButtonGroup: (groups: ReadonlyArray, isPrimaryGroup: boolean) => Array = (groups, isPrimaryGroup) => { - return groups.map((button, index) => { - button = setOnClickOnReset(button) - - const disabled: boolean = (button.isSubmit && isFormInvalid) || !!props.shouldDisableButton?.(isPrimaryGroup, index) - - return ( -