diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..753a9911 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +.eslintrc.js +**/node_modules +build/ +coverage/ diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..9394adf0 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,44 @@ +const pkg = require('./package.json'); + +module.exports = { + extends: [ + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin + 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier + 'plugin:prettier/recommended', + ], + env: { + browser: true, + node: true, + 'jest/globals': true, + }, + plugins: ['jest', 'react', 'prettier'], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features + sourceType: 'module', // Allows for the use of imports + ecmaFeatures: { + jsx: true, // Allows for the parsing of JSX + }, + }, + globals: { + Class: true, + Event: true, + Promise: true, + }, + settings: { + react: { + version: 'detect', + }, + }, + rules: { + '@typescript-eslint/explicit-function-return-type': false, + '@typescript-eslint/no-empty-interface': false, + 'sort-vars': 0, + 'max-lines-per-function': 0, + 'sort-imports': 0, + 'react/no-children-prop': 0, + 'react/prop-types': 0, + }, +}; diff --git a/package.json b/package.json index 1d2655a7..c2950e09 100644 --- a/package.json +++ b/package.json @@ -9,21 +9,19 @@ "howler": "^2.1.1", "inter-ui": "^3.2.0", "lodash": "^4.17.10", - "react": "^16.8.0", - "react-dom": "^16.8.0", + "react": "^16.8.2", + "react-dom": "^16.8.2", "react-intl": "^2.8.0", - "react-redux": "^5.0.7", "react-router": "^4.3.1", "react-router-dom": "^4.3.1", "react-scripts": "2.1.3", "redux": "^4.0.0", - "redux-react-hook": "^3.0.4", - "redux-thunk": "^2.3.0" + "redux-react-hook": "^3.0.4" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", - "lint": "tslint --project tsconfig.json --config tslint.json", + "lint": "eslint --ext .tsx,.ts src/", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, @@ -36,21 +34,24 @@ "@types/react": "16.8.1", "@types/react-dom": "16.0.11", "@types/react-intl": "^2.3.15", - "@types/react-redux": "6.0.6", "@types/react-router": "^4.4.3", "@types/react-router-dom": "^4.2.4", "@types/redux-mock-store": "^1.0.0", + "@typescript-eslint/eslint-plugin": "^1.3.0", + "@typescript-eslint/parser": "^1.3.0", "awesome-typescript-loader": "^5.2.0", - "babel-core": "^6.26.3", "babel-preset-jest": "^23.2.0", + "eslint-config-prettier": "^4.0.0", + "eslint-plugin-jest": "^22.3.0", + "eslint-plugin-prettier": "^3.0.1", + "eslint-plugin-react": "^7.12.4", "husky": "^1.1.2", "lint-staged": "^8.0.4", "prettier": "^1.16.4", - "react-testing-library": "^5.4.4", + "react-testing-library": "^5.8.0", "redux-mock-store": "^1.5.3", "ts-config": "^20.4.0", "ts-jest": "~23.10.4", - "tslint": "^5.12.1", "typescript": "3.3.3" }, "eslintConfig": { @@ -79,8 +80,8 @@ ], "*.{ts,tsx}": [ "prettier --write", - "git add", - "tslint --project tsconfig.json --config tslint.json" + "yarn lint --fix", + "git add" ], "*.css": [ "prettier --write", diff --git a/src/app.tsx b/src/app.tsx index dd6efb9f..5d0d6cbd 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; -import { Provider } from 'react-redux'; import { HashRouter, Redirect, Route, Switch } from 'react-router-dom'; -import { StoreContext } from 'redux-react-hook'; +import { StoreContext, useDispatch } from 'redux-react-hook'; import { Global, @@ -14,18 +13,17 @@ import { } from 'bricks-of-sand'; import { IntlProvider } from 'react-intl'; import { ArticleRouter } from './components/article/article-router'; -import { ConnectedErrorMessage } from './components/common/error-message'; +import { ErrorMessage } from './components/common/error-message'; import { HeaderMenu } from './components/common/header-menu'; -import { ConnectedSettingsLoader } from './components/settings'; import { SplitInvoiceForm } from './components/transaction'; import { MainFooter, baseCss, mobileStyles } from './components/ui'; -import { GlobalLoadingIndicator } from './components/ui/loader'; import { UserRouter } from './components/user/user-router'; import { en } from './locales/en'; import { store } from './store'; // tslint:disable-next-line:no-import-side-effect import 'inter-ui'; +import { startLoadingSettings } from './store/reducers'; const newLight: Theme = { ...light, @@ -54,29 +52,29 @@ const TouchStyles = () => { return null; }; -class Layout extends React.Component { - // tslint:disable-next-line:prefer-function-over-method - public render(): JSX.Element { - return ( - - - - - - - - - - - - - - - - - ); - } -} +const Layout = () => { + const dispatch = useDispatch(); + React.useEffect(() => { + startLoadingSettings(dispatch); + }, []); + + return ( + + + + + + + + + + + + + + + ); +}; class App extends React.Component { // tslint:disable-next-line:prefer-function-over-method @@ -84,17 +82,15 @@ class App extends React.Component { return ( - - - - - - - + + + + + ); diff --git a/src/components/article/__tests__/validator.spec.tsx b/src/components/article/__tests__/validator.spec.tsx index 38846817..cef89ff5 100644 --- a/src/components/article/__tests__/validator.spec.tsx +++ b/src/components/article/__tests__/validator.spec.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { cleanup } from 'react-testing-library'; import { renderWithContext } from '../../../spec-configs/render'; -import { ConnectedArticleValidator } from '../validator'; +import { ArticleValidator } from '../validator'; afterEach(cleanup); @@ -11,7 +11,7 @@ describe('ArticleValidator', () => { it('is valid if user has enough balance', () => { const mockRender = jest.fn(); renderWithContext( - , + , { user: { 1: { @@ -27,11 +27,7 @@ describe('ArticleValidator', () => { it('is invalid if user has not enough balance', () => { const mockRender = jest.fn(); renderWithContext( - , + , { user: { 1: { @@ -47,7 +43,7 @@ describe('ArticleValidator', () => { it('is valid if article is cheaper than boundary', () => { const mockRender = jest.fn(); renderWithContext( - , + , {} ); @@ -57,7 +53,7 @@ describe('ArticleValidator', () => { it('is invalid if article is more expensive then boundary', () => { const mockRender = jest.fn(); renderWithContext( - , + , {} ); expect(mockRender).toHaveBeenCalledWith(false); diff --git a/src/components/article/views/article-edit-form-view.tsx b/src/components/article/article-edit-form-view.tsx similarity index 81% rename from src/components/article/views/article-edit-form-view.tsx rename to src/components/article/article-edit-form-view.tsx index d8fe5502..129a5088 100644 --- a/src/components/article/views/article-edit-form-view.tsx +++ b/src/components/article/article-edit-form-view.tsx @@ -1,7 +1,7 @@ import { Card } from 'bricks-of-sand'; import * as React from 'react'; import { RouteComponentProps } from 'react-router'; -import { ConnectedArticleForm } from '../article-form'; +import { ArticleForm } from './article-form'; export function ArticleEditFormView({ match, @@ -9,7 +9,7 @@ export function ArticleEditFormView({ }: RouteComponentProps<{ id: string }>): JSX.Element { return ( - diff --git a/src/components/article/article-form.tsx b/src/components/article/article-form.tsx index 022c2a66..a70337ef 100644 --- a/src/components/article/article-form.tsx +++ b/src/components/article/article-form.tsx @@ -15,26 +15,20 @@ import { } from 'bricks-of-sand'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; -import { AppState } from '../../store'; -import { - AddArticleParams, - Article, - getArticleById, - startAddArticle, -} from '../../store/reducers'; +import { useDispatch } from 'redux-react-hook'; +import { useToggle } from '../../hooks/use-toggle'; +import { useArticle } from '../../store'; +import { Article, startAddArticle } from '../../store/reducers'; import { Scanner } from '../common/scanner'; import { Currency, CurrencyInput } from '../currency'; -import { ConnectedArticleValidator } from './validator'; - -// tslint:disable-next-line:no-any -const AnyInput: any = Input; +import { useArticleValidator } from './validator'; interface ButtonProps { isVisible: boolean; idArticle?: number; onClick(): void; } + const ToggleArticleButton: React.FC = props => { if (props.isVisible) { return ; @@ -58,6 +52,7 @@ const ArticleFormGrid = styled(Flex)({ div: { width: '100%!important', }, + input: { margin: '0 0 1rem 0', }, @@ -86,202 +81,139 @@ const TextRight = styled('div')({ }, }); -interface OwnProps { +interface Props { articleId?: number; onCreated(): void; } -interface ActionProps { - // tslint:disable-next-line:no-any - addArticle(article: AddArticleParams): any; -} - -interface ReduxStateProps { - article?: Article; -} - -interface State { - params: AddArticleParams; - isVisible: boolean; -} +const initialParams = { + name: '', + barcode: '', + amount: 0, + active: true, + precursor: null, +}; -type Props = OwnProps & ActionProps & ReduxStateProps; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const resetArticle = (article: Article | undefined, setParams: any) => { + if (article) { + setParams({ + amount: article.amount, + barcode: article.barcode, + name: article.name, + precursor: article, + active: article.active, + }); + } else { + setParams(initialParams); + } +}; -export class ArticleForm extends React.Component { - public state = { - isVisible: false, - params: { name: '', barcode: '', amount: 0, active: true, precursor: null }, - }; +export const ArticleForm: React.FC = props => { + const { toggle, updateToggle } = useToggle(false); + const [params, setParams] = React.useState(initialParams); + const isValidArticle = useArticleValidator(params.amount); - public componentDidMount(): void { - if (this.props.article) { - this.updateParams(this.props.article); - this.resetState(); - } - } + const dispatch = useDispatch(); + const article = useArticle(props.articleId); - public resetState = () => { - if (this.props.article) { - this.setState({ - params: { - name: this.props.article.name, - barcode: this.props.article.barcode, - amount: this.props.article.amount, - active: this.props.article.active, - precursor: this.props.article, - }, - }); - } else { - this.setState({ - params: { - name: '', - barcode: '', - amount: 0, - active: true, - precursor: null, - }, - }); - } - }; + React.useEffect(() => { + resetArticle(article, setParams); + }, [article]); - public submit = async (e: React.FormEvent, isValid: boolean) => { + const submit = async (e: React.FormEvent, isValid: boolean) => { e.preventDefault(); if (!isValid) { return; } - const maybeArticle = await this.props.addArticle(this.state.params); - if (maybeArticle) { - this.setState({ isVisible: false }); - this.props.onCreated(); - } - }; - public updateParams = (params: Partial) => { - this.setState(state => ({ - params: { - ...state.params, - ...params, - }, - })); - }; + const maybeArticle = await startAddArticle(dispatch, params); - public toggleIsVisible = () => { - this.setState(state => ({ isVisible: !state.isVisible })); - this.resetState(); + if (maybeArticle) { + updateToggle(); + props.onCreated(); + } }; - public render(): JSX.Element { - return ( - - - - {this.state.isVisible && ( - - - - - - this.updateParams({ name: e.target.value }) - } - type="text" - required - /> - - this.updateParams({ - barcode, - }) - } - /> - - - this.updateParams({ barcode: e.target.value }) - } - type="text" - required - /> - - ( - <> -
this.submit(e, isValid)}> - this.updateParams({ amount })} - /> - - - this.submit(e, isValid) - } - > - - - - )} + return ( + + + + {toggle && ( + + + + + setParams({ ...params, name: e.target.value })} + type="text" + required + /> + + setParams({ + ...params, + barcode, + }) + } + /> + + + setParams({ ...params, barcode: e.target.value }) + } + type="text" + required + /> + +
submit(e, isValidArticle)}> + setParams({ ...params, amount })} /> - - - - )} - {!this.state.isVisible && this.props.articleId && ( - - - {this.state.params.name} - - {this.state.params.barcode} - - - - - - - )} - {!this.state.isVisible && - !this.props.articleId && - this.props.children} - - - ); - } -} - -const mapsStateToProps = (state: AppState, { articleId }: OwnProps) => { - if (!articleId) { - return { article: undefined }; - } - - return { article: getArticleById(state, articleId) }; -}; - -const mapDispatchToProps: ActionProps = { - addArticle: startAddArticle, + + submit(e, isValidArticle)} + > + + +
+
+
+ )} + {!toggle && props.articleId && ( + + + {params.name} + + {params.barcode} + + + + + + + )} + {!toggle && !props.articleId && props.children} +
+
+ ); }; - -export const ConnectedArticleForm = connect( - mapsStateToProps, - mapDispatchToProps -)(ArticleForm); diff --git a/src/components/article/article-list.tsx b/src/components/article/article-list.tsx index ea258789..8b7af7f5 100644 --- a/src/components/article/article-list.tsx +++ b/src/components/article/article-list.tsx @@ -1,75 +1,42 @@ import { Block } from 'bricks-of-sand'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; -import { AppState } from '../../store'; -import { - Article, - getArticleList, - startLoadingArticles, -} from '../../store/reducers'; +import { useDispatch } from 'redux-react-hook'; +import { useArticles } from '../../store'; +import { startLoadingArticles } from '../../store/reducers'; import { NavTabMenus } from '../common/nav-tab-menu'; -import { ConnectedArticleForm } from './article-form'; - -interface OwnProps {} - -interface StateProps { - articles: Article[]; -} - -interface ActionProps { - // tslint:disable-next-line:no-any - loadArticles: any; -} - -type Props = ActionProps & StateProps & OwnProps; - -interface State {} - -export class ArticleList extends React.Component { - public state = {}; - - public componentDidMount(): void { - this.props.loadArticles(); - } - - public render(): JSX.Element { - return ( - - ''}> - } - tabs={[ - { - to: '/articles', - message: , - }, - ]} - /> - - {this.props.articles.map(article => ( - ''} - /> - ))} - - ); - } -} - -const mapStateToProps = (state: AppState): StateProps => ({ - articles: getArticleList(state), -}); - -const mapDispatchToProps = { - loadArticles: startLoadingArticles, +import { ArticleForm } from './article-form'; + +export const ArticleList: React.FC = () => { + const articles = useArticles(); + const dispatch = useDispatch(); + + React.useEffect(() => { + startLoadingArticles(dispatch); + }, []); + + return ( + + ''}> + } + tabs={[ + { + to: '/articles', + message: , + }, + ]} + /> + + {articles.map(article => ( + ''} + /> + ))} + + ); }; - -export const ConnectedArticleList = connect( - mapStateToProps, - mapDispatchToProps -)(ArticleList); diff --git a/src/components/article/article-router.tsx b/src/components/article/article-router.tsx index dfc2b4f7..7b89201b 100644 --- a/src/components/article/article-router.tsx +++ b/src/components/article/article-router.tsx @@ -1,15 +1,15 @@ import * as React from 'react'; -import { Route, RouteComponentProps, Switch } from 'react-router'; -import { ConnectedIdleTimer } from '../common/idle-timer'; -import { ConnectedArticleList } from './article-list'; -import { ArticleEditFormView } from './views/article-edit-form-view'; +import { Route, Switch } from 'react-router'; +import { WrappedIdleTimer } from '../common/idle-timer'; +import { ArticleEditFormView } from './article-edit-form-view'; +import { ArticleList } from './article-list'; -export function ArticleRouter(props: RouteComponentProps): JSX.Element { +export function ArticleRouter(): JSX.Element { return ( <> - props.history.push('/')} /> + - + { + const [message, setMessage] = React.useState(''); + const [article, setArticle] = React.useState
(undefined); + const dispatch = useDispatch(); -const initialState = { - message: '', - article: null, -}; -export class ArticleScanner extends React.Component { - public state = initialState; - - public resetState = () => { - this.setState(initialState); - }; - - public handleChange = async (barcode: string) => { - this.setState({ message: barcode }); + const handleChange = async (barcode: string) => { + setMessage(barcode); try { - const article: Article = await this.props.getArticleByBarcode(barcode); - this.setState({ message: 'ARTICLE_FETCHED_BY_BARCODE', article }); - this.createTransaction(article); + const article = await getArticleByBarcode(dispatch, barcode); + setMessage('ARTICLE_FETCHED_BY_BARCODE'); + setArticle(article); + if (article) { + startCreatingTransaction(dispatch, props.userId, { + articleId: article.id, + }); + } } catch (error) { - this.setState({ message: ':(' }); + setMessage(':('); } }; - - public createTransaction = (article: Article): void => { - this.props.startCreatingTransaction(this.props.userId, { - articleId: article.id, - }); + const resetState = () => { + setMessage(''); + setArticle(undefined); }; - public render(): JSX.Element | null { - return ( - <> - {this.state.message && ( - - - - )} - - - ); - } -} - -const mapDispatchToProps: ActionProps = { - getArticleByBarcode, - startCreatingTransaction, + return ( + <> + {message && ( + + + + )} + + + ); }; -export const ConnectedArticleScanner = connect( - undefined, - mapDispatchToProps -)(ArticleScanner); +interface ToastProps { + message: string; + article: Article | undefined; +} -function ToastContent({ article, message }: State): JSX.Element { - if (article === null) { +function ToastContent({ article, message }: ToastProps): JSX.Element { + if (article === undefined) { return <>{message}; } return ( diff --git a/src/components/article/article-selection-bubbles.tsx b/src/components/article/article-selection-bubbles.tsx index 3c8f41d6..f69546bf 100644 --- a/src/components/article/article-selection-bubbles.tsx +++ b/src/components/article/article-selection-bubbles.tsx @@ -1,14 +1,11 @@ import { CancelButton, Card, Flex, Input, styled } from 'bricks-of-sand'; import * as React from 'react'; -import { connect } from 'react-redux'; -import { AppState } from '../../store'; -import { - Article, - getPopularArticles, - startLoadingArticles, -} from '../../store/reducers'; +import { useDispatch } from 'redux-react-hook'; + +import { usePopularArticles } from '../../store'; +import { Article, startLoadingArticles } from '../../store/reducers'; import { Currency } from '../currency'; -import { ConnectedArticleValidator } from './validator'; +import { ArticleValidator } from './validator'; const InputSection = styled(Flex)({ padding: '0 1rem', @@ -19,96 +16,59 @@ const InputSection = styled(Flex)({ }, }); -interface OwnProps { - userId: number; +interface Props { + userId: string; onSelect(article: Article): void; onCancel(): void; } -interface StateProps { - articles: Article[]; -} - -interface ActionProps { - // tslint:disable-next-line:no-any - loadArticles: any; -} - -type Props = ActionProps & StateProps & OwnProps; - -interface State { - query: string; -} - const ARTICLE_BUBBLE_LIMIT = 10; -export class ArticleSelectionBubbles extends React.Component { - public state = { - query: '', - }; - - public componentDidMount(): void { - this.props.loadArticles(); - } +export const ArticleSelectionBubbles = (props: Props) => { + const items = usePopularArticles(); + const dispatch = useDispatch(); + const [query, setQuery] = React.useState(''); - public render(): JSX.Element { - const items = this.props.articles; - - return ( -
- - this.setState({ query: e.target.value })} - /> - - - - {items - .filter( - item => - !this.state.query || - item.name.toLowerCase().includes(this.state.query.toLowerCase()) - ) - .slice(0, ARTICLE_BUBBLE_LIMIT) - .map(item => ( - ( - { - if (isValid) { - this.props.onSelect(item); - } - }} - padding="0.5rem" - margin="0.3rem" - > - {item.name} | - - )} - /> - ))} - -
- ); - } -} + React.useEffect(() => { + startLoadingArticles(dispatch); + }, []); -const mapStateToProps = (state: AppState): StateProps => ({ - articles: getPopularArticles(state), -}); - -const mapDispatchToProps = { - loadArticles: startLoadingArticles, + return ( +
+ + setQuery(e.target.value)} /> + + + + {items + .filter( + item => + !query || item.name.toLowerCase().includes(query.toLowerCase()) + ) + .slice(0, ARTICLE_BUBBLE_LIMIT) + .map(item => ( + ( + { + if (isValid) { + props.onSelect(item); + } + }} + padding="0.5rem" + margin="0.3rem" + > + {item.name} | + + )} + /> + ))} + +
+ ); }; - -export const ConnectedArticleSelectionBubbles = connect( - mapStateToProps, - mapDispatchToProps -)(ArticleSelectionBubbles); diff --git a/src/components/article/validator.tsx b/src/components/article/validator.tsx index d04367c0..e1893230 100644 --- a/src/components/article/validator.tsx +++ b/src/components/article/validator.tsx @@ -1,51 +1,29 @@ -import * as React from 'react'; -import { connect } from 'react-redux'; -import { AppState } from '../../store'; -import { - Boundary, - getSettingsBalance, - getUserBalance, -} from '../../store/reducers'; - -interface OwnProps { - userId?: number; - value: number; - render(isValid: boolean): JSX.Element; -} - -interface StateProps { - balance: number | boolean; - boundary: Boundary; -} - -interface ActionProps {} - -export type ArticleValidatorProps = ActionProps & StateProps & OwnProps; - -export function ArticleValidator( - props: ArticleValidatorProps -): JSX.Element | null { - const userBuysArticles = props.userId; - if (userBuysArticles) { +import React from 'react'; +import { useSettings, useUserBalance } from '../../store'; + +export function useArticleValidator( + value: number, + userId: string = '' +): boolean { + const settings = useSettings(); + const userBalance = useUserBalance(userId); + const boundary = settings.payment.boundary; + + if (userId) { const newValue = - (typeof props.balance === 'boolean' ? 0 : props.balance) - props.value; - const buyArticleIsValid = props.boundary.lower < newValue; - - return <>{props.render(buyArticleIsValid)}; + (typeof userBalance === 'boolean' ? 0 : userBalance) - value; + return boundary.lower < newValue; } else { - const createArticleIsValid = - props.value > 0 && props.value * -1 > props.boundary.lower; - return <>{props.render(createArticleIsValid)}; + return value > 0 && value * -1 > boundary.lower; } } -const mapStateToProps = (state: AppState, props: OwnProps): StateProps => ({ - balance: props.userId - ? getUserBalance(state, props.userId) - : getSettingsBalance(state), - boundary: state.settings.payment.boundary, -}); - -export const ConnectedArticleValidator = connect(mapStateToProps)( - ArticleValidator -); +interface Props { + value: number; + userId?: string; + render(isValid: boolean): React.ReactNode; +} +export const ArticleValidator = (props: Props) => { + const isValid = useArticleValidator(props.value, props.userId); + return <>{props.render(isValid)}; +}; diff --git a/src/components/article/views/articles.tsx b/src/components/article/views/articles.tsx deleted file mode 100644 index e16dadfb..00000000 --- a/src/components/article/views/articles.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import * as React from 'react'; -import { ConnectedArticleList } from '../article-list'; - -export function Articles(): JSX.Element { - return ( - <> - - - ); -} diff --git a/src/components/common/error-message.tsx b/src/components/common/error-message.tsx index 469c4e3d..baf29cac 100644 --- a/src/components/common/error-message.tsx +++ b/src/components/common/error-message.tsx @@ -1,9 +1,7 @@ import { FixedContainer } from 'bricks-of-sand'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; -import { AppState } from '../../store'; -import { getGlobalError } from '../../store/reducers'; +import { useGlobalError } from '../../store'; import { Toast } from './toast'; interface OwnProps {} @@ -16,7 +14,9 @@ interface ActionProps {} export type ErrorMessageProps = ActionProps & StateProps & OwnProps; -export function ErrorMessage({ id }: ErrorMessageProps): JSX.Element | null { +export function ErrorMessage() { + const id = useGlobalError(); + if (!id) { return null; } @@ -29,14 +29,3 @@ export function ErrorMessage({ id }: ErrorMessageProps): JSX.Element | null { ); } - -const mapStateToProps = (state: AppState): StateProps => ({ - id: getGlobalError(state), -}); - -const mapDispatchToProps = {}; - -export const ConnectedErrorMessage = connect( - mapStateToProps, - mapDispatchToProps -)(ErrorMessage); diff --git a/src/components/common/header-menu.tsx b/src/components/common/header-menu.tsx index 7e25f1f7..07990156 100644 --- a/src/components/common/header-menu.tsx +++ b/src/components/common/header-menu.tsx @@ -3,9 +3,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { NavLink } from 'react-router-dom'; import { Logo } from '../ui/icons/logo'; -import { ConnectedSearchInput } from './search'; - -export interface HeaderMenuProps {} +import { SearchInput } from './search'; const HeaderLeft = styled(Flex)({ a: { @@ -29,7 +27,7 @@ const HeaderRight = withTheme( }) ); -export function HeaderMenu(props: HeaderMenuProps): JSX.Element { +export function HeaderMenu(): JSX.Element { return ( - + diff --git a/src/components/common/idle-timer.tsx b/src/components/common/idle-timer.tsx index a34ce82b..c75321a2 100644 --- a/src/components/common/idle-timer.tsx +++ b/src/components/common/idle-timer.tsx @@ -1,65 +1,35 @@ import * as React from 'react'; -import { connect } from 'react-redux'; -import { RouteComponentProps, withRouter } from 'react-router'; -import { AppState } from '../../store'; - -interface State { - timerId: NodeJS.Timer | number | undefined; -} -interface StateProps { - idleTimer: number; -} -interface OwnProps extends RouteComponentProps { - onTimeOut?(): void; -} - -type Props = StateProps & OwnProps; - -export class IdleTimer extends React.Component { - public state = { - timerId: undefined, +import { useSettings } from '../../store'; +import { withRouter } from 'react-router'; +import { RouteComponentProps } from 'react-router-dom'; + +export function useIdleTimer(onTimeOut: () => void) { + const settings = useSettings(); + const [timerId, setTimerId] = React.useState(0); + const resetTimer = () => { + clearTimeout(timerId); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const id: any = setTimeout(onTimeOut, settings.idleTimer); + setTimerId(id); }; - public componentDidMount(): void { - this.resetTimer(); - document.addEventListener('scroll', this.resetTimer); - document.addEventListener('click', this.resetTimer); - document.addEventListener('touch', this.resetTimer); - document.addEventListener('keyup', this.resetTimer); - } - - public componentWillUnmount(): void { - document.removeEventListener('scroll', this.resetTimer); - document.removeEventListener('click', this.resetTimer); - document.removeEventListener('touch', this.resetTimer); - document.removeEventListener('keyup', this.resetTimer); - clearTimeout(this.state.timerId); - } - - public handleTimeOut = () => { - if (typeof this.props.onTimeOut === 'function') { - this.props.onTimeOut(); - } else { - this.props.history.push('/'); - } - }; - - public resetTimer = () => { - clearTimeout(this.state.timerId); - const id = setTimeout(this.handleTimeOut, this.props.idleTimer); - this.setState({ timerId: id }); - }; - - // tslint:disable-next-line:prefer-function-over-method - public render(): null { - return null; - } + React.useEffect(() => { + resetTimer(); + document.addEventListener('scroll', resetTimer); + document.addEventListener('click', resetTimer); + document.addEventListener('touch', resetTimer); + document.addEventListener('keyup', resetTimer); + return () => { + document.removeEventListener('scroll', resetTimer); + document.removeEventListener('click', resetTimer); + document.removeEventListener('touch', resetTimer); + document.removeEventListener('keyup', resetTimer); + clearTimeout(timerId); + }; + }, []); } -const mapStateToProps = (state: AppState): StateProps => ({ - idleTimer: state.settings.idleTimer, +export const WrappedIdleTimer = withRouter((props: RouteComponentProps) => { + useIdleTimer(() => props.history.push('/')); + return null; }); - -export const ConnectedIdleTimer = withRouter( - connect(mapStateToProps)(IdleTimer) -); diff --git a/src/components/common/nav-tab-menu.tsx b/src/components/common/nav-tab-menu.tsx index 2ea9cada..12dc4f98 100644 --- a/src/components/common/nav-tab-menu.tsx +++ b/src/components/common/nav-tab-menu.tsx @@ -2,7 +2,7 @@ import { Flex, Menu, Tab, ThemeSwitcher } from 'bricks-of-sand'; import * as React from 'react'; import { NavLink } from 'react-router-dom'; -// tslint:disable-next-line:no-any +// eslint-disable-next-line @typescript-eslint/no-explicit-any const Tabs: any = Tab(NavLink); export interface NavTabMenusProps { diff --git a/src/components/common/pager.tsx b/src/components/common/pager.tsx index 8c0ede7e..47d75798 100644 --- a/src/components/common/pager.tsx +++ b/src/components/common/pager.tsx @@ -2,6 +2,28 @@ import { Flex, PrimaryButton } from 'bricks-of-sand'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; +function isPrevDisabled(props: PagerProps): boolean { + return props.currentPage === 0; +} + +function getPageCount(props: PagerProps): number { + return props.itemCount / props.limit; +} + +function pageUp(props: PagerProps): void { + const nextPage = props.currentPage + 1; + props.onChange(nextPage); +} +function pageDown(props: PagerProps): void { + const nextPage = props.currentPage - 1; + props.onChange(nextPage); +} + +function isNextDisabled(props: PagerProps): boolean { + const pageCount = getPageCount(props); + return pageCount - props.currentPage < 1; +} + export interface PagerProps { currentPage: number; itemCount: number; @@ -29,25 +51,3 @@ export function Pager(props: PagerProps): JSX.Element { ); } - -function isPrevDisabled(props: PagerProps): boolean { - return props.currentPage === 0; -} - -function isNextDisabled(props: PagerProps): boolean { - const pageCount = getPageCount(props); - return pageCount - props.currentPage < 1; -} - -function getPageCount(props: PagerProps): number { - return props.itemCount / props.limit; -} - -function pageUp(props: PagerProps): void { - const nextPage = props.currentPage + 1; - props.onChange(nextPage); -} -function pageDown(props: PagerProps): void { - const nextPage = props.currentPage - 1; - props.onChange(nextPage); -} diff --git a/src/components/common/search.tsx b/src/components/common/search.tsx index 94a080d9..3a2e0aaa 100644 --- a/src/components/common/search.tsx +++ b/src/components/common/search.tsx @@ -1,58 +1,26 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router'; -import { AppState } from '../../store'; -import { User, getUserArray, startLoadingUsers } from '../../store/reducers'; +import { store } from '../../store'; +import { startLoadingUsers } from '../../store/reducers'; import { getUserDetailLink } from '../user/user-router'; -import { ConnectedUserSearch } from '../user/user-selection'; - -interface StateProps { - users: User[]; -} - -interface ActionProps { - // tslint:disable-next-line:no-any - startLoadingUsers: any; -} - -export type Props = ActionProps & StateProps & RouteComponentProps; - -interface State {} - -export class SearchInput extends React.Component { - public componentDidMount(): void { - this.props.startLoadingUsers(); - } - - public render(): JSX.Element { - return ( - ( - - this.props.history.push(getUserDetailLink(user.id)) - } - /> - )} - /> - ); - } -} - -const mapStateToProps = (state: AppState): StateProps => ({ - users: getUserArray(state), +import { UserSearch } from '../user/user-selection'; + +export type Props = RouteComponentProps; + +export const SearchInput = withRouter((props: Props) => { + React.useEffect(() => { + startLoadingUsers(store.dispatch); + }, []); + return ( + ( + props.history.push(getUserDetailLink(user.id))} + /> + )} + /> + ); }); - -const mapDispatchToProps = { - startLoadingUsers, -}; - -export const ConnectedSearchInput = withRouter( - connect( - mapStateToProps, - mapDispatchToProps - )(SearchInput) -); diff --git a/src/components/currency/currency-input.tsx b/src/components/currency/currency-input.tsx index 1dd94397..98762b6a 100644 --- a/src/components/currency/currency-input.tsx +++ b/src/components/currency/currency-input.tsx @@ -2,7 +2,19 @@ import { Input } from 'bricks-of-sand'; import * as React from 'react'; import { FormattedNumber } from 'react-intl'; -// tslint:disable-next-line:no-any +function getPlaceholder( + placeholder: string | undefined, + value: string, + hasFocus: boolean +): string { + return !placeholder || value !== '0.00' || hasFocus ? value : placeholder; +} + +export function convertFormattedNumberToCents(rawValue: string): number { + return Number(rawValue.replace(/(-(?!\d))|[^0-9|-]/g, '')); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any function moveCursorToEnd(el: any): void { window.setTimeout(() => { if (typeof el.selectionStart === 'number') { @@ -108,15 +120,3 @@ export class CurrencyInput extends React.Component { ); } } - -function getPlaceholder( - placeholder: string | undefined, - value: string, - hasFocus: boolean -): string { - return !placeholder || value !== '0.00' || hasFocus ? value : placeholder; -} - -export function convertFormattedNumberToCents(rawValue: string): number { - return Number(rawValue.replace(/(-(?!\d))|[^0-9|-]/g, '')); -} diff --git a/src/components/currency/currency.tsx b/src/components/currency/currency.tsx index 0aecd5b5..ce62b04c 100644 --- a/src/components/currency/currency.tsx +++ b/src/components/currency/currency.tsx @@ -1,15 +1,10 @@ import * as React from 'react'; import { FormattedNumber } from 'react-intl'; -import { connect } from 'react-redux'; -interface OwnProps { +interface Props { value: number; } -// interface StateProps - -type Props = OwnProps; - export function Currency(props: Props): JSX.Element { return ( <> @@ -22,5 +17,3 @@ export function Currency(props: Props): JSX.Element { ); } - -export const ConnectedCurrency = connect()(Currency); diff --git a/src/components/metrics/metrics.tsx b/src/components/metrics/metrics.tsx index 972c29df..0d0d180b 100644 --- a/src/components/metrics/metrics.tsx +++ b/src/components/metrics/metrics.tsx @@ -7,7 +7,7 @@ import { Currency } from '../currency'; import { UserRouteProps } from '../user/user-router'; import { ArticleElement, useMetrics } from './resource'; -interface Props extends UserRouteProps {} +type Props = UserRouteProps; const H1 = styled('h1')({ marginBottom: '2rem', diff --git a/src/components/metrics/resource.ts b/src/components/metrics/resource.ts index 7dac4c08..19d52b5e 100644 --- a/src/components/metrics/resource.ts +++ b/src/components/metrics/resource.ts @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react'; import { get } from '../../services/api'; import { Article } from '../../store/reducers'; -// tslint:disable-next-line: no-any +// eslint-disable-next-line @typescript-eslint/no-explicit-any function useEffectAsync(effect: any, inputs: any): void { useEffect(() => { effect(); diff --git a/src/components/paypal/paypal-transaction-form.tsx b/src/components/paypal/paypal-transaction-form.tsx index 5a2f0dbf..16a8d928 100644 --- a/src/components/paypal/paypal-transaction-form.tsx +++ b/src/components/paypal/paypal-transaction-form.tsx @@ -19,9 +19,10 @@ const Wrapper = styled('div')({ interface Props { userName: string; - userId: number; + userId: string; } +// eslint-disable-next-line react/display-name export const PayPalTransactionForm = React.memo((props: Props) => { const settings = useSettings(); const [value, setValue] = React.useState(0); diff --git a/src/components/paypal/paypal-transaction.tsx b/src/components/paypal/paypal-transaction.tsx index a69e14d3..f39302e3 100644 --- a/src/components/paypal/paypal-transaction.tsx +++ b/src/components/paypal/paypal-transaction.tsx @@ -14,11 +14,14 @@ const H2 = styled('h2')({ marginBottom: '1rem', }); -export interface PayPalTransactionProps - extends RouteComponentProps<{ id: string; state: string; amount: string }> {} +export type PayPalTransactionProps = RouteComponentProps<{ + id: string; + state: string; + amount: string; +}>; export const PayPalTransaction = withRouter((props: PayPalTransactionProps) => { - const userId = Number(props.match.params.id); + const userId = props.match.params.id; const paidAmount = Number(props.match.params.amount); const userName = useUserName(userId); diff --git a/src/components/settings/index.ts b/src/components/settings/index.ts deleted file mode 100644 index d2492def..00000000 --- a/src/components/settings/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './settings-loader'; diff --git a/src/components/settings/settings-loader.tsx b/src/components/settings/settings-loader.tsx deleted file mode 100644 index 9534c0ef..00000000 --- a/src/components/settings/settings-loader.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from 'react'; -import { connect } from 'react-redux'; -import { startLoadingSettings } from '../../store/reducers'; -interface ActionProps { - // tslint:disable-next-line:no-any - startLoadingSettings: any; -} -export class SettingsLoader extends React.Component { - public componentDidMount(): void { - this.props.startLoadingSettings(); - } - - // tslint:disable-next-line:prefer-function-over-method - public render(): null { - return null; - } -} - -const mapDispatchToProps = { - startLoadingSettings, -}; - -export const ConnectedSettingsLoader = connect( - undefined, - mapDispatchToProps -)(SettingsLoader); diff --git a/src/components/transaction/__tests__/create-custom-transactions-form.spec.tsx b/src/components/transaction/__tests__/create-custom-transactions-form.spec.tsx index 03cc4cd3..a573d232 100644 --- a/src/components/transaction/__tests__/create-custom-transactions-form.spec.tsx +++ b/src/components/transaction/__tests__/create-custom-transactions-form.spec.tsx @@ -1,77 +1,23 @@ import * as React from 'react'; -import { cleanup, fireEvent } from 'react-testing-library'; +import { cleanup } from 'react-testing-library'; import { createConnectedComponent, getMockStore, } from '../../../spec-configs/mock-store'; import { renderWithContext } from '../../../spec-configs/render'; -import { - ConnectedCreateCustomTransactionForm, - CreateCustomTransactionForm, -} from '../create-custom-transaction-form'; +import { CreateCustomTransactionForm } from '../create-custom-transaction-form'; afterEach(cleanup); describe('CreateCustomTransactionForm', () => { it('matches the snapshot', () => { const store = getMockStore({}); - const Component = createConnectedComponent( - ConnectedCreateCustomTransactionForm - ); + const Component = createConnectedComponent(CreateCustomTransactionForm); const { container } = renderWithContext( , { user: { 12: { balance: 0 } } } ); expect(container.firstChild).toMatchSnapshot(); }); - - describe('Connected CreateCustomTransactionForm', () => { - describe('with deposit', () => { - it('submits form with cents value', () => { - const mock = jest.fn(); - const { getByPlaceholderText, getByText } = renderWithContext( - , - { - user: { 12: { balance: 0 } }, - } - ); - const input = getByPlaceholderText('CUSTOM AMOUNT'); - const button = getByText('+'); - - fireEvent.change(input, { target: { value: '1200' } }); - fireEvent.submit(button); - - // expect(mock).toHaveBeenCalledWith(12, { amount: 1200 }); - }); - }); - }); - describe('with dispense', () => { - it('submits form with negated cents value', async () => { - const mock = jest.fn(); - const mockOnCreate = jest.fn(); - - const { getByPlaceholderText, getByText } = renderWithContext( - , - { user: { 1: { balance: 0 } } } - ); - const input = getByPlaceholderText('CUSTOM AMOUNT'); - const button = getByText('-'); - - fireEvent.change(input, { target: { value: '120' } }); - fireEvent.submit(button); - - // expect(mock).toHaveBeenCalledWith(1, { amount: -120 }); - // await wait(() => expect(mockOnCreate).toHaveBeenCalled()); - }); - }); }); diff --git a/src/components/transaction/__tests__/user-to-user-validator.spec.tsx b/src/components/transaction/__tests__/user-to-user-validator.spec.tsx index fb587181..25aa7101 100644 --- a/src/components/transaction/__tests__/user-to-user-validator.spec.tsx +++ b/src/components/transaction/__tests__/user-to-user-validator.spec.tsx @@ -2,14 +2,14 @@ import * as React from 'react'; import { cleanup } from 'react-testing-library'; import { renderWithContext } from '../../../spec-configs/render'; -import { ConnectedUserToUserValidator } from '../user-to-user-validator'; +import { UserToUserValidator } from '../user-to-user-validator'; afterEach(cleanup); describe('UserToUserValidator', () => { it('returns true if the user has the money and the receiver can accept it', () => { const { getByTestId } = renderWithContext( - (
{isValid ? 'yes' : 'no'}
)} @@ -27,7 +27,7 @@ describe('UserToUserValidator', () => { it('returns false if the user does not have the money', () => { const { getByTestId } = renderWithContext( - (
{isValid ? 'yes' : 'no'}
)} @@ -44,7 +44,7 @@ describe('UserToUserValidator', () => { it('returns false if the receiver can not accept the money', () => { const { getByTestId } = renderWithContext( - (
{isValid ? 'yes' : 'no'}
)} diff --git a/src/components/transaction/__tests__/validator.spec.tsx b/src/components/transaction/__tests__/validator.spec.tsx index b537c0db..856db17a 100644 --- a/src/components/transaction/__tests__/validator.spec.tsx +++ b/src/components/transaction/__tests__/validator.spec.tsx @@ -1,7 +1,8 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import * as React from 'react'; -import { cleanup, render } from 'react-testing-library'; - -import { TransactionValidator, TransactionValidatorProps } from '../validator'; +import { cleanup } from 'react-testing-library'; +import { renderWithContext } from '../../../spec-configs/render'; +import { useTransactionValidator } from '../validator'; afterEach(cleanup); @@ -21,20 +22,18 @@ const renderTransactionValidator = ({ value = 10, balance = 0, isDeposit = true, -}: Partial) => { - const { getByTestId } = render( - ( -
{isValid ? 'yes' : 'no'}
- )} - /> - ); +}: any) => { + const Component = () => { + const isValid = useTransactionValidator(value, '1', isDeposit); + return
{isValid ? 'yes' : 'no'}
; + }; + const { getByTestId } = renderWithContext(, { + user: { '1': { balance } }, + settings: { + account: { boundary: accountBoundary }, + payment: { boundary: paymentBoundary }, + }, + }); return getByTestId; }; diff --git a/src/components/transaction/create-custom-transaction-form.tsx b/src/components/transaction/create-custom-transaction-form.tsx index fdd1973c..210ea8d1 100644 --- a/src/components/transaction/create-custom-transaction-form.tsx +++ b/src/components/transaction/create-custom-transaction-form.tsx @@ -1,14 +1,10 @@ -import * as React from 'react'; -import { connect } from 'react-redux'; +import React from 'react'; import { GreenButton, RedButton, ResponsiveGrid, styled } from 'bricks-of-sand'; -import { Dispatch } from '../../store'; -import { - CreateTransactionParams, - startCreatingTransaction, -} from '../../store/reducers'; +import { useDispatch } from 'redux-react-hook'; +import { startCreatingTransaction } from '../../store/reducers'; import { CurrencyInput } from '../currency'; -import { ConnectedTransactionValidator } from './validator'; +import { useTransactionValidator } from './validator'; const ButtonText = styled('div')({ fontSize: '1rem', @@ -16,102 +12,58 @@ const ButtonText = styled('div')({ lineHeight: 0, }); -export interface OwnProps { - userId: number; +interface Props { + userId: string; transactionCreated?(): void; } -interface StateProps { - value: number; -} - -interface ActionProps { - createTransaction( - userId: number, - params: CreateTransactionParams - ): // tslint:disable-next-line:no-any - Promise; -} +export const CreateCustomTransactionForm = (props: Props) => { + const { userId, transactionCreated } = props; -// tslint:disable-next-line:no-any -type Props = any; - -export class CreateCustomTransactionForm extends React.Component< - Props, - StateProps -> { - public state = { value: 0 }; - - public handleChange = (value: number) => { - this.setState({ value }); - }; + const dispatch = useDispatch(); + const [value, setValue] = React.useState(0); + const depositIsValid = useTransactionValidator(value, userId, true); + const dispenseIsValid = useTransactionValidator(value, userId, false); - public submit = async (isDeposit: boolean) => { - const { createTransaction, userId, transactionCreated } = this.props; + const submit = async (isDeposit: boolean) => { const multiplier = isDeposit ? 1 : -1; - const amount = this.state.value * multiplier; + const amount = value * multiplier; - const result = await createTransaction(userId, { amount }); + const result = await startCreatingTransaction(dispatch, userId, { + amount, + }); if (transactionCreated) { transactionCreated(); } if (result) { - this.setState({ value: 0 }); + setValue(0); } }; - - public render(): JSX.Element { - const { userId } = this.props; - return ( - - ( - this.submit(false)} - isRound - disabled={!isValid} - type="submit" - > - - - - )} - /> - - ( - this.submit(true)} - isRound - disabled={!isValid} - type="submit" - > - + - - )} - /> - - ); - } -} - -const mapDispatchToProps = (dispatch: Dispatch): ActionProps => ({ - createTransaction: (userId: number, params: CreateTransactionParams) => - dispatch(startCreatingTransaction(userId, params)), -}); - -export const ConnectedCreateCustomTransactionForm = connect( - undefined, - mapDispatchToProps -)(CreateCustomTransactionForm); + return ( + + submit(false)} + isRound + disabled={!dispenseIsValid} + type="submit" + > + - + + + submit(true)} + isRound + disabled={!depositIsValid} + type="submit" + > + + + + + ); +}; diff --git a/src/components/transaction/create-user-transaction-form.tsx b/src/components/transaction/create-user-transaction-form.tsx index 8135597f..310ef3fd 100644 --- a/src/components/transaction/create-user-transaction-form.tsx +++ b/src/components/transaction/create-user-transaction-form.tsx @@ -8,15 +8,15 @@ import { withTheme, } from 'bricks-of-sand'; import * as React from 'react'; -import { FormattedMessage, InjectedIntl, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; +import { FormattedMessage, InjectedIntl } from 'react-intl'; import { RouteComponentProps, withRouter } from 'react-router'; import { User, startCreatingTransaction } from '../../store/reducers'; import { Currency, CurrencyInput } from '../currency'; -import { ConnectedUserSelectionList } from '../user'; +import { UserSelection } from '../user'; import { UserName } from '../user/user-name'; -import { ConnectedTransactionUndoButton } from './transaction-undo-button'; -import { ConnectedUserToUserValidator } from './user-to-user-validator'; +import { TransactionUndoButton } from './transaction-undo-button'; +import { UserToUserValidator } from './user-to-user-validator'; +import { store } from '../../store'; export const AcceptWrapper = withTheme( styled('div')({}, props => ({ @@ -31,7 +31,7 @@ const initialState = { selectedAmount: 0, hasSelectionReady: false, selectedUser: { - id: 0, + id: '', name: '', isActive: false, balance: 0, @@ -43,11 +43,6 @@ const initialState = { comment: '', }; -interface ActionProps { - // tslint:disable-next-line:no-any - startCreatingTransaction: any; -} - interface State { amount: number; createdTransactionId: number; @@ -57,8 +52,7 @@ interface State { comment: string; } -type Props = RouteComponentProps<{ id: string }> & - ActionProps & { intl: InjectedIntl }; +type Props = RouteComponentProps<{ id: string }> & { intl: InjectedIntl }; export class CreateUserTransactionForm extends React.Component { public state = initialState; @@ -72,8 +66,9 @@ export class CreateUserTransactionForm extends React.Component { public createTransaction = async () => { if (this.state.selectedUser.id && this.state.selectedAmount) { - const res = await this.props.startCreatingTransaction( - Number(this.props.match.params.id), + const res = await startCreatingTransaction( + store.dispatch, + this.props.match.params.id, { amount: this.state.selectedAmount * -1, recipientId: this.state.selectedUser.id, @@ -113,14 +108,14 @@ export class CreateUserTransactionForm extends React.Component { → - this.setState({ hasSelectionReady: false, }) } transactionId={this.state.createdTransactionId} - userId={Number(this.props.match.params.id)} + userId={this.props.match.params.id || ''} /> ); @@ -134,32 +129,37 @@ export class CreateUserTransactionForm extends React.Component { alignItems="center" tabletColumns="4fr 1fr 4fr 1fr" > - - this.setState({ - selectedAmount: value, - }) - } - /> + + {text => ( + + this.setState({ + selectedAmount: value, + }) + } + /> + )} + → - user.name} - onSelect={this.submitUserId} - /> - + {text => ( + user.name} + onSelect={this.submitUserId} + /> + )} +
+ ( @@ -168,14 +168,15 @@ export class CreateUserTransactionForm extends React.Component { )} /> - + + {text => ( + + )} + ); @@ -183,15 +184,6 @@ export class CreateUserTransactionForm extends React.Component { } } -const mapDispatchToProps = { - startCreatingTransaction, -}; - -export const ConnectedCreateUserTransactionForm = injectIntl( - withRouter( - connect( - undefined, - mapDispatchToProps - )(CreateUserTransactionForm) - ) +export const ConnectedCreateCustomTransactionForm = withRouter( + CreateUserTransactionForm ); diff --git a/src/components/transaction/payment-button-steps.tsx b/src/components/transaction/payment-button-steps.tsx index 8e11e34e..b452ab78 100644 --- a/src/components/transaction/payment-button-steps.tsx +++ b/src/components/transaction/payment-button-steps.tsx @@ -1,11 +1,10 @@ import { ResponsiveGrid } from 'bricks-of-sand'; import * as React from 'react'; -import { ConnectedTransactionButton } from '.'; -import { ConnectedTransactionValidator } from './validator'; +import { TransactionButton } from './transaction-button'; export interface PaymentButtonListProps { steps: number[]; - userId: number; + userId: string; isDeposit: boolean; } @@ -14,19 +13,11 @@ export function PaymentButtonList(props: PaymentButtonListProps): JSX.Element { return ( {props.steps.map(step => ( - ( - - )} + userId={props.userId} + value={step * multiplier} /> ))} diff --git a/src/components/transaction/payment.tsx b/src/components/transaction/payment.tsx index 6836d059..1305c296 100644 --- a/src/components/transaction/payment.tsx +++ b/src/components/transaction/payment.tsx @@ -1,50 +1,35 @@ +import { Block, Card } from 'bricks-of-sand'; import * as React from 'react'; -import { connect } from 'react-redux'; -import { Block, Card } from 'bricks-of-sand'; -import { ConnectedCreateCustomTransactionForm } from '.'; -import { AppState } from '../../store'; -import { Payment, getPayment } from '../../store/reducers'; +import { useSettings } from '../../store'; +import { CreateCustomTransactionForm } from './create-custom-transaction-form'; import { PaymentButtonList } from './payment-button-steps'; -interface OwnProps { - userId: number; -} - -interface StateProps { - balance: number; - payment: Payment; +interface Props { + userId: string; } -type Props = OwnProps & StateProps; - -export function PaymentComponent(props: Props): JSX.Element { +export function Payment(props: Props): JSX.Element { + const payment = useSettings().payment; return ( - {props.payment.deposit.enabled && ( + {payment.deposit.enabled && ( )} - + - {props.payment.dispense.enabled && ( + {payment.dispense.enabled && ( )} ); } - -const mapStateToProps = (state: AppState, props: OwnProps): StateProps => ({ - balance: state.user[props.userId].balance, - payment: getPayment(state), -}); - -export const ConnectedPayment = connect(mapStateToProps)(PaymentComponent); diff --git a/src/components/transaction/split-invoice/split-invoice.tsx b/src/components/transaction/split-invoice/split-invoice.tsx index 9d1da267..940834cb 100644 --- a/src/components/transaction/split-invoice/split-invoice.tsx +++ b/src/components/transaction/split-invoice/split-invoice.tsx @@ -6,6 +6,7 @@ import { ResponsiveGrid, Separator, styled, + Icon, } from 'bricks-of-sand'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; @@ -17,11 +18,11 @@ import { UsersState, startCreatingTransaction, } from '../../../store/reducers'; -import { ConnectedIdleTimer } from '../../common/idle-timer'; +import { WrappedIdleTimer } from '../../common/idle-timer'; import { Currency, CurrencyInput } from '../../currency'; import { AcceptIcon } from '../../ui/icons/accept'; -import { ConnectedUserSelectionList } from '../../user'; -import { ConnectedUserMultiSelection } from '../../user/user-multi-selection'; +import { UserSelection } from '../../user'; +import { UserMultiSelection } from '../../user/user-multi-selection'; import { UserName } from '../../user/user-name'; import { isTransactionValid } from '../validator'; @@ -31,12 +32,10 @@ interface State { participants: User[]; comment: string; amount: number; - responseState: { [userId: number]: Transaction | 'error' }; - validation: { [userId: number]: string }; + responseState: { [userId: string]: Transaction | 'error' }; + validation: { [userId: string]: string }; } -interface Props {} - const initialState: State = { isLoading: false, recipient: undefined, @@ -66,7 +65,7 @@ const TextCenter = styled(Block)({ textAlign: 'center', }); -export class SplitInvoiceForm extends React.Component { +export class SplitInvoiceForm extends React.Component<{}, State> { public state = initialState; public resetState = () => { @@ -84,9 +83,12 @@ export class SplitInvoiceForm extends React.Component { this.setState({ isLoading: true }); const userId = participant.id; const params = this.getParams(this.state.recipient); - const result = await store.dispatch( - startCreatingTransaction(userId, params) + const result = await startCreatingTransaction( + store.dispatch, + userId, + params ); + this.setState(state => ({ responseState: { ...state.responseState, @@ -225,7 +227,7 @@ export class SplitInvoiceForm extends React.Component { {Object.keys(this.state.responseState).map(userId => { const item = this.state.responseState[userId]; const user = this.state.participants.find( - user => user.id === Number(userId) + user => user.id == userId ); const userName = user ? user.name : ''; @@ -242,7 +244,9 @@ export class SplitInvoiceForm extends React.Component { } return ( - + + +

{ return ( - +

{ id="SELECT_RECIPIENT" defaultMessage="select recipient" children={placeholder => ( - { - - props.startCreatingTransaction(props.userId, { amount: props.value }) + startCreatingTransaction(store.dispatch, props.userId, { + amount: props.value, + }) } type="button" - disabled={props.disabled} + disabled={!isValid} > ); } - -const mapDispatchToProps = { - startCreatingTransaction, -}; - -export const ConnectedTransactionButton = connect( - undefined, - mapDispatchToProps -)(TransactionButton); diff --git a/src/components/transaction/transaction-list-item.tsx b/src/components/transaction/transaction-list-item.tsx index abf5f043..9d9da1bf 100644 --- a/src/components/transaction/transaction-list-item.tsx +++ b/src/components/transaction/transaction-list-item.tsx @@ -9,12 +9,10 @@ import { withTheme, } from 'bricks-of-sand'; import * as React from 'react'; -import { connect } from 'react-redux'; -import { AppState } from '../../store'; -import { Transaction } from '../../store/reducers'; import { Currency } from '../currency'; import { ShoppingBagIcon } from '../ui/icons/shopping-bag'; -import { ConnectedTransactionUndoButton } from './transaction-undo-button'; +import { TransactionUndoButton } from './transaction-undo-button'; +import { useTransaction } from '../../store'; const ArticleIcon = withTheme( styled('span')({}, ({ theme }) => ({ @@ -22,27 +20,22 @@ const ArticleIcon = withTheme( })) ); -interface OwnProps { +interface Props { id: number | string; } -interface StateProps { - transaction: Transaction; -} - -type Props = OwnProps & StateProps; - const TextRight = styled(ResponsiveGrid)({ textAlign: 'right', }); -export function TransactionListItem(props: Props): JSX.Element | null { - if (!props.transaction) { +export function TransactionListItem({ id }: Props): JSX.Element | null { + const transaction = useTransaction(Number(id)); + if (!transaction) { return null; } return ( - + - - + + - {props.transaction.sender && ( - <>← {props.transaction.sender.name} : - )} - {props.transaction.recipient && ( - <>→ {props.transaction.recipient.name} : + {transaction.sender && <>← {transaction.sender.name} :} + {transaction.recipient && ( + <>→ {transaction.recipient.name} : )} - {props.transaction.article && ( + {transaction.article && ( <> {' '} - {props.transaction.article.name} + {transaction.article.name} )} - {props.transaction.comment && <> {props.transaction.comment}} + {transaction.comment && <> {transaction.comment}} - {props.transaction.isDeletable ? ( - ) : ( - {props.transaction.created} + {transaction.created} )} @@ -86,11 +77,3 @@ export function TransactionListItem(props: Props): JSX.Element | null { ); } - -const mapStateToProps = (state: AppState, { id }: OwnProps) => ({ - transaction: state.transaction[id], -}); - -export const ConnectedTransactionListItem = connect(mapStateToProps)( - TransactionListItem -); diff --git a/src/components/transaction/transaction-table.tsx b/src/components/transaction/transaction-table.tsx index 731be565..760187ef 100644 --- a/src/components/transaction/transaction-table.tsx +++ b/src/components/transaction/transaction-table.tsx @@ -1,31 +1,16 @@ import * as React from 'react'; -import { connect } from 'react-redux'; -import { Dispatch } from '../../store'; -import { - Transaction, - TransactionsResponse, - startLoadingTransactions, -} from '../../store/reducers'; +import { store } from '../../store'; +import { Transaction, startLoadingTransactions } from '../../store/reducers'; import { Pager } from '../common/pager'; import { getUserTransactionsLink } from '../user/user-router'; -import { ConnectedTransactionListItem } from './transaction-list-item'; +import { TransactionListItem } from './transaction-list-item'; -interface OwnProps { - userId: number; +interface Props { + userId: string; page: number; onPageChange(url: string): void; } -interface ActionProps { - loadTransactions( - userId: number, - offset?: number, - limit?: number - ): Promise; -} - -export type TransactionTableProps = ActionProps & OwnProps; - interface State { limit: number; offset: number; @@ -35,10 +20,7 @@ interface State { let lastPage = 0; -export class TransactionTable extends React.Component< - TransactionTableProps, - State -> { +export class TransactionTable extends React.Component { public state = { limit: 15, offset: 0, @@ -73,7 +55,8 @@ export class TransactionTable extends React.Component< public loadRows = async (): Promise => { const { limit, offset } = this.getLimitAndOffset(); - const res = await this.props.loadTransactions( + const res = await startLoadingTransactions( + store.dispatch, this.props.userId, offset, limit @@ -98,24 +81,11 @@ export class TransactionTable extends React.Component< } } -const mapDispatchToProps = (dispatch: Dispatch): ActionProps => ({ - loadTransactions: (id: number, offset: number, limit: number) => - dispatch(startLoadingTransactions(id, offset, limit)), -}); - -export const ConnectedTransactionTable = connect( - undefined, - mapDispatchToProps -)(TransactionTable); - function TransactionPage(props: { transactions: Transaction[] }): JSX.Element { return ( <> {props.transactions.map(transaction => ( - + ))} ); diff --git a/src/components/transaction/transaction-undo-button.tsx b/src/components/transaction/transaction-undo-button.tsx index 628ca692..ad162de1 100644 --- a/src/components/transaction/transaction-undo-button.tsx +++ b/src/components/transaction/transaction-undo-button.tsx @@ -1,33 +1,18 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; -import { AppState } from '../../store'; -import { - isTransactionDeletable, - startDeletingTransaction, -} from '../../store/reducers'; +import { useIsTransactionDeletable, store } from '../../store'; +import { startDeletingTransaction } from '../../store/reducers'; -interface OwnProps { - userId?: number; +interface Props { + userId?: string; transactionId: number; onSuccess?(): void; } -interface StateProps { - canBeDeleted: boolean; -} - -interface ActionProps { - // tslint:disable-next-line:no-any - startDeletingTransaction: any; -} - -export type TransactionUndoButtonProps = ActionProps & StateProps & OwnProps; +export function TransactionUndoButton(props: Props) { + const isDeletable = useIsTransactionDeletable(props.transactionId); -export function TransactionUndoButton( - props: TransactionUndoButtonProps -): JSX.Element | null { - if (!props.canBeDeleted) { + if (!isDeletable) { return null; } @@ -41,23 +26,14 @@ export function TransactionUndoButton( if (typeof props.onSuccess === 'function') { props.onSuccess(); } - props.startDeletingTransaction(props.userId || 0, props.transactionId); + startDeletingTransaction( + store.dispatch, + props.userId || '', + props.transactionId + ); }} > ); } - -const mapStateToProps = (state: AppState, props: OwnProps): StateProps => ({ - canBeDeleted: isTransactionDeletable(state, props.transactionId), -}); - -const mapDispatchToProps = { - startDeletingTransaction, -}; - -export const ConnectedTransactionUndoButton = connect( - mapStateToProps, - mapDispatchToProps -)(TransactionUndoButton); diff --git a/src/components/transaction/user-to-user-validator.tsx b/src/components/transaction/user-to-user-validator.tsx index bd0340ad..faefa20f 100644 --- a/src/components/transaction/user-to-user-validator.tsx +++ b/src/components/transaction/user-to-user-validator.tsx @@ -1,52 +1,23 @@ import * as React from 'react'; -import { connect } from 'react-redux'; -import { AppState } from '../../store'; -import { getUserBalance } from '../../store/reducers'; -import { ConnectedTransactionValidator } from './validator'; +import { useTransactionValidator } from './validator'; -interface OwnProps { - userId: number; - targetUserId: number; +interface Props { + userId: string; + targetUserId: string; value: number; render(isValid: boolean): JSX.Element; } -interface StateProps { - ownBalance: number; - targetBalance: number; -} - -export type UserToUserValidatorProps = StateProps & OwnProps; - -export function UserToUserValidator( - props: UserToUserValidatorProps -): JSX.Element | null { - return ( - ( - { - return ( - <>{props.render(userHasTheMoney && receiverCanAcceptTheMoney)} - ); - }} - /> - )} - /> +export function UserToUserValidator(props: Props): JSX.Element | null { + const userHasTheMoney = useTransactionValidator( + props.value, + props.userId, + false ); + const receiverCanAcceptTheMoney = useTransactionValidator( + props.value, + props.targetUserId, + true + ); + return <>{props.render(userHasTheMoney && receiverCanAcceptTheMoney)} ; } - -const mapStateToProps = (state: AppState, props: OwnProps): StateProps => ({ - ownBalance: getUserBalance(state, props.userId), - targetBalance: getUserBalance(state, props.userId), -}); - -export const ConnectedUserToUserValidator = connect(mapStateToProps)( - UserToUserValidator -); diff --git a/src/components/transaction/validator.tsx b/src/components/transaction/validator.tsx index 5d46b145..2b5f5b09 100644 --- a/src/components/transaction/validator.tsx +++ b/src/components/transaction/validator.tsx @@ -1,11 +1,5 @@ -import * as React from 'react'; -import { connect } from 'react-redux'; -import { AppState, useSettings, useUserBalance } from '../../store'; -import { - Boundary, - getSettingsBalance, - getUserBalance, -} from '../../store/reducers'; +import { useSettings, useUserBalance } from '../../store'; +import { Boundary } from '../../store/reducers'; interface TransactionArguments { accountBoundary: Boundary; @@ -15,33 +9,6 @@ interface TransactionArguments { value: number; } -export const isTransactionValid = ({ - accountBoundary, - paymentBoundary, - isDeposit, - balance, - value, -}: TransactionArguments): boolean => { - if (value === 0) { - return false; - } - if (isDeposit) { - return checkDepositIsValid({ - accountBoundaryValue: accountBoundary.upper, - paymentBoundaryValue: paymentBoundary.upper, - value, - balance, - }); - } else { - return checkDispenseIsValid({ - accountBoundaryValue: accountBoundary.lower, - paymentBoundaryValue: paymentBoundary.lower, - value, - balance, - }); - } -}; - interface CheckValidProps { accountBoundaryValue: number | boolean; paymentBoundaryValue: number | boolean; @@ -93,59 +60,46 @@ function checkDispenseIsValid({ return balance - value > accountBoundaryValue; } -interface OwnProps { - userId?: number; - value: number; - isDeposit: boolean; - render(isValid: boolean): JSX.Element; -} - -interface StateProps { - balance: number | boolean; - accountBoundary: Boundary; - paymentBoundary: Boundary; -} - -export type TransactionValidatorProps = StateProps & OwnProps; - -export function TransactionValidator( - props: TransactionValidatorProps -): JSX.Element | null { - const isValid = isTransactionValid({ - isDeposit: props.isDeposit, - value: props.value, - accountBoundary: props.accountBoundary, - paymentBoundary: props.paymentBoundary, - balance: props.balance, - }); - return <>{props.render(isValid)}; -} - -const mapStateToProps = (state: AppState, props: OwnProps): StateProps => ({ - balance: props.userId - ? getUserBalance(state, props.userId) - : getSettingsBalance(state), - accountBoundary: state.settings.account.boundary, - paymentBoundary: state.settings.payment.boundary, -}); - -export const ConnectedTransactionValidator = connect(mapStateToProps)( - TransactionValidator -); +export const isTransactionValid = ({ + accountBoundary, + paymentBoundary, + isDeposit, + balance, + value, +}: TransactionArguments): boolean => { + if (value === 0) { + return false; + } + if (isDeposit) { + return checkDepositIsValid({ + accountBoundaryValue: accountBoundary.upper, + paymentBoundaryValue: paymentBoundary.upper, + value, + balance, + }); + } else { + return checkDispenseIsValid({ + accountBoundaryValue: accountBoundary.lower, + paymentBoundaryValue: paymentBoundary.lower, + value, + balance, + }); + } +}; export function useTransactionValidator( value: number, - userId: number + userId: string, + isDeposit: boolean = true ): boolean { const settings = useSettings(); const balance = useUserBalance(userId); + return isTransactionValid({ value, balance, - isDeposit: true, + isDeposit, accountBoundary: settings.account.boundary, paymentBoundary: settings.payment.boundary, }); - - return true; } diff --git a/src/components/ui/footer.tsx b/src/components/ui/footer.tsx index e4242a53..317692ea 100644 --- a/src/components/ui/footer.tsx +++ b/src/components/ui/footer.tsx @@ -2,9 +2,7 @@ import { Footer } from 'bricks-of-sand'; import * as React from 'react'; import { GitHubIcon } from '../ui/icons/git-hub'; -export interface MainFooterProps {} - -export function MainFooter(props: MainFooterProps): JSX.Element { +export function MainFooter(): JSX.Element { return (