diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index d54dd932..00000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -/public -/src/serviceWorker.js -.eslintrc.js -/dist \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 2f265e94..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,79 +0,0 @@ -module.exports = { - env: { - browser: true, - es2021: true, - }, - extends: [ - 'eslint:recommended', - 'plugin:react/recommended', - 'plugin:react-hooks/recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:@typescript-eslint/recommended-requiring-type-checking', - 'prettier', - ], - parser: '@typescript-eslint/parser', - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - ecmaVersion: 'latest', - sourceType: 'module', - project: './tsconfig.json', - }, - plugins: ['react', '@typescript-eslint', 'regex', 'lodash'], - rules: { - 'lodash/import-scope': 'error', - 'regex/required': [ - 'error', - [ - { - regex: 'describe.+from.+vitest', - message: 'Import `describe` explicitly.', - files: { - inspect: '\\.(test|spec)\\.tsx?$', - }, - }, - { - regex: 'it.+from.+vitest', - message: 'Import `it` explicitly.', - files: { - inspect: '\\.(test|spec)\\.tsx?$', - }, - }, - { - regex: 'expect.+from.+vitest', - message: 'Import `expect` explicitly.', - files: { - inspect: '\\.(test|spec)\\.tsx?$', - }, - }, - ], - ], - }, - settings: { - react: { - version: 'detect', - }, - }, - overrides: [ - { - files: [ - '*.spec.ts', - '*.spec.tsx', - '**/__mocks__/**/*', - '**/__tests__/**/*', - 'global-setup.ts', - 'src/lib/test/**/*', - ], - extends: ['plugin:testing-library/react'], - rules: { - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-unsafe-return': 'off', - '@typescript-eslint/no-unsafe-member-access': 'off', - '@typescript-eslint/no-unsafe-assignment': 'off', - '@typescript-eslint/no-unsafe-argument': 'off', - '@typescript-eslint/no-unsafe-call': 'off', - }, - }, - ], -}; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..fa4bef30 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,124 @@ +import { fixupConfigRules, fixupPluginRules } from '@eslint/compat'; +import react from 'eslint-plugin-react'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import regex from 'eslint-plugin-regex'; +import lodash from 'eslint-plugin-lodash'; +import globals from 'globals'; +import tsParser from '@typescript-eslint/parser'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import js from '@eslint/js'; +import { FlatCompat } from '@eslint/eslintrc'; +import testingLibrary from 'eslint-plugin-testing-library'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default [ + { + ignores: ['public', 'src/serviceWorker.js', '**/eslint.config.mjs', 'dist'], + }, + ...fixupConfigRules( + compat.extends( + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'prettier', + ), + ), + { + plugins: { + react: fixupPluginRules(react), + '@typescript-eslint': fixupPluginRules(typescriptEslint), + regex, + lodash, + 'testing-library': fixupPluginRules({ + rules: testingLibrary.rules, + }), + }, + + languageOptions: { + globals: { + ...globals.browser, + }, + + parser: tsParser, + ecmaVersion: 'latest', + sourceType: 'module', + + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + + project: './tsconfig.json', + }, + }, + + settings: { + react: { + version: 'detect', + }, + }, + + rules: { + 'lodash/import-scope': 'error', + + 'regex/required': [ + 'error', + [ + { + regex: 'describe.+from.+vitest', + message: 'Import `describe` explicitly.', + + files: { + inspect: '\\.(test|spec)\\.tsx?$', + }, + }, + { + regex: 'it.+from.+vitest', + message: 'Import `it` explicitly.', + + files: { + inspect: '\\.(test|spec)\\.tsx?$', + }, + }, + { + regex: 'expect.+from.+vitest', + message: 'Import `expect` explicitly.', + + files: { + inspect: '\\.(test|spec)\\.tsx?$', + }, + }, + ], + ], + }, + }, + { + files: [ + '**/*.spec.ts', + '**/*.spec.tsx', + '**/__mocks__/**/*', + '**/__tests__/**/*', + '**/global-setup.ts', + 'src/lib/test/**/*', + ], + + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + }, + }, +]; diff --git a/package.json b/package.json index de3388bc..13f11479 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,9 @@ "react-toastify": "^10.0.5" }, "devDependencies": { + "@eslint/compat": "^1.1.1", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.8.0", "@testing-library/jest-dom": "^6.4.8", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", @@ -80,6 +83,7 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-regex": "^1.10.0", "eslint-plugin-testing-library": "^6.2.2", + "globals": "^15.9.0", "jsdom": "^24.1.1", "prettier": "3.3.3", "rollup-plugin-visualizer": "^5.12.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49072cba..9653a655 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,6 +63,15 @@ importers: specifier: ^10.0.5 version: 10.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) devDependencies: + '@eslint/compat': + specifier: ^1.1.1 + version: 1.1.1 + '@eslint/eslintrc': + specifier: ^3.1.0 + version: 3.1.0 + '@eslint/js': + specifier: ^9.8.0 + version: 9.8.0 '@testing-library/jest-dom': specifier: ^6.4.8 version: 6.4.8 @@ -129,6 +138,9 @@ importers: eslint-plugin-testing-library: specifier: ^6.2.2 version: 6.2.2(eslint@9.8.0)(typescript@5.5.4) + globals: + specifier: ^15.9.0 + version: 15.9.0 jsdom: specifier: ^24.1.1 version: 24.1.1 @@ -459,6 +471,10 @@ packages: resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint/compat@1.1.1': + resolution: {integrity: sha512-lpHyRyplhGPL5mGEh6M9O5nnKk0Gz4bFI+Zu6tKlPpDUN7XshWvH9C/px4UVm87IAANE0W81CEsNGbS1KlzXpA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-array@0.17.1': resolution: {integrity: sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1776,6 +1792,10 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + globals@15.9.0: + resolution: {integrity: sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -3215,6 +3235,8 @@ snapshots: '@eslint-community/regexpp@4.11.0': {} + '@eslint/compat@1.1.1': {} + '@eslint/config-array@0.17.1': dependencies: '@eslint/object-schema': 2.1.4 @@ -4878,6 +4900,8 @@ snapshots: globals@14.0.0: {} + globals@15.9.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 diff --git a/src/components/HOCs/AndTheme.tsx b/src/components/HOCs/AndTheme.tsx index 3d05cb10..152b803f 100644 --- a/src/components/HOCs/AndTheme.tsx +++ b/src/components/HOCs/AndTheme.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { Link as RouterLink, LinkProps as RouterLinkProps, + To, } from 'react-router-dom'; import { LinkProps } from '@mui/material/Link'; import { ToastContainer } from 'react-toastify'; @@ -29,18 +30,19 @@ const colors = { }, }; -const LinkBehavior = React.forwardRef< - HTMLAnchorElement, - Omit & { href: RouterLinkProps['to'] } ->(function LinkBehavior(props, ref) { - const { href, ...other } = props; +type Props = Omit & { href: To }; - if (props.href.toString().match(/^https?:\/\//)) { - return ; - } +const LinkBehavior = React.forwardRef( + function LinkBehavior(props: Props, ref) { + const { href, ...other } = props; - return ; -}); + if (String(href).match(/^https?:\/\//)) { + return ; + } + + return ; + }, +); export default function AndTheme({ children, diff --git a/src/components/organisms/NavBar.tsx b/src/components/organisms/NavBar.tsx index 7ede8a21..540a313d 100644 --- a/src/components/organisms/NavBar.tsx +++ b/src/components/organisms/NavBar.tsx @@ -26,7 +26,7 @@ export default function NavBar({ onTodayClick }: NavBarProps): JSX.Element { const [isOpen, setIsOpen] = useState(false); const toggleMenu = () => setIsOpen(!isOpen); const handleTodayClick = () => { - onTodayClick && onTodayClick(); + onTodayClick?.(); navigate('/'); }; diff --git a/src/components/organisms/TaskEdit.tsx b/src/components/organisms/TaskEdit.tsx index 2c12e3bb..18cfed9a 100644 --- a/src/components/organisms/TaskEdit.tsx +++ b/src/components/organisms/TaskEdit.tsx @@ -53,7 +53,7 @@ const TaskEdit = ({ <> { - onOpen && onOpen(); + onOpen?.(); setIsOpen(true); }} disabled={!task.id || task.status !== 'pending'} diff --git a/src/components/organisms/UncleButton.tsx b/src/components/organisms/UncleButton.tsx index 8a91f2c7..d189762a 100644 --- a/src/components/organisms/UncleButton.tsx +++ b/src/components/organisms/UncleButton.tsx @@ -25,7 +25,7 @@ export default function UncleButton({ /* TODO: Disable if task id not set */ disabled={task.status !== 'pending'} onClick={() => { - onClick && onClick(); + onClick?.(); setOpen(true); }} > diff --git a/src/lib/api/useMe.ts b/src/lib/api/useMe.ts index 99e1e5dc..90ac34b8 100644 --- a/src/lib/api/useMe.ts +++ b/src/lib/api/useMe.ts @@ -1,33 +1,13 @@ import { QueryObserverResult, useQuery } from 'react-query'; import { UseQueryOptions } from 'react-query'; -import { useEffect } from 'react'; import { User, getMe } from '@taskratchet/sdk'; export function useMe( queryOptions: UseQueryOptions | undefined = {}, ): QueryObserverResult { - const result = useQuery({ + return useQuery({ queryKey: 'me', queryFn: getMe, ...queryOptions, }); - - const { data } = result; - - useEffect(() => { - if (!data) return; - - const metadata = Object.keys(data).reduce( - (prev, key) => { - const value = data[key as keyof User]; - - prev[key] = typeof value === 'string' ? value : JSON.stringify(value); - - return prev; - }, - {} as Record, - ); - }, [data]); - - return result; } diff --git a/src/lib/saveFeedback.ts b/src/lib/saveFeedback.ts index e5e06fe6..743e3c5a 100644 --- a/src/lib/saveFeedback.ts +++ b/src/lib/saveFeedback.ts @@ -6,7 +6,7 @@ type Options = { }; export default function saveFeedback(options: Options): void { - fetch('https://api.web3forms.com/submit', { + void fetch('https://api.web3forms.com/submit', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/lib/test/queries.tsx b/src/lib/test/queries.tsx index b3e4817d..7e58cf85 100644 --- a/src/lib/test/queries.tsx +++ b/src/lib/test/queries.tsx @@ -6,11 +6,9 @@ export async function findTaskCheckbox( const desc = await screen.findByText(task); let checkbox; await waitFor(() => { - /* eslint-disable testing-library/no-node-access */ const c = desc .closest('.molecule-task') ?.querySelector('[type="checkbox"]'); - /* eslint-enable testing-library/no-node-access */ if (!c) { throw new Error(); } @@ -22,7 +20,6 @@ export async function findTaskCheckbox( export function queryTaskCheckbox(task = 'the_task'): HTMLInputElement | null { const desc = screen.getByText(task); - // eslint-disable-next-line testing-library/no-node-access const c = desc.closest('.molecule-task')?.querySelector('[type="checkbox"]'); return c ? (c as unknown as HTMLInputElement) : null;