Skip to content

Commit b94a5f6

Browse files
committed
feat: add chain filter pills with client-side filtering (All, Base, Solana, Ethereum)
1 parent 5554c47 commit b94a5f6

5 files changed

Lines changed: 89 additions & 68 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
@@ -151,4 +151,4 @@ const TraderRow: React.FC<TraderRowProps> = ({
151151
);
152152
};
153153

154-
export default TraderRow;
154+
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
@@ -26,6 +26,7 @@ const fixtureTraders: TopTrader[] = [
2626
avatarUri: 'https://example.com/avatar1.png',
2727
percentageChange: 43,
2828
pnlValue: 963146.8,
29+
pnlPerChain: { base: 500000, ethereum: 463146.8 },
2930
isFollowing: false,
3031
},
3132
{
@@ -35,6 +36,7 @@ const fixtureTraders: TopTrader[] = [
3536
avatarUri: 'https://example.com/avatar2.png',
3637
percentageChange: 359,
3738
pnlValue: 474751.45,
39+
pnlPerChain: { base: 474751.45 },
3840
isFollowing: false,
3941
},
4042
{
@@ -44,6 +46,7 @@ const fixtureTraders: TopTrader[] = [
4446
avatarUri: 'https://example.com/avatar3.png',
4547
percentageChange: 617,
4648
pnlValue: 374735.16,
49+
pnlPerChain: { solana: 374735.16 },
4750
isFollowing: false,
4851
},
4952
];

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

Lines changed: 82 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React, { useCallback, useState } from 'react';
2-
import { ScrollView } from 'react-native-gesture-handler';
1+
import React, { useCallback, useMemo, useState } from 'react';
2+
import { FlatList, TouchableOpacity } from 'react-native';
33
import { useNavigation } from '@react-navigation/native';
44
import { SafeAreaView } from 'react-native-safe-area-context';
55
import { useTailwind } from '@metamask/design-system-twrnc-preset';
@@ -18,68 +18,88 @@ import {
1818
} from '@metamask/design-system-react-native';
1919
import { strings } from '../../../../../locales/i18n';
2020
import { TopTradersViewSelectorsIDs } from './TopTradersView.testIds';
21-
import { TrendingTokenNetworkBottomSheet } from '../../../UI/Trending/components/TrendingTokensBottomSheet';
2221
import {
2322
TraderRow,
2423
TraderRowSkeleton,
25-
NetworkFilterButton,
2624
} from '../../Homepage/Sections/TopTraders/components';
2725
import { useTopTraders } from '../../Homepage/Sections/TopTraders/hooks';
28-
import type { NetworkFilterSelection } from '../../Homepage/Sections/TopTraders/types';
29-
import type { CaipChainId } from '@metamask/utils';
3026

3127
const SKELETON_COUNT = 5;
3228
const SKELETON_KEYS = Array.from(
3329
{ length: SKELETON_COUNT },
3430
(_, i) => `top-trader-skeleton-${i}`,
3531
);
3632

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

47-
const { traders, isLoading, toggleFollow } = useTopTraders();
77+
const [selectedChain, setSelectedChain] = useState<ChainFilter>('all');
78+
79+
const { traders, isLoading, toggleFollow } = useTopTraders({ limit: 250 });
4880

49-
const [showNetworkBottomSheet, setShowNetworkBottomSheet] = useState(false);
50-
const [selectedNetwork, setSelectedNetwork] =
51-
useState<NetworkFilterSelection>(null);
81+
const filteredTraders = useMemo(() => {
82+
const filtered =
83+
selectedChain === 'all'
84+
? traders
85+
: traders.filter((t) => (t.pnlPerChain[selectedChain] ?? 0) !== 0);
86+
87+
return filtered.slice(0, 50).map((t, i) => ({ ...t, rank: i + 1 }));
88+
}, [traders, selectedChain]);
5289

5390
const handleBack = useCallback(() => {
5491
navigation.goBack();
5592
}, [navigation]);
5693

5794
const handleSearchPress = useCallback(() => {
58-
// Search UI will be wired when the leaderboard data layer ships.
59-
}, []);
60-
61-
const handleNetworkPress = useCallback(() => {
62-
setShowNetworkBottomSheet(true);
63-
}, []);
64-
65-
const handleNetworkSelect = useCallback((chainIds: CaipChainId[] | null) => {
66-
setSelectedNetwork(chainIds ? chainIds[0] : null);
95+
// Search UI will be wired in a future ticket.
6796
}, []);
6897

69-
const handleNetworkBottomSheetClose = useCallback(() => {
70-
setShowNetworkBottomSheet(false);
71-
}, []);
72-
73-
const selectedNetworkCaip = selectedNetwork
74-
? ([selectedNetwork] as CaipChainId[])
75-
: null;
76-
7798
return (
7899
<SafeAreaView
79100
style={tw.style('flex-1 bg-default')}
80101
testID={TopTradersViewSelectorsIDs.CONTAINER}
81102
>
82-
{/* Header row */}
83103
<Box
84104
flexDirection={BoxFlexDirection.Row}
85105
alignItems={BoxAlignItems.Center}
@@ -100,7 +120,6 @@ const TopTradersView = () => {
100120
/>
101121
</Box>
102122

103-
{/* Title */}
104123
<Box twClassName="px-4 pt-2 pb-3">
105124
<Text
106125
variant={TextVariant.HeadingLg}
@@ -111,40 +130,36 @@ const TopTradersView = () => {
111130
</Text>
112131
</Box>
113132

114-
{/* Network filter */}
115-
<Box twClassName="px-4 pb-3">
116-
<NetworkFilterButton
117-
selectedNetwork={selectedNetwork}
118-
onPress={handleNetworkPress}
119-
testID="top-traders-view-network-filter"
120-
/>
133+
<Box
134+
flexDirection={BoxFlexDirection.Row}
135+
twClassName="px-4 pb-3 justify-between"
136+
>
137+
{CHAIN_FILTERS.map(({ key, label }) => (
138+
<ChainPill
139+
key={key}
140+
label={label}
141+
isSelected={selectedChain === key}
142+
onPress={() => setSelectedChain(key)}
143+
/>
144+
))}
121145
</Box>
122146

123-
{/* Trader list */}
124-
<ScrollView
125-
showsVerticalScrollIndicator={false}
126-
contentContainerStyle={tw.style('pb-6')}
127-
testID="top-traders-view-list"
128-
>
129-
{isLoading
130-
? SKELETON_KEYS.map((key) => <TraderRowSkeleton key={key} />)
131-
: traders.map((trader) => (
132-
<TraderRow
133-
key={trader.id}
134-
trader={trader}
135-
onFollowPress={toggleFollow}
136-
/>
137-
))}
138-
</ScrollView>
139-
140-
{/* Network filter bottom sheet */}
141-
<TrendingTokenNetworkBottomSheet
142-
isVisible={showNetworkBottomSheet}
143-
onClose={handleNetworkBottomSheetClose}
144-
onNetworkSelect={handleNetworkSelect}
145-
selectedNetwork={selectedNetworkCaip}
146-
networks={[]}
147-
/>
147+
{isLoading ? (
148+
SKELETON_KEYS.map((key) => <TraderRowSkeleton key={key} />)
149+
) : (
150+
<FlatList
151+
data={filteredTraders}
152+
keyExtractor={(item) => item.id}
153+
renderItem={({ item }) => (
154+
<TraderRow trader={item} onFollowPress={toggleFollow} />
155+
)}
156+
showsVerticalScrollIndicator={false}
157+
contentContainerStyle={tw.style('pb-6')}
158+
testID="top-traders-view-list"
159+
initialNumToRender={15}
160+
windowSize={5}
161+
/>
162+
)}
148163
</SafeAreaView>
149164
);
150165
};

0 commit comments

Comments
 (0)