From e2ce35fca11b62502aa697de1c706524756eed78 Mon Sep 17 00:00:00 2001 From: hugolxt <87241914+hugolxt@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:17:46 +0100 Subject: [PATCH] Feat/leaderboard display (#7) * Update the display of daily rewards * refactor: update CampaignTableRow to use Dropdown for blacklist and whitelist, and improve layout with Divider * wip handling rewards --- bun.lockb | Bin 827807 -> 829288 bytes package.json | 2 +- .../services/campaigns/campaign.service.ts | 16 ++- src/api/services/reward.service.ts | 56 ++++++--- src/components/composite/Hero.tsx | 2 - .../element/campaign/CampaignTable.tsx | 6 +- .../element/campaign/CampaignTableRow.tsx | 109 +++++++++--------- .../element/campaign/CampaignTooltipDates.tsx | 35 ++++++ .../element/campaign/CampaignTooltipToken.tsx | 40 +++++++ .../tableCollumns/RestrictionsCollumn.tsx | 2 +- .../leaderboard/LeaderboardLibrary.tsx | 20 +++- .../leaderboard/LeaderboardTableRow.tsx | 22 ++-- .../opportunity/OpportunityPagination.tsx | 2 +- .../participate/ParticipateTester.client.tsx | 2 +- src/components/element/token/Token.tsx | 25 +++- src/components/element/token/TokenTooltip.tsx | 1 - src/hooks/resources/useCampaign.tsx | 76 +++++++++--- ...rtunities.$chain.$type.$id.leaderboard.tsx | 103 ++++++++++++----- .../_merkl.opportunities.$chain.$type.$id.tsx | 7 +- 19 files changed, 380 insertions(+), 146 deletions(-) create mode 100644 src/components/element/campaign/CampaignTooltipDates.tsx create mode 100644 src/components/element/campaign/CampaignTooltipToken.tsx diff --git a/bun.lockb b/bun.lockb index 796bc6ad0317938d36009a264bb3010cd8a169af..5be51465c0b4faa1c7b5b499d2bad2a8a0ccb966 100755 GIT binary patch delta 12640 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 Yq3{>f`SFDcsB5DO6;bc4Ds<{U07k1MtN;K2 diff --git a/package.json b/package.json index 43905758..092347ce 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ ], "dependencies": { "@acab/ecsstatic": "^0.8.0", - "@merkl/api": "0.10.107", + "@merkl/api": "0.10.114", "@ariakit/react": "^0.4.12", "@elysiajs/eden": "^1.1.3", "@emotion/css": "^11.13.4", diff --git a/src/api/services/campaigns/campaign.service.ts b/src/api/services/campaigns/campaign.service.ts index bd563d02..7ebaf94c 100644 --- a/src/api/services/campaigns/campaign.service.ts +++ b/src/api/services/campaigns/campaign.service.ts @@ -1,7 +1,20 @@ import type { Campaign } from "@merkl/api"; +import { fetchWithLogs } from "src/api/utils"; import { api } from "../../index.server"; export abstract class CampaignService { + static async #fetch( + call: () => Promise, + resource = "Opportunity", + ): Promise> { + const { data, status } = await fetchWithLogs(call); + + if (status === 404) throw new Response(`${resource} not found`, { status }); + if (status === 500) throw new Response(`${resource} unavailable`, { status }); + if (data == null) throw new Response(`${resource} unavailable`, { status }); + return data; + } + /** * Retrieves opportunities query params from page request * @param request request containing query params such as chains, status, pagination... @@ -43,8 +56,7 @@ export abstract class CampaignService { } static async getByParams(query: Parameters[0]["query"]) { - const { data } = await api.v4.campaigns.index.get({ query }); - return data; + return await CampaignService.#fetch(async () => api.v4.campaigns.index.get({ query })); } // ------ Fetch a campaign by ID diff --git a/src/api/services/reward.service.ts b/src/api/services/reward.service.ts index d671bacc..3f2c5c52 100644 --- a/src/api/services/reward.service.ts +++ b/src/api/services/reward.service.ts @@ -50,6 +50,35 @@ export abstract class RewardService { return data; } + /** + * Retrieves opportunities query params from page request + * @param request request containing query params such as chains, status, pagination... + * @param override params for which to override value + * @returns query + */ + static #getQueryFromRequest(request: Request, override?: Parameters[0]["query"]) { + const campaignId = new URL(request.url).searchParams.get("campaignId"); + const page = new URL(request.url).searchParams.get("page"); + const items = new URL(request.url).searchParams.get("items"); + + const filters = Object.assign( + { + campaignId, + items: items ?? 50, + page, + }, + override ?? {}, + page !== null && { page: Number(page) - 1 }, + ); + + const query = Object.entries(filters).reduce( + (_query, [key, filter]) => Object.assign(_query, filter == null ? {} : { [key]: filter }), + {}, + ); + + return query; + } + static async getForUser(address: string): Promise { const rewards = await RewardService.#fetch(async () => api.v4.users({ address }).rewards.full.get()); @@ -57,33 +86,34 @@ export abstract class RewardService { return rewards; } - static async getByParams(query: { - items?: number; - page?: number; - chainId: number; - campaignIds: string[]; - }) { + static async getManyFromRequest( + request: Request, + overrides?: Parameters[0]["query"], + ) { + return RewardService.getByParams(Object.assign(RewardService.#getQueryFromRequest(request), overrides ?? {})); + } + + static async getByParams(query: Parameters[0]["query"]) { const rewards = await RewardService.#fetch(async () => api.v4.rewards.index.get({ - query: { - ...query, - campaignIds: query.campaignIds.join(","), - }, + query, }), ); - return rewards as unknown as IRewards[]; + const count = await RewardService.#fetch(async () => api.v4.rewards.count.get({ query })); + + return { count, rewards }; } static async total(query: { chainId: number; - campaignIds: string[]; + campaignId: string; }): Promise { const total = await RewardService.#fetch(async () => api.v4.rewards.total.get({ query: { ...query, - campaignIds: query.campaignIds.join(","), + campaignId: query.campaignId, }, }), ); diff --git a/src/components/composite/Hero.tsx b/src/components/composite/Hero.tsx index ba2d45c1..ef5c5e1d 100644 --- a/src/components/composite/Hero.tsx +++ b/src/components/composite/Hero.tsx @@ -3,7 +3,6 @@ import { Container, Divider, Group, Icon, type IconProps, Icons, Tabs, Text, Tit import { Button } from "dappkit"; import config from "merkl.config"; import type { PropsWithChildren, ReactNode } from "react"; -import type { Opportunity } from "src/api/services/opportunity/opportunity.model"; export type HeroProps = PropsWithChildren<{ icons?: IconProps[]; @@ -14,7 +13,6 @@ export type HeroProps = PropsWithChildren<{ tags?: ReactNode[]; sideDatas?: { data: ReactNode; label: string; key: string }[]; tabs?: { label: ReactNode; link: string; key: string }[]; - opportunity?: Opportunity; }>; export default function Hero({ diff --git a/src/components/element/campaign/CampaignTable.tsx b/src/components/element/campaign/CampaignTable.tsx index a268cf8d..84050bed 100644 --- a/src/components/element/campaign/CampaignTable.tsx +++ b/src/components/element/campaign/CampaignTable.tsx @@ -9,19 +9,19 @@ export const [CampaignTable, CampaignRow, CampaignColumns] = createTable({ main: true, }, restrictions: { - name: "Conditions", + name: "", size: "minmax(170px,1fr)", compactSize: "1fr", className: "justify-start", }, chain: { - name: "chain", + name: "Chain", size: "minmax(30px,150px)", compactSize: "minmax(20px,1fr)", className: "justify-start", }, timeRemaining: { - name: "Time Left", + name: "End", size: "minmax(30px,150px)", compactSize: "minmax(20px,1fr)", className: "justify-center", diff --git a/src/components/element/campaign/CampaignTableRow.tsx b/src/components/element/campaign/CampaignTableRow.tsx index 3c047776..9620404d 100644 --- a/src/components/element/campaign/CampaignTableRow.tsx +++ b/src/components/element/campaign/CampaignTableRow.tsx @@ -1,13 +1,23 @@ -import { type Component, Group, Hash, Icon, OverrideTheme, Text, Value, mergeClass } from "dappkit"; +import type { Campaign } from "@merkl/api"; +import { + type Component, + Divider, + Dropdown, + Group, + Hash, + Icon, + OverrideTheme, + PrimitiveTag, + Text, + mergeClass, +} from "dappkit"; import moment from "moment"; -import Tooltip from "packages/dappkit/src/components/primitives/Tooltip"; -import { useCallback, useMemo, useState } from "react"; -import type { Campaign } from "src/api/services/campaigns/campaign.model"; +import { useCallback, useState } from "react"; import useCampaign from "src/hooks/resources/useCampaign"; -import { formatUnits, parseUnits } from "viem"; import Chain from "../chain/Chain"; import Token from "../token/Token"; import { CampaignRow } from "./CampaignTable"; +import CampaignTooltipDates from "./CampaignTooltipDates"; import RestrictionsCollumn from "./tableCollumns/RestrictionsCollumn"; export type CampaignTableRowProps = Component<{ @@ -16,55 +26,51 @@ export type CampaignTableRowProps = Component<{ }>; export default function CampaignTableRow({ campaign, startsOpen, className, ...props }: CampaignTableRowProps) { - const { time, profile, dailyRewards, active } = useCampaign(campaign); + const { time, profile, dailyRewards, active, amount } = useCampaign(campaign); const [isOpen, setIsOpen] = useState(startsOpen); const toggleIsOpen = useCallback(() => setIsOpen(o => !o), []); - const campaignAmount = useMemo( - () => formatUnits(parseUnits(campaign.amount, 0), campaign.rewardToken.decimals), - [campaign], - ); - return ( } restrictionsColumn={} dailyRewardsColumn={ - + - + } timeRemainingColumn={ - - {time} - + }> + {time} + } arrowColumn={}> {isOpen && (
- + Campaign information
Total - - {campaignAmount} - +
Dates - - {moment.unix(Number(campaign.startTimestamp)).format("DD MMMM YYYY")}- - {moment.unix(Number(campaign.endTimestamp)).format("DD MMMM YYYY")} - + }> + + {moment.unix(Number(campaign.startTimestamp)).format("DD MMMM YYYY")} + + {moment.unix(Number(campaign.endTimestamp)).format("DD MMMM YYYY")} + +
{/*
@@ -73,49 +79,44 @@ export default function CampaignTableRow({ campaign, startsOpen, className, ...p
*/}
Campaign creator - + {campaign.creatorAddress}
Campaign id - + {campaign.campaignId}
+ - Conditions Incentivized Liquidity {profile} - - Blacklisted for - - {campaign.params.blacklist.length > 0 - ? campaign.params.blacklist.map((blacklist: string) => blacklist) - : "No address"} -
- }> - {campaign.params.blacklist.length} address - - - - Whitelisted for - - {campaign.params.whitelist.length > 0 - ? campaign.params.whitelist.map((blacklist: string) => blacklist) - : "No address"} - - }> - {campaign.params.whitelist.length} address - - + + {/* Todo: Need to be refacto @Clement */} + + {campaign.params.blacklist.length > 0 && ( + {address})}> + + Blacklist ({campaign.params.blacklist.length} address) + + + )} + + {campaign.params.whitelist.length > 0 && ( + {address})}> + + Whitelist ({campaign.params.whitelist.length} address) + + + )} + diff --git a/src/components/element/campaign/CampaignTooltipDates.tsx b/src/components/element/campaign/CampaignTooltipDates.tsx new file mode 100644 index 00000000..dda85695 --- /dev/null +++ b/src/components/element/campaign/CampaignTooltipDates.tsx @@ -0,0 +1,35 @@ +import type { Campaign } from "@merkl/api"; +import moment from "moment"; +import { Divider, Group, Icon, Text } from "packages/dappkit/src"; + +export type IProps = { + campaign: Campaign; +}; + +export default function CampaignTooltipDates({ campaign }: IProps) { + return ( + <> + + + Campaign dates + + + + + Start + + + {moment.unix(Number(campaign.startTimestamp)).format("DD MMMM YYYY ha (UTC Z)").replace("+", "+ ")} + + + + + End + + {moment.unix(Number(campaign.endTimestamp)).format("DD MMMM YYYY ha (UTC Z)")} + + + + + ); +} diff --git a/src/components/element/campaign/CampaignTooltipToken.tsx b/src/components/element/campaign/CampaignTooltipToken.tsx new file mode 100644 index 00000000..3a2d66b6 --- /dev/null +++ b/src/components/element/campaign/CampaignTooltipToken.tsx @@ -0,0 +1,40 @@ +import type { Campaign } from "@merkl/api"; +import { Button, Divider, Group, Icon, Text, Value } from "packages/dappkit/src"; +import useCampaign from "src/hooks/resources/useCampaign"; + +export type IProps = { + campaign: Campaign; +}; + +export default function CampaignTooltipToken({ campaign }: IProps) { + const { amount, amountUsd } = useCampaign(campaign); + + return ( + <> + + + + Total rewards + + + + {amount} + + + {amountUsd} + + + + + + + + + + + ); +} diff --git a/src/components/element/campaign/tableCollumns/RestrictionsCollumn.tsx b/src/components/element/campaign/tableCollumns/RestrictionsCollumn.tsx index d939198c..2b965022 100644 --- a/src/components/element/campaign/tableCollumns/RestrictionsCollumn.tsx +++ b/src/components/element/campaign/tableCollumns/RestrictionsCollumn.tsx @@ -1,4 +1,4 @@ -import type { Campaign } from "@angleprotocol/merkl-api"; +import type { Campaign } from "@merkl/api"; import { Button, Dropdown } from "packages/dappkit/src"; type IProps = { diff --git a/src/components/element/leaderboard/LeaderboardLibrary.tsx b/src/components/element/leaderboard/LeaderboardLibrary.tsx index 17eaea50..637b3412 100644 --- a/src/components/element/leaderboard/LeaderboardLibrary.tsx +++ b/src/components/element/leaderboard/LeaderboardLibrary.tsx @@ -1,3 +1,5 @@ +import type { Campaign } from "@merkl/api"; +import { useSearchParams } from "@remix-run/react"; import { Text } from "dappkit"; import { useMemo } from "react"; import type { IRewards } from "src/api/services/reward.service"; @@ -8,14 +10,26 @@ import LeaderboardTableRow from "./LeaderboardTableRow"; export type IProps = { leaderboard: IRewards[]; count?: number; + campaign: Campaign; }; export default function LeaderboardLibrary(props: IProps) { - const { leaderboard, count } = props; + const { leaderboard, count, campaign } = props; + const [searchParams] = useSearchParams(); + + const items = searchParams.get("items"); + const page = searchParams.get("page"); const rows = useMemo(() => { - return leaderboard?.map((row, index) => ); - }, [leaderboard]); + return leaderboard?.map((row, index) => ( + + )); + }, [leaderboard, page, items, campaign]); return ( ; export default function LeaderboardTableRow({ row, rank, className, ...props }: CampaignTableRowProps) { - const rewardAmount = useMemo(() => formatUnits(parseUnits(row?.amount, 0), row?.Token?.decimals), [row]); + const { campaign } = props; return ( #{rank}} - addressColumn={{row?.recipient}} - rewardsColumn={ - - {rewardAmount} - + addressColumn={ + + {row?.recipient} + } - protocolColumn={{row?.reason.split("_")[0]}} + rewardsColumn={} + protocolColumn={{row?.reason?.split("_")[0]}} /> ); } diff --git a/src/components/element/opportunity/OpportunityPagination.tsx b/src/components/element/opportunity/OpportunityPagination.tsx index dfd6f471..078805b5 100644 --- a/src/components/element/opportunity/OpportunityPagination.tsx +++ b/src/components/element/opportunity/OpportunityPagination.tsx @@ -19,7 +19,7 @@ export default function OpportunityPagination({ count }: OpportunityPaginationPr v => Number.parseInt(v), ); - const pages = useMemo(() => Math.round((count ?? 0) / (itemsFilter ?? 20)) - 1, [count, itemsFilter]); + const pages = useMemo(() => Math.ceil((count ?? 0) / (itemsFilter ?? 20)), [count, itemsFilter]); const pageOptions = useMemo(() => { return [...Array(Math.max(Math.round(pages ?? 0), 1)).fill(0)] .map((_, index) => index + 1) diff --git a/src/components/element/participate/ParticipateTester.client.tsx b/src/components/element/participate/ParticipateTester.client.tsx index eb6ebb53..9ec55b20 100644 --- a/src/components/element/participate/ParticipateTester.client.tsx +++ b/src/components/element/participate/ParticipateTester.client.tsx @@ -75,7 +75,7 @@ export default function ParticipateTester({ chains }: ParticipateTesterProps) { {/* */} {/* {target.name} */} {target?.tokens.map(tkn => ( - + ))} ), diff --git a/src/components/element/token/Token.tsx b/src/components/element/token/Token.tsx index 7bcfc26e..1635636b 100644 --- a/src/components/element/token/Token.tsx +++ b/src/components/element/token/Token.tsx @@ -1,26 +1,43 @@ import type { Token as TokenType } from "@merkl/api"; import { Button, Dropdown, Icon, Value } from "packages/dappkit/src"; import { useMemo } from "react"; +import { formatUnits, parseUnits } from "viem"; import TokenTooltip from "./TokenTooltip"; export type TokenProps = { token: TokenType; + format?: "amount" | "price" | "amount_price"; + amount?: bigint; value?: boolean; - amount?: number; }; -export default function Token({ token, amount, value }: TokenProps) { +export default function Token({ token, amount, format = "amount", value }: TokenProps) { + const amoutFormated = amount ? formatUnits(amount, token.decimals) : undefined; + + const price = parseUnits(token.price?.toString() ?? "0", 0); + + const amountUSD = price * (amount ?? 0n); + const display = useMemo( () => ( <> - {amount && {amount}} + {format === "amount" || + (format === "amount_price" && !!amount && {amoutFormated})}{" "} + {token.symbol} + {format === "price" || + (format === "amount_price" && !!amount && ( + + {formatUnits(amountUSD, token.decimals)} + + ))} ), - [token, amount], + [token, format, amoutFormated, amountUSD, amount], ); if (value) return display; + return ( }> diff --git a/src/components/element/token/TokenTooltip.tsx b/src/components/element/token/TokenTooltip.tsx index b688b087..d2ad1998 100644 --- a/src/components/element/token/TokenTooltip.tsx +++ b/src/components/element/token/TokenTooltip.tsx @@ -3,7 +3,6 @@ import { Button, Divider, Group, Hash, Icon, Text, Title } from "packages/dappki export type TokenTooltipProps = { token: Token; - amount?: number; }; export default function TokenTooltip({ token }: TokenTooltipProps) { diff --git a/src/hooks/resources/useCampaign.tsx b/src/hooks/resources/useCampaign.tsx index 97e30241..be92b12f 100644 --- a/src/hooks/resources/useCampaign.tsx +++ b/src/hooks/resources/useCampaign.tsx @@ -1,23 +1,55 @@ +import type { Campaign, Opportunity } from "@merkl/api"; import { Bar } from "dappkit"; import moment from "moment"; import { Group, Text, Value } from "packages/dappkit/src"; import Time from "packages/dappkit/src/components/primitives/Time"; import { type ReactNode, useMemo } from "react"; -import type { Campaign } from "src/api/services/campaigns/campaign.model"; -import type { Opportunity } from "src/api/services/opportunity/opportunity.model"; -import { formatUnits } from "viem"; +import { formatUnits, parseUnits } from "viem"; + +export default function useCampaign(campaign?: Campaign) { + if (!campaign) + return { + amount: undefined, + time: undefined, + profile: undefined, + dailyRewards: undefined, + progressBar: undefined, + active: undefined, + }; + + // ─── Campaign Amount Prices ────────────────────────────────── -export default function useCampaign(campaign: Campaign) { const amount = useMemo(() => { - return Number.parseFloat(formatUnits(BigInt(campaign.amount), campaign.rewardToken.decimals)); - }, [campaign?.amount, campaign?.rewardToken?.decimals]); + return parseUnits(campaign.amount, 0); + }, [campaign?.amount]); + + const dailyRewards = useMemo(() => { + const duration = campaign.endTimestamp - campaign.startTimestamp; + const oneDayInSeconds = BigInt(3600 * 24); + const dayspan = BigInt(duration) / BigInt(oneDayInSeconds) || BigInt(1); + const amountInUnits = parseUnits(amount.toString(), 0); + const dailyReward = amountInUnits / dayspan; + + return dailyReward; + }, [campaign, amount]); + + const dailyRewardsUsd = useMemo(() => { + return formatUnits( + parseUnits(dailyRewards.toString(), 0) * parseUnits(campaign.rewardToken.price?.toString() ?? "0", 18), + 18, + ); + }, [campaign, dailyRewards]); + + // ─── Campaign Amount Time displaying ────────────────────────────────── const time = useMemo(() => { return