Skip to content

Commit 20479a5

Browse files
feat(home): Improve home screen card components
- Update FriendsCard with better styling and layout - Enhance InvestmentsBalanceCard with improved visual hierarchy - Refine RewardsCard component styling - Update SavingsBalanceCard with better spacing and typography - Improve StablesBalanceList component layout - Clean up home screen component structure and spacing
1 parent 49dd401 commit 20479a5

File tree

6 files changed

+133
-121
lines changed

6 files changed

+133
-121
lines changed

packages/app/features/home/FriendsCard.tsx

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@ import { HomeBodyCard } from './screen'
1717

1818
export const FriendsCard = ({ href, ...props }: Omit<CardProps & LinkProps, 'children'>) => {
1919
const linkProps = useLink({ href })
20+
const limit = 3
21+
const { data, isLoading } = useFriends(limit)
22+
const friendsCount = data?.friends.length ?? 0
2023

2124
return (
2225
<HomeBodyCard {...linkProps} {...props}>
2326
<Card.Header padded pb={0} fd="row" ai="center" jc="space-between">
2427
<Paragraph fontSize={'$5'} fontWeight="400">
25-
Friends
28+
{isLoading ? '' : friendsCount <= 0 ? 'Invite Friends' : 'Friends'}
2629
</Paragraph>
2730
<XStack flex={1} />
2831
<ChevronRight
@@ -32,8 +35,27 @@ export const FriendsCard = ({ href, ...props }: Omit<CardProps & LinkProps, 'chi
3235
/>
3336
</Card.Header>
3437
<Card.Footer padded pt={0} fd="column">
35-
<FriendsPreview />
36-
<Paragraph color={'$color10'}>Network with Future Cash</Paragraph>
38+
{isLoading ? (
39+
<Spinner size="small" />
40+
) : (
41+
<XStack ai="center" jc="space-between">
42+
{data?.friends && <OverlappingFriendAvatars friends={data.friends} />}
43+
<ThemeableStack
44+
circular
45+
ai="center"
46+
jc="center"
47+
bc="$color0"
48+
w={'$3.5'}
49+
h="$3.5"
50+
mih={0}
51+
miw={0}
52+
>
53+
<Paragraph fontSize={'$4'} fontWeight="500">
54+
{`${data?.count ?? 0}`}
55+
</Paragraph>
56+
</ThemeableStack>
57+
</XStack>
58+
)}
3759
</Card.Footer>
3860
</HomeBodyCard>
3961
)
@@ -44,39 +66,6 @@ type Friend = {
4466
avatar_url?: string
4567
}
4668

47-
function FriendsPreview({ limit = 3 }: { limit?: number }) {
48-
const { data, isLoading } = useFriends(limit)
49-
if (isLoading) return <Spinner size="small" />
50-
51-
const friendsArray = data?.friends || []
52-
const filledFriends: Friend[] = [...friendsArray, ...Array(3 - friendsArray.length).fill({})]
53-
54-
return (
55-
<XStack
56-
ai="center"
57-
jc="space-between"
58-
/* hack to match the height of the rewards $ card */
59-
h={50}
60-
>
61-
<OverlappingFriendAvatars friends={filledFriends} />
62-
<ThemeableStack
63-
circular
64-
ai="center"
65-
jc="center"
66-
bc="$color0"
67-
w={'$3.5'}
68-
h="$3.5"
69-
mih={0}
70-
miw={0}
71-
>
72-
<Paragraph fontSize={'$4'} fontWeight="500">
73-
{`${data?.count ?? 0}`}
74-
</Paragraph>
75-
</ThemeableStack>
76-
</XStack>
77-
)
78-
}
79-
8069
function OverlappingFriendAvatars({ friends, ...props }: { friends: Friend[] } & XStackProps) {
8170
return (
8271
<XStack ai="center" {...props}>

packages/app/features/home/InvestmentsBalanceCard.tsx

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const InvestmentsBalanceCard = (props: CardProps) => {
5555

5656
return (
5757
<HomeBodyCard onPress={toggleSubScreen} {...props}>
58-
<Card.Header padded pb={0} fd="row" ai="center" jc="space-between">
58+
<Card.Header padded pb="$4" jc="space-between" fd="row">
5959
<Paragraph fontSize={'$5'} fontWeight="400">
6060
Invest
6161
</Paragraph>
@@ -74,9 +74,9 @@ export const InvestmentsBalanceCard = (props: CardProps) => {
7474
/>
7575
)}
7676
</Card.Header>
77-
<Card.Footer padded pt={0} jc="space-between" ai="center">
78-
<YStack jc="space-between">
79-
<Paragraph color={'$color12'} fontWeight={500} size={'$10'}>
77+
<Card.Footer padded size="$4" pt={0} jc="space-between" ai="center">
78+
<YStack jc="space-between" gap="$4">
79+
<Paragraph color={'$color12'} fontWeight={600} size={'$9'}>
8080
{(() => {
8181
switch (true) {
8282
case isPriceHidden:
@@ -102,26 +102,44 @@ function InvestmentsPreview() {
102102

103103
if (isLoading) return <Spinner size="small" />
104104

105-
const existingSymbols = new Set(investmentCoins.map((coin) => coin.symbol))
106-
const coins = [
107-
...investmentCoins,
108-
...investmentCoinsList
109-
.filter((coin) => !existingSymbols.has(coin.symbol))
110-
.map((coin) => ({ ...coin, balance: 0n })),
111-
]
105+
// Get SEND token
106+
const sendCoin = investmentCoinsList.find((coin) => coin.symbol === 'SEND')
107+
if (!sendCoin) return null
112108

113-
const sortedByBalance = [...coins].sort((a, b) =>
109+
// Filter coins that have a balance > 0 (excluding SEND to handle separately)
110+
const ownedCoins = investmentCoins.filter(
111+
(coin) => coin.balance && coin.balance > 0n && coin.symbol !== 'SEND'
112+
)
113+
114+
// Get SEND token with its actual balance or 0
115+
const sendCoinWithBalance = investmentCoins.find((coin) => coin.symbol === 'SEND') || {
116+
...sendCoin,
117+
balance: 0n,
118+
}
119+
120+
// Sort owned coins by balance (highest first)
121+
const sortedOwnedCoins = ownedCoins.sort((a, b) =>
114122
(b?.balance ?? 0n) > (a?.balance ?? 0n) ? 1 : -1
115123
)
116124

125+
// Always start with SEND token, then add other owned tokens
126+
const allCoinsToShow = [sendCoinWithBalance, ...sortedOwnedCoins]
127+
128+
// Show up to 3 tokens total (SEND + 2 others max)
129+
const maxDisplay = 3
130+
const coinsToShow = allCoinsToShow.slice(0, maxDisplay)
131+
const remainingCount = allCoinsToShow.length - maxDisplay
132+
117133
return (
118-
<XStack ai="center">
119-
<OverlappingCoinIcons coins={sortedByBalance} />
120-
<ThemeableStack circular ai="center" jc="center" bc="$color0" w={'$3.5'} h="$3.5">
121-
<Paragraph fontSize={'$4'} fontWeight="500">
122-
{`+${investmentCoinsList.length - 3}`}
123-
</Paragraph>
124-
</ThemeableStack>
134+
<XStack ai="center" mr={remainingCount > 0 ? '$0' : '$3.5'}>
135+
<OverlappingCoinIcons coins={coinsToShow} length={coinsToShow.length} />
136+
{remainingCount > 0 ? (
137+
<ThemeableStack circular ai="center" jc="center" bc="$color0" w={'$3.5'} h="$3.5">
138+
<Paragraph fontSize={'$4'} fontWeight="500">
139+
+{remainingCount}
140+
</Paragraph>
141+
</ThemeableStack>
142+
) : null}
125143
</XStack>
126144
)
127145
}
@@ -133,8 +151,15 @@ function OverlappingCoinIcons({
133151
}: { coins: CoinWithBalance[]; length?: number } & XStackProps) {
134152
return (
135153
<XStack ai="center" {...props}>
136-
{coins.slice(0, length).map(({ symbol }) => (
137-
<ThemeableStack key={symbol} circular mr={'$-3.5'} bc="transparent" ai="center" jc="center">
154+
{coins.slice(0, length).map(({ symbol }, index) => (
155+
<ThemeableStack
156+
key={symbol}
157+
circular
158+
mr={index === coins.slice(0, length).length - 1 ? '$0' : '$-3.5'}
159+
bc="transparent"
160+
ai="center"
161+
jc="center"
162+
>
138163
<IconCoin size={'$3'} symbol={symbol} />
139164
</ThemeableStack>
140165
))}

packages/app/features/home/RewardsCard.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export const RewardsCard = ({ href, ...props }: Omit<CardProps & LinkProps, 'chi
4545
/>
4646
</Card.Header>
4747
<Card.Footer padded pt={0} fd="column">
48-
<Paragraph color={'$color12'} fontWeight={500} size={'$10'}>
48+
<Paragraph color={'$color12'} fontWeight={600} size={'$9'}>
4949
{(() => {
5050
switch (true) {
5151
case isPriceHidden:
@@ -57,7 +57,6 @@ export const RewardsCard = ({ href, ...props }: Omit<CardProps & LinkProps, 'chi
5757
}
5858
})()}
5959
</Paragraph>
60-
<Paragraph color={'$color10'}>Complete Tasks for $SEND Back</Paragraph>
6160
</Card.Footer>
6261
</HomeBodyCard>
6362
)

packages/app/features/home/SavingsBalanceCard.tsx

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,40 @@ import formatAmount from 'app/utils/formatAmount'
33

44
import { ChevronRight } from '@tamagui/lucide-icons'
55
import { useMemo } from 'react'
6-
import { useSendEarnBalances, useVaultConvertSharesToAssets } from '../earn/hooks'
6+
import { useSendEarnAPY } from '../earn/hooks'
7+
import { useSendEarnCoin } from '../earn/providers/SendEarnProvider'
78
import { useIsPriceHidden } from './utils/useIsPriceHidden'
89
import { formatUnits } from 'viem'
910
import { type LinkProps, useLink } from 'solito/link'
1011
import { HomeBodyCard } from './screen'
12+
import { usdcCoin } from 'app/data/coins'
1113

1214
export const SavingsBalanceCard = ({ href, ...props }: Omit<CardProps & LinkProps, 'children'>) => {
1315
const linkProps = useLink({ href })
1416
const { isPriceHidden } = useIsPriceHidden()
15-
const { data: balances, isLoading } = useSendEarnBalances()
16-
// Extract vaults and shares from balances for conversion
17-
const vaults =
18-
balances
19-
?.filter((balance) => balance.shares > 0n && balance.log_addr !== null)
20-
.map((balance) => balance.log_addr) || []
2117

22-
const shares =
23-
balances
24-
?.filter((balance) => balance.shares > 0n && balance.log_addr !== null)
25-
.map((balance) => balance.shares) || []
18+
// Use the SendEarnProvider pattern
19+
const { coinBalances, getTotalAssets } = useSendEarnCoin(usdcCoin)
20+
const { totalCurrentValue, vaults } = getTotalAssets()
2621

27-
// Use the hook to get current asset values based on onchain rate
28-
const currentAssets = useVaultConvertSharesToAssets({ vaults, shares })
22+
const hasExistingDeposit = totalCurrentValue > 0n
2923

30-
const totalAssets = useMemo(
31-
() => formatUSDCValue(currentAssets.data?.reduce((sum, assets) => sum + assets, 0n) ?? 0n),
32-
[currentAssets.data]
33-
)
24+
// Only fetch APY if user has existing deposits
25+
const { data: apyData, isLoading: isApyLoading } = useSendEarnAPY({
26+
vault: hasExistingDeposit && vaults?.[0] ? vaults[0] : undefined,
27+
})
28+
29+
const totalAssets = useMemo(() => {
30+
if (!hasExistingDeposit) return formatUSDCValue(0n)
31+
return formatUSDCValue(totalCurrentValue)
32+
}, [hasExistingDeposit, totalCurrentValue])
33+
34+
// Single loader for both values
35+
const isLoading = coinBalances.isLoading || (hasExistingDeposit && isApyLoading)
3436

3537
return (
3638
<HomeBodyCard {...linkProps} {...props}>
37-
<Card.Header padded pb={0} fd="row" ai="center" jc="space-between">
39+
<Card.Header padded pb="$4" jc="space-between" fd="row">
3840
<Paragraph fontSize={'$5'} fontWeight="400">
3941
Save
4042
</Paragraph>
@@ -45,20 +47,21 @@ export const SavingsBalanceCard = ({ href, ...props }: Omit<CardProps & LinkProp
4547
$theme-light={{ color: '$darkGrayTextField' }}
4648
/>
4749
</Card.Header>
48-
<Card.Footer padded pt={0} fd="column">
49-
<Paragraph color={'$color12'} fontWeight={500} size={'$10'}>
50-
{(() => {
51-
switch (true) {
52-
case isPriceHidden:
53-
return '///////'
54-
case isLoading || !balances:
55-
return <Spinner size={'large'} color={'$color12'} />
56-
default:
57-
return `$${totalAssets}`
58-
}
59-
})()}
60-
</Paragraph>
61-
<Paragraph color={'$color10'}>Up to 12% Interest</Paragraph>
50+
<Card.Footer padded size="$4" pt={0} fd="column" gap="$4">
51+
{isLoading ? (
52+
<Spinner size={'large'} color={'$color12'} />
53+
) : (
54+
<>
55+
<Paragraph color={'$color12'} fontWeight={600} size={'$9'}>
56+
{isPriceHidden ? '///////' : `$${totalAssets}`}
57+
</Paragraph>
58+
<Paragraph color={'$color10'}>
59+
{hasExistingDeposit
60+
? `Earning ${apyData?.baseApy.toFixed(2)}%`
61+
: 'Up to 12% Interest'}
62+
</Paragraph>
63+
</>
64+
)}
6265
</Card.Footer>
6366
</HomeBodyCard>
6467
)

packages/app/features/home/StablesBalanceList.tsx

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useHoverStyles } from 'app/utils/useHoverStyles'
1010
import { convertBalanceToFiat } from 'app/utils/convertBalanceToUSD'
1111
import { useTokenPrices } from 'app/utils/useTokenPrices'
1212
import { useIsPriceHidden } from 'app/features/home/utils/useIsPriceHidden'
13+
import { ChevronRight } from '@tamagui/lucide-icons'
1314

1415
export const StablesBalanceList = () => {
1516
const { stableCoins, isLoading } = useCoins()
@@ -42,13 +43,6 @@ const TokenBalanceItem = ({
4243
}: {
4344
coin: CoinWithBalance
4445
} & Omit<LinkProps, 'children'>) => {
45-
const { data: tokenPrices, isLoading: isLoadingTokenPrices } = useTokenPrices()
46-
const balanceInUSD = convertBalanceToFiat(
47-
coin,
48-
coin.symbol === 'USDC' ? 1 : tokenPrices?.[coin.token]
49-
)
50-
const { isPriceHidden } = useIsPriceHidden()
51-
5246
return (
5347
<Link display="flex" {...props}>
5448
<XStack f={1} gap={'$3.5'} ai={'center'}>
@@ -58,37 +52,35 @@ const TokenBalanceItem = ({
5852
<Paragraph fontSize={'$6'} fontWeight={'500'} color={'$color12'}>
5953
{coin.shortLabel || coin.label}
6054
</Paragraph>
61-
<TokenBalance coin={coin} />
6255
</XStack>
63-
<XStack jc={'space-between'} ai={'center'}>
56+
<XStack jc={'space-between'} ai={'center'} miw={0}>
6457
<Paragraph
6558
fontSize={'$5'}
6659
color={'$lightGrayTextField'}
6760
$theme-light={{ color: '$darkGrayTextField' }}
6861
>
69-
{(() => {
70-
switch (true) {
71-
case isLoadingTokenPrices || balanceInUSD === undefined:
72-
return '$0.00'
73-
case isPriceHidden:
74-
return '///////'
75-
default:
76-
return `$${formatAmount(balanceInUSD, 12, 2)}`
77-
}
78-
})()}
62+
Base
7963
</Paragraph>
8064
</XStack>
8165
</YStack>
66+
<XStack ai={'center'} gap="$2">
67+
<TokenBalance coin={coin} />
68+
<ChevronRight size={'$1'} color={'$color12'} />
69+
</XStack>
8270
</XStack>
8371
</Link>
8472
)
8573
}
8674

8775
const TokenBalance = ({
88-
coin: { decimals, balance, formatDecimals },
76+
coin,
8977
}: {
9078
coin: CoinWithBalance
9179
}) => {
80+
const { balance, symbol } = coin
81+
const { data: tokenPrices, isLoading: isLoadingTokenPrices } = useTokenPrices()
82+
const balanceInUSD = convertBalanceToFiat(coin, symbol === 'USDC' ? 1 : tokenPrices?.[coin.token])
83+
9284
const { isPriceHidden } = useIsPriceHidden()
9385

9486
if (balance === undefined) return <></>
@@ -99,9 +91,16 @@ const TokenBalance = ({
9991
col="$color12"
10092
$gtSm={{ fontSize: '$8', fontWeight: '600' }}
10193
>
102-
{isPriceHidden
103-
? '//////'
104-
: formatAmount((Number(balance) / 10 ** decimals).toString(), 10, formatDecimals ?? 5)}
94+
{(() => {
95+
switch (true) {
96+
case isLoadingTokenPrices || balanceInUSD === undefined:
97+
return '$0.00'
98+
case isPriceHidden:
99+
return '///////'
100+
default:
101+
return `$${formatAmount(balanceInUSD, 12, 2)}`
102+
}
103+
})()}
105104
</Paragraph>
106105
)
107106
}

0 commit comments

Comments
 (0)