From bb75b70021eb4e67c27f775fc922e07643925a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolai=20R=C3=B8ed=20Kristiansen?= Date: Sat, 6 Jul 2024 14:08:18 +0200 Subject: [PATCH] =?UTF-8?q?Migrate=20to=20typescript=20=F0=9F=92=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 25 ++- .gitlab-ci.yml | 17 ++ App.js => App.ts | 0 index.js => index.ts | 4 +- package-lock.json | 182 ++++++++++++++++-- package.json | 12 +- src/{App.js => App.tsx} | 2 +- src/{Dusken.js => Dusken.tsx} | 0 src/{Navigation.js => Navigation.tsx} | 9 +- src/__tests__/{app.js => app.tsx} | 2 +- src/{actions.js => actions.ts} | 0 src/{api.js => api.ts} | 3 +- .../{FormErrors.js => FormErrors.tsx} | 0 .../{FormInput.js => FormInput.tsx} | 27 ++- src/{hooks.js => hooks.tsx} | 9 + src/index.d.ts | 1 + src/modules/about/{About.js => About.tsx} | 3 +- .../{EventDetail.js => EventDetail.tsx} | 5 +- .../events/{EventList.js => EventList.tsx} | 9 +- .../{useEventList.js => useEventList.tsx} | 9 +- src/modules/login/{Login.js => Login.tsx} | 13 +- .../login/{useLogin.js => useLogin.tsx} | 22 ++- .../{Membership.js => Membership.tsx} | 3 +- .../{MembershipInfo.js => MembershipInfo.tsx} | 2 +- .../membership/{Proof.js => Proof.tsx} | 3 +- .../{useMembership.js => useMembership.tsx} | 25 ++- .../membership/{useProof.js => useProof.tsx} | 7 +- .../{UserRegister.js => UserRegister.tsx} | 35 +--- ...useUserRegister.js => useUserRegister.tsx} | 28 +-- src/{reducers.js => reducers.ts} | 9 +- src/{reduxStore.js => reduxStore.ts} | 4 + src/{theme.js => theme.ts} | 0 src/types/data.ts | 13 ++ src/types/navigation.ts | 30 +++ src/{utils.js => utils.ts} | 4 +- tsconfig.json | 6 + 36 files changed, 392 insertions(+), 131 deletions(-) create mode 100644 .gitlab-ci.yml rename App.js => App.ts (100%) rename index.js => index.ts (69%) rename src/{App.js => App.tsx} (96%) rename src/{Dusken.js => Dusken.tsx} (100%) rename src/{Navigation.js => Navigation.tsx} (90%) rename src/__tests__/{app.js => app.tsx} (88%) rename src/{actions.js => actions.ts} (100%) rename src/{api.js => api.ts} (97%) rename src/components/{FormErrors.js => FormErrors.tsx} (100%) rename src/components/{FormInput.js => FormInput.tsx} (55%) rename src/{hooks.js => hooks.tsx} (72%) create mode 100644 src/index.d.ts rename src/modules/about/{About.js => About.tsx} (99%) rename src/modules/events/{EventDetail.js => EventDetail.tsx} (98%) rename src/modules/events/{EventList.js => EventList.tsx} (94%) rename src/modules/events/{useEventList.js => useEventList.tsx} (92%) rename src/modules/login/{Login.js => Login.tsx} (83%) rename src/modules/login/{useLogin.js => useLogin.tsx} (54%) rename src/modules/membership/{Membership.js => Membership.tsx} (97%) rename src/modules/membership/{MembershipInfo.js => MembershipInfo.tsx} (98%) rename src/modules/membership/{Proof.js => Proof.tsx} (99%) rename src/modules/membership/{useMembership.js => useMembership.tsx} (53%) rename src/modules/membership/{useProof.js => useProof.tsx} (91%) rename src/modules/register/{UserRegister.js => UserRegister.tsx} (71%) rename src/modules/register/{useUserRegister.js => useUserRegister.tsx} (57%) rename src/{reducers.js => reducers.ts} (92%) rename src/{reduxStore.js => reduxStore.ts} (81%) rename src/{theme.js => theme.ts} (100%) create mode 100644 src/types/data.ts create mode 100644 src/types/navigation.ts rename src/{utils.js => utils.ts} (91%) create mode 100644 tsconfig.json diff --git a/.eslintrc.js b/.eslintrc.js index 2b0bf29..86ff09b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,10 +3,13 @@ module.exports = { extends: [ '@react-native', 'airbnb-base', + 'airbnb-typescript/base', 'plugin:prettier/recommended', 'plugin:react/recommended', 'plugin:import/errors', 'plugin:import/warnings', + 'plugin:import/typescript', + 'plugin:@typescript-eslint/recommended', ], rules: { 'class-methods-use-this': 'error', // FIXME: refactor @@ -16,11 +19,11 @@ module.exports = { 'no-underscore-dangle': 'off', 'no-param-reassign': 'off', 'no-unused-vars': 'off', - 'no-use-before-define': ['error', { functions: true, classes: true, variables: false }], // disable the rule for variables, but enable it for functions and classes + '@typescript-eslint/no-use-before-define': ['error', { functions: true, classes: true, variables: false }], // disable the rule for variables, but enable it for functions and classes 'react/prefer-stateless-function': 1, - 'react/prop-types': 'off', // FIXME: typescript - 'react/jsx-filename-extension': ['error', { extensions: ['.js', '.jsx'] }], + 'react/prop-types': 'off', + 'react/jsx-filename-extension': ['error', { extensions: ['.js', '.jsx', '.ts', '.tsx'] }], 'react/sort-comp': 'off', 'react-native/no-inline-styles': 'off', // FIXME: refactor 'import/no-extraneous-dependencies': [ @@ -41,10 +44,24 @@ module.exports = { 'unused-imports/no-unused-imports': 'error', }, plugins: ['prettier', 'unused-imports'], - parser: '@babel/eslint-parser', + parser: '@typescript-eslint/parser', + parserOptions: { + project: ['./tsconfig.json'], + }, settings: { react: { version: 'detect', }, + 'import/resolver': { + typescript: { + project: ['./tsconfig.json'], + }, + }, }, + overrides: [ + { + extends: ['plugin:@typescript-eslint/disable-type-checked'], + files: ['./**/*.js'], + }, + ], }; diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..01c1261 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,17 @@ +default: + image: node:20-slim + cache: + paths: + - node_modules + +check_types: + stage: test + script: + - npm i + - npm run check-types + +lint: + stage: test + script: + - npm i + - npm run lint diff --git a/App.js b/App.ts similarity index 100% rename from App.js rename to App.ts diff --git a/index.js b/index.ts similarity index 69% rename from index.js rename to index.ts index d6c810f..253f7a9 100644 --- a/index.js +++ b/index.ts @@ -2,11 +2,11 @@ import { AppRegistry } from 'react-native'; // import * as Sentry from '@sentry/react-native'; -import { name as appName } from './app.json'; +import { expo as expoConfig } from './app.json'; import App from './src/App'; // if (Config.SENTRY_DSN) { // Sentry.init({ dsn: Config.SENTRY_DSN }); // } -AppRegistry.registerComponent(appName, () => App); +AppRegistry.registerComponent(expoConfig.name, () => App); diff --git a/package-lock.json b/package-lock.json index b2557b5..32e757a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "moment": "^2.29.4", "native-base": "^3.4.28", "react": "18.2.0", - "react-hook-form": "^7.45.2", + "react-hook-form": "^7.52.1", "react-native": "0.74.3", "react-native-confetti": "^0.1.0", "react-native-gesture-handler": "~2.16.1", @@ -48,16 +48,21 @@ "@babel/eslint-parser": "^7.24.7", "@react-native/eslint-config": "^0.74.85", "@testing-library/react-native": "^12.3.0", + "@types/jest": "^29.5.12", + "@types/react": "18.2", "eslint": "^8.57.0", "eslint-config-airbnb": "^19.0.4", + "eslint-config-airbnb-typescript": "^18.0.0", "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.9.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-react": "^7.34.3", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-unused-imports": "^3.2.0", - "prettier": "^3.3.2" + "prettier": "^3.3.2", + "typescript": "^5.5.3" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -3835,6 +3840,18 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jest/fake-timers": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", @@ -7981,6 +7998,48 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -8020,12 +8079,11 @@ } }, "node_modules/@types/react": { - "version": "18.2.18", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.18.tgz", - "integrity": "sha512-da4NTSeBv/P34xoZPhtcLkmZuJ+oYaCxHmyHzwaDQo9RQPBeXV+06gEk2FpqEcsX9XrnNLvRpVh6bdavDSjtiQ==", + "version": "18.2.79", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.79.tgz", + "integrity": "sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w==", "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, @@ -8039,11 +8097,6 @@ "@types/react": "*" } }, - "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" - }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -10427,6 +10480,19 @@ "once": "^1.4.0" } }, + "node_modules/enhanced-resolve": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", + "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -10790,6 +10856,20 @@ "eslint-plugin-import": "^2.25.2" } }, + "node_modules/eslint-config-airbnb-typescript": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-18.0.0.tgz", + "integrity": "sha512-oc+Lxzgzsu8FQyFVa4QFaVKiitTYiiW3frB9KYW5OWdPrqFc7FzxgB20hP4cHMlr+MBzGcLl3jnCOVOydL9mIg==", + "dev": true, + "dependencies": { + "eslint-config-airbnb-base": "^15.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + } + }, "node_modules/eslint-config-prettier": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", @@ -10839,6 +10919,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz", + "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "enhanced-resolve": "^5.12.0", + "eslint-module-utils": "^2.7.4", + "fast-glob": "^3.3.1", + "get-tsconfig": "^4.5.0", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, "node_modules/eslint-module-utils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", @@ -11680,6 +11785,22 @@ "which": "bin/which" } }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/expo": { "version": "51.0.18", "resolved": "https://registry.npmjs.org/expo/-/expo-51.0.18.tgz", @@ -12425,6 +12546,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.5.tgz", + "integrity": "sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/getenv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/getenv/-/getenv-1.0.0.tgz", @@ -17228,9 +17361,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.45.2", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.45.2.tgz", - "integrity": "sha512-9s45OdTaKN+4NSTbXVqeDITd/nwIg++nxJGL8+OD5uf1DxvhsXQ641kaYHk5K28cpIOTYm71O/fYk7rFaygb3A==", + "version": "7.52.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.1.tgz", + "integrity": "sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==", "engines": { "node": ">=12.22.0" }, @@ -17239,7 +17372,7 @@ "url": "https://opencollective.com/react-hook-form" }, "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18" + "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "node_modules/react-is": { @@ -17977,6 +18110,15 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -18956,6 +19098,15 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -19470,7 +19621,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 2d51369..e69c5c5 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "ios": "expo start --ios", "web": "expo start --web", "lint": "eslint .", - "format": "eslint --fix ." + "format": "eslint --fix .", + "check-types": "tsc" }, "dependencies": { "@apollo/client": "^3.7.17", @@ -30,7 +31,7 @@ "moment": "^2.29.4", "native-base": "^3.4.28", "react": "18.2.0", - "react-hook-form": "^7.45.2", + "react-hook-form": "^7.52.1", "react-native": "0.74.3", "react-native-confetti": "^0.1.0", "react-native-gesture-handler": "~2.16.1", @@ -51,16 +52,21 @@ "@babel/eslint-parser": "^7.24.7", "@react-native/eslint-config": "^0.74.85", "@testing-library/react-native": "^12.3.0", + "@types/jest": "^29.5.12", + "@types/react": "18.2", "eslint": "^8.57.0", "eslint-config-airbnb": "^19.0.4", + "eslint-config-airbnb-typescript": "^18.0.0", "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.9.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-react": "^7.34.3", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-unused-imports": "^3.2.0", - "prettier": "^3.3.2" + "prettier": "^3.3.2", + "typescript": "^5.5.3" }, "private": true } diff --git a/src/App.js b/src/App.tsx similarity index 96% rename from src/App.js rename to src/App.tsx index 688b672..2065aad 100644 --- a/src/App.js +++ b/src/App.tsx @@ -31,7 +31,7 @@ export default function App() { {/* FIXME: Read urlScheme and merchantIdentifier from .Config */} diff --git a/src/Dusken.js b/src/Dusken.tsx similarity index 100% rename from src/Dusken.js rename to src/Dusken.tsx diff --git a/src/Navigation.js b/src/Navigation.tsx similarity index 90% rename from src/Navigation.js rename to src/Navigation.tsx index b960dfa..e9a989d 100644 --- a/src/Navigation.js +++ b/src/Navigation.tsx @@ -11,6 +11,7 @@ import Login from './modules/login/Login'; import Membership from './modules/membership/Membership'; import UserRegister from './modules/register/UserRegister'; import theme from './theme'; +import type { EventStackParamList, HomeTabParamList, UserStackParamList } from './types/navigation'; // This looks ugly, even though it's a recommended style from the branding profile. // TODO: Change this @@ -21,9 +22,9 @@ const defaultNavigationStyle = { headerStyle: { backgroundColor: theme.colors.neufPink, }, -}; +} as const; -const EventStack = createNativeStackNavigator(); +const EventStack = createNativeStackNavigator(); const EventStackScreen = () => ( ( ); -const UserStack = createNativeStackNavigator(); +const UserStack = createNativeStackNavigator(); const UserStackScreen = () => ( @@ -56,7 +57,7 @@ const UserStackScreen = () => ( ); -const HomeTabs = createBottomTabNavigator(); +const HomeTabs = createBottomTabNavigator(); const EventIcon = ({ color, size }) => ; const UserIcon = ({ color, size }) => ; const AboutIcon = ({ color, size }) => ; diff --git a/src/__tests__/app.js b/src/__tests__/app.tsx similarity index 88% rename from src/__tests__/app.js rename to src/__tests__/app.tsx index 7fcf427..698fa88 100644 --- a/src/__tests__/app.js +++ b/src/__tests__/app.tsx @@ -2,7 +2,7 @@ import 'react-native'; import { render } from '@testing-library/react-native'; import React from 'react'; -import App from '../App'; +import App from 'src/App'; it('renders correctly', () => { const { toJSON } = render(); diff --git a/src/actions.js b/src/actions.ts similarity index 100% rename from src/actions.js rename to src/actions.ts diff --git a/src/api.js b/src/api.ts similarity index 97% rename from src/api.js rename to src/api.ts index c565c0f..0d39da6 100644 --- a/src/api.js +++ b/src/api.ts @@ -12,6 +12,7 @@ import { membershipChargeRequest, membershipChargeSuccess, } from './actions'; +import type { UserRegisterFormValues } from './modules/register/useUserRegister'; import { fetchWithTimeout } from './utils'; export function requestLogin(username, password) { @@ -80,7 +81,7 @@ export function requestUserData(authToken) { }; } -export function requestRegisterUser({ firstName, lastName, email, phoneNumber, password }) { +export function requestRegisterUser({ firstName, lastName, email, phoneNumber, password }: UserRegisterFormValues) { return (dispatch) => { dispatch(registerUserRequest()); diff --git a/src/components/FormErrors.js b/src/components/FormErrors.tsx similarity index 100% rename from src/components/FormErrors.js rename to src/components/FormErrors.tsx diff --git a/src/components/FormInput.js b/src/components/FormInput.tsx similarity index 55% rename from src/components/FormInput.js rename to src/components/FormInput.tsx index 5298ed9..8baa9db 100644 --- a/src/components/FormInput.js +++ b/src/components/FormInput.tsx @@ -1,10 +1,16 @@ -import { Input, Text, WarningOutlineIcon, FormControl } from 'native-base'; +import { Input, Text, WarningOutlineIcon, FormControl, IInputProps } from 'native-base'; import React from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, UseControllerProps, useForm } from 'react-hook-form'; +import { ReturnKeyTypeOptions } from 'react-native'; + +type FormInputProps = { + name: string; + label: string; + rules?: UseControllerProps['rules']; + returnKeyType?: ReturnKeyTypeOptions; +} & IInputProps; const FormInput = ({ - control, - errors, name, label, rules = { @@ -12,13 +18,18 @@ const FormInput = ({ }, returnKeyType = 'next', ...rest -}) => { +}: FormInputProps) => { + const { + formState: { errors }, + control, + } = useForm(); + const fieldError = errors[name]; return ( ( - + {label} - {errors[name] && ( + {fieldError && ( }> - {errors[name].message} + {fieldError.message.toString()} )} diff --git a/src/hooks.js b/src/hooks.tsx similarity index 72% rename from src/hooks.js rename to src/hooks.tsx index 70f171f..664f749 100644 --- a/src/hooks.js +++ b/src/hooks.tsx @@ -1,6 +1,10 @@ import { useStripe } from '@stripe/stripe-react-native'; import { useCallback, useRef, useEffect } from 'react'; import { Linking } from 'react-native'; +import type { TypedUseSelectorHook } from 'react-redux'; +import { useDispatch, useSelector, useStore } from 'react-redux'; + +import type { AppDispatch, AppStore, RootState } from './reduxStore'; export function usePrevious(value) { const ref = useRef(); @@ -46,3 +50,8 @@ export const useDeepLinking = () => { return () => deepLinkListener.remove(); }, [handleDeepLink]); }; + +// Use throughout your app instead of plain `useDispatch` and `useSelector` +export const useAppDispatch: () => AppDispatch = useDispatch; +export const useAppSelector: TypedUseSelectorHook = useSelector; +export const useAppStore: () => AppStore = useStore; diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 0000000..e2937d4 --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1 @@ +declare module '*.png'; diff --git a/src/modules/about/About.js b/src/modules/about/About.tsx similarity index 99% rename from src/modules/about/About.js rename to src/modules/about/About.tsx index eff5c0c..73f1a4a 100644 --- a/src/modules/about/About.js +++ b/src/modules/about/About.tsx @@ -13,8 +13,9 @@ import { } from 'react-native'; import { Button } from 'react-native-ui-lib'; +import theme from 'src/theme'; + import logo from '../../../assets/DNS-logo-svart.png'; -import theme from '../../theme'; const CHATEAU_NEUF_ADDRESS = 'Det Norske Studentersamfund, Slemdalsveien 15, 0369 Oslo'; diff --git a/src/modules/events/EventDetail.js b/src/modules/events/EventDetail.tsx similarity index 98% rename from src/modules/events/EventDetail.js rename to src/modules/events/EventDetail.tsx index eb1b755..cf78ba9 100644 --- a/src/modules/events/EventDetail.js +++ b/src/modules/events/EventDetail.tsx @@ -14,7 +14,8 @@ import { } from 'react-native'; import RenderHtml from 'react-native-render-html'; -import theme from '../../theme'; +import theme from 'src/theme'; +import type { EventDetailProps } from 'src/types/navigation'; moment.locale('nb'); @@ -157,7 +158,7 @@ const EventDetail = ({ route: { params: { event }, }, -}) => { +}: EventDetailProps) => { const { width } = useWindowDimensions(); return ( diff --git a/src/modules/events/EventList.js b/src/modules/events/EventList.tsx similarity index 94% rename from src/modules/events/EventList.js rename to src/modules/events/EventList.tsx index d8c8aeb..9a25af9 100644 --- a/src/modules/events/EventList.js +++ b/src/modules/events/EventList.tsx @@ -5,12 +5,13 @@ import { StyleSheet, View, Platform, SectionList, TouchableOpacity } from 'react import { Button } from 'react-native-ui-lib'; import 'moment/locale/nb'; +import theme from 'src/theme'; + import useEventList from './useEventList'; -import theme from '../../theme'; moment.locale('nb'); -const _formatTime = (time) => { +const formatTime = (time: string) => { return moment(time).format('llll'); }; @@ -36,7 +37,7 @@ const EventList = () => { {item.title.decoded} - {_formatTime(item.start_time)} + {formatTime(item.start_time)} @@ -80,7 +81,7 @@ const EventList = () => { refreshing={refreshing} onRefresh={handleRefresh} onEndReached={handleLoadMore} - onEndReachedThreshould={10} + onEndReachedThreshold={10} initialNumToRender={10} ListFooterComponent={Footer} style={[styles.card, styles.list]} diff --git a/src/modules/events/useEventList.js b/src/modules/events/useEventList.tsx similarity index 92% rename from src/modules/events/useEventList.js rename to src/modules/events/useEventList.tsx index 727512f..fb68ea6 100644 --- a/src/modules/events/useEventList.js +++ b/src/modules/events/useEventList.tsx @@ -1,9 +1,10 @@ import { useNavigation } from '@react-navigation/native'; import moment from 'moment'; import { useEffect, useState } from 'react'; -import 'moment/locale/nb'; -import { fetchWithTimeout } from '../../utils'; +import 'moment/locale/nb'; +import type { EventType } from 'src/types/data'; +import { fetchWithTimeout } from 'src/utils'; moment.locale('nb'); @@ -81,8 +82,8 @@ const useEventList = () => { }, []); return { - showEvent: (event) => { - navigate('EventDetail', { event }); + showEvent: (event: EventType) => { + navigate('EventStack', { screen: 'EventDetail', params: { event } }); }, handleRefresh, handleLoadMore, diff --git a/src/modules/login/Login.js b/src/modules/login/Login.tsx similarity index 83% rename from src/modules/login/Login.js rename to src/modules/login/Login.tsx index 3db790a..01a0003 100644 --- a/src/modules/login/Login.js +++ b/src/modules/login/Login.tsx @@ -3,13 +3,14 @@ import React from 'react'; import { ActivityIndicator, Linking, StyleSheet, View, Text } from 'react-native'; import { Button } from 'react-native-ui-lib'; +import FormErrors from 'src/components/FormErrors'; +import FormInput from 'src/components/FormInput'; +import theme from 'src/theme'; + import useLogin from './useLogin'; -import FormErrors from '../../components/FormErrors'; -import FormInput from '../../components/FormInput'; -import theme from '../../theme'; const Login = () => { - const { isLoggingIn, errors, control, setFocus, onSubmit } = useLogin(); + const { isLoggingIn, errors, setFocus, onSubmit } = useLogin(); return ( @@ -19,8 +20,6 @@ const Login = () => { name="email" label="E-post / Brukernavn" keyboardType="email-address" - control={control} - errors={errors} autoFocus autoCapitalize="none" autoCorrect={false} @@ -30,8 +29,6 @@ const Login = () => { name="password" label="Passord" secureTextEntry - control={control} - errors={errors} autoCapitalize="none" autoCorrect={false} onSubmitEditing={onSubmit} diff --git a/src/modules/login/useLogin.js b/src/modules/login/useLogin.tsx similarity index 54% rename from src/modules/login/useLogin.js rename to src/modules/login/useLogin.tsx index ea15dc8..eca7c93 100644 --- a/src/modules/login/useLogin.js +++ b/src/modules/login/useLogin.tsx @@ -1,25 +1,29 @@ import { useNavigation } from '@react-navigation/native'; import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; -import { useDispatch, useSelector } from 'react-redux'; -import { requestLogin } from '../../api'; +import { requestLogin } from 'src/api'; +import { useAppDispatch, useAppSelector } from 'src/hooks'; + +interface LoginFormValues { + email: string; + password: string; +} const useLogin = () => { const navigation = useNavigation(); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); - const isAuthenticated = useSelector((store) => store.isAuthenticated); - const isLoggingIn = useSelector((store) => store.isLoggingIn); - const loginError = useSelector((store) => store.loginError); + const isAuthenticated = useAppSelector((store) => store.isAuthenticated); + const isLoggingIn = useAppSelector((store) => store.isLoggingIn); + const loginError = useAppSelector((store) => store.loginError); const { - control, handleSubmit, formState: { errors }, setFocus, - } = useForm(); + } = useForm(); const onSubmit = handleSubmit(({ email, password }) => { dispatch(requestLogin(email, password)); @@ -32,7 +36,7 @@ const useLogin = () => { } }, [isAuthenticated, navigation]); - return { onSubmit, isLoggingIn, control, setFocus, errors: { ...errors, ...loginError } }; + return { onSubmit, isLoggingIn, setFocus, errors: { ...errors, ...loginError } }; }; export default useLogin; diff --git a/src/modules/membership/Membership.js b/src/modules/membership/Membership.tsx similarity index 97% rename from src/modules/membership/Membership.js rename to src/modules/membership/Membership.tsx index f0ab45d..98ff525 100644 --- a/src/modules/membership/Membership.js +++ b/src/modules/membership/Membership.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { View, StyleSheet, ActivityIndicator } from 'react-native'; +import theme from 'src/theme'; + import MembershipInfo from './MembershipInfo'; import Proof from './Proof'; import useMembership from './useMembership'; -import theme from '../../theme'; const Membership = () => { const { isFetchingUserData, onRegisterPress, onLoginPress, onLogoutPress, user, isAuthenticated, fetchUser } = diff --git a/src/modules/membership/MembershipInfo.js b/src/modules/membership/MembershipInfo.tsx similarity index 98% rename from src/modules/membership/MembershipInfo.js rename to src/modules/membership/MembershipInfo.tsx index 8c7f4f0..af54ab6 100644 --- a/src/modules/membership/MembershipInfo.js +++ b/src/modules/membership/MembershipInfo.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { ScrollView, Text, View, StyleSheet } from 'react-native'; import { Button } from 'react-native-ui-lib'; -import theme from '../../theme'; +import theme from 'src/theme'; const MembershipInfo = ({ onRegister, onLogin }) => ( diff --git a/src/modules/membership/Proof.js b/src/modules/membership/Proof.tsx similarity index 99% rename from src/modules/membership/Proof.js rename to src/modules/membership/Proof.tsx index b71b9a2..df5bfb9 100644 --- a/src/modules/membership/Proof.js +++ b/src/modules/membership/Proof.tsx @@ -13,9 +13,10 @@ import { import Confetti from 'react-native-confetti'; import { Button, Incubator } from 'react-native-ui-lib'; +import theme from 'src/theme'; + import useProof from './useProof'; import logo from '../../../assets/DNS-logo-svart.png'; -import theme from '../../theme'; const { Toast } = Incubator; diff --git a/src/modules/membership/useMembership.js b/src/modules/membership/useMembership.tsx similarity index 53% rename from src/modules/membership/useMembership.js rename to src/modules/membership/useMembership.tsx index 9d5c83b..850a6ff 100644 --- a/src/modules/membership/useMembership.js +++ b/src/modules/membership/useMembership.tsx @@ -1,28 +1,27 @@ import { useNavigation } from '@react-navigation/native'; import { useEffect, useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { logout } from '../../actions'; -import { requestUserData } from '../../api'; -import { usePrevious } from '../../hooks'; +import { logout } from 'src/actions'; +import { requestUserData } from 'src/api'; +import { useAppDispatch, useAppSelector, usePrevious } from 'src/hooks'; const useMembership = () => { const navigation = useNavigation(); - const dispatch = useDispatch(); - const isAuthenticated = useSelector((store) => store.isAuthenticated); - const isFetchingUserData = useSelector((store) => store.isFetchingUserData); - const lastOrder = useSelector((store) => store.lastOrder); - const user = useSelector((store) => store.user); - const userToken = useSelector((store) => store.userToken); + const dispatch = useAppDispatch(); + const isAuthenticated = useAppSelector((store) => store.isAuthenticated); + const isFetchingUserData = useAppSelector((store) => store.isFetchingUserData); + const lastOrder = useAppSelector((store) => store.lastOrder); + const user = useAppSelector((store) => store.user); + const userToken = useAppSelector((store) => store.userToken); const prevUserToken = usePrevious(userToken); const prevLastOrder = usePrevious(lastOrder); const onLogoutPress = () => { dispatch(logout()); - navigation.navigate('Membership'); + navigation.navigate('UserStack', { screen: 'Membership' }); }; - const onRegisterPress = () => navigation.navigate('UserRegister'); - const onLoginPress = () => navigation.navigate('Login'); + const onRegisterPress = () => navigation.navigate('UserStack', { screen: 'UserRegister' }); + const onLoginPress = () => navigation.navigate('UserStack', { screen: 'Login' }); const fetchUser = useCallback(() => { dispatch(requestUserData(userToken)); }, [dispatch, userToken]); diff --git a/src/modules/membership/useProof.js b/src/modules/membership/useProof.tsx similarity index 91% rename from src/modules/membership/useProof.js rename to src/modules/membership/useProof.tsx index c6de667..d3376d0 100644 --- a/src/modules/membership/useProof.js +++ b/src/modules/membership/useProof.tsx @@ -4,7 +4,8 @@ import Constants from 'expo-constants'; import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; -import { requestStripePaymentSheet } from '../../api'; +import { requestStripePaymentSheet } from 'src/api'; +import { RootState } from 'src/reduxStore'; const PROOF_QUERY = gql` { @@ -24,8 +25,8 @@ function getPrice(membershipTypes) { } const useProof = ({ user, fetchUser }) => { - const userToken = useSelector((state) => state.userToken); - const chargeError = useSelector((state) => state.chargeError); + const userToken = useSelector((state: RootState) => state.userToken); + const chargeError = useSelector((state: RootState) => state.chargeError); const { data, loading } = useQuery(PROOF_QUERY); const { initPaymentSheet, presentPaymentSheet } = useStripe(); const [paymentLoading, setPaymentLoading] = useState(false); diff --git a/src/modules/register/UserRegister.js b/src/modules/register/UserRegister.tsx similarity index 71% rename from src/modules/register/UserRegister.js rename to src/modules/register/UserRegister.tsx index 88ae328..bb28850 100644 --- a/src/modules/register/UserRegister.js +++ b/src/modules/register/UserRegister.tsx @@ -3,16 +3,16 @@ import React from 'react'; import { ActivityIndicator, StyleSheet, View, Text, ScrollView } from 'react-native'; import { Button, Incubator } from 'react-native-ui-lib'; +import FormErrors from 'src/components/FormErrors'; +import FormInput from 'src/components/FormInput'; +import theme from 'src/theme'; + import useUserRegister from './useUserRegister'; -import FormErrors from '../../components/FormErrors'; -import FormInput from '../../components/FormInput'; -import theme from '../../theme'; const { Toast } = Incubator; const UserRegister = () => { - const { isRegisteringUser, onSubmit, control, isValid, errors, registerError, setFocus, toast, setToast } = - useUserRegister(); + const { isRegisteringUser, onSubmit, isValid, errors, registerError, setFocus, toast, setToast } = useUserRegister(); const isDisabled = isRegisteringUser || !isValid; // TODO: validate on type // TODO: highlight errorfields @@ -21,26 +21,11 @@ const UserRegister = () => { <> - setFocus('lastName')} - /> - setFocus('email')} - /> + setFocus('lastName')} /> + setFocus('email')} /> { setFocus('password')} /> { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const navigation = useNavigation(); - const isAuthenticated = useSelector((store) => store.isAuthenticated); - const isRegisteringUser = useSelector((store) => store.isRegisteringUser); - const registerError = useSelector((store) => store.registerError) || {}; + const isAuthenticated = useAppSelector((store) => store.isAuthenticated); + const isRegisteringUser = useAppSelector((store) => store.isRegisteringUser); + const registerError = useAppSelector((store) => store.registerError) || {}; const [toast, setToast] = useState(null); const isAuthenticatedPrev = usePrevious(isAuthenticated); const { - control, handleSubmit, formState: { errors, isValid }, setFocus, setError, - } = useForm(); + } = useForm(); const onSubmit = handleSubmit((values) => { if (!isValid) { setToast({ @@ -38,7 +44,7 @@ const useUserRegister = () => { // Display error messages from API on form fields Object.keys(registerError).forEach((key) => { - setError(key, { type: 'manual', message: registerError[key] }); + setError(key as keyof UserRegisterFormValues, { type: 'manual', message: registerError[key] }); }); }); @@ -48,6 +54,6 @@ const useUserRegister = () => { } }, [isAuthenticatedPrev, isAuthenticated, navigation]); - return { isRegisteringUser, onSubmit, control, isValid, registerError, errors, setFocus, toast, setToast }; + return { isRegisteringUser, onSubmit, isValid, registerError, errors, setFocus, toast, setToast }; }; export default useUserRegister; diff --git a/src/reducers.js b/src/reducers.ts similarity index 92% rename from src/reducers.js rename to src/reducers.ts index c54c5c2..be0f77f 100644 --- a/src/reducers.js +++ b/src/reducers.ts @@ -15,6 +15,7 @@ import { } from './actions'; import { snakeToCamelCase } from './utils'; +// FIXME: type this const initialState = { isAuthenticated: false, isLoggingIn: false, @@ -31,14 +32,14 @@ const initialState = { }; function formatErrors(errs) { - const _errs = {}; + const errors = {}; Object.entries(errs).forEach(([key, value]) => { - _errs[snakeToCamelCase(key)] = Array.isArray(value) ? value.join('\n') : value; + errors[snakeToCamelCase(key)] = Array.isArray(value) ? value.join('\n') : value; }); - return _errs; + return errors; } -// eslint-disable-next-line default-param-last +// eslint-disable-next-line @typescript-eslint/default-param-last export default function duskenApp(state = initialState, action) { switch (action.type) { /* Login */ diff --git a/src/reduxStore.js b/src/reduxStore.ts similarity index 81% rename from src/reduxStore.js rename to src/reduxStore.ts index 322f904..6cab4dd 100644 --- a/src/reduxStore.js +++ b/src/reduxStore.ts @@ -15,3 +15,7 @@ const persistedReducer = persistReducer(persistConfig, rootReducer); export const store = createStore(persistedReducer, applyMiddleware(...[thunk])); export const persistor = persistStore(store); + +export type AppStore = typeof store; +export type RootState = ReturnType; +export type AppDispatch = AppStore['dispatch']; diff --git a/src/theme.js b/src/theme.ts similarity index 100% rename from src/theme.js rename to src/theme.ts diff --git a/src/types/data.ts b/src/types/data.ts new file mode 100644 index 0000000..c2cb71f --- /dev/null +++ b/src/types/data.ts @@ -0,0 +1,13 @@ +export interface EventType { + start_time: string; + content: { + rendered: string; + }; + venue: string; + title: { + decoded: string; + }; + thumbnail: { + medium_large: string; + }; +} diff --git a/src/types/navigation.ts b/src/types/navigation.ts new file mode 100644 index 0000000..08c0ec4 --- /dev/null +++ b/src/types/navigation.ts @@ -0,0 +1,30 @@ +import { NavigatorScreenParams } from '@react-navigation/native'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; + +import { EventType } from './data'; + +export type EventStackParamList = { + EventList: undefined; + EventDetail: { event: EventType }; +}; + +export type EventDetailProps = NativeStackScreenProps; + +export type UserStackParamList = { + Membership: undefined; + Login: undefined; + UserRegister: undefined; +}; + +export type HomeTabParamList = { + About: undefined; + EventStack: NavigatorScreenParams; + UserStack: NavigatorScreenParams; +}; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace ReactNavigation { + interface RootParamList extends HomeTabParamList {} + } +} diff --git a/src/utils.js b/src/utils.ts similarity index 91% rename from src/utils.js rename to src/utils.ts index f88ce0b..15d8e82 100644 --- a/src/utils.js +++ b/src/utils.ts @@ -4,12 +4,12 @@ export function snakeToCamelCase(val) { return val.replace(/_([a-z])/g, (g) => g[1].toUpperCase()); } -export function fetchWithTimeout(url, options, timeout = 10000) { +export function fetchWithTimeout(url: string, options?: RequestInit, timeout = 10000) { /** Wraps fetch to allow it to time out */ // FIXME: show timeout message in UI return Promise.race([ fetch(url, options), - new Promise((_, reject) => { + new Promise((_, reject) => { setTimeout(() => { reject(new Error('Fetch request timed out')); }, timeout); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..65c69d3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "baseUrl": "./" + }, + "extends": "expo/tsconfig.base" +}