Skip to content

Commit a016745

Browse files
committed
feat(copy-trading): add leaderboard sorting and chain filter
1 parent f0c9bf4 commit a016745

26 files changed

Lines changed: 1263 additions & 945 deletions
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { type HTMLAttributes, type ReactNode } from 'react'
2+
import { ChevronDown, ChevronUp, Zap } from 'react-feather'
3+
import { useNavigate } from 'react-router-dom'
4+
import type { AgentCard, LeaderboardSortBy, SortOrder } from 'services/copyTrading/types'
5+
6+
import { ButtonEmpty, ButtonPrimary } from 'components/Button'
7+
import { HStack, Stack } from 'components/Stack'
8+
import { APP_PATHS } from 'constants/index'
9+
import { AgentCell, TableCell, TableHeader, TableRow } from 'pages/CopyTrading/components/common'
10+
import { compactUsd, percent } from 'pages/CopyTrading/helpers'
11+
import { cn } from 'utils/cn'
12+
13+
const HeaderCell = ({
14+
children,
15+
className,
16+
sortField,
17+
activeSortBy,
18+
sortOrder,
19+
onSortChange,
20+
}: {
21+
children: ReactNode
22+
className?: string
23+
sortField?: LeaderboardSortBy
24+
activeSortBy?: LeaderboardSortBy
25+
sortOrder?: SortOrder
26+
onSortChange?: (sortBy: LeaderboardSortBy) => void
27+
}) => {
28+
const sortable = !!sortField
29+
const active = sortable && activeSortBy === sortField
30+
const SortIcon = active && sortOrder === 'asc' ? ChevronUp : ChevronDown
31+
const content = (
32+
<HStack
33+
className={cn(
34+
'w-full items-center gap-1 whitespace-nowrap px-3 py-2 text-xs font-medium uppercase text-subText',
35+
className,
36+
active && 'text-primary',
37+
)}
38+
>
39+
{children}
40+
{sortable && <SortIcon size={12} className={cn('shrink-0', !active && 'opacity-50')} />}
41+
</HStack>
42+
)
43+
44+
if (!sortField) return content
45+
46+
return (
47+
<ButtonEmpty type="button" onClick={() => onSortChange?.(sortField)} padding="0">
48+
{content}
49+
</ButtonEmpty>
50+
)
51+
}
52+
53+
const LeaderboardGrid = ({ header, ...props }: HTMLAttributes<HTMLDivElement> & { header?: boolean }) => {
54+
const Grid = header ? TableHeader : TableRow
55+
56+
return (
57+
<Grid
58+
columns="minmax(0, 2.2fr) minmax(0, 0.9fr) minmax(0, 0.85fr) minmax(0, 0.85fr) minmax(0, 0.75fr) minmax(0, 0.85fr) minmax(0, 0.75fr) minmax(0, 0.8fr)"
59+
{...props}
60+
/>
61+
)
62+
}
63+
64+
const AgentTable = ({
65+
agents,
66+
sortBy,
67+
sortOrder,
68+
onSortChange,
69+
}: {
70+
agents: AgentCard[]
71+
sortBy?: LeaderboardSortBy
72+
sortOrder?: SortOrder
73+
onSortChange: (sortBy: LeaderboardSortBy) => void
74+
}) => {
75+
const navigate = useNavigate()
76+
77+
const openAgent = (agentId: string) => {
78+
navigate(`${APP_PATHS.COPY_TRADING}/${agentId}`)
79+
}
80+
81+
return (
82+
<Stack className="overflow-hidden">
83+
<LeaderboardGrid header className="normal-case">
84+
<HeaderCell>Agent</HeaderCell>
85+
<HeaderCell
86+
activeSortBy={sortBy}
87+
className="justify-end text-right"
88+
onSortChange={onSortChange}
89+
sortField="apr_30d_pct"
90+
sortOrder={sortOrder}
91+
>
92+
Agent APR <span className="rounded-md bg-background px-2 py-1">30D</span>
93+
</HeaderCell>
94+
<HeaderCell
95+
activeSortBy={sortBy}
96+
className="justify-end text-right"
97+
onSortChange={onSortChange}
98+
sortField="win_rate_pct"
99+
sortOrder={sortOrder}
100+
>
101+
Win Rates
102+
</HeaderCell>
103+
<HeaderCell
104+
activeSortBy={sortBy}
105+
className="justify-end text-right"
106+
onSortChange={onSortChange}
107+
sortField="volume_usd"
108+
sortOrder={sortOrder}
109+
>
110+
Volume
111+
</HeaderCell>
112+
<HeaderCell
113+
activeSortBy={sortBy}
114+
className="justify-end text-right"
115+
onSortChange={onSortChange}
116+
sortField="copiers"
117+
sortOrder={sortOrder}
118+
>
119+
Copiers
120+
</HeaderCell>
121+
<HeaderCell
122+
activeSortBy={sortBy}
123+
className="justify-end text-right"
124+
onSortChange={onSortChange}
125+
sortField="aum_usd"
126+
sortOrder={sortOrder}
127+
>
128+
AUM
129+
</HeaderCell>
130+
<HeaderCell
131+
activeSortBy={sortBy}
132+
className="justify-end text-right"
133+
onSortChange={onSortChange}
134+
sortOrder={sortOrder}
135+
>
136+
Position
137+
</HeaderCell>
138+
<TableCell className="text-right" />
139+
</LeaderboardGrid>
140+
141+
{agents.map(agent => (
142+
<LeaderboardGrid
143+
key={agent.agentId}
144+
onKeyDown={event => {
145+
if ((event.target as HTMLElement).closest('button')) return
146+
if (event.key === 'Enter' || event.key === ' ') {
147+
openAgent(agent.agentId)
148+
}
149+
}}
150+
role="button"
151+
tabIndex={0}
152+
className="cursor-pointer text-base"
153+
onClick={event => {
154+
if ((event.target as HTMLElement).closest('button')) return
155+
openAgent(agent.agentId)
156+
}}
157+
>
158+
<AgentCell agent={agent} className="px-3 py-2" />
159+
<HStack className="items-center justify-end gap-1.5 whitespace-nowrap px-3 py-2 text-right text-primary">
160+
<Zap size={14} className="fill-warning text-warning" />
161+
<span>{percent(agent.stats.apr30dPct)}</span>
162+
</HStack>
163+
<TableCell className="whitespace-nowrap text-right">{percent(agent.stats.winRatePct)}</TableCell>
164+
<TableCell className="whitespace-nowrap text-right">{compactUsd(agent.stats.volumeUsd)}</TableCell>
165+
<TableCell className="whitespace-nowrap text-right">{agent.stats.copiers.toLocaleString()}</TableCell>
166+
<TableCell className="whitespace-nowrap text-right">{compactUsd(agent.stats.aumUsd)}</TableCell>
167+
<TableCell className="whitespace-nowrap text-right">{agent.stats.openPositions}</TableCell>
168+
<div className="flex justify-end px-3 py-2">
169+
<div className="w-fit">
170+
<ButtonPrimary type="button" padding="8px 12px">
171+
Copy
172+
</ButtonPrimary>
173+
</div>
174+
</div>
175+
</LeaderboardGrid>
176+
))}
177+
178+
{!agents.length && (
179+
<Stack className="items-center gap-2 px-6 py-10 text-center">
180+
<span className="text-base font-medium text-text">No agents found</span>
181+
<span className="text-sm text-subText">Try another strategy or search term.</span>
182+
</Stack>
183+
)}
184+
</Stack>
185+
)
186+
}
187+
188+
export default AgentTable
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { LeaderboardSummary as LeaderboardSummaryData } from 'services/copyTrading/types'
2+
3+
import { StatCard } from 'pages/CopyTrading/components/common'
4+
import { compactUsd } from 'pages/CopyTrading/helpers'
5+
6+
const LeaderboardSummary = ({
7+
summary,
8+
fallbackAgentCount,
9+
}: {
10+
summary?: LeaderboardSummaryData
11+
fallbackAgentCount?: number
12+
}) => {
13+
const stats = [
14+
{
15+
icon: 'agent',
16+
value: String(summary?.totalAgents || fallbackAgentCount || 0),
17+
label: 'Total Agents',
18+
color: 'bg-warning-20 text-warning',
19+
},
20+
{
21+
icon: 'aum',
22+
value: compactUsd(summary?.totalAumUsd),
23+
label: 'Total AUM',
24+
color: 'bg-blue/20 text-blue',
25+
},
26+
{
27+
icon: 'copiers',
28+
value: String(summary?.totalCopiers || 0),
29+
label: 'Total Copiers',
30+
color: 'bg-primary-20 text-primary',
31+
},
32+
{
33+
icon: 'volume',
34+
value: compactUsd(summary?.totalVolumeUsd),
35+
label: 'Total Volume',
36+
color: 'bg-primary-12 text-primary',
37+
},
38+
] as const
39+
40+
return (
41+
<div className="grid grid-cols-4 gap-6 max-xl:grid-cols-2 max-md:grid-cols-1">
42+
{stats.map(item => (
43+
<StatCard key={item.label} item={item} />
44+
))}
45+
</div>
46+
)
47+
}
48+
49+
export default LeaderboardSummary

0 commit comments

Comments
 (0)