-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create initial portfolio screen #15
Changes from all commits
1ed27e9
51f26ab
9081dcc
1b65bf8
273ec4e
e77749f
38fbb20
f215a4c
46bd5be
d03663f
1b6f291
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { Trans } from '@lingui/react/macro'; | ||
import { Sx, Text, View } from 'dripsy'; | ||
import Button from '../Button'; | ||
import { useAppDispatch, useAppSelector } from '@/store/hooks'; | ||
import { close, open } from '@/store/depositFlow'; | ||
import DepositFlow from '../DepositFlow'; | ||
|
||
/** | ||
* Renders the user's current balance, as well as buttons for actions related to | ||
* their balance (such as Send/Deposit/Request). | ||
*/ | ||
export default function BalanceAndActions() { | ||
const dispatch = useAppDispatch(); | ||
const isOpen = useAppSelector(state => state.depositFlow.isOpen); | ||
|
||
return ( | ||
<> | ||
<View sx={sx.root}> | ||
<View sx={sx.balanceWrapper}> | ||
<Text sx={sx.balanceLabel}> | ||
<Trans>Balance</Trans> | ||
</Text> | ||
<Text sx={sx.balance}>0.00 USDC</Text> | ||
</View> | ||
|
||
<View sx={sx.buttons}> | ||
<Button actionType='accent' onPress={() => dispatch(open())}> | ||
<Trans>Deposit</Trans> | ||
</Button> | ||
<Button> | ||
<Trans>Request</Trans> | ||
</Button> | ||
</View> | ||
</View> | ||
|
||
<DepositFlow isOpen={isOpen} onClose={() => dispatch(close())} /> | ||
</> | ||
); | ||
} | ||
|
||
const sx = { | ||
balance: { | ||
variant: 'text.h4', | ||
}, | ||
|
||
balanceLabel: { | ||
variant: 'text.small', | ||
|
||
color: 'neutralLight', | ||
}, | ||
|
||
balanceWrapper: { | ||
flexDirection: 'column', | ||
alignItems: 'center', | ||
justifyContent: 'center', | ||
flexGrow: 1, | ||
}, | ||
|
||
buttons: { | ||
flexDirection: 'row', | ||
flexGrow: 0, | ||
gap: '$2', | ||
px: '$4', | ||
pb: '$4', | ||
}, | ||
|
||
root: { | ||
flexGrow: 1, | ||
flexDirection: 'column', | ||
}, | ||
} satisfies Record<string, Sx>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,72 +1,21 @@ | ||
import Button from '../Button'; | ||
import { Sx, Text, View } from 'dripsy'; | ||
import DepositFlow from '../DepositFlow'; | ||
import { useAppDispatch, useAppSelector } from '@/store/hooks'; | ||
import { close, open } from '@/store/depositFlow'; | ||
import { Trans } from '@lingui/react/macro'; | ||
import { Sx, View } from 'dripsy'; | ||
import HomeScreenTransactionsList from './HomeScreenTransactionsList'; | ||
import BalanceAndActions from '../BalanceAndActions'; | ||
|
||
export default function HomeScreen() { | ||
const dispatch = useAppDispatch(); | ||
const isOpen = useAppSelector(state => state.depositFlow.isOpen); | ||
|
||
return ( | ||
<> | ||
<View sx={sx.root}> | ||
{/** @todo: Make this a `ScrollView`. */} | ||
<View sx={sx.root}> | ||
<View sx={sx.balanceWrapper}> | ||
<Text sx={sx.balanceLabel}> | ||
<Trans>Balance</Trans> | ||
</Text> | ||
<Text sx={sx.balance}>0.00 USDC</Text> | ||
</View> | ||
|
||
<View sx={sx.buttons}> | ||
<Button actionType='accent' onPress={() => dispatch(open())}> | ||
<Trans>Deposit</Trans> | ||
</Button> | ||
<Button> | ||
<Trans>Request</Trans> | ||
</Button> | ||
</View> | ||
<BalanceAndActions /> | ||
|
||
<HomeScreenTransactionsList /> | ||
</View> | ||
|
||
<DepositFlow isOpen={isOpen} onClose={() => dispatch(close())} /> | ||
</> | ||
<HomeScreenTransactionsList /> | ||
</View> | ||
); | ||
} | ||
|
||
const sx = { | ||
balance: { | ||
variant: 'text.h4', | ||
}, | ||
|
||
balanceLabel: { | ||
variant: 'text.small', | ||
|
||
color: 'neutralLight', | ||
}, | ||
|
||
balanceWrapper: { | ||
flexDirection: 'column', | ||
alignItems: 'center', | ||
justifyContent: 'center', | ||
flexGrow: 1, | ||
}, | ||
|
||
buttons: { | ||
flexDirection: 'row', | ||
flexGrow: 0, | ||
gap: '$2', | ||
px: '$4', | ||
pb: '$4', | ||
}, | ||
|
||
root: { | ||
flexGrow: 1, | ||
flexDirection: 'column', | ||
px: 'screenHorizontalMargin', | ||
}, | ||
} satisfies Record<string, Sx>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import ActionSheet from '@/components/ActionSheet'; | ||
import AssetIcon from '@/components/AssetIcon'; | ||
import TransactionList from '@/components/TransactionList'; | ||
import { useAppDispatch, useAppSelector } from '@/store/hooks'; | ||
import { setSelectedAssetSymbol } from '@/store/portfolioScreen'; | ||
import { Sx, Text, View } from 'dripsy'; | ||
import useTransactionsForAsset from './useTransactionsForAsset'; | ||
|
||
/** | ||
* An action sheet for a given asset, with a list of relevant transactions and | ||
* buttons for actions related to the asset (such as sending). | ||
*/ | ||
export default function AssetActionSheet() { | ||
const selectedAssetSymbol = useAppSelector(state => state.portfolioScreen.selectedAssetSymbol); | ||
const balance = useAppSelector(state => | ||
state.balances.balances.find(balance => balance.assetSymbol === selectedAssetSymbol), | ||
); | ||
const dispatch = useAppDispatch(); | ||
const transactions = useTransactionsForAsset(selectedAssetSymbol); | ||
|
||
return ( | ||
<ActionSheet | ||
isOpen={!!selectedAssetSymbol} | ||
onClose={() => dispatch(setSelectedAssetSymbol(undefined))} | ||
> | ||
<View sx={sx.header}> | ||
<View sx={sx.assetIconWrapper}> | ||
<AssetIcon /> | ||
</View> | ||
|
||
<Text sx={sx.balance}> | ||
{balance?.amount} {balance?.assetSymbol} | ||
</Text> | ||
|
||
<Text sx={sx.equivalentValue}>{balance?.equivalentValue} USDC</Text> | ||
</View> | ||
|
||
<TransactionList transactions={transactions} showTitle /> | ||
</ActionSheet> | ||
); | ||
} | ||
|
||
const sx = { | ||
assetIconWrapper: { | ||
pb: '$2', | ||
}, | ||
|
||
balance: { | ||
variant: 'text.h4', | ||
|
||
textAlign: 'center', | ||
}, | ||
|
||
equivalentValue: { | ||
variant: 'text.small', | ||
|
||
color: 'neutralLight', | ||
textAlign: 'center', | ||
}, | ||
|
||
header: { | ||
flexDirection: 'column', | ||
alignItems: 'center', | ||
justifyContent: 'center', | ||
flexGrow: 1, | ||
gap: '$1', | ||
mb: '$4', | ||
}, | ||
} satisfies Record<string, Sx>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { useAppSelector } from '@/store/hooks'; | ||
import { useMemo } from 'react'; | ||
|
||
/** Returns a memoized array of the transactions for a given asset. */ | ||
export default function useTransactionsForAsset( | ||
/** The symbol of the asset that transactions should be filtered by. */ | ||
assetSymbol?: string, | ||
) { | ||
const transactions = useAppSelector(state => state.transactions.transactions); | ||
|
||
const transactionsForAsset = useMemo( | ||
() => | ||
assetSymbol | ||
? transactions.filter(transaction => transaction.assetSymbol === assetSymbol) | ||
: [], | ||
[transactions, assetSymbol], | ||
); | ||
|
||
return transactionsForAsset; | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { Meta, StoryObj } from '@storybook/react'; | ||
|
||
import Balance from '.'; | ||
import balanceFactory from '@/factories/balance'; | ||
|
||
const meta: Meta<typeof Balance> = { | ||
component: Balance, | ||
tags: ['autodocs'], | ||
argTypes: { | ||
balance: { control: false }, | ||
}, | ||
}; | ||
|
||
export default meta; | ||
|
||
export const Basic: StoryObj<typeof Balance> = { | ||
args: { | ||
balance: balanceFactory.build(), | ||
}, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import AssetIcon from '@/components/AssetIcon'; | ||
import ListItem from '@/components/ListItem'; | ||
import { useAppDispatch } from '@/store/hooks'; | ||
import { setSelectedAssetSymbol } from '@/store/portfolioScreen'; | ||
import IBalance from '@/types/Balance'; | ||
import { Sx, Text, View } from 'dripsy'; | ||
|
||
export interface BalanceProps { | ||
balance: IBalance; | ||
} | ||
|
||
export default function Balance({ balance }: BalanceProps) { | ||
const dispatch = useAppDispatch(); | ||
|
||
return ( | ||
<ListItem | ||
avatar={<AssetIcon />} | ||
primaryText={balance.assetSymbol} | ||
secondaryText={balance.assetName} | ||
suffix={ | ||
<View sx={sx.suffix}> | ||
<Text variant='small'>{balance.amount}</Text> | ||
<Text sx={sx.equivalentValue}>{balance.equivalentValue} USDC</Text> | ||
</View> | ||
} | ||
onPress={() => dispatch(setSelectedAssetSymbol(balance.assetSymbol))} | ||
/> | ||
); | ||
} | ||
|
||
const sx = { | ||
equivalentValue: { | ||
variant: 'text.detail', | ||
color: 'neutralLight', | ||
}, | ||
|
||
suffix: { | ||
flexDirection: 'column', | ||
alignItems: 'flex-end', | ||
}, | ||
} satisfies Record<string, Sx>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import List from '@/components/List'; | ||
import IBalance from '@/types/Balance'; | ||
import { useLingui } from '@lingui/react/macro'; | ||
import Balance from './Balance'; | ||
import AssetActionSheet from './AssetActionSheet'; | ||
|
||
export interface BalanceListProps { | ||
balances: IBalance[]; | ||
} | ||
|
||
/** Shows a list of the user's balances in every asset they hold. */ | ||
export default function BalanceList({ balances }: BalanceListProps) { | ||
const { t } = useLingui(); | ||
|
||
return ( | ||
<> | ||
<List title={t`Assets`}> | ||
{balances.map(balance => ( | ||
<Balance key={balance.assetSymbol} balance={balance} /> | ||
))} | ||
</List> | ||
|
||
<AssetActionSheet /> | ||
</> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,24 @@ | ||
import { Text } from 'dripsy'; | ||
import { Sx, View } from 'dripsy'; | ||
import BalanceAndActions from '../BalanceAndActions'; | ||
import BalanceList from './BalanceList'; | ||
import { useAppSelector } from '@/store/hooks'; | ||
|
||
export default function PortfolioScreen() { | ||
return <Text>PortfolioScreen</Text>; | ||
const balances = useAppSelector(state => state.balances.balances); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: general thoughts on identifying areas that could take advantage of memoized selectors, using reselect, to avoid unnecessary re-renders? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call — I've actually started doing that in #19 |
||
|
||
return ( | ||
<View sx={sx.root}> | ||
{/** @todo: Make this a `ScrollView`. */} | ||
<BalanceAndActions /> | ||
|
||
<BalanceList balances={balances} /> | ||
</View> | ||
); | ||
} | ||
|
||
const sx = { | ||
root: { | ||
flexGrow: 1, | ||
px: 'screenHorizontalMargin', | ||
}, | ||
} satisfies Record<string, Sx>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needed because
<Balance />
, which has a Storybook story, dispatches Redux actions