Merge branch 'nikolaik/migrate-ts' into 'main'

Migrate to typescript 💥

See merge request edb/cnapp!3
This commit is contained in:
nikolark committed 2024-07-07 10:44:02 +00:00
commit ce43893f9d
36 files changed
+392 -131

No files matched your search

+21 -4
View File
@@ -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'],
},
],
};
+17
View File
@@ -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
View File
File renamed without changes.
+2 -2
View File
@@ -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);
+166 -16
View File
@@ -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"
+9 -3
View File
@@ -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
}
+1 -1
View File
@@ -31,7 +31,7 @@ export default function App() {
{/* FIXME: Read urlScheme and merchantIdentifier from .Config */}
<StripeProvider
publishableKey={process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY || ''}
urlScheme={Constants.expoConfig.scheme}
urlScheme={Constants.expoConfig.scheme as string}
merchantIdentifier="merchant.no.neuf.chateau" // https://developer.apple.com/account/resources/identifiers/list/merchant
>
<Dusken />
View File
File renamed without changes.
+5 -4
View File
@@ -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<EventStackParamList>();
const EventStackScreen = () => (
<EventStack.Navigator initialRouteName="EventList" screenOptions={{ ...defaultNavigationStyle }}>
<EventStack.Screen
@@ -41,7 +42,7 @@ const EventStackScreen = () => (
</EventStack.Navigator>
);
const UserStack = createNativeStackNavigator();
const UserStack = createNativeStackNavigator<UserStackParamList>();
const UserStackScreen = () => (
<UserStack.Navigator initialRouteName="Membership" screenOptions={{ ...defaultNavigationStyle }}>
<UserStack.Screen name="Membership" component={Membership} options={{ title: 'Medlemskap' }} />
@@ -56,7 +57,7 @@ const UserStackScreen = () => (
</UserStack.Navigator>
);
const HomeTabs = createBottomTabNavigator();
const HomeTabs = createBottomTabNavigator<HomeTabParamList>();
const EventIcon = ({ color, size }) => <Icon name="event" size={size} color={color} style={{ marginTop: 5 }} />;
const UserIcon = ({ color, size }) => <Icon name="user" size={size} color={color} style={{ marginTop: 5 }} />;
const AboutIcon = ({ color, size }) => <Icon name="info" size={size} color={color} style={{ marginTop: 5 }} />;
@@ -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(<App />);
View File
File renamed without changes.
+2 -1
View File
@@ -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());
File renamed without changes.
@@ -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 (
<Controller
control={control}
rules={rules}
render={({ field: { onChange, onBlur, value, ref } }) => (
<FormControl isInvalid={Boolean(errors[name])}>
<FormControl isInvalid={Boolean(fieldError)}>
<FormControl.Label>{label}</FormControl.Label>
<Input
ref={ref}
@@ -28,9 +39,9 @@ const FormInput = ({
value={value}
{...rest}
/>
{errors[name] && (
{fieldError && (
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
<Text>{errors[name].message}</Text>
<Text>{fieldError.message.toString()}</Text>
</FormControl.ErrorMessage>
)}
</FormControl>
+9
View File
@@ -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<RootState> = useSelector;
export const useAppStore: () => AppStore = useStore;
+1
View File
@@ -0,0 +1 @@
declare module '*.png';
@@ -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';
@@ -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 (
<ScrollView>
@@ -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 = () => {
<Text style={styles.listItemTitle} numberOfLines={1}>
{item.title.decoded}
</Text>
<Text style={styles.listItemTime}>{_formatTime(item.start_time)}</Text>
<Text style={styles.listItemTime}>{formatTime(item.start_time)}</Text>
</View>
<View style={styles.arrow}>
<ArrowForwardIcon size={4} color={theme.colors.neufBlack} marginTop={3} />
@@ -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]}
@@ -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,
@@ -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 (
<ScrollView>
@@ -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}
@@ -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<LoginFormValues>();
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;
@@ -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 } =
@@ -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 }) => (
<ScrollView>
@@ -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;
@@ -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]);
@@ -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);
@@ -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 = () => {
<>
<View style={styles.card}>
<ScrollView>
<FormInput
name="firstName"
label="Fornavn"
control={control}
errors={errors}
autoFocus={true}
onSubmitEditing={() => setFocus('lastName')}
/>
<FormInput
name="lastName"
label="Etternavn"
control={control}
errors={errors}
onSubmitEditing={() => setFocus('email')}
/>
<FormInput name="firstName" label="Fornavn" autoFocus onSubmitEditing={() => setFocus('lastName')} />
<FormInput name="lastName" label="Etternavn" onSubmitEditing={() => setFocus('email')} />
<FormInput
name="email"
label="E-post"
control={control}
errors={errors}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
@@ -53,17 +38,13 @@ const UserRegister = () => {
<FormInput
name="phoneNumber"
label="Mobilnummer"
control={control}
errors={errors}
keyboardType="phone-pad"
onSubmitEditing={() => setFocus('password')}
/>
<FormInput
name="password"
label="Passord"
control={control}
errors={errors}
secureTextEntry={true}
secureTextEntry
autoCapitalize="none"
autoCorrect={false}
returnKeyType="send"
@@ -1,28 +1,34 @@
import { useNavigation } from '@react-navigation/native';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useSelector, useDispatch } from 'react-redux';
import { requestRegisterUser } from '../../api';
import { usePrevious } from '../../hooks';
import { requestRegisterUser } from 'src/api';
import { useAppDispatch, useAppSelector, usePrevious } from 'src/hooks';
export interface UserRegisterFormValues {
firstName: string;
lastName: string;
email: string;
phoneNumber: string;
password: string;
}
const useUserRegister = () => {
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<UserRegisterFormValues>();
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;
+5 -4
View File
@@ -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 */
+4
View File
@@ -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<AppStore['getState']>;
export type AppDispatch = AppStore['dispatch'];
View File
File renamed without changes.
+13
View File
@@ -0,0 +1,13 @@
export interface EventType {
start_time: string;
content: {
rendered: string;
};
venue: string;
title: {
decoded: string;
};
thumbnail: {
medium_large: string;
};
}
+30
View File
@@ -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<EventStackParamList, 'EventDetail'>;
export type UserStackParamList = {
Membership: undefined;
Login: undefined;
UserRegister: undefined;
};
export type HomeTabParamList = {
About: undefined;
EventStack: NavigatorScreenParams<EventStackParamList>;
UserStack: NavigatorScreenParams<UserStackParamList>;
};
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace ReactNavigation {
interface RootParamList extends HomeTabParamList {}
}
}
+2 -2
View File
@@ -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<Response>((_, reject) => {
setTimeout(() => {
reject(new Error('Fetch request timed out'));
}, timeout);
+6
View File
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"baseUrl": "./"
},
"extends": "expo/tsconfig.base"
}