Skip to content

Commit

Permalink
Feat/leaderboard display (#7)
Browse files Browse the repository at this point in the history
* Update the display of daily rewards

* refactor: update CampaignTableRow to use Dropdown for blacklist and whitelist, and improve layout with Divider

* wip handling rewards
  • Loading branch information
hugolxt authored Dec 9, 2024
1 parent 6f6f09a commit e2ce35f
Show file tree
Hide file tree
Showing 19 changed files with 380 additions and 146 deletions.
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
16 changes: 14 additions & 2 deletions src/api/services/campaigns/campaign.service.ts
Original file line number Diff line number Diff line change
@@ -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<R, T extends { data: R; status: number; response: Response }>(
call: () => Promise<T>,
resource = "Opportunity",
): Promise<NonNullable<T["data"]>> {
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...
Expand Down Expand Up @@ -43,8 +56,7 @@ export abstract class CampaignService {
}

static async getByParams(query: Parameters<typeof api.v4.campaigns.index.get>[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
Expand Down
56 changes: 43 additions & 13 deletions src/api/services/reward.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,40 +50,70 @@ 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<typeof api.v4.rewards.index.get>[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<Reward[]> {
const rewards = await RewardService.#fetch(async () => api.v4.users({ address }).rewards.full.get());

//TODO: add some cache here
return rewards;
}

static async getByParams(query: {
items?: number;
page?: number;
chainId: number;
campaignIds: string[];
}) {
static async getManyFromRequest(
request: Request,
overrides?: Parameters<typeof api.v4.rewards.index.get>[0]["query"],
) {
return RewardService.getByParams(Object.assign(RewardService.#getQueryFromRequest(request), overrides ?? {}));
}

static async getByParams(query: Parameters<typeof api.v4.rewards.index.get>[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<ITotalRewards> {
const total = await RewardService.#fetch(async () =>
api.v4.rewards.total.get({
query: {
...query,
campaignIds: query.campaignIds.join(","),
campaignId: query.campaignId,
},
}),
);
Expand Down
2 changes: 0 additions & 2 deletions src/components/composite/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -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({
Expand Down
6 changes: 3 additions & 3 deletions src/components/element/campaign/CampaignTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
109 changes: 55 additions & 54 deletions src/components/element/campaign/CampaignTableRow.tsx
Original file line number Diff line number Diff line change
@@ -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<{
Expand All @@ -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 (
<CampaignRow
{...props}
className={mergeClass("cursor-pointer", className)}
className={mergeClass("cursor-pointer py-4", className)}
onClick={toggleIsOpen}
chainColumn={<Chain chain={campaign.chain} />}
restrictionsColumn={<RestrictionsCollumn campaign={campaign} />}
dailyRewardsColumn={
<Group className="align-middle items-center">
<OverrideTheme accent={"good"}>
<Icon className={active ? "text-accent-10" : "text-main-10"} remix="RiCircleFill" size="xs" />
<Icon className={active ? "text-accent-10" : "text-main-10"} remix="RiCircleFill" />
</OverrideTheme>
<Token token={campaign.rewardToken} amount={dailyRewards} />
<Token token={campaign.rewardToken} amount={dailyRewards} format="amount_price" />
</Group>
}
timeRemainingColumn={
<Group className="py-xl">
<Text>{time}</Text>
</Group>
<Dropdown content={<CampaignTooltipDates campaign={campaign} />}>
<PrimitiveTag look="base">{time}</PrimitiveTag>
</Dropdown>
}
arrowColumn={<Icon remix={!isOpen ? "RiArrowDownSLine" : "RiArrowUpSLine"} />}>
{isOpen && (
<div className="animate-drop">
<Group className="flex-nowrap" size="lg">
<Group className="flex-nowrap p-lg" size="lg">
<Group className="justify-between flex-col size-full">
<Text size="md">Campaign information</Text>
<div className="flex justify-between">
<Text size="sm">Total</Text>
<Value className="text-right" look={campaignAmount === "0" ? "soft" : "base"} format="$0,0.#">
{campaignAmount}
</Value>
<Token token={campaign.rewardToken} amount={amount} format="amount_price" />
</div>
<div className="flex justify-between">
<Text size="sm">Dates</Text>
<span className="flex">
<Text size="sm">
{moment.unix(Number(campaign.startTimestamp)).format("DD MMMM YYYY")}-
{moment.unix(Number(campaign.endTimestamp)).format("DD MMMM YYYY")}
</Text>
<Dropdown content={<CampaignTooltipDates campaign={campaign} />}>
<Text size="sm" className="flex">
{moment.unix(Number(campaign.startTimestamp)).format("DD MMMM YYYY")}
<Icon remix="RiArrowRightLine" />
{moment.unix(Number(campaign.endTimestamp)).format("DD MMMM YYYY")}
</Text>
</Dropdown>
</span>
</div>
{/* <div className="flex justify-between">
Expand All @@ -73,49 +79,44 @@ export default function CampaignTableRow({ campaign, startsOpen, className, ...p
</div> */}
<div className="flex justify-between">
<Text size="sm">Campaign creator</Text>
<Hash size="sm" format="short">
<Hash size="sm" format="short" copy>
{campaign.creatorAddress}
</Hash>
</div>
<div className="flex justify-between">
<Text size="sm">Campaign id</Text>
<Hash format="short" size="sm">
<Hash format="short" size="sm" copy>
{campaign.campaignId}
</Hash>
</div>
</Group>
<Divider vertical={true} />
<Group className="justify-between flex-col size-full">
<Text size={"md"}>Conditions</Text>
<Group className="flex justify-between item-center">
<Text size="sm">Incentivized Liquidity</Text>
{profile}
</Group>
<span className="flex justify-between">
<Text size="sm">Blacklisted for</Text>
<Tooltip
helper={
<div>
{campaign.params.blacklist.length > 0
? campaign.params.blacklist.map((blacklist: string) => blacklist)
: "No address"}
</div>
}>
<Text size="sm">{campaign.params.blacklist.length} address</Text>
</Tooltip>
</span>
<span className="flex justify-between">
<Text size="sm">Whitelisted for</Text>
<Tooltip
helper={
<div>
{campaign.params.whitelist.length > 0
? campaign.params.whitelist.map((blacklist: string) => blacklist)
: "No address"}
</div>
}>
<Text size="sm">{campaign.params.whitelist.length} address</Text>
</Tooltip>
</span>

{/* Todo: Need to be refacto @Clement */}
<Group>
{campaign.params.blacklist.length > 0 && (
<Dropdown
content={campaign.params.blacklist.map((address: string) => <Text key={address}>{address}</Text>)}>
<PrimitiveTag look="soft" size="sm">
Blacklist ({campaign.params.blacklist.length} address)
</PrimitiveTag>
</Dropdown>
)}

{campaign.params.whitelist.length > 0 && (
<Dropdown
content={campaign.params.whitelist.map((address: string) => <Text key={address}>{address}</Text>)}>
<PrimitiveTag look="soft" size="sm">
Whitelist ({campaign.params.whitelist.length} address)
</PrimitiveTag>
</Dropdown>
)}
</Group>
</Group>
</Group>
</div>
Expand Down
35 changes: 35 additions & 0 deletions src/components/element/campaign/CampaignTooltipDates.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Group>
<Icon remix={"RiCalendar2Line"} />
<Text look="bold">Campaign dates</Text>
<Divider look="soft" horizontal />
<Group className="flex-col">
<Group>
<Text size="sm" look={"bold"}>
Start
</Text>
<Text size="sm">
{moment.unix(Number(campaign.startTimestamp)).format("DD MMMM YYYY ha (UTC Z)").replace("+", "+ ")}
</Text>
</Group>
<Group>
<Text size="sm" look={"bold"}>
End
</Text>
<Text size="sm">{moment.unix(Number(campaign.endTimestamp)).format("DD MMMM YYYY ha (UTC Z)")}</Text>
</Group>
</Group>
</Group>
</>
);
}
Loading

0 comments on commit e2ce35f

Please sign in to comment.