Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/app/leaderboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { DashboardLayout } from 'src/layouts/dashboard'

// ----------------------------------------------------------------------

type Props = {
children: React.ReactNode
}

export default function Layout({ children }: Props) {
return <DashboardLayout>{children}</DashboardLayout>
}
7 changes: 7 additions & 0 deletions src/app/leaderboard/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { LoadingScreen } from 'src/components/loading-screen'

// ----------------------------------------------------------------------

export default function Loading() {
return <LoadingScreen />
}
30 changes: 30 additions & 0 deletions src/app/leaderboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client'

import { Box, Typography } from '@mui/material'
import { LeaderboardTable } from 'src/components/leaderboard'
import { DashboardContent } from 'src/layouts/dashboard'
import { Iconify } from 'src/components/iconify'

export default function LeaderboardPage() {
return (
<DashboardContent maxWidth="xl">
{/* Page Header */}
<Box sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 0.5 }}>
<Iconify
icon="solar:cup-star-bold-duotone"
width={28}
sx={{ color: 'warning.main' }}
/>
<Typography variant="h4">Leaderboard</Typography>
</Box>
<Typography variant="body2" color="text.secondary">
Top bridge users ranked by volume and transaction activity
</Typography>
</Box>

{/* Leaderboard Table */}
<LeaderboardTable />
</DashboardContent>
)
}
129 changes: 129 additions & 0 deletions src/components/leaderboard/activity-sparkline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Box } from '@mui/material'
import { useTheme, alpha } from '@mui/material/styles'
import { useMemo, useId } from 'react'

interface ActivitySparklineProps {
data: number[]
width?: number
height?: number
color?: string
}

interface Point {
x: number
y: number
}

export function ActivitySparkline({
data,
width = 80,
height = 30,
color,
}: ActivitySparklineProps) {
const theme = useTheme()
const strokeColor = color || theme.palette.primary.main
const gradientId = useId()

const { path, points, hasActivity } = useMemo(() => {
if (!data || data.length === 0) {
return { path: '', points: [] as Point[], hasActivity: false }
}

const max = Math.max(...data, 1) // Ensure at least 1 to avoid division by zero
const hasAct = data.some(v => v > 0)

const padding = 2
const chartWidth = width - padding * 2
const chartHeight = height - padding * 2

const pts = data.map((value, index) => {
const x = padding + (index / (data.length - 1 || 1)) * chartWidth
const y = padding + chartHeight - (value / max) * chartHeight
return { x, y }
})

// Create SVG path
const pathData = pts
.map((point, index) => {
if (index === 0) return `M ${point.x} ${point.y}`
return `L ${point.x} ${point.y}`
})
.join(' ')

return { path: pathData, points: pts, hasActivity: hasAct }
}, [data, width, height])

// Create area path (for fill) - reuse points from above
const areaPath = useMemo(() => {
if (points.length === 0 || !path) return ''

const padding = 2
const chartHeight = height - padding * 2
const bottomY = padding + chartHeight
const firstX = points[0]?.x || padding
const lastX = points[points.length - 1]?.x || width - padding

return `${path} L ${lastX} ${bottomY} L ${firstX} ${bottomY} Z`
}, [points, path, width, height])

// No activity - show placeholder (after all hooks)
if (!hasActivity) {
return (
<Box
sx={{
width,
height,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bgcolor: alpha(theme.palette.grey[500], 0.08),
borderRadius: 1,
}}
>
<Box
sx={{
width: '60%',
height: 2,
bgcolor: alpha(theme.palette.grey[500], 0.3),
borderRadius: 1,
}}
/>
</Box>
)
}

return (
<Box
sx={{
width,
height,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<svg width={width} height={height}>
{/* Gradient definition */}
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={strokeColor} stopOpacity={0.3} />
<stop offset="100%" stopColor={strokeColor} stopOpacity={0.05} />
</linearGradient>
</defs>

{/* Area fill */}
<path d={areaPath} fill={`url(#${gradientId})`} />

{/* Line */}
<path
d={path}
fill="none"
stroke={strokeColor}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
)
}
101 changes: 101 additions & 0 deletions src/components/leaderboard/address-cell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Box, IconButton, Tooltip, Typography } from '@mui/material'
import { useTheme, alpha } from '@mui/material/styles'
import { useState } from 'react'
import { Iconify } from 'src/components/iconify'
import { truncateAddress } from 'src/config/helper'

interface AddressCellProps {
address: string
addressType: 'sui' | 'eth'
onClick?: () => void
}

export function AddressCell({ address, addressType, onClick }: AddressCellProps) {
const theme = useTheme()
const [copied, setCopied] = useState(false)

const handleCopy = async (e: React.MouseEvent) => {
e.stopPropagation()
try {
const fullAddress = `0x${address}`
await navigator.clipboard.writeText(fullAddress)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (err) {
console.error('Failed to copy address:', err)
}
}

const chainColor = addressType === 'sui' ? '#4DA2FF' : '#627EEA'
const chainIcon =
addressType === 'sui' ? '/assets/icons/brands/sui.svg' : '/assets/icons/brands/eth.svg'

return (
<Box
onClick={onClick}
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
cursor: onClick ? 'pointer' : 'default',
padding: '4px 8px',
borderRadius: 1,
transition: 'background-color 0.2s',
'&:hover': onClick
? {
bgcolor: alpha(theme.palette.primary.main, 0.08),
}
: {},
}}
>
{/* Chain icon */}
<Tooltip title={addressType === 'sui' ? 'SUI Address' : 'Ethereum Address'}>
<Box
sx={{
width: 24,
height: 24,
borderRadius: '50%',
bgcolor: alpha(chainColor, 0.12),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}
>
<img src={chainIcon} alt={addressType} style={{ width: 16, height: 16 }} />
</Box>
</Tooltip>

{/* Address text */}
<Typography
variant="body2"
sx={{
fontFamily: 'monospace',
fontSize: '0.85rem',
color: onClick ? theme.palette.primary.main : theme.palette.text.primary,
fontWeight: 500,
}}
>
{truncateAddress(address, 6)}
</Typography>

{/* Copy button */}
<Tooltip title={copied ? 'Copied!' : 'Copy address'}>
<IconButton
size="small"
onClick={handleCopy}
sx={{
ml: 0.5,
p: 0.5,
color: copied ? 'success.main' : 'text.secondary',
'&:hover': {
bgcolor: alpha(theme.palette.primary.main, 0.08),
},
}}
>
<Iconify icon={copied ? 'eva:checkmark-fill' : 'eva:copy-fill'} width={16} />
</IconButton>
</Tooltip>
</Box>
)
}
5 changes: 5 additions & 0 deletions src/components/leaderboard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './leaderboard-table'
export * from './leaderboard-stats'
export * from './medal-rank'
export * from './activity-sparkline'
export * from './address-cell'
Loading