Skip to content
6 changes: 3 additions & 3 deletions campaign-launcher/client/src/api/recordingApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type {
CampaignsResponse,
UserProgress,
CheckCampaignJoinStatusResponse,
LeaderboardResponse,
LeaderboardResponseDto,
} from '@/types';
import { HttpClient, HttpError } from '@/utils/HttpClient';
import type { TokenData, TokenManager } from '@/utils/TokenManager';
Expand Down Expand Up @@ -224,8 +224,8 @@ export class RecordingApiClient extends HttpClient {
async getLeaderboard(
chain_id: ChainId,
campaign_address: string
): Promise<LeaderboardResponse> {
const response = await this.get<LeaderboardResponse>(
): Promise<LeaderboardResponseDto> {
const response = await this.get<LeaderboardResponseDto>(
`/campaigns/${chain_id}-${campaign_address}/leaderboard`
);
return response;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { useExchangesContext } from '@/providers/ExchangesProvider';
import { useWeb3Auth } from '@/providers/Web3AuthProvider';
import {
CampaignStatus,
type LeaderboardResponse,
type Leaderboard,
type CampaignDetails,
} from '@/types';
import {
Expand Down Expand Up @@ -117,7 +117,7 @@ type Props = {
campaign: CampaignDetails | null | undefined;
isJoined: boolean;
isCampaignLoading: boolean;
leaderboard?: LeaderboardResponse;
leaderboard?: Leaderboard;
};

const CampaignStats: FC<Props> = ({
Expand Down
87 changes: 87 additions & 0 deletions campaign-launcher/client/src/components/Leaderboard/List.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { memo } from 'react';

import { Box, Stack, Typography } from '@mui/material';

import FormattedNumber from '@/components/FormattedNumber';
import { type EvmAddress, type LeaderboardEntry } from '@/types';
import { formatAddress, getCompactNumberParts } from '@/utils';

import MyEntryLabel from './MyEntryLabel';

type Props = {
data: LeaderboardEntry[];
activeAddress: EvmAddress | undefined;
};

const LeaderboardList = memo(({ data, activeAddress }: Props) => (
<Stack flex={1} minHeight={0} overflow="auto">
{data.map((entry) => {
const { address, rank, result, score } = entry;
const {
value: resultValue,
suffix: resultSuffix,
decimals: resultDecimals,
} = getCompactNumberParts(result);
const {
value: scoreValue,
suffix: scoreSuffix,
decimals: scoreDecimals,
} = getCompactNumberParts(score);
const isMyEntry = address.toLowerCase() === activeAddress?.toLowerCase();
return (
<Box
key={address}
display="flex"
alignItems="center"
justifyContent="space-between"
height="60px"
gap={1}
px={{ xs: 2, md: 4 }}
py={1.5}
bgcolor={isMyEntry ? '#3a2e6f' : 'transparent'}
borderBottom="1px solid #3a2e6f"
sx={{
'&:last-of-type': {
borderBottom: 'none',
},
}}
>
<Box display="flex" alignItems="center" gap={1} color="white">
<Typography variant="body1" fontWeight={500} mr={1}>
#{rank}
</Typography>
<Typography variant="body2" fontWeight={500}>
{formatAddress(address)}
</Typography>
{isMyEntry && <MyEntryLabel />}
</Box>
<Box display="flex" alignItems="center" gap={3}>
<Stack alignItems="center">
<Typography variant="body2" color="white" fontWeight={500}>
<FormattedNumber
value={scoreValue}
decimals={scoreDecimals}
suffix={scoreSuffix}
/>
</Typography>
<Typography variant="caption">Score</Typography>
</Stack>
<Stack alignItems="center">
<Typography variant="body2" color="white" fontWeight={500}>
<FormattedNumber
value={resultValue}
prefix="$"
decimals={resultDecimals}
suffix={resultSuffix}
/>
</Typography>
<Typography variant="caption">Volume</Typography>
</Stack>
</Box>
</Box>
);
})}
</Stack>
));

export default LeaderboardList;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Box, Typography } from '@mui/material';

const MyEntryLabel = () => (
<Box
display="flex"
alignItems="center"
justifyContent="center"
py={0.5}
px={1}
borderRadius="9px"
sx={{
background: 'linear-gradient(98deg, #FFF -10.24%, #FEC0D6 106.59%)',
}}
>
<Typography
variant="caption"
color="#e65d8e"
fontWeight={600}
letterSpacing={0}
lineHeight={1}
>
You
</Typography>
</Box>
);

export default MyEntryLabel;
135 changes: 135 additions & 0 deletions campaign-launcher/client/src/components/Leaderboard/Overlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {
type ChangeEvent,
type FC,
useDeferredValue,
useMemo,
useState,
} from 'react';

import SearchIcon from '@mui/icons-material/Search';
import {
Box,
InputAdornment,
Stack,
TextField,
Typography,
} from '@mui/material';

import ResponsiveOverlay from '@/components/ResponsiveOverlay';
import { useIsMobile } from '@/hooks/useBreakpoints';
import { useActiveAccount } from '@/providers/ActiveAccountProvider';
import { type LeaderboardEntry } from '@/types';

import LeaderboardList from './List';

import { formatActualOnDate } from '.';

type Props = {
open: boolean;
onClose: () => void;
data: LeaderboardEntry[];
updatedAt: string;
symbol: string;
};

const LeaderboardOverlay: FC<Props> = ({
open,
onClose,
data,
updatedAt,
symbol,
}) => {
const [search, setSearch] = useState('');
const deferredSearch = useDeferredValue(search);
const isMobile = useIsMobile();
const { activeAddress } = useActiveAccount();

const filteredData = useMemo(() => {
const normalizedSearch = deferredSearch.trim().toLowerCase();
if (!normalizedSearch) return data;

return data.filter(({ address }) =>
address.toLowerCase().includes(normalizedSearch)
);
}, [data, deferredSearch]);

const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
setSearch(event.target.value);
};

const handleClose = () => {
setSearch('');
onClose();
};

return (
<ResponsiveOverlay
open={open}
onClose={handleClose}
desktopSx={{ p: 0, height: 650 }}
mobileSx={{ p: 0 }}
closeButtonSx={{
top: { xs: 20, md: 32 },
right: { xs: 16, md: 32 },
}}
>
<Stack height="100%" minHeight={0}>
<Box
px={{ xs: 2, md: 4 }}
pt={{ xs: 2, md: 4 }}
pb={{ xs: 2, md: 3 }}
bgcolor="background.default"
overflow="hidden"
>
<Typography
component="h6"
variant={isMobile ? 'h6' : 'h5'}
color="white"
fontWeight={700}
>
{`Leaderboard (${symbol})`}
</Typography>
<Typography fontSize="12px" fontWeight={500} lineHeight={1} mt={0.5}>
Actual on: {formatActualOnDate(updatedAt)}
</Typography>
<TextField
fullWidth
size="small"
placeholder="Search Wallet"
value={search}
onChange={handleSearchChange}
Comment thread
KirillKirill marked this conversation as resolved.
slotProps={{
input: {
'aria-label': 'Search Wallet',
endAdornment: (
<InputAdornment position="end">
<SearchIcon sx={{ color: 'white', fontSize: 24 }} />
</InputAdornment>
),
},
}}
sx={{
mt: 2,
'& .MuiOutlinedInput-root': {
color: 'white',
bgcolor: '#382c6b',
borderRadius: '28px',
border: 'none',
'& fieldset': {
border: 'none',
},
},
'& .MuiInputBase-input::placeholder': {
color: 'white',
opacity: 1,
},
}}
/>
</Box>
<LeaderboardList data={filteredData} activeAddress={activeAddress} />
</Stack>
</ResponsiveOverlay>
);
};

export default LeaderboardOverlay;
Loading