From 279110f011d58844444637639432bac3f9d93725 Mon Sep 17 00:00:00 2001 From: mahula Date: Tue, 16 Dec 2025 21:21:28 +0100 Subject: [PATCH] refactor(other): update eslint to v9 with the flat configuration (#405) --- .gitignore | 2 + app/.eslintignore | 3 - app/.eslintrc.cjs | 223 -- app/.prettierrc.json | 2 +- app/eslint.config.js | 260 +++ app/package.json | 21 +- app/src/App.tsx | 4 +- app/src/ModalContent.tsx | 9 +- app/src/api/directus.ts | 7 +- app/src/api/inviteApi.ts | 1 - app/src/api/itemsApi.ts | 8 +- app/src/api/layersApi.ts | 1 + app/src/api/permissionsApi.ts | 1 - app/src/api/refiBcnApi.ts | 2 +- app/src/api/userApi.ts | 10 +- app/src/main.tsx | 3 +- app/src/pages/Landingpage.tsx | 2 +- app/src/pages/MapContainer.tsx | 4 +- app/src/pages/data.ts | 1 + app/tsconfig.json | 36 +- lib/.eslintignore | 5 - lib/.eslintrc.cjs | 221 -- lib/.prettierrc.json | 2 +- lib/cypress/support/component.ts | 2 +- lib/eslint.config.js | 260 +++ lib/package.json | 14 +- lib/postcss.config.cjs | 2 +- lib/setupTest.ts | 2 +- lib/src/Components/AppShell/NavBar.tsx | 8 +- lib/src/Components/AppShell/SideBar.tsx | 14 +- .../Components/AppShell/SidebarSubmenu.tsx | 10 +- .../Components/AppShell/hooks/useAppState.tsx | 1 - lib/src/Components/Auth/LoginPage.tsx | 11 +- .../Components/Auth/RequestPasswordPage.tsx | 6 +- .../Components/Auth/SetNewPasswordPage.tsx | 6 +- lib/src/Components/Auth/SignupPage.tsx | 15 +- lib/src/Components/Auth/useAuth.tsx | 8 +- lib/src/Components/Gaming/Quests.tsx | 4 +- lib/src/Components/Gaming/hooks/useQuests.tsx | 1 - lib/src/Components/Input/Autocomplete.tsx | 11 +- lib/src/Components/Item/PopupView.tsx | 2 + .../Map/Subcomponents/AddButton.tsx | 5 +- .../Subcomponents/Controls/FilterControl.tsx | 9 +- .../Subcomponents/Controls/LayerControl.tsx | 4 +- .../Controls/LocateControl.spec.tsx | 1 + .../Subcomponents/Controls/LocateControl.tsx | 14 +- .../Subcomponents/Controls/QuestControl.tsx | 8 +- .../Subcomponents/Controls/SearchControl.tsx | 9 +- .../Subcomponents/Controls/SidebarControl.tsx | 4 +- .../Subcomponents/Controls/TagsControl.tsx | 4 +- .../Map/Subcomponents/ItemFormPopup.tsx | 4 +- .../HeaderView/ConnectionStatus.tsx | 3 +- .../HeaderView/DeleteModal.tsx | 6 +- .../HeaderView/EditMenu.tsx | 11 +- .../HeaderView/ItemAvatar.tsx | 9 +- .../HeaderView/ItemTitle.tsx | 1 + .../HeaderView/QRModal.tsx | 7 +- .../ItemPopupComponents/HeaderView/hooks.ts | 1 + .../ItemPopupComponents/HeaderView/index.tsx | 20 +- .../ItemPopupComponents/HeaderView/types.ts | 2 +- .../ItemPopupComponents/PopupButton.tsx | 3 + .../PopupTextAreaInput.tsx | 1 + .../ItemPopupComponents/PopupTextInput.tsx | 1 + .../Map/Subcomponents/ItemViewPopup.tsx | 4 +- .../Map/Subcomponents/MapLibreLayer.tsx | 2 +- .../Map/Subcomponents/SelectPositionToast.tsx | 4 +- lib/src/Components/Map/UtopiaMapInner.tsx | 13 +- .../Components/Map/hooks/useClusterRef.tsx | 1 - lib/src/Components/Map/hooks/useFilter.tsx | 14 +- lib/src/Components/Map/hooks/useItems.tsx | 1 - lib/src/Components/Map/hooks/useLayers.tsx | 2 +- .../Components/Map/hooks/useLeafletRefs.tsx | 3 +- .../Components/Map/hooks/usePermissions.tsx | 2 +- .../Components/Map/hooks/useReverseGeocode.ts | 1 + .../Map/hooks/useSelectPosition.tsx | 2 +- lib/src/Components/Map/hooks/useTags.tsx | 1 - .../Map/hooks/useWindowDimension.tsx | 4 +- .../Components/Profile/ItemFunctions.spec.tsx | 13 +- lib/src/Components/Profile/ProfileForm.tsx | 2 + lib/src/Components/Profile/ProfileView.tsx | 6 +- .../Profile/Subcomponents/ActionsButton.tsx | 5 +- .../Profile/Subcomponents/AvatarWidget.tsx | 9 +- .../Profile/Subcomponents/ColorPicker.tsx | 21 +- .../Profile/Subcomponents/ContactInfoForm.tsx | 8 +- .../Subcomponents/CrowdfundingForm.tsx | 4 +- .../Profile/Subcomponents/FormHeader.tsx | 17 +- .../Subcomponents/GalleryForm.spec.tsx | 2 + .../Profile/Subcomponents/GalleryForm.tsx | 19 +- .../Profile/Subcomponents/GalleryView.tsx | 13 +- .../Subcomponents/GroupSubheaderForm.tsx | 9 +- .../Subcomponents/LinkedItemsHeaderView.tsx | 9 +- .../Profile/Subcomponents/MarkdownHint.tsx | 4 +- .../Subcomponents/ProfileStartEndForm.tsx | 8 +- .../Profile/Subcomponents/ProfileTextForm.tsx | 4 +- .../Profile/Subcomponents/SocialShareBar.tsx | 9 +- .../Profile/Subcomponents/TagsWidget.tsx | 9 +- .../Profile/Templates/OnepagerForm.tsx | 4 +- .../Profile/Templates/SimpleForm.tsx | 1 + .../Components/Profile/Templates/TabsForm.tsx | 5 +- .../Components/Profile/Templates/TabsView.tsx | 44 +- lib/src/Components/Profile/UserSettings.tsx | 15 +- lib/src/Components/Profile/itemFunctions.ts | 2 + .../Components/Templates/AttestationForm.tsx | 10 +- lib/src/Components/Templates/DateUserInfo.tsx | 12 +- lib/src/Components/Templates/DialogModal.tsx | 8 +- lib/src/Components/Templates/EmojiPicker.tsx | 12 +- lib/src/Components/Templates/ItemCard.tsx | 12 +- .../Components/Templates/MapOverlayPage.tsx | 4 +- lib/src/Components/Templates/MarketView.tsx | 9 +- .../Templates/OverlayItemsIndexPage.tsx | 5 +- lib/src/Components/Templates/SelectUser.tsx | 4 +- lib/src/Components/Templates/Tabs.tsx | 5 +- lib/src/Components/Templates/ThemeControl.tsx | 4 +- lib/src/Utils/MarkerIconFactory.ts | 3 +- lib/src/Utils/ReplaceURLs.ts | 2 +- lib/src/Utils/ReverseGeocoder.ts | 1 + lib/src/Utils/TimeAgo.ts | 1 + lib/src/Utils/getImageDimensions.ts | 5 +- lib/src/index.tsx | 2 +- lib/src/types/FormState.d.ts | 1 + lib/tsconfig.json | 62 +- package-lock.json | 1924 +++++++++++------ 122 files changed, 2337 insertions(+), 1377 deletions(-) delete mode 100644 app/.eslintignore delete mode 100644 app/.eslintrc.cjs create mode 100644 app/eslint.config.js delete mode 100644 lib/.eslintignore delete mode 100644 lib/.eslintrc.cjs create mode 100644 lib/eslint.config.js diff --git a/.gitignore b/.gitignore index 10379bdf..5a4915fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .claude/ +app/node_modules/ +lib/node_modules/ data/ node_modules/ cypress/node_modules/ diff --git a/app/.eslintignore b/app/.eslintignore deleted file mode 100644 index 2f3546e5..00000000 --- a/app/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules/ -dist/ -data/ \ No newline at end of file diff --git a/app/.eslintrc.cjs b/app/.eslintrc.cjs deleted file mode 100644 index af43f19b..00000000 --- a/app/.eslintrc.cjs +++ /dev/null @@ -1,223 +0,0 @@ -// eslint-disable-next-line import/no-commonjs -module.exports = { - env: { - browser: true, - es2021: true, - }, - extends: [ - 'standard', - 'eslint:recommended', - 'plugin:@eslint-community/eslint-comments/recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:import/recommended', - 'plugin:import/typescript', - // 'plugin:promise/recommended', - 'plugin:security/recommended-legacy', - 'plugin:react/recommended', - ], - parserOptions: { - ecmaVersion: 'latest', - parser: '@typescript-eslint/parser', - sourceType: 'module', - }, - plugins: [ - '@typescript-eslint', - 'import', - 'promise', - 'security', - 'no-catch-all', - 'react', - 'react-hooks', - 'react-refresh', - ], - // TODO also parse this - ignorePatterns: ['vite.config.ts'], - settings: { - 'import/resolver': { - typescript: true, - node: { - extensions: ['.js', '.jsx', '.ts', '.tsx'], - }, - }, - react: { - version: '18.2.0', - }, - }, - rules: { - 'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks - 'react-hooks/exhaustive-deps': 'warn', // Checks effect dependencies - 'react/react-in-jsx-scope': 'off', // Disable requirement for React import - 'no-catch-all/no-catch-all': 'error', - 'no-console': 'error', - 'no-debugger': 'error', - camelcase: 'error', - indent: ['error', 2], - 'linebreak-style': ['error', 'unix'], - semi: ['error', 'never'], - // Optional eslint-comments rule - '@eslint-community/eslint-comments/no-unused-disable': 'error', - '@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }], - // import - 'import/export': 'error', - 'import/no-deprecated': 'error', - 'import/no-empty-named-blocks': 'error', - 'import/no-extraneous-dependencies': 'error', - 'import/no-mutable-exports': 'error', - 'import/no-unused-modules': 'error', - 'import/no-named-as-default': 'error', - 'import/no-named-as-default-member': 'error', - 'import/no-amd': 'error', - 'import/no-commonjs': 'error', - 'import/no-import-module-exports': 'error', - 'import/no-nodejs-modules': 'off', - 'import/unambiguous': 'off', // not compatible with scriptless vue files - 'import/default': 'error', - 'import/named': 'error', - 'import/namespace': 'error', - 'import/no-absolute-path': 'error', - 'import/no-cycle': 'error', - 'import/no-dynamic-require': 'error', - 'import/no-internal-modules': 'off', - 'import/no-relative-packages': 'error', - 'import/no-relative-parent-imports': [ - 'error', - { - ignore: ['#[src,types,root,components,utils,assets]/*', '@/config/*'], - }, - ], - 'import/no-self-import': 'error', - 'import/no-unresolved': [ - 'error', - { - ignore: ['react'], - }, - ], - 'import/no-useless-path-segments': 'error', - 'import/no-webpack-loader-syntax': 'error', - 'import/consistent-type-specifier-style': 'error', - 'import/exports-last': 'off', - 'import/extensions': [ - 'error', - 'never', - { - json: 'always', - }, - ], - 'import/first': 'error', - 'import/group-exports': 'off', - 'import/newline-after-import': 'error', - 'import/no-anonymous-default-export': 'off', // todo - consider to enable again - 'import/no-default-export': 'off', // incompatible with vite & vike - 'import/no-duplicates': 'error', - 'import/no-named-default': 'error', - 'import/no-namespace': 'error', - 'import/no-unassigned-import': [ - 'error', - { - allow: ['**/*.css'], - }, - ], - 'import/order': [ - 'error', - { - groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], - 'newlines-between': 'always', - alphabetize: { - order: 'asc', // sort in ascending order. Options: ["ignore", "asc", "desc"] - caseInsensitive: true, // ignore case. Options: [true, false] - }, - distinctGroup: true, - }, - ], - 'import/prefer-default-export': 'off', - // promise - 'promise/catch-or-return': 'error', - 'promise/no-return-wrap': 'error', - 'promise/param-names': 'error', - 'promise/always-return': 'error', - 'promise/no-native': 'off', - 'promise/no-nesting': 'warn', - 'promise/no-promise-in-callback': 'warn', - 'promise/no-callback-in-promise': 'warn', - 'promise/avoid-new': 'warn', - 'promise/no-new-statics': 'error', - 'promise/no-return-in-finally': 'warn', - 'promise/valid-params': 'warn', - 'promise/prefer-await-to-callbacks': 'error', - 'promise/no-multiple-resolved': 'error', - }, - overrides: [ - { - files: ['*.ts', '*.tsx'], - parser: '@typescript-eslint/parser', - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig.json', '**/tsconfig.json'], - ecmaVersion: 'latest', - parser: '@typescript-eslint/parser', - sourceType: 'module', - }, - plugins: ['@typescript-eslint'], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:@typescript-eslint/recommended-requiring-type-checking', - 'plugin:@typescript-eslint/strict', - ], - rules: { - '@typescript-eslint/consistent-type-imports': 'error', - // allow explicitly defined dangling promises - '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }], - 'no-void': ['error', { allowAsStatement: true }], - }, - }, - { - files: ['!*.json'], - plugins: ['prettier'], - extends: ['plugin:prettier/recommended'], - rules: { - 'prettier/prettier': 'error', - }, - }, - { - files: ['*.json'], - plugins: ['json'], - extends: ['plugin:json/recommended-with-comments'], - }, - // { - // files: ['*.{test,spec}.[tj]s'], - // plugins: ['vitest'], - // extends: ['plugin:vitest/all'], - // rules: { - // 'vitest/prefer-lowercase-title': 'off', - // 'vitest/no-hooks': 'off', - // 'vitest/consistent-test-filename': 'off', - // 'vitest/prefer-expect-assertions': [ - // 'off', - // { - // onlyFunctionsWithExpectInLoop: true, - // onlyFunctionsWithExpectInCallback: true, - // }, - // ], - // 'vitest/prefer-strict-equal': 'off', - // 'vitest/prefer-to-be-falsy': 'off', - // 'vitest/prefer-to-be-truthy': 'off', - // 'vitest/require-hook': [ - // 'error', - // { - // allowedFunctionCalls: [ - // 'mockClient.setRequestHandler', - // 'setActivePinia', - // 'provideApolloClient', - // ], - // }, - // ], - // }, - // }, - { - files: ['*.yaml', '*.yml'], - parser: 'yaml-eslint-parser', - plugins: ['yml'], - extends: ['plugin:yml/prettier'], - }, - ], -} diff --git a/app/.prettierrc.json b/app/.prettierrc.json index 1db2a8cf..72e17590 100644 --- a/app/.prettierrc.json +++ b/app/.prettierrc.json @@ -11,4 +11,4 @@ "bracketSameLine": false, "arrowParens": "always", "endOfLine": "auto" -} \ No newline at end of file +} diff --git a/app/eslint.config.js b/app/eslint.config.js new file mode 100644 index 00000000..805ec2df --- /dev/null +++ b/app/eslint.config.js @@ -0,0 +1,260 @@ +// ESLint v9 flat config for Utopia Map App +import js from '@eslint/js' +import eslintCommentsConfigs from '@eslint-community/eslint-plugin-eslint-comments/configs' +import importXPlugin from 'eslint-plugin-import-x' +import jsonPlugin from 'eslint-plugin-json' +import noCatchAllPlugin from 'eslint-plugin-no-catch-all' +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' +import promisePlugin from 'eslint-plugin-promise' +import react from 'eslint-plugin-react' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import securityPlugin from 'eslint-plugin-security' +import globals from 'globals' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + // Ignore patterns + { + ignores: ['dist/**', 'node_modules/**', 'data/**', 'vite.config.ts'], + }, + + // Report unused eslint-disable directives (catches stale comments after rule renames) + { + linterOptions: { + reportUnusedDisableDirectives: 'error', + }, + }, + + // Base ESLint recommended config + js.configs.recommended, + + // ESLint comments recommended config + eslintCommentsConfigs.recommended, + + // Security recommended config + securityPlugin.configs.recommended, + + // React recommended configs + react.configs.flat.recommended, + react.configs.flat['jsx-runtime'], + + // Main configuration for JavaScript/TypeScript files + { + files: ['**/*.{js,jsx,ts,tsx,cjs,mjs}'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.es2021, + }, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + plugins: { + react: react, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + 'import-x': importXPlugin, + promise: promisePlugin, + 'no-catch-all': noCatchAllPlugin, + }, + settings: { + react: { + version: '18.2.0', + }, + 'import-x/resolver': { + typescript: true, + node: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + }, + }, + rules: { + // ESLint comments rules - allow whole-file disables without eslint-enable + '@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }], + + // Basic rules + 'no-console': 'error', + 'no-debugger': 'error', + camelcase: 'error', + + // Standard JS rules (replacing eslint-config-standard) + semi: ['error', 'never'], + quotes: ['error', 'single', { avoidEscape: true }], + 'comma-dangle': ['error', 'always-multiline'], + // Disabled: conflicts with common TypeScript/React patterns + // 'space-before-function-paren': ['error', 'always'], + 'keyword-spacing': ['error', { before: true, after: true }], + 'space-infix-ops': 'error', + 'eol-last': ['error', 'always'], + 'no-trailing-spaces': 'error', + 'object-curly-spacing': ['error', 'always'], + 'array-bracket-spacing': ['error', 'never'], + 'computed-property-spacing': ['error', 'never'], + 'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0 }], + // Disable indent rule due to known issues with TypeScript/JSX + // 'indent': ['error', 2], + 'linebreak-style': ['error', 'unix'], + + // Additional standard rules that were missing + eqeqeq: ['error', 'always', { null: 'ignore' }], + 'new-cap': ['error', { newIsCap: true, capIsNew: false, properties: true }], + 'array-callback-return': ['error', { allowImplicit: false, checkForEach: false }], + + // React rules + 'react/react-in-jsx-scope': 'off', + 'react/no-unescaped-entities': 'error', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + + // Import rules + 'import-x/export': 'error', + 'import-x/no-deprecated': 'error', + 'import-x/no-empty-named-blocks': 'error', + 'import-x/no-extraneous-dependencies': 'error', + 'import-x/no-mutable-exports': 'error', + 'import-x/no-named-as-default': 'error', + 'import-x/no-named-as-default-member': 'error', + 'import-x/no-amd': 'error', + 'import-x/no-commonjs': 'error', + 'import-x/no-nodejs-modules': 'off', + 'import-x/default': 'error', + 'import-x/named': 'error', + 'import-x/namespace': 'error', + 'import-x/no-absolute-path': 'error', + 'import-x/no-cycle': 'error', + 'import-x/no-dynamic-require': 'error', + 'import-x/no-internal-modules': 'off', + 'import-x/no-relative-packages': 'error', + 'import-x/no-self-import': 'error', + 'import-x/no-unresolved': ['error', { ignore: ['react'] }], + 'import-x/no-useless-path-segments': 'error', + 'import-x/no-webpack-loader-syntax': 'error', + 'import-x/consistent-type-specifier-style': 'error', + 'import-x/exports-last': 'off', + 'import-x/extensions': ['error', 'never', { json: 'always' }], + 'import-x/first': 'error', + 'import-x/group-exports': 'off', + 'import-x/newline-after-import': 'error', + 'import-x/no-anonymous-default-export': 'off', + 'import-x/no-default-export': 'off', + 'import-x/no-duplicates': 'error', + 'import-x/no-named-default': 'error', + 'import-x/no-namespace': 'error', + 'import-x/no-unassigned-import': ['error', { allow: ['**/*.css'] }], + 'import-x/order': [ + 'error', + { + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + 'sibling', + 'index', + 'object', + 'type', + ], + 'newlines-between': 'always', + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + distinctGroup: true, + }, + ], + 'import-x/prefer-default-export': 'off', + + // Promise rules + 'promise/catch-or-return': 'error', + 'promise/no-return-wrap': 'error', + 'promise/param-names': 'error', + 'promise/always-return': 'error', + 'promise/no-native': 'off', + 'promise/no-nesting': 'warn', + 'promise/no-promise-in-callback': 'warn', + 'promise/no-callback-in-promise': 'warn', + 'promise/avoid-new': 'warn', + 'promise/no-new-statics': 'error', + 'promise/no-return-in-finally': 'warn', + 'promise/valid-params': 'warn', + 'promise/prefer-await-to-callbacks': 'error', + 'promise/no-multiple-resolved': 'error', + + // Security and other rules + 'no-catch-all/no-catch-all': 'error', + + // Additional import rules + 'import-x/no-unused-modules': 'error', + 'import-x/no-import-module-exports': 'error', + 'import-x/unambiguous': 'off', + 'import-x/no-relative-parent-imports': [ + 'error', + { + ignore: ['#[src,types,root,components,utils,assets]/*', '@/config/*'], + }, + ], + }, + }, + + // TypeScript configs (applied after main config) + ...tseslint.configs.recommended, + ...tseslint.configs.strict, + ...tseslint.configs.stylistic, + + // TypeScript type-checking configuration + { + files: ['**/*.{ts,tsx}'], + extends: [ + ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.strictTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + '@typescript-eslint/consistent-type-imports': 'error', + '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }], + 'no-void': ['error', { allowAsStatement: true }], + + // Disable empty function rule - legitimate use in React contexts and empty constructors + '@typescript-eslint/no-empty-function': 'off', + + // Configure no-unused-expressions to allow logical AND and ternary patterns + '@typescript-eslint/no-unused-expressions': [ + 'error', + { + allowShortCircuit: true, + allowTernary: true, + }, + ], + }, + }, + + // JSON files configuration + { + files: ['**/*.json'], + plugins: { + json: jsonPlugin, + }, + rules: { + // Disable TypeScript-specific rules for JSON files + '@typescript-eslint/no-unused-expressions': 'off', + // JSON-specific rules + 'json/*': 'error', + }, + }, + + // Prettier recommended config (should be last to override other formatting rules) + eslintPluginPrettierRecommended, +) diff --git a/app/package.json b/app/package.json index a7fbb136..74eccabf 100644 --- a/app/package.json +++ b/app/package.json @@ -9,7 +9,7 @@ "scripts": { "dev": "vite --host", "build": "tsc && vite build", - "test:lint:eslint": "eslint --ext .ts,.tsx,.js,.jsx,.cjs,.mjs,.json,.yml,.yaml --max-warnings 0 .", + "test:lint:eslint": "eslint --max-warnings 0 .", "preview": "vite preview" }, "dependencies": { @@ -22,36 +22,33 @@ "react-dom": "^18.2.0", "react-rnd": "^10.4.1", "react-router-dom": "^6.23.0", - "vite-tsconfig-paths": "^5.1.4", - "utopia-ui": "^3.0.111" + "utopia-ui": "^3.0.111", + "vite-tsconfig-paths": "^5.1.4" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^4.4.1", + "@eslint/js": "^9.36.0", "@types/node": "^24.10.2", "@types/react": "^18.2.79", "@types/react-dom": "^18.2.25", - "@typescript-eslint/eslint-plugin": "^5.62.0", - "@typescript-eslint/parser": "^5.62.0", "@vitejs/plugin-react": "^4.0.0", "daisyui": "^5.5.5", - "eslint": "^8.24.0", - "eslint-config-prettier": "^10.1.8", - "eslint-config-standard": "^17.1.0", + "eslint": "^9.36.0", "eslint-import-resolver-typescript": "^4.4.4", - "eslint-plugin-import": "^2.31.0", + "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-json": "^3.1.0", - "eslint-plugin-n": "^17.23.1", "eslint-plugin-no-catch-all": "^1.1.0", - "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-promise": "^7.2.1", "eslint-plugin-react": "^7.31.8", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "eslint-plugin-security": "^3.0.1", - "eslint-plugin-yml": "^1.14.0", + "globals": "^16.3.0", "postcss": "^8.4.30", "tailwindcss": "^4.1.17", "typescript": "^5.9.3", + "typescript-eslint": "^8.9.0", "vite": "^7.2.7", "vite-plugin-pwa": "^1.2.0" } diff --git a/app/src/App.tsx b/app/src/App.tsx index 36a3de38..e21ef608 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unnecessary-condition */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -/* eslint-disable import/order */ +/* eslint-disable import-x/order */ /* eslint-disable eqeqeq */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable react-hooks/exhaustive-deps */ @@ -8,6 +8,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable new-cap */ /* eslint-disable @typescript-eslint/prefer-optional-chain */ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/restrict-plus-operands */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ diff --git a/app/src/ModalContent.tsx b/app/src/ModalContent.tsx index f501ddab..c79a850b 100644 --- a/app/src/ModalContent.tsx +++ b/app/src/ModalContent.tsx @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/restrict-plus-operands */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ import { useEffect, useState } from 'react' import { TextView } from 'utopia-ui' @@ -22,7 +23,9 @@ export function Welcome1({ clickAction1, map }: ChapterProps) {
@@ -45,7 +48,9 @@ export function Welcome1({ clickAction1, map }: ChapterProps) {
diff --git a/app/src/api/directus.ts b/app/src/api/directus.ts index 140ca4ba..2e28ed01 100644 --- a/app/src/api/directus.ts +++ b/app/src/api/directus.ts @@ -4,7 +4,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ import { createDirectus, rest, authentication } from '@directus/sdk' -// eslint-disable-next-line import/no-relative-parent-imports +// eslint-disable-next-line import-x/no-relative-parent-imports import { config } from '../config' import type { AuthenticationData, AuthenticationStorage } from '@directus/sdk' @@ -86,9 +86,10 @@ export const authLocalStorage = (mainKey = 'directus_storage') => // implementation of set, here set the value at mainKey in localStorage, or remove it if value is null set: async (value: AuthenticationData | null) => { if (!value) { - return window.localStorage.removeItem(mainKey) + window.localStorage.removeItem(mainKey) + return } - return window.localStorage.setItem(mainKey, JSON.stringify(value)) + window.localStorage.setItem(mainKey, JSON.stringify(value)) }, }) as AuthenticationStorage diff --git a/app/src/api/inviteApi.ts b/app/src/api/inviteApi.ts index 7a6d2c24..8dcd8d8a 100644 --- a/app/src/api/inviteApi.ts +++ b/app/src/api/inviteApi.ts @@ -1,4 +1,3 @@ -/* @eslint-disable-next-line import/no-relative-parent-imports */ import { config } from '@/config' import type { UserApi } from 'utopia-ui' diff --git a/app/src/api/itemsApi.ts b/app/src/api/itemsApi.ts index defb7c99..7b60142f 100644 --- a/app/src/api/itemsApi.ts +++ b/app/src/api/itemsApi.ts @@ -20,8 +20,8 @@ export class itemsApi implements ItemsApi { constructor( collectionName: keyof MyCollections, - layerId?: string | undefined, - mapId?: string | undefined, + layerId?: string, + mapId?: string, filter?: any, customParameter?: any, ) { @@ -111,8 +111,8 @@ export class itemsApi implements ItemsApi { async deleteItem(id: string): Promise { try { - const result = await directusClient.request(deleteItem(this.collectionName, id)) - return result as unknown as boolean + await directusClient.request(deleteItem(this.collectionName, id)) + return true } catch (error: any) { console.log(error) if (error.errors[0].message) throw error.errors[0].message diff --git a/app/src/api/layersApi.ts b/app/src/api/layersApi.ts index b91e5881..fa779ecf 100644 --- a/app/src/api/layersApi.ts +++ b/app/src/api/layersApi.ts @@ -21,6 +21,7 @@ export class layersApi { { itemType: ['*.*', { profileTemplate: ['*', 'item.*.*.*.*'] }] }, { markerIcon: ['*'] } as any, ], + // eslint-disable-next-line camelcase filter: { maps: { maps_id: { id: { _eq: this.mapId } } } }, limit: 500, sort: ['sort'], diff --git a/app/src/api/permissionsApi.ts b/app/src/api/permissionsApi.ts index c0f42647..ed1c027e 100644 --- a/app/src/api/permissionsApi.ts +++ b/app/src/api/permissionsApi.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-useless-constructor */ -/* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable no-console */ import { readPermissions } from '@directus/sdk' diff --git a/app/src/api/refiBcnApi.ts b/app/src/api/refiBcnApi.ts index 5f685ed8..65717ad3 100644 --- a/app/src/api/refiBcnApi.ts +++ b/app/src/api/refiBcnApi.ts @@ -1,7 +1,7 @@ /* eslint-disable no-console */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import axios from 'axios' import type { ItemsApi } from 'utopia-ui' diff --git a/app/src/api/userApi.ts b/app/src/api/userApi.ts index c63e1baa..ff8e7744 100644 --- a/app/src/api/userApi.ts +++ b/app/src/api/userApi.ts @@ -3,7 +3,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable no-console */ /* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ import { createUser, passwordRequest, passwordReset, readMe, updateMe } from '@directus/sdk' import { directusClient } from './directus' @@ -54,7 +53,8 @@ export class UserApi { async logout(): Promise { try { - return await directusClient.logout() + await directusClient.logout() + return } catch (error: any) { console.log(error) if (error.errors[0].message) throw error.errors[0].message @@ -98,7 +98,8 @@ export class UserApi { async requestPasswordReset(email: string, reset_url?: string): Promise { try { - return await directusClient.request(passwordRequest(email, reset_url)) + await directusClient.request(passwordRequest(email, reset_url)) + return } catch (error: any) { console.log(error) if (error.errors[0].message) throw error.errors[0].message @@ -108,7 +109,8 @@ export class UserApi { async passwordReset(reset_token: string, new_password: string): Promise { try { - return await directusClient.request(passwordReset(reset_token, new_password)) + await directusClient.request(passwordReset(reset_token, new_password)) + return } catch (error: any) { console.log(error) if (error.errors[0].message) throw error.errors[0].message diff --git a/app/src/main.tsx b/app/src/main.tsx index 86140f18..ccb66470 100644 --- a/app/src/main.tsx +++ b/app/src/main.tsx @@ -1,5 +1,4 @@ -/* eslint-disable import/extensions */ -/* eslint-disable import/no-named-as-default-member */ +/* eslint-disable import-x/extensions */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import React from 'react' import ReactDOM from 'react-dom/client' diff --git a/app/src/pages/Landingpage.tsx b/app/src/pages/Landingpage.tsx index a5d221fa..97c355c0 100644 --- a/app/src/pages/Landingpage.tsx +++ b/app/src/pages/Landingpage.tsx @@ -1,7 +1,7 @@ /* eslint-disable react/no-unescaped-entities */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable import/no-relative-parent-imports */ +/* eslint-disable import-x/no-relative-parent-imports */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable new-cap */ diff --git a/app/src/pages/MapContainer.tsx b/app/src/pages/MapContainer.tsx index 0d305d3d..f0b0cd9f 100644 --- a/app/src/pages/MapContainer.tsx +++ b/app/src/pages/MapContainer.tsx @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-unnecessary-condition */ /* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable import/no-relative-parent-imports */ +/* eslint-disable import-x/no-relative-parent-imports */ /* eslint-disable new-cap */ -/* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { useEffect, useState } from 'react' import { diff --git a/app/src/pages/data.ts b/app/src/pages/data.ts index f72437dc..cd36c415 100644 --- a/app/src/pages/data.ts +++ b/app/src/pages/data.ts @@ -1,3 +1,4 @@ +/* eslint-disable camelcase */ import type { Item, Tag } from 'utopia-ui' export const tags: Tag[] = [ diff --git a/app/tsconfig.json b/app/tsconfig.json index 95a0cf49..784becfb 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -12,35 +12,17 @@ "noFallthroughCasesInSwitch": true, "baseUrl": ".", "paths": { - "@/*": [ - "src/*" - ], - "utopia-ui": [ - "../lib/src" - ], - "#components/*": [ - "../lib/src/Components/*" - ], - "#utils/*": [ - "../lib/src/Utils/*" - ], - "#types/*": [ - "../lib/src/types/*" - ], - "#assets/*": [ - "../lib/src/assets/*" - ], - "#src/*": [ - "../lib/src/*" - ], - "#root/*": [ - "../lib/*" - ] + "@/*": ["src/*"], + "utopia-ui": ["../lib/src"], + "#components/*": ["../lib/src/Components/*"], + "#utils/*": ["../lib/src/Utils/*"], + "#types/*": ["../lib/src/types/*"], + "#assets/*": ["../lib/src/assets/*"], + "#src/*": ["../lib/src/*"], + "#root/*": ["../lib/*"] } }, - "include": [ - "src" - ], + "include": ["src"], "references": [ { "path": "./tsconfig.node.json" diff --git a/lib/.eslintignore b/lib/.eslintignore deleted file mode 100644 index c24c07ac..00000000 --- a/lib/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules/ -dist/ -examples/ -docs/ -coverage/ \ No newline at end of file diff --git a/lib/.eslintrc.cjs b/lib/.eslintrc.cjs deleted file mode 100644 index e4a3299b..00000000 --- a/lib/.eslintrc.cjs +++ /dev/null @@ -1,221 +0,0 @@ -// eslint-disable-next-line import/no-commonjs -module.exports = { - env: { - browser: true, - es2021: true, - }, - extends: [ - 'standard', - 'eslint:recommended', - 'plugin:@eslint-community/eslint-comments/recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:import/recommended', - 'plugin:import/typescript', - // 'plugin:promise/recommended', - 'plugin:security/recommended-legacy', - 'plugin:react/recommended', - ], - parserOptions: { - ecmaVersion: 'latest', - parser: '@typescript-eslint/parser', - sourceType: 'module', - }, - plugins: [ - '@typescript-eslint', - 'import', - 'promise', - 'security', - 'no-catch-all', - 'react', - 'react-hooks', - 'react-refresh', - ], - settings: { - 'import/resolver': { - typescript: true, - node: { - extensions: ['.js', '.jsx', '.ts', '.tsx'], - }, - }, - react: { - version: '18.2.0', - }, - }, - rules: { - 'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks - 'react-hooks/exhaustive-deps': 'warn', // Checks effect dependencies - 'react/react-in-jsx-scope': 'off', // Disable requirement for React import - 'no-catch-all/no-catch-all': 'error', - 'no-console': 'error', - 'no-debugger': 'error', - camelcase: 'error', - indent: ['error', 2], - 'linebreak-style': ['error', 'unix'], - semi: ['error', 'never'], - // Optional eslint-comments rule - '@eslint-community/eslint-comments/no-unused-disable': 'error', - '@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }], - // import - 'import/export': 'error', - 'import/no-deprecated': 'error', - 'import/no-empty-named-blocks': 'error', - 'import/no-extraneous-dependencies': 'error', - 'import/no-mutable-exports': 'error', - 'import/no-unused-modules': 'error', - 'import/no-named-as-default': 'error', - 'import/no-named-as-default-member': 'error', - 'import/no-amd': 'error', - 'import/no-commonjs': 'error', - 'import/no-import-module-exports': 'error', - 'import/no-nodejs-modules': 'off', - 'import/unambiguous': 'off', // not compatible with scriptless vue files - 'import/default': 'error', - 'import/named': 'error', - 'import/namespace': 'error', - 'import/no-absolute-path': 'error', - 'import/no-cycle': 'error', - 'import/no-dynamic-require': 'error', - 'import/no-internal-modules': 'off', - 'import/no-relative-packages': 'error', - 'import/no-relative-parent-imports': [ - 'error', - { - ignore: ['#[src,types,root,components,utils,assets]/*'], - }, - ], - 'import/no-self-import': 'error', - 'import/no-unresolved': [ - 'error', - { - ignore: ['react'], - }, - ], - 'import/no-useless-path-segments': 'error', - 'import/no-webpack-loader-syntax': 'error', - 'import/consistent-type-specifier-style': 'error', - 'import/exports-last': 'off', - 'import/extensions': [ - 'error', - 'never', - { - json: 'always', - }, - ], - 'import/first': 'error', - 'import/group-exports': 'off', - 'import/newline-after-import': 'error', - 'import/no-anonymous-default-export': 'off', // todo - consider to enable again - 'import/no-default-export': 'off', // incompatible with vite & vike - 'import/no-duplicates': 'error', - 'import/no-named-default': 'error', - 'import/no-namespace': 'error', - 'import/no-unassigned-import': [ - 'error', - { - allow: ['**/*.css'], - }, - ], - 'import/order': [ - 'error', - { - groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], - 'newlines-between': 'always', - alphabetize: { - order: 'asc', // sort in ascending order. Options: ["ignore", "asc", "desc"] - caseInsensitive: true, // ignore case. Options: [true, false] - }, - distinctGroup: true, - }, - ], - 'import/prefer-default-export': 'off', - // promise - 'promise/catch-or-return': 'error', - 'promise/no-return-wrap': 'error', - 'promise/param-names': 'error', - 'promise/always-return': 'error', - 'promise/no-native': 'off', - 'promise/no-nesting': 'warn', - 'promise/no-promise-in-callback': 'warn', - 'promise/no-callback-in-promise': 'warn', - 'promise/avoid-new': 'warn', - 'promise/no-new-statics': 'error', - 'promise/no-return-in-finally': 'warn', - 'promise/valid-params': 'warn', - 'promise/prefer-await-to-callbacks': 'error', - 'promise/no-multiple-resolved': 'error', - }, - overrides: [ - { - files: ['*.ts', '*.tsx'], - parser: '@typescript-eslint/parser', - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig.json', '**/tsconfig.json'], - ecmaVersion: 'latest', - parser: '@typescript-eslint/parser', - sourceType: 'module', - }, - plugins: ['@typescript-eslint'], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:@typescript-eslint/recommended-requiring-type-checking', - 'plugin:@typescript-eslint/strict', - ], - rules: { - '@typescript-eslint/consistent-type-imports': 'error', - // allow explicitly defined dangling promises - '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }], - 'no-void': ['error', { allowAsStatement: true }], - }, - }, - { - files: ['!*.json'], - plugins: ['prettier'], - extends: ['plugin:prettier/recommended'], - rules: { - 'prettier/prettier': 'error', - }, - }, - { - files: ['*.json'], - plugins: ['json'], - extends: ['plugin:json/recommended-with-comments'], - }, - // { - // files: ['*.{test,spec}.[tj]s'], - // plugins: ['vitest'], - // extends: ['plugin:vitest/all'], - // rules: { - // 'vitest/prefer-lowercase-title': 'off', - // 'vitest/no-hooks': 'off', - // 'vitest/consistent-test-filename': 'off', - // 'vitest/prefer-expect-assertions': [ - // 'off', - // { - // onlyFunctionsWithExpectInLoop: true, - // onlyFunctionsWithExpectInCallback: true, - // }, - // ], - // 'vitest/prefer-strict-equal': 'off', - // 'vitest/prefer-to-be-falsy': 'off', - // 'vitest/prefer-to-be-truthy': 'off', - // 'vitest/require-hook': [ - // 'error', - // { - // allowedFunctionCalls: [ - // 'mockClient.setRequestHandler', - // 'setActivePinia', - // 'provideApolloClient', - // ], - // }, - // ], - // }, - // }, - { - files: ['*.yaml', '*.yml'], - parser: 'yaml-eslint-parser', - plugins: ['yml'], - extends: ['plugin:yml/prettier'], - }, - ], -} diff --git a/lib/.prettierrc.json b/lib/.prettierrc.json index 1db2a8cf..72e17590 100644 --- a/lib/.prettierrc.json +++ b/lib/.prettierrc.json @@ -11,4 +11,4 @@ "bracketSameLine": false, "arrowParens": "always", "endOfLine": "auto" -} \ No newline at end of file +} diff --git a/lib/cypress/support/component.ts b/lib/cypress/support/component.ts index 13ea7ddf..86f1ddf0 100644 --- a/lib/cypress/support/component.ts +++ b/lib/cypress/support/component.ts @@ -14,7 +14,7 @@ // *********************************************************** // Import commands.js using ES2015 syntax: -// eslint-disable-next-line import/no-unassigned-import +// eslint-disable-next-line import-x/no-unassigned-import import './commands' import { mount } from 'cypress/react' diff --git a/lib/eslint.config.js b/lib/eslint.config.js new file mode 100644 index 00000000..855a03ad --- /dev/null +++ b/lib/eslint.config.js @@ -0,0 +1,260 @@ +// ESLint v9 flat config for Utopia UI Library +import js from '@eslint/js' +import eslintCommentsConfigs from '@eslint-community/eslint-plugin-eslint-comments/configs' +import importXPlugin from 'eslint-plugin-import-x' +import jsonPlugin from 'eslint-plugin-json' +import noCatchAllPlugin from 'eslint-plugin-no-catch-all' +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' +import promisePlugin from 'eslint-plugin-promise' +import react from 'eslint-plugin-react' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import securityPlugin from 'eslint-plugin-security' +import globals from 'globals' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + // Ignore patterns + { + ignores: ['dist/**', 'node_modules/**', 'coverage/**', 'docs/**', 'examples/**'], + }, + + // Report unused eslint-disable directives (catches stale comments after rule renames) + { + linterOptions: { + reportUnusedDisableDirectives: 'error', + }, + }, + + // Base ESLint recommended config + js.configs.recommended, + + // ESLint comments recommended config + eslintCommentsConfigs.recommended, + + // Security recommended config + securityPlugin.configs.recommended, + + // React recommended configs + react.configs.flat.recommended, + react.configs.flat['jsx-runtime'], + + // Main configuration for JavaScript/TypeScript files + { + files: ['**/*.{js,jsx,ts,tsx,cjs,mjs}'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.es2021, + }, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + plugins: { + react: react, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + 'import-x': importXPlugin, + promise: promisePlugin, + 'no-catch-all': noCatchAllPlugin, + }, + settings: { + react: { + version: '18.2.0', + }, + 'import-x/resolver': { + typescript: true, + node: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + }, + }, + rules: { + // ESLint comments rules - allow whole-file disables without eslint-enable + '@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }], + + // Basic rules + 'no-console': 'error', + 'no-debugger': 'error', + camelcase: 'error', + + // Standard JS rules + semi: ['error', 'never'], + quotes: ['error', 'single', { avoidEscape: true }], + 'comma-dangle': ['error', 'always-multiline'], + 'keyword-spacing': ['error', { before: true, after: true }], + 'space-infix-ops': 'error', + 'eol-last': ['error', 'always'], + 'no-trailing-spaces': 'error', + 'object-curly-spacing': ['error', 'always'], + 'array-bracket-spacing': ['error', 'never'], + 'computed-property-spacing': ['error', 'never'], + 'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0 }], + indent: ['error', 2], + 'linebreak-style': ['error', 'unix'], + eqeqeq: ['error', 'always', { null: 'ignore' }], + 'new-cap': ['error', { newIsCap: true, capIsNew: false, properties: true }], + 'array-callback-return': ['error', { allowImplicit: false, checkForEach: false }], + + // React rules + 'react/react-in-jsx-scope': 'off', + 'react/no-unescaped-entities': 'error', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + + // Import rules + 'import-x/export': 'error', + 'import-x/no-deprecated': 'error', + 'import-x/no-empty-named-blocks': 'error', + 'import-x/no-extraneous-dependencies': 'error', + 'import-x/no-mutable-exports': 'error', + 'import-x/no-named-as-default': 'error', + 'import-x/no-named-as-default-member': 'error', + 'import-x/no-amd': 'error', + 'import-x/no-commonjs': 'error', + 'import-x/no-nodejs-modules': 'off', + 'import-x/default': 'error', + 'import-x/named': 'error', + 'import-x/namespace': 'error', + 'import-x/no-absolute-path': 'error', + 'import-x/no-cycle': 'error', + 'import-x/no-dynamic-require': 'error', + 'import-x/no-internal-modules': 'off', + 'import-x/no-relative-packages': 'error', + 'import-x/no-self-import': 'error', + 'import-x/no-unresolved': ['error', { ignore: ['react'] }], + 'import-x/no-useless-path-segments': 'error', + 'import-x/no-webpack-loader-syntax': 'error', + 'import-x/consistent-type-specifier-style': 'error', + 'import-x/exports-last': 'off', + 'import-x/extensions': ['error', 'never', { json: 'always' }], + 'import-x/first': 'error', + 'import-x/group-exports': 'off', + 'import-x/newline-after-import': 'error', + 'import-x/no-anonymous-default-export': 'off', + 'import-x/no-default-export': 'off', + 'import-x/no-duplicates': 'error', + 'import-x/no-named-default': 'error', + 'import-x/no-namespace': 'error', + 'import-x/no-unassigned-import': ['error', { allow: ['**/*.css'] }], + 'import-x/order': [ + 'error', + { + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + 'sibling', + 'index', + 'object', + 'type', + ], + 'newlines-between': 'always', + alphabetize: { order: 'asc', caseInsensitive: true }, + distinctGroup: true, + }, + ], + 'import-x/prefer-default-export': 'off', + 'import-x/no-unused-modules': 'error', + 'import-x/no-import-module-exports': 'error', + 'import-x/unambiguous': 'off', + 'import-x/no-relative-parent-imports': [ + 'error', + { ignore: ['#[src,types,root,components,utils,assets]/*'] }, + ], + + // Promise rules + 'promise/catch-or-return': 'error', + 'promise/no-return-wrap': 'error', + 'promise/param-names': 'error', + 'promise/always-return': 'error', + 'promise/no-native': 'off', + 'promise/no-nesting': 'warn', + 'promise/no-promise-in-callback': 'warn', + 'promise/no-callback-in-promise': 'warn', + 'promise/avoid-new': 'warn', + 'promise/no-new-statics': 'error', + 'promise/no-return-in-finally': 'warn', + 'promise/valid-params': 'warn', + 'promise/prefer-await-to-callbacks': 'error', + 'promise/no-multiple-resolved': 'error', + + // Security and other rules + 'no-catch-all/no-catch-all': 'error', + }, + }, + + // TypeScript configs (applied after main config) + ...tseslint.configs.recommended, + ...tseslint.configs.strict, + ...tseslint.configs.stylistic, + + // TypeScript type-checking configuration + { + files: ['**/*.{ts,tsx}'], + extends: [ + ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.strictTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + '@typescript-eslint/consistent-type-imports': 'error', + '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }], + 'no-void': ['error', { allowAsStatement: true }], + + // Disable empty function rule - legitimate use in React contexts + '@typescript-eslint/no-empty-function': 'off', + + // Configure no-unused-expressions to allow logical AND and ternary patterns + '@typescript-eslint/no-unused-expressions': [ + 'error', + { + allowShortCircuit: true, + allowTernary: true, + }, + ], + }, + }, + + // JSON files configuration + { + files: ['**/*.json'], + plugins: { + json: jsonPlugin, + }, + rules: { + // Disable TypeScript-specific rules for JSON files + '@typescript-eslint/no-unused-expressions': 'off', + // JSON-specific rules + 'json/*': 'error', + }, + }, + + // CommonJS configuration files (e.g., postcss.config.cjs) + // Provides Node.js globals (module, require, etc.) that were previously + // included via eslint-config-standard in ESLint v8 + { + files: ['**/*.cjs'], + languageOptions: { + globals: { + ...globals.node, + }, + sourceType: 'commonjs', + }, + }, + + // Prettier recommended config (should be last to override other formatting rules) + eslintPluginPrettierRecommended, +) diff --git a/lib/package.json b/lib/package.json index 888159af..5eb4be30 100644 --- a/lib/package.json +++ b/lib/package.json @@ -26,7 +26,7 @@ "scripts": { "build": "rollup -c", "start": "rollup -c -w", - "test:lint:eslint": "eslint --ext .ts,.tsx,.js,.jsx,.cjs,.mjs,.json,.yml,.yaml --max-warnings 0 .", + "test:lint:eslint": "eslint --max-warnings 0 .", "lint": "npm run test:lint:eslint", "lintfix": "npm run test:lint:eslint -- --fix", "test:component": "cypress run --component --browser electron", @@ -44,6 +44,7 @@ "license": "GPL-3.0-only", "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^4.4.1", + "@eslint/js": "^9.36.0", "@rollup/plugin-alias": "^6.0.0", "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-node-resolve": "^16.0.3", @@ -56,19 +57,15 @@ "@types/leaflet.markercluster": "^1.5.5", "@types/react": "^18.2.0", "@types/react-dom": "^18.0.5", - "@typescript-eslint/eslint-plugin": "^5.62.0", - "@typescript-eslint/parser": "^5.62.0", "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "^3.0.5", "cypress": "^15.7.1", "daisyui": "^5.5.5", - "eslint": "^8.24.0", + "eslint": "^9.36.0", "eslint-config-prettier": "^10.1.8", - "eslint-config-standard": "^17.1.0", "eslint-import-resolver-typescript": "^4.4.4", - "eslint-plugin-import": "^2.31.0", + "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-json": "^3.1.0", - "eslint-plugin-n": "^17.23.1", "eslint-plugin-no-catch-all": "^1.1.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-promise": "^7.2.1", @@ -76,7 +73,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "eslint-plugin-security": "^3.0.1", - "eslint-plugin-yml": "^1.14.0", + "globals": "^16.3.0", "happy-dom": "^20.0.11", "postcss": "^8.4.21", "prettier": "^3.7.4", @@ -91,6 +88,7 @@ "typedoc-plugin-coverage": "^3.4.1", "typedoc-plugin-missing-exports": "^3.1.0", "typescript": "^5.9.3", + "typescript-eslint": "^8.9.0", "vite": "^7.2.7", "vite-plugin-svgr": "^4.3.0", "vitest": "^3.0.5" diff --git a/lib/postcss.config.cjs b/lib/postcss.config.cjs index 6bb9c83c..4d0b5bd1 100644 --- a/lib/postcss.config.cjs +++ b/lib/postcss.config.cjs @@ -1,4 +1,4 @@ -// eslint-disable-next-line import/no-commonjs +// eslint-disable-next-line import-x/no-commonjs module.exports = { plugins: { '@tailwindcss/postcss': {}, diff --git a/lib/setupTest.ts b/lib/setupTest.ts index 3ed9ed03..0f35e917 100644 --- a/lib/setupTest.ts +++ b/lib/setupTest.ts @@ -1,2 +1,2 @@ -// eslint-disable-next-line import/no-unassigned-import +// eslint-disable-next-line import-x/no-unassigned-import import '@testing-library/jest-dom' diff --git a/lib/src/Components/AppShell/NavBar.tsx b/lib/src/Components/AppShell/NavBar.tsx index 51f521b9..17eecc01 100644 --- a/lib/src/Components/AppShell/NavBar.tsx +++ b/lib/src/Components/AppShell/NavBar.tsx @@ -31,7 +31,9 @@ export default function NavBar({ appName }: { appName: string }) { className='tw:btn tw:btn-square tw:btn-ghost tw:ml-3' aria-controls='#sidenav' aria-haspopup='true' - onClick={() => toggleSidebar()} + onClick={() => { + toggleSidebar() + }} > @@ -50,7 +52,9 @@ export default function NavBar({ appName }: { appName: string }) { diff --git a/lib/src/Components/AppShell/SideBar.tsx b/lib/src/Components/AppShell/SideBar.tsx index 4c7d6799..1e8a1747 100644 --- a/lib/src/Components/AppShell/SideBar.tsx +++ b/lib/src/Components/AppShell/SideBar.tsx @@ -6,7 +6,7 @@ import SidebarSubmenu from './SidebarSubmenu' export interface Route { path: string - icon: JSX.Element + icon: React.JSX.Element name: string submenu?: Route[] blank?: boolean @@ -57,7 +57,9 @@ export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoute // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition to={`${route.path}${params && '?' + params.toString()}`} className={({ isActive }) => - `${isActive ? 'tw:font-semibold tw:bg-base-200 tw:rounded-none!' : 'tw:font-normal tw:rounded-none!'}` + isActive + ? 'tw:font-semibold tw:bg-base-200 tw:rounded-none!' + : 'tw:font-normal tw:rounded-none!' } onClick={() => { if (screen.width < 640 && !appState.sideBarSlim) toggleSidebarOpen() @@ -70,7 +72,7 @@ export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoute {route.icon}
{route.name} @@ -119,7 +121,7 @@ export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoute > {route.icon} {route.name} @@ -143,7 +145,9 @@ export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoute 'tw:w-5 tw:h-5 tw:mb-4 tw:mr-5 tw:mt-2 tw:cursor-pointer tw:float-right tw:delay-400 tw:duration-500 tw:transition-all ' + (!appState.sideBarSlim ? 'tw:rotate-180' : '') } - onClick={() => toggleSidebarSlim()} + onClick={() => { + toggleSidebarSlim() + }} />
diff --git a/lib/src/Components/AppShell/SidebarSubmenu.tsx b/lib/src/Components/AppShell/SidebarSubmenu.tsx index 347d1323..a5f39ea0 100644 --- a/lib/src/Components/AppShell/SidebarSubmenu.tsx +++ b/lib/src/Components/AppShell/SidebarSubmenu.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/prefer-find */ import ChevronDownIcon from '@heroicons/react/24/outline/ChevronDownIcon' import { useEffect, useState } from 'react' import { Link, useLocation } from 'react-router-dom' @@ -10,7 +11,7 @@ function SidebarSubmenu({ icon, }: { path: string - icon: JSX.Element + icon: React.JSX.Element name: string submenu?: Route[] }) { @@ -31,7 +32,12 @@ function SidebarSubmenu({ return (
{/** Route header */} -
setIsExpanded(!isExpanded)}> +
{ + setIsExpanded(!isExpanded) + }} + > {icon} {name} setEmail(e.target.value)} + onChange={(e) => { + setEmail(e.target.value) + }} className='tw:input tw:input-bordered tw:w-full tw:max-w-xs' /> setPassword(e.target.value)} + onChange={(e) => { + setPassword(e.target.value) + }} className='tw:input tw:input-bordered tw:w-full tw:max-w-xs' />
diff --git a/lib/src/Components/Auth/RequestPasswordPage.tsx b/lib/src/Components/Auth/RequestPasswordPage.tsx index dcfb416f..713516c1 100644 --- a/lib/src/Components/Auth/RequestPasswordPage.tsx +++ b/lib/src/Components/Auth/RequestPasswordPage.tsx @@ -28,7 +28,7 @@ export function RequestPasswordPage({ resetUrl }: { resetUrl: string }) { }, error: { render({ data }) { - return `${data as string}` + return data as string }, }, pending: 'sending email ...', @@ -42,7 +42,9 @@ export function RequestPasswordPage({ resetUrl }: { resetUrl: string }) { type='email' placeholder='E-Mail' value={email} - onChange={(e) => setEmail(e.target.value)} + onChange={(e) => { + setEmail(e.target.value) + }} className='tw:input tw:input-bordered tw:w-full tw:max-w-xs' />
diff --git a/lib/src/Components/Auth/SetNewPasswordPage.tsx b/lib/src/Components/Auth/SetNewPasswordPage.tsx index c8f59591..fc00649c 100644 --- a/lib/src/Components/Auth/SetNewPasswordPage.tsx +++ b/lib/src/Components/Auth/SetNewPasswordPage.tsx @@ -28,7 +28,7 @@ export function SetNewPasswordPage() { }, error: { render({ data }) { - return `${data as string}` + return data as string }, }, pending: 'setting password ...', @@ -41,7 +41,9 @@ export function SetNewPasswordPage() { setPassword(e.target.value)} + onChange={(e) => { + setPassword(e.target.value) + }} className='tw:input tw:input-bordered tw:w-full tw:max-w-xs' />
diff --git a/lib/src/Components/Auth/SignupPage.tsx b/lib/src/Components/Auth/SignupPage.tsx index 1eb51ea9..13c0089f 100644 --- a/lib/src/Components/Auth/SignupPage.tsx +++ b/lib/src/Components/Auth/SignupPage.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { toast } from 'react-toastify' @@ -32,7 +33,7 @@ export function SignupPage() { }, error: { render({ data }) { - return `${data as string}` + return data as string }, autoClose: 10000, }, @@ -61,20 +62,26 @@ export function SignupPage() { type='text' placeholder='Name' value={userName} - onChange={(e) => setUserName(e.target.value)} + onChange={(e) => { + setUserName(e.target.value) + }} className='tw:input tw:input-bordered tw:w-full tw:max-w-xs' /> setEmail(e.target.value)} + onChange={(e) => { + setEmail(e.target.value) + }} className='tw:input tw:input-bordered tw:w-full tw:max-w-xs' /> setPassword(e.target.value)} + onChange={(e) => { + setPassword(e.target.value) + }} className='tw:input tw:input-bordered tw:w-full tw:max-w-xs' />
diff --git a/lib/src/Components/Auth/useAuth.tsx b/lib/src/Components/Auth/useAuth.tsx index 413e88f5..fe332d80 100644 --- a/lib/src/Components/Auth/useAuth.tsx +++ b/lib/src/Components/Auth/useAuth.tsx @@ -67,7 +67,7 @@ export const AuthProvider = ({ userApi, children }: AuthProviderProps) => { return undefined } // eslint-disable-next-line no-catch-all/no-catch-all - } catch (error) { + } catch { setLoading(false) return undefined } finally { @@ -135,7 +135,8 @@ export const AuthProvider = ({ userApi, children }: AuthProviderProps) => { setLoading(true) try { await userApi.requestPasswordReset(email, resetUrl) - return setLoading(false) + setLoading(false) + return } catch (error) { setLoading(false) throw error @@ -146,7 +147,8 @@ export const AuthProvider = ({ userApi, children }: AuthProviderProps) => { setLoading(true) try { await userApi.passwordReset(token, newPassword) - return setLoading(false) + setLoading(false) + return } catch (error) { setLoading(false) throw error diff --git a/lib/src/Components/Gaming/Quests.tsx b/lib/src/Components/Gaming/Quests.tsx index 944a2633..35d512b6 100644 --- a/lib/src/Components/Gaming/Quests.tsx +++ b/lib/src/Components/Gaming/Quests.tsx @@ -44,7 +44,9 @@ export function Quests() {
diff --git a/lib/src/Components/Gaming/hooks/useQuests.tsx b/lib/src/Components/Gaming/hooks/useQuests.tsx index 1b3cc1d4..6539a008 100644 --- a/lib/src/Components/Gaming/hooks/useQuests.tsx +++ b/lib/src/Components/Gaming/hooks/useQuests.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-empty-function */ import { useCallback, useState, createContext, useContext } from 'react' type UseQuestManagerResult = ReturnType diff --git a/lib/src/Components/Input/Autocomplete.tsx b/lib/src/Components/Input/Autocomplete.tsx index 2bb97d2c..ae9be9c7 100644 --- a/lib/src/Components/Input/Autocomplete.tsx +++ b/lib/src/Components/Input/Autocomplete.tsx @@ -85,7 +85,9 @@ export const Autocomplete = ({ ref={inputRef} {...inputProps} type='text' - onChange={(e) => handleChange(e)} + onChange={(e) => { + handleChange(e) + }} tabIndex='-1' onKeyDown={handleKeyDown} className='tw:border-none tw:focus:outline-none tw:focus:ring-0 tw:mt-5' @@ -94,7 +96,12 @@ export const Autocomplete = ({ className={`tw:absolute tw:z-4000 ${filteredSuggestions.length > 0 && 'tw:bg-base-100 tw:rounded-xl tw:p-2'}`} > {filteredSuggestions.map((suggestion, index) => ( -
  • handleSuggestionClick(suggestion)}> +
  • { + handleSuggestionClick(suggestion) + }} + >
  • ))} diff --git a/lib/src/Components/Item/PopupView.tsx b/lib/src/Components/Item/PopupView.tsx index 98640f38..c73ac6a4 100644 --- a/lib/src/Components/Item/PopupView.tsx +++ b/lib/src/Components/Item/PopupView.tsx @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ import { useContext, useMemo, useState } from 'react' import { Marker, Tooltip } from 'react-leaflet' diff --git a/lib/src/Components/Map/Subcomponents/AddButton.tsx b/lib/src/Components/Map/Subcomponents/AddButton.tsx index ab0e744b..c1944827 100644 --- a/lib/src/Components/Map/Subcomponents/AddButton.tsx +++ b/lib/src/Components/Map/Subcomponents/AddButton.tsx @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ import { MapPinIcon } from '@heroicons/react/24/solid' import { useState } from 'react' import SVG from 'react-inlinesvg' @@ -89,7 +90,9 @@ export default function AddButton({ tabIndex={0} className='tw:z-500 tw:border-0 tw:p-0 tw:w-10 tw:h-10 tw:cursor-pointer tw:rounded-full tw:mouse tw:drop-shadow-md tw:transition tw:ease-in tw:duration-200 tw:focus:outline-hidden tw:flex tw:items-center tw:justify-center' style={{ backgroundColor: layer.menuColor || '#777' }} - onClick={() => handleLayerClick(layer)} + onClick={() => { + handleLayerClick(layer) + }} onTouchEnd={(e) => { handleLayerClick(layer) e.preventDefault() diff --git a/lib/src/Components/Map/Subcomponents/Controls/FilterControl.tsx b/lib/src/Components/Map/Subcomponents/Controls/FilterControl.tsx index e5c6c340..03a6f8ce 100644 --- a/lib/src/Components/Map/Subcomponents/Controls/FilterControl.tsx +++ b/lib/src/Components/Map/Subcomponents/Controls/FilterControl.tsx @@ -1,3 +1,4 @@ +/* eslint-disable array-callback-return */ import FunnelIcon from '@heroicons/react/24/outline/FunnelIcon' import { useEffect, useState } from 'react' @@ -18,7 +19,9 @@ export function FilterControl() { ] useEffect(() => { - groupTypes.map((layer) => addVisibleGroupType(layer.value)) + groupTypes.map((layer) => { + addVisibleGroupType(layer.value) + }) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -48,7 +51,9 @@ export function FilterControl() { > toggleVisibleGroupType(groupType.value)} + onChange={() => { + toggleVisibleGroupType(groupType.value) + }} type='checkbox' className='tw:checkbox tw:checkbox-xs tw:checkbox-success' checked={isGroupTypeVisible(groupType.value)} diff --git a/lib/src/Components/Map/Subcomponents/Controls/LayerControl.tsx b/lib/src/Components/Map/Subcomponents/Controls/LayerControl.tsx index 83a1a29d..6e40bd07 100644 --- a/lib/src/Components/Map/Subcomponents/Controls/LayerControl.tsx +++ b/lib/src/Components/Map/Subcomponents/Controls/LayerControl.tsx @@ -36,7 +36,9 @@ export function LayerControl({ expandLayerControl = false }: { expandLayerContro > toggleVisibleLayer(layer)} + onChange={() => { + toggleVisibleLayer(layer) + }} type='checkbox' className='tw:checkbox tw:checkbox-xs tw:checkbox-success tw:text-white' checked={isLayerVisible(layer)} diff --git a/lib/src/Components/Map/Subcomponents/Controls/LocateControl.spec.tsx b/lib/src/Components/Map/Subcomponents/Controls/LocateControl.spec.tsx index 8bfbfd5c..909a0238 100644 --- a/lib/src/Components/Map/Subcomponents/Controls/LocateControl.spec.tsx +++ b/lib/src/Components/Map/Subcomponents/Controls/LocateControl.spec.tsx @@ -1,3 +1,4 @@ +/* eslint-disable camelcase */ // Directus database fields use snake_case /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-call */ diff --git a/lib/src/Components/Map/Subcomponents/Controls/LocateControl.tsx b/lib/src/Components/Map/Subcomponents/Controls/LocateControl.tsx index 536ee3e5..f0907f81 100644 --- a/lib/src/Components/Map/Subcomponents/Controls/LocateControl.tsx +++ b/lib/src/Components/Map/Subcomponents/Controls/LocateControl.tsx @@ -1,3 +1,5 @@ +/* eslint-disable camelcase */ // Directus database fields use snake_case +/* eslint-disable promise/always-return */ import { control } from 'leaflet' import { useCallback, useEffect, useRef, useState } from 'react' import SVG from 'react-inlinesvg' @@ -15,7 +17,7 @@ import DialogModal from '#components/Templates/DialogModal' import type { Item } from '#types/Item' import type { LatLng } from 'leaflet' -// eslint-disable-next-line import/no-unassigned-import +// eslint-disable-next-line import-x/no-unassigned-import import 'leaflet.locatecontrol' // Type definitions for leaflet.locatecontrol @@ -31,7 +33,7 @@ declare module 'leaflet' { * React wrapper for leaflet.locatecontrol that provides user geolocation functionality * @category Map Controls */ -export const LocateControl = (): JSX.Element => { +export const LocateControl = (): React.JSX.Element => { const map = useMap() const myProfile = useMyProfile() const updateItem = useUpdateItem() @@ -206,7 +208,9 @@ export const LocateControl = (): JSX.Element => { // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access if (lc) lc.stop() // Reset flag after a delay to allow future updates - setTimeout(() => setHasUpdatedPosition(false), 5000) + setTimeout(() => { + setHasUpdatedPosition(false) + }, 5000) } catch (error: unknown) { if (error instanceof Error) { toast.update(toastId, { @@ -278,7 +282,9 @@ export const LocateControl = (): JSX.Element => {