Skip to content

Commit ab2321d

Browse files
authored
Leaderboard page with top bridge users ranking (#60)
* setup leaderboard * fix top cards * fix activity tab * fix PR comments * Update activity-sparkline.tsx
1 parent fda582d commit ab2321d

14 files changed

Lines changed: 1204 additions & 0 deletions

File tree

src/app/leaderboard/layout.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { DashboardLayout } from 'src/layouts/dashboard'
2+
3+
// ----------------------------------------------------------------------
4+
5+
type Props = {
6+
children: React.ReactNode
7+
}
8+
9+
export default function Layout({ children }: Props) {
10+
return <DashboardLayout>{children}</DashboardLayout>
11+
}

src/app/leaderboard/loading.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { LoadingScreen } from 'src/components/loading-screen'
2+
3+
// ----------------------------------------------------------------------
4+
5+
export default function Loading() {
6+
return <LoadingScreen />
7+
}

src/app/leaderboard/page.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use client'
2+
3+
import { Box, Typography } from '@mui/material'
4+
import { LeaderboardTable } from 'src/components/leaderboard'
5+
import { DashboardContent } from 'src/layouts/dashboard'
6+
import { Iconify } from 'src/components/iconify'
7+
8+
export default function LeaderboardPage() {
9+
return (
10+
<DashboardContent maxWidth="xl">
11+
{/* Page Header */}
12+
<Box sx={{ mb: 3 }}>
13+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 0.5 }}>
14+
<Iconify
15+
icon="solar:cup-star-bold-duotone"
16+
width={28}
17+
sx={{ color: 'warning.main' }}
18+
/>
19+
<Typography variant="h4">Leaderboard</Typography>
20+
</Box>
21+
<Typography variant="body2" color="text.secondary">
22+
Top bridge users ranked by volume and transaction activity
23+
</Typography>
24+
</Box>
25+
26+
{/* Leaderboard Table */}
27+
<LeaderboardTable />
28+
</DashboardContent>
29+
)
30+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { Box } from '@mui/material'
2+
import { useTheme, alpha } from '@mui/material/styles'
3+
import { useMemo, useId } from 'react'
4+
5+
interface ActivitySparklineProps {
6+
data: number[]
7+
width?: number
8+
height?: number
9+
color?: string
10+
}
11+
12+
interface Point {
13+
x: number
14+
y: number
15+
}
16+
17+
export function ActivitySparkline({
18+
data,
19+
width = 80,
20+
height = 30,
21+
color,
22+
}: ActivitySparklineProps) {
23+
const theme = useTheme()
24+
const strokeColor = color || theme.palette.primary.main
25+
const gradientId = useId()
26+
27+
const { path, points, hasActivity } = useMemo(() => {
28+
if (!data || data.length === 0) {
29+
return { path: '', points: [] as Point[], hasActivity: false }
30+
}
31+
32+
const max = Math.max(...data, 1) // Ensure at least 1 to avoid division by zero
33+
const hasAct = data.some(v => v > 0)
34+
35+
const padding = 2
36+
const chartWidth = width - padding * 2
37+
const chartHeight = height - padding * 2
38+
39+
const pts = data.map((value, index) => {
40+
const x = padding + (index / (data.length - 1 || 1)) * chartWidth
41+
const y = padding + chartHeight - (value / max) * chartHeight
42+
return { x, y }
43+
})
44+
45+
// Create SVG path
46+
const pathData = pts
47+
.map((point, index) => {
48+
if (index === 0) return `M ${point.x} ${point.y}`
49+
return `L ${point.x} ${point.y}`
50+
})
51+
.join(' ')
52+
53+
return { path: pathData, points: pts, hasActivity: hasAct }
54+
}, [data, width, height])
55+
56+
// Create area path (for fill) - reuse points from above
57+
const areaPath = useMemo(() => {
58+
if (points.length === 0 || !path) return ''
59+
60+
const padding = 2
61+
const chartHeight = height - padding * 2
62+
const bottomY = padding + chartHeight
63+
const firstX = points[0]?.x || padding
64+
const lastX = points[points.length - 1]?.x || width - padding
65+
66+
return `${path} L ${lastX} ${bottomY} L ${firstX} ${bottomY} Z`
67+
}, [points, path, width, height])
68+
69+
// No activity - show placeholder (after all hooks)
70+
if (!hasActivity) {
71+
return (
72+
<Box
73+
sx={{
74+
width,
75+
height,
76+
display: 'flex',
77+
alignItems: 'center',
78+
justifyContent: 'center',
79+
bgcolor: alpha(theme.palette.grey[500], 0.08),
80+
borderRadius: 1,
81+
}}
82+
>
83+
<Box
84+
sx={{
85+
width: '60%',
86+
height: 2,
87+
bgcolor: alpha(theme.palette.grey[500], 0.3),
88+
borderRadius: 1,
89+
}}
90+
/>
91+
</Box>
92+
)
93+
}
94+
95+
return (
96+
<Box
97+
sx={{
98+
width,
99+
height,
100+
display: 'flex',
101+
alignItems: 'center',
102+
justifyContent: 'center',
103+
}}
104+
>
105+
<svg width={width} height={height}>
106+
{/* Gradient definition */}
107+
<defs>
108+
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
109+
<stop offset="0%" stopColor={strokeColor} stopOpacity={0.3} />
110+
<stop offset="100%" stopColor={strokeColor} stopOpacity={0.05} />
111+
</linearGradient>
112+
</defs>
113+
114+
{/* Area fill */}
115+
<path d={areaPath} fill={`url(#${gradientId})`} />
116+
117+
{/* Line */}
118+
<path
119+
d={path}
120+
fill="none"
121+
stroke={strokeColor}
122+
strokeWidth={2}
123+
strokeLinecap="round"
124+
strokeLinejoin="round"
125+
/>
126+
</svg>
127+
</Box>
128+
)
129+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { Box, IconButton, Tooltip, Typography } from '@mui/material'
2+
import { useTheme, alpha } from '@mui/material/styles'
3+
import { useState } from 'react'
4+
import { Iconify } from 'src/components/iconify'
5+
import { truncateAddress } from 'src/config/helper'
6+
7+
interface AddressCellProps {
8+
address: string
9+
addressType: 'sui' | 'eth'
10+
onClick?: () => void
11+
}
12+
13+
export function AddressCell({ address, addressType, onClick }: AddressCellProps) {
14+
const theme = useTheme()
15+
const [copied, setCopied] = useState(false)
16+
17+
const handleCopy = async (e: React.MouseEvent) => {
18+
e.stopPropagation()
19+
try {
20+
const fullAddress = `0x${address}`
21+
await navigator.clipboard.writeText(fullAddress)
22+
setCopied(true)
23+
setTimeout(() => setCopied(false), 2000)
24+
} catch (err) {
25+
console.error('Failed to copy address:', err)
26+
}
27+
}
28+
29+
const chainColor = addressType === 'sui' ? '#4DA2FF' : '#627EEA'
30+
const chainIcon =
31+
addressType === 'sui' ? '/assets/icons/brands/sui.svg' : '/assets/icons/brands/eth.svg'
32+
33+
return (
34+
<Box
35+
onClick={onClick}
36+
sx={{
37+
display: 'flex',
38+
alignItems: 'center',
39+
gap: 1,
40+
cursor: onClick ? 'pointer' : 'default',
41+
padding: '4px 8px',
42+
borderRadius: 1,
43+
transition: 'background-color 0.2s',
44+
'&:hover': onClick
45+
? {
46+
bgcolor: alpha(theme.palette.primary.main, 0.08),
47+
}
48+
: {},
49+
}}
50+
>
51+
{/* Chain icon */}
52+
<Tooltip title={addressType === 'sui' ? 'SUI Address' : 'Ethereum Address'}>
53+
<Box
54+
sx={{
55+
width: 24,
56+
height: 24,
57+
borderRadius: '50%',
58+
bgcolor: alpha(chainColor, 0.12),
59+
display: 'flex',
60+
alignItems: 'center',
61+
justifyContent: 'center',
62+
flexShrink: 0,
63+
}}
64+
>
65+
<img src={chainIcon} alt={addressType} style={{ width: 16, height: 16 }} />
66+
</Box>
67+
</Tooltip>
68+
69+
{/* Address text */}
70+
<Typography
71+
variant="body2"
72+
sx={{
73+
fontFamily: 'monospace',
74+
fontSize: '0.85rem',
75+
color: onClick ? theme.palette.primary.main : theme.palette.text.primary,
76+
fontWeight: 500,
77+
}}
78+
>
79+
{truncateAddress(address, 6)}
80+
</Typography>
81+
82+
{/* Copy button */}
83+
<Tooltip title={copied ? 'Copied!' : 'Copy address'}>
84+
<IconButton
85+
size="small"
86+
onClick={handleCopy}
87+
sx={{
88+
ml: 0.5,
89+
p: 0.5,
90+
color: copied ? 'success.main' : 'text.secondary',
91+
'&:hover': {
92+
bgcolor: alpha(theme.palette.primary.main, 0.08),
93+
},
94+
}}
95+
>
96+
<Iconify icon={copied ? 'eva:checkmark-fill' : 'eva:copy-fill'} width={16} />
97+
</IconButton>
98+
</Tooltip>
99+
</Box>
100+
)
101+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from './leaderboard-table'
2+
export * from './leaderboard-stats'
3+
export * from './medal-rank'
4+
export * from './activity-sparkline'
5+
export * from './address-cell'

0 commit comments

Comments
 (0)