Skip to content

Commit 9d88f82

Browse files
committed
feat: add chain filter pills with client-side filtering (All, Base, Solana, Ethereum)
1 parent 67ce7d4 commit 9d88f82

5 files changed

Lines changed: 102 additions & 78 deletions

File tree

app/components/Views/Homepage/Sections/TopTraders/components/TraderRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,4 @@ const TraderRow: React.FC<TraderRowProps> = ({
159159
);
160160
};
161161

162-
export default TraderRow;
162+
export default React.memo(TraderRow);

app/components/Views/Homepage/Sections/TopTraders/hooks/useTopTraders.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export const useTopTraders = (
6767
avatarUri: entry.imageUrl ?? undefined,
6868
percentageChange: (entry.roiPercent30d ?? 0) * 100,
6969
pnlValue: entry.pnl30d,
70+
pnlPerChain: entry.pnlPerChain ?? {},
7071
isFollowing: localFollowOverrides[entry.profileId] ?? false,
7172
}));
7273
}, [data, localFollowOverrides]);

app/components/Views/Homepage/Sections/TopTraders/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export interface TopTrader {
1616
percentageChange: number;
1717
/** Absolute PnL over 30 days in USD (raw number, formatted by the UI). */
1818
pnlValue: number;
19+
/** PnL broken down by chain. Used for client-side chain filtering. */
20+
pnlPerChain: Record<string, number>;
1921
/** Whether the current user is following this trader. */
2022
isFollowing: boolean;
2123
}

app/components/Views/SocialLeaderboard/TopTradersView/TopTradersView.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const fixtureTraders: TopTrader[] = [
3232
avatarUri: 'https://example.com/avatar1.png',
3333
percentageChange: 43,
3434
pnlValue: 963146.8,
35+
pnlPerChain: { base: 500000, ethereum: 463146.8 },
3536
isFollowing: false,
3637
},
3738
{
@@ -41,6 +42,7 @@ const fixtureTraders: TopTrader[] = [
4142
avatarUri: 'https://example.com/avatar2.png',
4243
percentageChange: 359,
4344
pnlValue: 474751.45,
45+
pnlPerChain: { base: 474751.45 },
4446
isFollowing: false,
4547
},
4648
{
@@ -50,6 +52,7 @@ const fixtureTraders: TopTrader[] = [
5052
avatarUri: 'https://example.com/avatar3.png',
5153
percentageChange: 617,
5254
pnlValue: 374735.16,
55+
pnlPerChain: { solana: 374735.16 },
5356
isFollowing: false,
5457
},
5558
];

app/components/Views/SocialLeaderboard/TopTradersView/TopTradersView.tsx

Lines changed: 95 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React, { useCallback, useEffect, useState } from 'react';
2-
import { RefreshControl, ScrollView } from 'react-native';
1+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
2+
import { FlatList, RefreshControl, TouchableOpacity } from 'react-native';
33
import { useNavigation } from '@react-navigation/native';
44
import { useSelector } from 'react-redux';
55
import { SafeAreaView } from 'react-native-safe-area-context';
@@ -21,15 +21,11 @@ import { useTheme } from '../../../../util/theme';
2121
import Logger from '../../../../util/Logger';
2222
import { strings } from '../../../../../locales/i18n';
2323
import { TopTradersViewSelectorsIDs } from './TopTradersView.testIds';
24-
import { TrendingTokenNetworkBottomSheet } from '../../../UI/Trending/components/TrendingTokensBottomSheet';
2524
import {
2625
TraderRow,
2726
TraderRowSkeleton,
28-
NetworkFilterButton,
2927
} from '../../Homepage/Sections/TopTraders/components';
3028
import { useTopTraders } from '../../Homepage/Sections/TopTraders/hooks';
31-
import type { NetworkFilterSelection } from '../../Homepage/Sections/TopTraders/types';
32-
import type { CaipChainId } from '@metamask/utils';
3329
import Routes from '../../../../constants/navigation/Routes';
3430
import { selectSocialLeaderboardEnabled } from '../../../../selectors/featureFlagController/socialLeaderboard';
3531

@@ -39,52 +35,81 @@ const SKELETON_KEYS = Array.from(
3935
(_, i) => `top-trader-skeleton-${i}`,
4036
);
4137

42-
/**
43-
* TopTradersView — Social leaderboard detail screen.
44-
*
45-
* Displays the full ranked list of top-performing traders with
46-
* network filtering and Follow / Following actions.
47-
*/
38+
type ChainFilter = 'all' | 'base' | 'solana' | 'ethereum';
39+
40+
const CHAIN_FILTERS: { key: ChainFilter; label: string }[] = [
41+
{ key: 'all', label: 'All' },
42+
{ key: 'base', label: 'Base' },
43+
{ key: 'solana', label: 'Solana' },
44+
{ key: 'ethereum', label: 'Ethereum' },
45+
];
46+
47+
interface ChainPillProps {
48+
label: string;
49+
isSelected: boolean;
50+
onPress: () => void;
51+
}
52+
53+
const ChainPill: React.FC<ChainPillProps> = ({
54+
label,
55+
isSelected,
56+
onPress,
57+
}) => (
58+
<TouchableOpacity
59+
onPress={onPress}
60+
testID={`chain-filter-${label.toLowerCase()}`}
61+
>
62+
<Box
63+
twClassName={`px-4 py-2 rounded-xl border ${
64+
isSelected ? 'bg-default border-white' : 'border-muted'
65+
}`}
66+
>
67+
<Text
68+
variant={TextVariant.BodySm}
69+
fontWeight={FontWeight.Medium}
70+
color={isSelected ? TextColor.TextDefault : TextColor.TextMuted}
71+
>
72+
{label}
73+
</Text>
74+
</Box>
75+
</TouchableOpacity>
76+
);
77+
4878
const TopTradersView = () => {
4979
const navigation = useNavigation();
5080
const tw = useTailwind();
5181
const { colors } = useTheme();
5282
const isEnabled = useSelector(selectSocialLeaderboardEnabled);
5383

84+
const [selectedChain, setSelectedChain] = useState<ChainFilter>('all');
85+
const [refreshing, setRefreshing] = useState(false);
86+
5487
const { traders, isLoading, refresh, toggleFollow } = useTopTraders({
88+
limit: 250,
5589
enabled: isEnabled,
5690
});
5791

58-
const [refreshing, setRefreshing] = useState(false);
59-
6092
useEffect(() => {
6193
if (!isEnabled) {
6294
navigation.goBack();
6395
}
6496
}, [isEnabled, navigation]);
6597

66-
const [showNetworkBottomSheet, setShowNetworkBottomSheet] = useState(false);
67-
const [selectedNetwork, setSelectedNetwork] =
68-
useState<NetworkFilterSelection>(null);
98+
const filteredTraders = useMemo(() => {
99+
const filtered =
100+
selectedChain === 'all'
101+
? traders
102+
: traders.filter((t) => (t.pnlPerChain[selectedChain] ?? 0) !== 0);
103+
104+
return filtered.slice(0, 50).map((t, i) => ({ ...t, rank: i + 1 }));
105+
}, [traders, selectedChain]);
69106

70107
const handleBack = useCallback(() => {
71108
navigation.goBack();
72109
}, [navigation]);
73110

74111
const handleSearchPress = useCallback(() => {
75-
// Search UI will be wired when the leaderboard data layer ships.
76-
}, []);
77-
78-
const handleNetworkPress = useCallback(() => {
79-
setShowNetworkBottomSheet(true);
80-
}, []);
81-
82-
const handleNetworkSelect = useCallback((chainIds: CaipChainId[] | null) => {
83-
setSelectedNetwork(chainIds ? chainIds[0] : null);
84-
}, []);
85-
86-
const handleNetworkBottomSheetClose = useCallback(() => {
87-
setShowNetworkBottomSheet(false);
112+
// Search UI will be wired in a future ticket.
88113
}, []);
89114

90115
const handleRefresh = useCallback(async () => {
@@ -111,16 +136,11 @@ const TopTradersView = () => {
111136
[navigation],
112137
);
113138

114-
const selectedNetworkCaip = selectedNetwork
115-
? ([selectedNetwork] as CaipChainId[])
116-
: null;
117-
118139
return (
119140
<SafeAreaView
120141
style={tw.style('flex-1 bg-default')}
121142
testID={TopTradersViewSelectorsIDs.CONTAINER}
122143
>
123-
{/* Header row */}
124144
<Box
125145
flexDirection={BoxFlexDirection.Row}
126146
alignItems={BoxAlignItems.Center}
@@ -141,7 +161,6 @@ const TopTradersView = () => {
141161
/>
142162
</Box>
143163

144-
{/* Title */}
145164
<Box twClassName="px-4 pt-2 pb-3">
146165
<Text
147166
variant={TextVariant.HeadingLg}
@@ -152,49 +171,48 @@ const TopTradersView = () => {
152171
</Text>
153172
</Box>
154173

155-
{/* Network filter */}
156-
<Box twClassName="px-4 pb-3">
157-
<NetworkFilterButton
158-
selectedNetwork={selectedNetwork}
159-
onPress={handleNetworkPress}
160-
testID="top-traders-view-network-filter"
161-
/>
174+
<Box
175+
flexDirection={BoxFlexDirection.Row}
176+
twClassName="px-4 pb-3 justify-between"
177+
>
178+
{CHAIN_FILTERS.map(({ key, label }) => (
179+
<ChainPill
180+
key={key}
181+
label={label}
182+
isSelected={selectedChain === key}
183+
onPress={() => setSelectedChain(key)}
184+
/>
185+
))}
162186
</Box>
163187

164-
{/* Trader list */}
165-
<ScrollView
166-
showsVerticalScrollIndicator={false}
167-
contentContainerStyle={tw.style('pb-6')}
168-
testID={TopTradersViewSelectorsIDs.TRADER_LIST}
169-
refreshControl={
170-
<RefreshControl
171-
colors={[colors.primary.default]}
172-
tintColor={colors.icon.default}
173-
refreshing={refreshing}
174-
onRefresh={handleRefresh}
175-
/>
176-
}
177-
>
178-
{isLoading
179-
? SKELETON_KEYS.map((key) => <TraderRowSkeleton key={key} />)
180-
: traders.map((trader) => (
181-
<TraderRow
182-
key={trader.id}
183-
trader={trader}
184-
onFollowPress={toggleFollow}
185-
onTraderPress={handleTraderPress}
186-
/>
187-
))}
188-
</ScrollView>
189-
190-
{/* Network filter bottom sheet */}
191-
<TrendingTokenNetworkBottomSheet
192-
isVisible={showNetworkBottomSheet}
193-
onClose={handleNetworkBottomSheetClose}
194-
onNetworkSelect={handleNetworkSelect}
195-
selectedNetwork={selectedNetworkCaip}
196-
networks={[]}
197-
/>
188+
{isLoading ? (
189+
SKELETON_KEYS.map((key) => <TraderRowSkeleton key={key} />)
190+
) : (
191+
<FlatList
192+
data={filteredTraders}
193+
keyExtractor={(item) => item.id}
194+
renderItem={({ item }) => (
195+
<TraderRow
196+
trader={item}
197+
onFollowPress={toggleFollow}
198+
onTraderPress={handleTraderPress}
199+
/>
200+
)}
201+
showsVerticalScrollIndicator={false}
202+
contentContainerStyle={tw.style('pb-6')}
203+
testID={TopTradersViewSelectorsIDs.TRADER_LIST}
204+
initialNumToRender={15}
205+
windowSize={5}
206+
refreshControl={
207+
<RefreshControl
208+
colors={[colors.primary.default]}
209+
tintColor={colors.icon.default}
210+
refreshing={refreshing}
211+
onRefresh={handleRefresh}
212+
/>
213+
}
214+
/>
215+
)}
198216
</SafeAreaView>
199217
);
200218
};

0 commit comments

Comments
 (0)