Skip to content

Commit

Permalink
Leaderboard - Adding Sessions and Identity Info, Support Search Funct…
Browse files Browse the repository at this point in the history
…ionality (#1864)

Co-authored-by: Pavan Soratur <[email protected]>
  • Loading branch information
AtelyPham and devpavan04 authored Nov 30, 2023
1 parent e8445e2 commit b6211a0
Show file tree
Hide file tree
Showing 18 changed files with 968 additions and 565 deletions.
2 changes: 1 addition & 1 deletion apps/testnet-leaderboard/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default function Index() {
<div className="lg:max-w-[1440px] lg:mx-auto">
<div
className={cx(
'lg:w-[77.5%] lg:mx-auto py-[48px] px-2 lg:px-[192px]',
'xl:w-[80%] lg:w-full lg:mx-auto py-[48px] px-2 lg:px-[192px]',
'border-2 border-mono-0 rounded-2xl',
'flex flex-col gap-[64px]',
'bg-[linear-gradient(180deg,rgba(255,255,255,0.2)_0%,rgba(255,255,255,0)_100%)]'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const Badges = () => {
<div className="flex flex-wrap justify-center gap-1">
{Object.entries(BADGE_ICON_RECORD).map(([badge, icon], idx) => {
const name = getBadgeName(badge) ?? badge;
const fmtName = name.split(' ').map(capitalize).join(' ');
const fmtName = name.split('_').map(capitalize).join(' ');

return (
<Badge
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
Tooltip,
TooltipBody,
TooltipTrigger,
} from '@webb-tools/webb-ui-components/components/Tooltip';
import { Typography } from '@webb-tools/webb-ui-components/typography/Typography';
import cx from 'classnames';
import capitalize from 'lodash/capitalize';
import { FC } from 'react';

type Props = {
badge: string;
emoji: string;
};

const BadgeWithTooltip: FC<Props> = ({ badge, emoji }) => {
return (
<Tooltip>
<TooltipTrigger asChild>
<div
className={cx(
'w-2 md:w-6 aspect-square rounded-full',
'flex items-center justify-center',
'bg-[rgba(31,29,43,0.1)] cursor-pointer'
)}
>
<Typography variant="mkt-caption">{emoji}</Typography>
</div>
</TooltipTrigger>

<TooltipBody>{badge.split('_').map(capitalize).join(' ')}</TooltipBody>
</Tooltip>
);
};

export default BadgeWithTooltip;
Original file line number Diff line number Diff line change
@@ -1,37 +1,18 @@
import {
Tooltip,
TooltipBody,
TooltipTrigger,
} from '@webb-tools/webb-ui-components';
import { Typography } from '@webb-tools/webb-ui-components/typography/Typography';
import cx from 'classnames';
import capitalize from 'lodash/capitalize';
import type { FC } from 'react';

import { BADGE_ICON_RECORD } from '../../constants';
import { BadgeEnum } from '../../types';
import BadgeWithTooltip from './BadgeWithTooltip';

const BadgesCell: FC<{ badges: BadgeEnum[] }> = ({ badges }) => {
return (
<div className="flex flex-wrap gap-[2px]">
{badges.map((badge, idx) => (
<Tooltip key={idx}>
<TooltipTrigger asChild>
<div
className={cx(
'w-6 md:w-[30px] aspect-square rounded-full',
'flex items-center justify-center',
'bg-[rgba(31,29,43,0.1)] cursor-pointer'
)}
>
<Typography variant="mkt-body2">
{BADGE_ICON_RECORD[badge]}
</Typography>
</div>
</TooltipTrigger>

<TooltipBody>{capitalize(badge.toString())}</TooltipBody>
</Tooltip>
<BadgeWithTooltip
key={`${idx}-${badge.toString()}`}
emoji={BADGE_ICON_RECORD[badge]}
badge={badge.toString()}
/>
))}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use client';

import { isEthereumAddress } from '@polkadot/util-crypto';
import { FileCopyLine } from '@webb-tools/icons';
import { Avatar } from '@webb-tools/webb-ui-components/components/Avatar';
import {
Tooltip,
TooltipBody,
TooltipTrigger,
} from '@webb-tools/webb-ui-components/components/Tooltip';
import { useCopyable } from '@webb-tools/webb-ui-components/hooks/useCopyable';
import { Typography } from '@webb-tools/webb-ui-components/typography/Typography';
import { shortenString } from '@webb-tools/webb-ui-components/utils/shortenString';
import cx from 'classnames';
import { type FC, useEffect, useState } from 'react';

import { IdentityType } from './types';

function handleClick() {
console.log('click');
}

const IdentityCell: FC<{ address: string; identity: IdentityType }> = ({
address,
identity,
}) => {
const { isCopied, copy } = useCopyable();

return (
<div className="flex items-center gap-2">
<Avatar
value={address}
theme={isEthereumAddress(address) ? 'ethereum' : 'substrate'}
className="[&_*]:!cursor-copy"
size="sm"
/>

<Typography
variant="mkt-small-caps"
fw="semibold"
className="!normal-case"
>
{identity?.info.display || shortenString(address)}
</Typography>

<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => copy(address)}
disabled={isCopied}
className="disabled:cursor-not-allowed"
>
<FileCopyLine className="!fill-current" />
</button>
</TooltipTrigger>

<TooltipBody className="font-semibold">
{isCopied ? 'Copied' : address}
</TooltipBody>
</Tooltip>
</div>
);
};

export default IdentityCell;
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,71 @@ import {
PaginationState,
useReactTable,
} from '@tanstack/react-table';
import { LoggerService } from '@webb-tools/browser-utils/logger';
import { Search, Spinner } from '@webb-tools/icons';
import { Input, Pagination, Typography } from '@webb-tools/webb-ui-components';
import cx from 'classnames';
import { type FC, useMemo, useState } from 'react';
import useSWR from 'swr';

import { BadgeEnum } from '../../types';
import AddressCell from './AddressCell';
import BadgesCell from './BadgesCell';
import fetchLeaderboardData from './fetchLeaderboardData';
import HeaderCell from './HeaderCell';
import IdentityCell from './IdentityCell';
import ParseReponseErrorView from './ParseReponseErrorView';
import type { LeaderboardSuccessResponseType, ParticipantType } from './types';
import SessionsCell from './SessionsCell';
import SocialLinksCell from './SocialLinksCell';
import type {
IdentityType,
LeaderboardSuccessResponseType,
ParticipantType,
SessionsType,
} from './types';

export type RankingItemType = {
address: string;
badges: BadgeEnum[];
points: number;
sessions: SessionsType;
identity: IdentityType;
};

const columnHelper = createColumnHelper<RankingItemType>();

const columns = [
const getColumns = (pageIndex: number, pageSize: number) => [
columnHelper.accessor('points', {
header: () => <HeaderCell title="Rank" />,
cell: (points) => (
<Typography variant="mkt-small-caps" fw="bold">
#{points.row.index + 1}
</Typography>
),
cell: (points) => {
const globalRowIndex = points.row.index + pageIndex * pageSize;
return (
<Typography variant="mkt-small-caps" fw="bold">
#{globalRowIndex + 1}
</Typography>
);
},
}),

columnHelper.accessor('address', {
header: () => <HeaderCell title="Address" />,
cell: (address) => <AddressCell address={address.getValue()} />,
header: () => <HeaderCell title="Identity" />,
cell: (cellCtx) => (
<IdentityCell
address={cellCtx.getValue()}
identity={cellCtx.row.original.identity}
/>
),
}),

columnHelper.accessor('badges', {
header: () => <HeaderCell title="Badges" />,
cell: (badges) => <BadgesCell badges={badges.getValue()} />,
}),

columnHelper.accessor('sessions', {
header: () => <HeaderCell title="Number of sessions" />,
cell: (cellCtx) => <SessionsCell sessions={cellCtx.getValue()} />,
}),

columnHelper.accessor('points', {
header: () => <HeaderCell title="Points" />,
cell: (points) => (
Expand All @@ -54,6 +80,11 @@ const columns = [
</Typography>
),
}),

columnHelper.accessor('identity', {
header: () => <HeaderCell title="Social" />,
cell: (cellCtx) => <SocialLinksCell identity={cellCtx.getValue()} />,
}),
];

type Props = LeaderboardSuccessResponseType['data'];
Expand All @@ -63,21 +94,27 @@ const participantToRankingItem = (participant: ParticipantType) =>
address: participant.addresses[0].address,
badges: participant.badges,
points: participant.points,
sessions: participant.sessions,
identity: participant.identity,
} satisfies RankingItemType);

const logger = LoggerService.get('RankingTableView');

const RankingTableView: FC<Props> = ({
participants,
limit: defaultLimit,
skip: defaultSkip,
total: defaultTotal,
}) => {
const [searchTerm, setSearchTerm] = useState<string>('');

const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
pageIndex: defaultSkip,
pageSize: defaultLimit,
});

const { data, isLoading } = useSWR(
[fetchLeaderboardData.name, pageIndex * pageSize, pageSize],
[fetchLeaderboardData.name, pageIndex * pageSize, pageSize, searchTerm],
([, ...args]) => fetchLeaderboardData(...args),
{ keepPreviousData: true }
);
Expand All @@ -96,12 +133,19 @@ const RankingTableView: FC<Props> = ({
}

if (data && data.success && data.data.success) {
return data.data.data.participants.map(participantToRankingItem);
return data.data.data.participants
.filter((p) => p.addresses.length > 0)
.map(participantToRankingItem);
}

return [];
}, [data, participants]);

const columns = useMemo(
() => getColumns(pageIndex, pageSize),
[pageIndex, pageSize]
);

const {
getHeaderGroups,
getRowModel,
Expand Down Expand Up @@ -132,6 +176,10 @@ const RankingTableView: FC<Props> = ({
});

if (!isLoading && data && !data.success) {
logger.error(
'Error when parsing the response',
data.error.issues.map((issue) => issue.message).join('\n')
);
return <ParseReponseErrorView />;
}

Expand All @@ -145,8 +193,7 @@ const RankingTableView: FC<Props> = ({
<Typography variant="mkt-body2" fw="black">
Latest ranking:
</Typography>
{/** TODO: Implement search by address with the server side data */}
{/* <div className="flex items-center gap-2 w-max md:w-1/2">
<div className="flex items-center gap-2 w-max md:w-1/2">
<Typography variant="mkt-body2" fw="black">
Search:
</Typography>
Expand All @@ -155,22 +202,19 @@ const RankingTableView: FC<Props> = ({
placeholder="Enter to search address"
className="flex-[1]"
rightIcon={<Search className="fill-mono-140" />}
value={searchTerm}
onChange={setSearchTerm}
debounceTime={500}
/>
</div> */}
</div>
</div>
<div className="relative overflow-hidden border rounded-lg border-mono-60">
<div className="relative overflow-scroll border rounded-lg border-mono-60">
<table className="w-full">
<thead className="border-b border-mono-60">
{getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header, idx) => (
<th
key={idx}
className={cx('px-2 py-2 md:px-6 md:py-3', {
'w-[10%]': header.id === 'points',
'w-[40%]': !(header.id === 'points'),
})}
>
<th key={idx} className={cx('px-2 py-2 md:px-4 md:py-2')}>
{header.isPlaceholder
? null
: flexRender(
Expand All @@ -187,7 +231,7 @@ const RankingTableView: FC<Props> = ({
getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell, idx) => (
<td key={idx} className="px-2 py-2 md:px-6 md:py-3">
<td key={idx} className="px-2 py-2 md:px-4 md:py-2">
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
Expand Down
Loading

0 comments on commit b6211a0

Please sign in to comment.