From d1dbd2a9809682e68389d144bb30e6ed2283ef87 Mon Sep 17 00:00:00 2001 From: joaomlg Date: Wed, 15 Feb 2023 18:31:47 -0300 Subject: [PATCH] feat: transactions screen tabs --- app.json | 2 +- package-lock.json | 42 ++++++- package.json | 6 +- src/contexts/AppContext.tsx | 29 +++-- .../Transactions/TransactionList/index.tsx | 99 ++++++++++++++++ .../Transactions/TransactionList/styles.ts | 35 ++++++ .../Transactions/TransactionTabs/index.tsx | 55 +++++++++ .../Transactions/TransactionTabs/styles.ts | 6 + src/pages/Transactions/index.tsx | 112 ++++++------------ src/pages/Transactions/styles.ts | 34 ------ 10 files changed, 297 insertions(+), 123 deletions(-) create mode 100644 src/pages/Transactions/TransactionList/index.tsx create mode 100644 src/pages/Transactions/TransactionList/styles.ts create mode 100644 src/pages/Transactions/TransactionTabs/index.tsx create mode 100644 src/pages/Transactions/TransactionTabs/styles.ts diff --git a/app.json b/app.json index f2bef91..69b90e8 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Finans", "slug": "finance-app", - "version": "1.1.4", + "version": "1.1.5", "orientation": "portrait", "icon": "./src/assets/icon.png", "userInterfaceStyle": "automatic", diff --git a/package-lock.json b/package-lock.json index 8e2e7fd..fab6c9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "finance-app", - "version": "1.1.4", + "version": "1.1.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "finance-app", - "version": "1.1.4", + "version": "1.1.5", "dependencies": { "@expo-google-fonts/inter": "^0.2.2", "@gorhom/bottom-sheet": "^4.4.5", @@ -28,11 +28,13 @@ "react-native": "0.70.5", "react-native-dotenv": "^3.4.6", "react-native-gesture-handler": "~2.8.0", + "react-native-pager-view": "6.0.1", "react-native-pluggy-connect": "^1.1.0", "react-native-reanimated": "~2.12.0", "react-native-safe-area-context": "4.4.1", "react-native-screens": "~3.18.0", "react-native-svg": "13.4.0", + "react-native-tab-view": "^3.4.0", "react-native-toast-message": "^2.1.5", "react-native-web": "~0.18.7", "react-native-webview": "11.23.1", @@ -12891,6 +12893,15 @@ "resolved": "https://registry.npmjs.org/react-native-gradle-plugin/-/react-native-gradle-plugin-0.70.3.tgz", "integrity": "sha512-oOanj84fJEXUg9FoEAQomA8ISG+DVIrTZ3qF7m69VQUJyOGYyDZmPqKcjvRku4KXlEH6hWO9i4ACLzNBh8gC0A==" }, + "node_modules/react-native-pager-view": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.0.1.tgz", + "integrity": "sha512-kOVNu+4JnR3Gpykviy4WbOAnQz8TgP6O2pRvne221oPUDQLYrvEE/FINR0P85TxbMgvKTPlLejGw0ZHQbezK/g==", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-pluggy-connect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/react-native-pluggy-connect/-/react-native-pluggy-connect-1.1.0.tgz", @@ -12967,6 +12978,19 @@ "react-native": "*" } }, + "node_modules/react-native-tab-view": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-3.4.0.tgz", + "integrity": "sha512-AGvwzkh8+Hq6EuXAkofcrJBTU3LXzns64+Bfrh/80t7rIGCAbd8dFFPPgMASisfCYttpx6FGoRGsGSr8qXjSMQ==", + "dependencies": { + "use-latest-callback": "^0.1.5" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-pager-view": "*" + } + }, "node_modules/react-native-toast-message": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/react-native-toast-message/-/react-native-toast-message-2.1.5.tgz", @@ -25089,6 +25113,12 @@ "resolved": "https://registry.npmjs.org/react-native-gradle-plugin/-/react-native-gradle-plugin-0.70.3.tgz", "integrity": "sha512-oOanj84fJEXUg9FoEAQomA8ISG+DVIrTZ3qF7m69VQUJyOGYyDZmPqKcjvRku4KXlEH6hWO9i4ACLzNBh8gC0A==" }, + "react-native-pager-view": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.0.1.tgz", + "integrity": "sha512-kOVNu+4JnR3Gpykviy4WbOAnQz8TgP6O2pRvne221oPUDQLYrvEE/FINR0P85TxbMgvKTPlLejGw0ZHQbezK/g==", + "requires": {} + }, "react-native-pluggy-connect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/react-native-pluggy-connect/-/react-native-pluggy-connect-1.1.0.tgz", @@ -25138,6 +25168,14 @@ "css-tree": "^1.1.3" } }, + "react-native-tab-view": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-3.4.0.tgz", + "integrity": "sha512-AGvwzkh8+Hq6EuXAkofcrJBTU3LXzns64+Bfrh/80t7rIGCAbd8dFFPPgMASisfCYttpx6FGoRGsGSr8qXjSMQ==", + "requires": { + "use-latest-callback": "^0.1.5" + } + }, "react-native-toast-message": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/react-native-toast-message/-/react-native-toast-message-2.1.5.tgz", diff --git a/package.json b/package.json index 82e4495..a7fb9fa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "finance-app", - "version": "1.1.4", + "version": "1.1.5", "main": "node_modules/expo/AppEntry.js", "scripts": { "start": "expo start", @@ -38,10 +38,12 @@ "react-native-safe-area-context": "4.4.1", "react-native-screens": "~3.18.0", "react-native-svg": "13.4.0", + "react-native-tab-view": "^3.4.0", "react-native-toast-message": "^2.1.5", "react-native-web": "~0.18.7", "react-native-webview": "11.23.1", - "styled-components": "^5.3.6" + "styled-components": "^5.3.6", + "react-native-pager-view": "6.0.1" }, "devDependencies": { "@babel/core": "^7.19.3", diff --git a/src/contexts/AppContext.tsx b/src/contexts/AppContext.tsx index 70cd373..8cb4c43 100644 --- a/src/contexts/AppContext.tsx +++ b/src/contexts/AppContext.tsx @@ -40,7 +40,9 @@ export type AppContextValue = { totalBalance: number; totalInvoice: number; totalInvestment: number; + incomeTransactions: Transaction[]; totalIncomes: number; + expenseTransactions: Transaction[]; totalExpenses: number; }; @@ -99,23 +101,26 @@ export const AppContextProvider: React.FC<{ children: React.ReactNode }> = ({ ch [investments], ); + const incomeTransactions = useMemo( + () => transactions.filter(({ type }) => type === 'CREDIT'), + [transactions], + ); + const totalIncomes = useMemo( () => - transactions.reduce( - (total, transaction) => - transaction.type === 'CREDIT' ? total + Math.abs(transaction.amount) : total, - 0, - ), + incomeTransactions.reduce((total, transaction) => total + Math.abs(transaction.amount), 0), + [incomeTransactions], + ); + + const expenseTransactions = useMemo( + () => transactions.filter(({ type }) => type === 'DEBIT'), [transactions], ); + const totalExpenses = useMemo( () => - transactions.reduce( - (total, transaction) => - transaction.type === 'DEBIT' ? total + Math.abs(transaction.amount) : total, - 0, - ), - [transactions], + expenseTransactions.reduce((total, transaction) => total + Math.abs(transaction.amount), 0), + [expenseTransactions], ); const storeItem = useCallback( @@ -352,7 +357,9 @@ export const AppContextProvider: React.FC<{ children: React.ReactNode }> = ({ ch totalBalance, totalInvoice, totalInvestment, + incomeTransactions, totalIncomes, + expenseTransactions, totalExpenses, }} > diff --git a/src/pages/Transactions/TransactionList/index.tsx b/src/pages/Transactions/TransactionList/index.tsx new file mode 100644 index 0000000..2ca7342 --- /dev/null +++ b/src/pages/Transactions/TransactionList/index.tsx @@ -0,0 +1,99 @@ +import moment from 'moment'; +import React, { useCallback, useRef } from 'react'; +import { ListRenderItemInfo, RefreshControl } from 'react-native'; +import { useTheme } from 'styled-components/native'; +import Divider from '../../../components/Divider'; +import Money from '../../../components/Money'; +import Text from '../../../components/Text'; +import { Transaction } from '../../../services/pluggy'; +import { + ListHeaderContainer, + ListSeparatorContainer, + ListSeparatorDate, + StyledFlatList, + StyledTransactionListItem, +} from './styles'; + +export interface TransactionListProps { + transactions: Transaction[]; + reducedValue: number; + isLoading?: boolean; + onRefresh?: () => void; +} + +const TransactionList: React.FC = ({ + transactions, + reducedValue, + isLoading, + onRefresh, +}) => { + const theme = useTheme(); + + const ItemDividerPreviousDateRef = useRef(moment(0)); + + const renderHeaderComponent = useCallback(() => { + return ( + + + {transactions.length} transações + + + + ); + }, [transactions, reducedValue]); + + const renderListItemSeparator = useCallback((transaction: Transaction, index: number) => { + const date = moment(transaction.date).startOf('day'); + + const component = + index === 0 || date.isBefore(ItemDividerPreviousDateRef.current, 'day') ? ( + + + + {date.format('DD')} + + + {date.format('MMM')} + + + + + ) : ( + <> + ); + + ItemDividerPreviousDateRef.current = date; + + return component; + }, []); + + const renderItem = useCallback( + ({ item, index }: ListRenderItemInfo) => { + return ( + <> + {renderListItemSeparator(item, index)} + + + ); + }, + [renderListItemSeparator], + ); + + return ( + + } + data={transactions} + renderItem={renderItem} + keyExtractor={(item) => item.id} + ListHeaderComponent={renderHeaderComponent} + /> + ); +}; + +export default React.memo(TransactionList); diff --git a/src/pages/Transactions/TransactionList/styles.ts b/src/pages/Transactions/TransactionList/styles.ts new file mode 100644 index 0000000..150af62 --- /dev/null +++ b/src/pages/Transactions/TransactionList/styles.ts @@ -0,0 +1,35 @@ +import { FlatList, FlatListProps } from 'react-native'; +import styled from 'styled-components/native'; +import FlexContainer from '../../../components/FlexContainer'; +import TransactionListItem from '../../../components/TransactionListItem'; +import { Transaction } from '../../../services/pluggy'; + +export const StyledFlatList = styled( + FlatList as new (props: FlatListProps) => FlatList, +).attrs(() => ({ + contentContainerStyle: { + padding: 24, + }, +}))` + flex: 1; + background-color: ${({ theme }) => theme.colors.backgroundWhite}; +`; + +export const ListHeaderContainer = styled(FlexContainer).attrs({ gap: 4 })` + margin-bottom: 12px; +`; + +export const ListSeparatorContainer = styled.View` + flex-direction: row; + align-items: center; + margin: 12px 0; +`; + +export const ListSeparatorDate = styled.View` + align-items: center; + margin-right: 12px; +`; + +export const StyledTransactionListItem = styled(TransactionListItem)` + padding: 12px 0; +`; diff --git a/src/pages/Transactions/TransactionTabs/index.tsx b/src/pages/Transactions/TransactionTabs/index.tsx new file mode 100644 index 0000000..e1aa171 --- /dev/null +++ b/src/pages/Transactions/TransactionTabs/index.tsx @@ -0,0 +1,55 @@ +import React, { useState } from 'react'; +import { TabBar, TabBarProps, TabView } from 'react-native-tab-view'; +import { useTheme } from 'styled-components/native'; +import Text from '../../../components/Text'; +import { TabLazyPlaceholder } from './styles'; + +type TransactionTabsRouteKey = 'default' | 'incomes' | 'expenses'; + +export type TransactionTabsRoute = { + key: TransactionTabsRouteKey; + title: string; +}; + +export interface TransactionTabsProps { + renderScene: (props: { route: TransactionTabsRoute }) => React.ReactNode; +} + +const TransactionTabs: React.FC = ({ renderScene }) => { + const theme = useTheme(); + + const [index, setIndex] = useState(0); + + const routes: TransactionTabsRoute[] = [ + { key: 'default', title: 'Tudo' }, + { key: 'incomes', title: 'Entradas' }, + { key: 'expenses', title: 'Saídas' }, + ]; + + const renderTabBar = (props: TabBarProps) => ( + ( + + {route.title} + + )} + /> + ); + + return ( + route.key !== 'default'} + renderLazyPlaceholder={() => } + /> + ); +}; + +export default TransactionTabs; diff --git a/src/pages/Transactions/TransactionTabs/styles.ts b/src/pages/Transactions/TransactionTabs/styles.ts new file mode 100644 index 0000000..d323f2c --- /dev/null +++ b/src/pages/Transactions/TransactionTabs/styles.ts @@ -0,0 +1,6 @@ +import styled from 'styled-components/native'; + +export const TabLazyPlaceholder = styled.View` + flex: 1; + background-color: ${({ theme }) => theme.colors.backgroundWhite}; +`; diff --git a/src/pages/Transactions/index.tsx b/src/pages/Transactions/index.tsx index fd9ce7c..dcec0b8 100644 --- a/src/pages/Transactions/index.tsx +++ b/src/pages/Transactions/index.tsx @@ -1,22 +1,11 @@ -import moment from 'moment'; -import React, { useCallback, useContext, useRef } from 'react'; -import { ListRenderItemInfo, RefreshControl } from 'react-native'; -import { useTheme } from 'styled-components/native'; -import Divider from '../../components/Divider'; -import Money from '../../components/Money'; +import React, { useCallback, useContext } from 'react'; import ScreenContainer from '../../components/ScreenContainer'; -import Text from '../../components/Text'; import AppContext from '../../contexts/AppContext'; import { Transaction } from '../../services/pluggy'; import { formatMonthYearDate } from '../../utils/date'; -import { - ListHeaderContainer, - ListSeparatorContainer, - ListSeparatorDate, - StyledHeader, - StyledTransactionListItem, - TransactionList, -} from './styles'; +import { StyledHeader } from './styles'; +import TransactionList from './TransactionList'; +import TransactionTabs, { TransactionTabsRoute } from './TransactionTabs'; const Transactions: React.FC = () => { const { @@ -26,60 +15,49 @@ const Transactions: React.FC = () => { transactions, fetchTransactions, date, + incomeTransactions, totalIncomes, + expenseTransactions, totalExpenses, } = useContext(AppContext); - const theme = useTheme(); + const renderScene = useCallback( + ({ route }: { route: TransactionTabsRoute }) => { + let data: Transaction[]; + let balance: number; - const ItemDividerPreviousDateRef = useRef(moment(0)); + switch (route.key) { + case 'incomes': + data = incomeTransactions; + balance = totalIncomes; + break; + case 'expenses': + data = expenseTransactions; + balance = totalExpenses; + break; + default: + data = transactions; + balance = totalIncomes - totalExpenses; + } - const renderHeaderComponent = useCallback(() => { - return ( - - - {transactions.length} transações - - - - ); - }, [transactions, totalIncomes, totalExpenses]); - - const renderListItemSeparator = useCallback((transaction: Transaction, index: number) => { - const date = moment(transaction.date).startOf('day'); - - const component = - index === 0 || date.isBefore(ItemDividerPreviousDateRef.current, 'day') ? ( - - - - {date.format('DD')} - - - {date.format('MMM')} - - - - - ) : ( - <> - ); - - ItemDividerPreviousDateRef.current = date; - - return component; - }, []); - - const renderItem = useCallback( - ({ item, index }: ListRenderItemInfo) => { return ( - <> - {renderListItemSeparator(item, index)} - - + ); }, - [renderListItemSeparator], + [ + expenseTransactions, + fetchTransactions, + incomeTransactions, + isLoading, + totalExpenses, + totalIncomes, + transactions, + ], ); return ( @@ -93,19 +71,7 @@ const Transactions: React.FC = () => { }, ]} /> - - } - data={transactions} - renderItem={renderItem} - keyExtractor={(item) => item.id} - ListHeaderComponent={renderHeaderComponent} - /> + ); }; diff --git a/src/pages/Transactions/styles.ts b/src/pages/Transactions/styles.ts index 2ba519a..ad4622f 100644 --- a/src/pages/Transactions/styles.ts +++ b/src/pages/Transactions/styles.ts @@ -1,41 +1,7 @@ -import { FlatList, FlatListProps } from 'react-native'; import styled from 'styled-components/native'; -import FlexContainer from '../../components/FlexContainer'; import Header from '../../components/Header'; -import TransactionListItem from '../../components/TransactionListItem'; -import { Transaction } from '../../services/pluggy'; export const StyledHeader = styled(Header)` padding: 24px; padding-left: 16px; `; - -export const ListHeaderContainer = styled(FlexContainer).attrs({ gap: 4 })` - margin-bottom: 12px; -`; - -export const TransactionList = styled( - FlatList as new (props: FlatListProps) => FlatList, -).attrs(() => ({ - contentContainerStyle: { - padding: 24, - }, -}))` - flex: 1; - background-color: ${({ theme }) => theme.colors.backgroundWhite}; -`; - -export const ListSeparatorContainer = styled.View` - flex-direction: row; - align-items: center; - margin: 12px 0; -`; - -export const ListSeparatorDate = styled.View` - align-items: center; - margin-right: 12px; -`; - -export const StyledTransactionListItem = styled(TransactionListItem)` - padding: 12px 0; -`;