From bd7de558d71b381e9f37976d141ce7c26737c5b0 Mon Sep 17 00:00:00 2001 From: Hugo Lextrait Date: Wed, 27 Nov 2024 16:08:12 +0100 Subject: [PATCH 01/11] wip leaderboard and opportunity page --- bun.lockb | Bin 827874 -> 827834 bytes src/components/composite/Heading.tsx | 90 +++++++++----- .../element/campaign/CampaignLibrary.tsx | 16 ++- .../element/campaign/CampaignTable.tsx | 26 +++-- .../element/campaign/CampaignTableRow.tsx | 110 +++++++++++++----- .../tableCollumns/RestrictionsCollumn.tsx | 29 +++++ .../leaderboard/LeaderboardLibrary.tsx | 23 ++++ .../element/leaderboard/LeaderboardTable.tsx | 29 +++++ .../leaderboard/LeaderboardTableRow.tsx | 20 ++++ .../rewards/ClaimRewardsTokenTableRow.tsx | 9 -- src/hooks/resources/useCampaign.tsx | 2 +- ...pportunity.$chain.$type.$id.(overview).tsx | 23 ++-- ...opportunity.$chain.$type.$id.analytics.tsx | 10 -- ...portunity.$chain.$type.$id.leaderboard.tsx | 23 +++- .../_merkl.opportunity.$chain.$type.$id.tsx | 68 ++++++----- 15 files changed, 344 insertions(+), 134 deletions(-) create mode 100644 src/components/element/campaign/tableCollumns/RestrictionsCollumn.tsx create mode 100644 src/components/element/leaderboard/LeaderboardLibrary.tsx create mode 100644 src/components/element/leaderboard/LeaderboardTable.tsx create mode 100644 src/components/element/leaderboard/LeaderboardTableRow.tsx delete mode 100644 src/routes/_merkl.opportunity.$chain.$type.$id.analytics.tsx diff --git a/bun.lockb b/bun.lockb index 3deacacd4b6b2c6b6b9cb3f9ddd4907412c6be45..dcb752de05f1cb2b0888d1cedb8b6638c9d97c40 100755 GIT binary patch delta 6388 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 Q>69dT#n|bTO8ch%314oDX#fBK diff --git a/src/components/composite/Heading.tsx b/src/components/composite/Heading.tsx index ac997b53..f9a17006 100644 --- a/src/components/composite/Heading.tsx +++ b/src/components/composite/Heading.tsx @@ -1,7 +1,9 @@ +import type { Campaign, Opportunity } from "@angleprotocol/merkl-api"; import { useLocation } from "@remix-run/react"; -import { Box, Divider, Group, Icon, type IconProps, Icons, Text, Title } from "dappkit"; +import { Box, Divider, Group, Icon, type IconProps, Icons, Text, Title, Value } from "dappkit"; import { Button } from "dappkit"; -import type { PropsWithChildren, ReactNode } from "react"; +import { useMemo, type PropsWithChildren, type ReactNode } from "react"; +import { formatUnits, parseUnits } from "viem"; export type HeadingProps = PropsWithChildren<{ icons: IconProps[]; @@ -10,6 +12,7 @@ export type HeadingProps = PropsWithChildren<{ description: ReactNode; tags?: ReactNode[]; tabs?: { label: ReactNode; link: string }[]; + campaigns?: Campaign[]; }>; export default function Heading({ @@ -20,40 +23,71 @@ export default function Heading({ tags, tabs, children, + campaigns, }: HeadingProps) { const location = useLocation(); + const totalRewards = useMemo(() => { + const amounts = campaigns?.map(campaign => { + const duration = campaign.endTimestamp - campaign.startTimestamp; + const dayspan = BigInt(duration) / BigInt(3600 * 24); + + return parseUnits(campaign.amount, 0) / BigInt(dayspan); + }); + + const sum = amounts?.reduce((accumulator, currentValue) => accumulator + currentValue, 0n); + if (!sum) return "0.0"; + return formatUnits(sum, 18); + }, [campaigns]); + return ( <> - - {/** Disabled and set invisible when undefined to preserve layout height */} - - - - - {icons?.map((icon) => ( - - ))} - - {title} + +
+ + {/** Disabled and set invisible when undefined to preserve layout height */} + + + + + {icons?.map(icon => ( + + ))} + + + {title} + + + {tags && {tags}} + {description} +
+ + + + {totalRewards} + + Daily rewards + + + todo + APR + + + {campaigns?.length} + Active campaigns + +
- {tags && {tags}} - {description} {tabs?.map(tab => ( + +
+ )} ); } diff --git a/src/components/element/campaign/CampaignTable.tsx b/src/components/element/campaign/CampaignTable.tsx index a97a2f21..3dbd74f3 100644 --- a/src/components/element/campaign/CampaignTable.tsx +++ b/src/components/element/campaign/CampaignTable.tsx @@ -2,26 +2,32 @@ import { createTable } from "dappkit"; export const [CampaignTable, CampaignRow, CampaignColumns] = createTable({ dailyRewards: { - name: "DAILY REWARDS", + name: "Daily rewards", size: "minmax(200px,250px)", compact: "1fr", className: "justify-start", main: true, }, + restrictions: { + name: "Conditions", + size: "minmax(170px,1fr)", + compactSize: "1fr", + className: "justify-start", + }, + chain: { + name: "chain", + size: "minmax(30px,150px)", + compactSize: "minmax(20px,1fr)", + className: "justify-start", + }, timeRemaining: { - name: "END", - size: "minmax(10px,150px)", + name: "Time Left", + size: "minmax(30px,150px)", compactSize: "minmax(20px,1fr)", className: "justify-center", }, - restrictions: { - name: "RESTRICTIONS", - size: "minmax(120px,1fr)", - compactSize: "minmax(10px,1fr)", - className: "justify-start", - }, profile: { - name: "PROFILE", + name: "Incentivized Liquidity", size: "minmax(100px,200px)", compactSize: "minmax(100px,1fr)", className: "justify-start", diff --git a/src/components/element/campaign/CampaignTableRow.tsx b/src/components/element/campaign/CampaignTableRow.tsx index 2be95f2f..f9ff9e49 100644 --- a/src/components/element/campaign/CampaignTableRow.tsx +++ b/src/components/element/campaign/CampaignTableRow.tsx @@ -1,9 +1,13 @@ import type { Campaign } from "@angleprotocol/merkl-api"; -import { type Component, Group, Hash, Icon, OverrideTheme, Text, mergeClass } from "dappkit"; -import { useState } from "react"; +import { type Component, Group, Icon, OverrideTheme, Text, Value, mergeClass } from "dappkit"; +import { useCallback, useMemo, useState } from "react"; import useCampaign from "src/hooks/resources/useCampaign"; import Token from "../token/Token"; import { CampaignRow } from "./CampaignTable"; +import { formatUnits, parseUnits } from "viem"; +import moment from "moment"; +import RestrictionsCollumn from "./tableCollumns/RestrictionsCollumn"; +import Tooltip from "packages/dappkit/src/components/primitives/Tooltip"; export type CampaignTableRowProps = Component<{ campaign: Campaign; @@ -11,14 +15,21 @@ export type CampaignTableRowProps = Component<{ }>; export default function CampaignTableRow({ campaign, startsOpen, className, ...props }: CampaignTableRowProps) { - const { time, profile, dailyRewards, progressBar, active } = useCampaign(campaign); - const [open, setOpen] = useState(startsOpen); + const { time, profile, dailyRewards, active } = 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 ( setOpen(o => !o)} + onClick={toggleIsOpen} dailyRewardsColumn={ @@ -27,34 +38,75 @@ export default function CampaignTableRow({ campaign, startsOpen, className, ...p } - timeRemainingColumn={ - - {time} - - } - restrictionsColumn={[]} + timeRemainingColumn={{time}} + restrictionsColumn={} profileColumn={profile} - arrowColumn={}> - {open && ( + chainColumn={ + + + {campaign.chain.name} + + } + arrowColumn={}> + {isOpen && (
- {progressBar} - - - created by +
+ + Campaign information +
+ Total + + {campaignAmount} + +
+
+ Dates + + + {moment.unix(Number(campaign.startTimestamp)).format("DD MMMM YYYY")}- + {moment.unix(Number(campaign.endTimestamp)).format("DD MMMM YYYY")} + + +
+
+ Last snapshot + {/*
+
+ Campaign creator + {campaign.creatorAddress} +
- - id: - - {campaign.id} - + + Conditions + + 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 + +
-
- - - distributed on - {campaign.distributionChain?.name} - - + )} diff --git a/src/components/element/campaign/tableCollumns/RestrictionsCollumn.tsx b/src/components/element/campaign/tableCollumns/RestrictionsCollumn.tsx new file mode 100644 index 00000000..d939198c --- /dev/null +++ b/src/components/element/campaign/tableCollumns/RestrictionsCollumn.tsx @@ -0,0 +1,29 @@ +import type { Campaign } from "@angleprotocol/merkl-api"; +import { Button, Dropdown } from "packages/dappkit/src"; + +type IProps = { + campaign: Campaign; +}; + +export default function RestrictionsCollumn(props: IProps) { + const { campaign } = props; + + const hasWhitelist = campaign.params.whitelist.length > 0; + const hasBlacklist = campaign.params.blacklist.length > 0; + + return ( + + {hasWhitelist && ( + + )} + + {hasBlacklist && ( + + )} + + ); +} diff --git a/src/components/element/leaderboard/LeaderboardLibrary.tsx b/src/components/element/leaderboard/LeaderboardLibrary.tsx new file mode 100644 index 00000000..e5713a1e --- /dev/null +++ b/src/components/element/leaderboard/LeaderboardLibrary.tsx @@ -0,0 +1,23 @@ +import { Text } from "dappkit"; +import { useMemo } from "react"; +import { LeaderboardTable } from "./LeaderboardTable"; +import LeaderboardTableRow from "./LeaderboardTableRow"; +import type { DummyLeaderboard } from "src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard"; + +export type IProps = { + leaderboard: DummyLeaderboard[]; +}; + +export default function LeaderboardLibrary(props: IProps) { + const { leaderboard } = props; + + const rows = useMemo(() => { + return leaderboard?.map(row => ); + }, [leaderboard]); + + return ( + Leaderboard} footer={"Something"}> + {!!rows.length ? rows : No rewarded users} + + ); +} diff --git a/src/components/element/leaderboard/LeaderboardTable.tsx b/src/components/element/leaderboard/LeaderboardTable.tsx new file mode 100644 index 00000000..4d069c49 --- /dev/null +++ b/src/components/element/leaderboard/LeaderboardTable.tsx @@ -0,0 +1,29 @@ +import { createTable } from "dappkit"; + +export const [LeaderboardTable, LeaderboardRow, LeaderboardColumns] = createTable({ + rank: { + name: "Rank", + size: "minmax(30px,60px)", + compact: "1fr", + className: "justify-start", + main: true, + }, + address: { + name: "Address", + size: "minmax(170px,1fr)", + compactSize: "1fr", + className: "justify-start", + }, + rewards: { + name: "Rewards earned", + size: "minmax(30px,1fr)", + compactSize: "minmax(20px,1fr)", + className: "justify-start", + }, + protocol: { + name: "Protocols", + size: "minmax(30px,0.5fr)", + compactSize: "minmax(20px,1fr)", + className: "justify-center", + }, +}); diff --git a/src/components/element/leaderboard/LeaderboardTableRow.tsx b/src/components/element/leaderboard/LeaderboardTableRow.tsx new file mode 100644 index 00000000..5ddde2af --- /dev/null +++ b/src/components/element/leaderboard/LeaderboardTableRow.tsx @@ -0,0 +1,20 @@ +import { type Component, mergeClass, Text } from "dappkit"; +import { LeaderboardRow } from "./LeaderboardTable"; +import type { DummyLeaderboard } from "src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard"; + +export type CampaignTableRowProps = Component<{ + row?: DummyLeaderboard; +}>; + +export default function LeaderboardTableRow({ row, className, ...props }: CampaignTableRowProps) { + return ( + #{row?.rank}} + addressColumn={{row?.address}} + rewardsColumn={{row?.rewards}} + protocolColumn={{row?.protocol}} + /> + ); +} diff --git a/src/components/element/rewards/ClaimRewardsTokenTableRow.tsx b/src/components/element/rewards/ClaimRewardsTokenTableRow.tsx index 5817fc1d..1eb1a9dc 100644 --- a/src/components/element/rewards/ClaimRewardsTokenTableRow.tsx +++ b/src/components/element/rewards/ClaimRewardsTokenTableRow.tsx @@ -17,15 +17,6 @@ export default function ClaimRewardsTokenTableRow({ reward, checkedState, ...pro const unclaimed = useMemo(() => BigInt(reward.amount) - BigInt(reward.claimed), [reward]); - console.log( - "WSH", - reward.token.chainId, - reward.amount, - reward.claimed, - BigInt(reward.amount) - BigInt(reward.claimed), - unclaimed, - ); - return ( {[ - { label: "FEES", value: params.weightFees / 10000 }, + { label: "Fees", value: params.weightFees / 10000 }, { label: params.symbolToken0, value: params.weightToken0 / 10000 }, { label: params.symbolToken1, value: params.weightToken1 / 10000 }, ].map(({ label, value }) => { diff --git a/src/routes/_merkl.opportunity.$chain.$type.$id.(overview).tsx b/src/routes/_merkl.opportunity.$chain.$type.$id.(overview).tsx index 92d08633..063ae570 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.(overview).tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.(overview).tsx @@ -1,11 +1,10 @@ -import type { Campaign, Opportunity } from "@angleprotocol/merkl-api"; +import type { Campaign } from "@angleprotocol/merkl-api"; import { Group } from "@ariakit/react"; import { type LoaderFunctionArgs, json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { Space } from "packages/dappkit/src"; import { api } from "src/api/index.server"; import CampaignLibrary from "src/components/element/campaign/CampaignLibrary"; -import Participate from "src/components/element/participate/Participate"; export async function loader({ params: { id, type, chain: chainId } }: LoaderFunctionArgs) { if (!chainId || !id || !type) throw ""; @@ -17,29 +16,25 @@ export async function loader({ params: { id, type, chain: chainId } }: LoaderFun if (!chain) throw "DSS"; - const { data: opportunity } = await api.v4.opportunities({ id: `${chain.id}-${type}-${id}` }).get(); - - if (!opportunity) throw "No Opportunity"; - const { data: campaigns } = await api.v4.opportunities({ id: `${chain.id}-${type}-${id}` }).campaigns.get(); - if (!opportunity || !campaigns) throw "DAZZ"; + if (!campaigns) throw "DAZZ"; - return json({ opportunity, campaigns: campaigns.campaigns }); + return json({ campaigns: campaigns.campaigns }); } export default function Index() { - const { opportunity, campaigns } = useLoaderData(); + const { campaigns } = useLoaderData(); return ( - - - + + {/* */} + {/* - - + */} + {/* */} ); } diff --git a/src/routes/_merkl.opportunity.$chain.$type.$id.analytics.tsx b/src/routes/_merkl.opportunity.$chain.$type.$id.analytics.tsx deleted file mode 100644 index a6cedacd..00000000 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.analytics.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { type LoaderFunctionArgs, json } from "@remix-run/node"; -import { Text } from "dappkit/src"; - -export async function loader({ params }: LoaderFunctionArgs) { - return json({ chain: params.chain }); -} - -export default function Index() { - return Analytics; -} diff --git a/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx b/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx index f1420477..30997669 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx @@ -1,10 +1,27 @@ import { type LoaderFunctionArgs, json } from "@remix-run/node"; -import { Text } from "dappkit/src"; +import { useLoaderData } from "@remix-run/react"; +import LeaderboardLibrary from "src/components/element/leaderboard/LeaderboardLibrary"; + +export type DummyLeaderboard = { + rank: number; + address: string; + rewards: number; + protocol: string; +}; export async function loader({ params }: LoaderFunctionArgs) { - return json({ chain: params.chain }); + const leaderboard: DummyLeaderboard[] = [ + { rank: 1, address: "0x1234", rewards: 100, protocol: "Aave" }, + { rank: 2, address: "0x5678", rewards: 50, protocol: "Compound" }, + { rank: 3, address: "0x9abc", rewards: 25, protocol: "Aave" }, + { rank: 4, address: "0xdef0", rewards: 10, protocol: "Compound" }, + { rank: 5, address: "0x1234", rewards: 5, protocol: "Aave" }, + ]; + + return json(leaderboard); } export default function Index() { - return Leaderboard WIP; + const leaderboard = useLoaderData(); + return ; } diff --git a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx index 4d51b205..d8ef0c9d 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx @@ -6,11 +6,14 @@ import Heading from "src/components/composite/Heading"; import { Container } from "dappkit"; import Tag from "src/components/element/Tag"; import useOpportunity from "src/hooks/resources/useOpportunity"; +import type { Campaign } from "@angleprotocol/merkl-api"; +import moment from "moment"; +import { useMemo } from "react"; export async function loader({ params: { id, type, chain: chainId } }: LoaderFunctionArgs) { if (!chainId || !id || !type) throw ""; - const { data: chains } = await api.v4.chains.index.get({ query: { search: chainId } }); + const { data: chains } = await api.v4.chains.get({ query: { search: chainId } }); const chain = chains?.[0]; if (!chain) throw ""; @@ -19,7 +22,10 @@ export async function loader({ params: { id, type, chain: chainId } }: LoaderFun if (!opportunity) throw "Opportunity"; - return json(opportunity); + const { data: campaigns } = await api.v4.opportunities({ id: `${chain.id}-${type}-${id}` }).campaigns.get(); + if (!campaigns || !opportunity) throw "DAZZ"; + + return json({ opportunity, campaigns: campaigns.campaigns }); } export const meta: MetaFunction = ({ data }) => { @@ -27,32 +33,6 @@ export const meta: MetaFunction = ({ data }) => { return [{ title: `${data?.name} on Merkl` }]; }; -export default function Index() { - const opportunity = useLoaderData(); - const { tags, description, link } = useOpportunity(opportunity); - - return ( - - - ({ src: t.icon }))} - navigation={{ label: "Back to opportunities", link: "/" }} - title={opportunity.name} - description={description} - tabs={[ - { label: "Overview", link }, - { label: "Leaderboard", link: `${link}/leaderboard` }, - { label: "Analytics", link: `${link}/analytics` }, - ]} - tags={tags.map(tag => ( - - ))}> - - - - ); -} - export function ErrorBoundary() { const error = useRouteError(); @@ -78,3 +58,35 @@ export function ErrorBoundary() { } return

Unknown Error

; } + +export default function Index() { + const { opportunity, campaigns } = useLoaderData(); + + const { tags, description, link } = useOpportunity(opportunity); + + const filteredCampaigns = useMemo(() => { + const now = moment().unix(); + return campaigns.filter((c: Campaign) => Number(c.endTimestamp) > now); + }, [campaigns]); + + return ( + + + ({ src: t.icon }))} + navigation={{ label: "Back to opportunities", link: "/" }} + title={opportunity.name} + description={description} + tabs={[ + { label: "Overview", link }, + { label: "Leaderboard", link: `${link}/leaderboard` }, + ]} + tags={tags.map(tag => ( + + ))} + campaigns={filteredCampaigns}> + + + + ); +} From d2fdc92352d416744a1d4825c498e5f15ed29d66 Mon Sep 17 00:00:00 2001 From: Hugo Lextrait Date: Mon, 2 Dec 2024 10:25:16 +0100 Subject: [PATCH 02/11] :constuction: wip merge --- merkl.config.ts | 3 + src/components/composite/Hero.tsx | 191 ++++++++++++++++++ .../element/campaign/CampaignTableRow.tsx | 94 ++++++--- src/config/type.ts | 3 + ...pportunity.$chain.$type.$id.(overview).tsx | 27 +-- ...portunity.$chain.$type.$id.leaderboard.tsx | 70 ++++++- .../_merkl.opportunity.$chain.$type.$id.tsx | 157 +++++++++----- 7 files changed, 439 insertions(+), 106 deletions(-) create mode 100644 src/components/composite/Hero.tsx diff --git a/merkl.config.ts b/merkl.config.ts index 3d1ffe09..8cb67459 100644 --- a/merkl.config.ts +++ b/merkl.config.ts @@ -39,6 +39,9 @@ import { coinbaseWallet, walletConnect } from "wagmi/connectors"; export default createConfig({ appName: "Merkl", + images: { + hero: "todo", + }, wagmi: { chains: [ mainnet, diff --git a/src/components/composite/Hero.tsx b/src/components/composite/Hero.tsx new file mode 100644 index 00000000..cbea76ff --- /dev/null +++ b/src/components/composite/Hero.tsx @@ -0,0 +1,191 @@ +import type { Campaign } from "@angleprotocol/merkl-api"; +import { useLocation } from "@remix-run/react"; +import { + Container, + Divider, + Group, + Icon, + type IconProps, + Icons, + Text, + Title, + Value, +} from "dappkit"; +import { Button } from "dappkit"; +import { useMemo, type PropsWithChildren, type ReactNode } from "react"; +import config from "merkl.config"; +import { formatUnits, parseUnits } from "viem"; + +export type HeroProps = PropsWithChildren<{ + icons?: IconProps[]; + title: ReactNode; + navigation?: { label: ReactNode; link: string }; + description: ReactNode; + tags?: ReactNode[]; + tabs?: { label: ReactNode; link: string }[]; + campaigns?: Campaign[]; +}>; + +export default function Hero({ + navigation, + icons, + title, + description, + tags, + children, + campaigns, +}: HeroProps) { + const location = useLocation(); + + const totalRewards = useMemo(() => { + const amounts = campaigns?.map((campaign) => { + const duration = campaign.endTimestamp - campaign.startTimestamp; + const dayspan = BigInt(duration) / BigInt(3600 * 24); + + return parseUnits(campaign.amount, 0) / BigInt(dayspan); + }); + + const sum = amounts?.reduce( + (accumulator, currentValue) => accumulator + currentValue, + 0n + ); + if (!sum) return "0.0"; + return formatUnits(sum, 18); + }, [campaigns]); + + return ( + <> + + + + + {/* TODO: Build dynamic breadcrumbs */} + {/** Disabled and set invisible when undefined to preserve layout height */} + + + {location.pathname.includes("opportunity") && ( + + )} + {location.pathname.includes("user") && ( + + )} + + {!location.pathname.includes("user") && ( + + )} + + + + + + {!!icons && ( + + {icons?.length > 1 + ? icons?.map((icon) => ( + + )) + : icons?.map((icon) => ( + + ))} + + )} + + {title} + + + {tags && ( + + {description} + + )} + + + {tags && {tags}} + {!tags && ( + + {description} + + )} + + {/* TODO: Show "Opportunities" or "Campaigns" according to the page */} + {!location?.pathname.includes("user") && ( + + + + {totalRewards} + + + + Daily rewards + + + + todo + + APR + + + + {campaigns?.length} + + Active campaigns + + + + )} + {/* {!!tabs && ( + + {tabs?.map((tab) => ( + + ))} + + )} */} + + + + +
{children}
+ + ); +} diff --git a/src/components/element/campaign/CampaignTableRow.tsx b/src/components/element/campaign/CampaignTableRow.tsx index f9ff9e49..02a52bd6 100644 --- a/src/components/element/campaign/CampaignTableRow.tsx +++ b/src/components/element/campaign/CampaignTableRow.tsx @@ -1,5 +1,13 @@ import type { Campaign } from "@angleprotocol/merkl-api"; -import { type Component, Group, Icon, OverrideTheme, Text, Value, mergeClass } from "dappkit"; +import { + type Component, + Group, + Icon, + OverrideTheme, + Text, + Value, + mergeClass, +} from "dappkit"; import { useCallback, useMemo, useState } from "react"; import useCampaign from "src/hooks/resources/useCampaign"; import Token from "../token/Token"; @@ -14,15 +22,24 @@ export type CampaignTableRowProps = Component<{ startsOpen?: boolean; }>; -export default function CampaignTableRow({ campaign, startsOpen, className, ...props }: CampaignTableRowProps) { +export default function CampaignTableRow({ + campaign, + startsOpen, + className, + ...props +}: CampaignTableRowProps) { const { time, profile, dailyRewards, active } = useCampaign(campaign); const [isOpen, setIsOpen] = useState(startsOpen); - const toggleIsOpen = useCallback(() => setIsOpen(o => !o), []); + const toggleIsOpen = useCallback(() => setIsOpen((o) => !o), []); const campaignAmount = useMemo( - () => formatUnits(parseUnits(campaign.amount, 0), campaign.rewardToken.decimals), - [campaign], + () => + formatUnits( + parseUnits(campaign.amount, 0), + campaign.rewardToken.decimals + ), + [campaign] ); return ( @@ -30,32 +47,42 @@ export default function CampaignTableRow({ campaign, startsOpen, className, ...p {...props} className={mergeClass("cursor-pointer", className)} onClick={toggleIsOpen} + chainColumn={null} + profileColumn={profile} + restrictionsColumn={} dailyRewardsColumn={ - + } - timeRemainingColumn={{time}} - restrictionsColumn={} - profileColumn={profile} - chainColumn={ - - - {campaign.chain.name} - + timeRemainingColumn={ + + {time} + } - arrowColumn={}> + arrowColumn={ + + } + > {isOpen && (
-
+ Campaign information
Total - + {campaignAmount}
@@ -63,8 +90,13 @@ export default function CampaignTableRow({ campaign, startsOpen, className, ...p 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")}
@@ -85,11 +117,16 @@ export default function CampaignTableRow({ campaign, startsOpen, className, ...p helper={
{campaign.params.blacklist.length > 0 - ? campaign.params.blacklist.map((blacklist: string) => blacklist) + ? campaign.params.blacklist.map( + (blacklist: string) => blacklist + ) : "No address"}
- }> - {campaign.params.blacklist.length} address + } + > + + {campaign.params.blacklist.length} address + @@ -98,15 +135,20 @@ export default function CampaignTableRow({ campaign, startsOpen, className, ...p helper={
{campaign.params.whitelist.length > 0 - ? campaign.params.whitelist.map((blacklist: string) => blacklist) + ? campaign.params.whitelist.map( + (blacklist: string) => blacklist + ) : "No address"}
- }> - {campaign.params.whitelist.length} address + } + > + + {campaign.params.whitelist.length} address +
-
+ )} diff --git a/src/config/type.ts b/src/config/type.ts index 7e4562cf..fdc53a77 100644 --- a/src/config/type.ts +++ b/src/config/type.ts @@ -26,6 +26,9 @@ export type MerklConfig = { links: { [key: string]: string; }; + images: { + hero: string; + }; }; export function createConfig({ diff --git a/src/routes/_merkl.opportunity.$chain.$type.$id.(overview).tsx b/src/routes/_merkl.opportunity.$chain.$type.$id.(overview).tsx index 063ae570..6aa33d3f 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.(overview).tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.(overview).tsx @@ -1,35 +1,16 @@ -import type { Campaign } from "@angleprotocol/merkl-api"; import { Group } from "@ariakit/react"; -import { type LoaderFunctionArgs, json } from "@remix-run/node"; -import { useLoaderData } from "@remix-run/react"; +import { useOutletContext } from "@remix-run/react"; import { Space } from "packages/dappkit/src"; -import { api } from "src/api/index.server"; import CampaignLibrary from "src/components/element/campaign/CampaignLibrary"; -export async function loader({ params: { id, type, chain: chainId } }: LoaderFunctionArgs) { - if (!chainId || !id || !type) throw ""; - - const { data: chains } = await api.v4.chains.index.get({ - query: { search: chainId }, - }); - const chain = chains?.[0]; - - if (!chain) throw "DSS"; - - const { data: campaigns } = await api.v4.opportunities({ id: `${chain.id}-${type}-${id}` }).campaigns.get(); - - if (!campaigns) throw "DAZZ"; - - return json({ campaigns: campaigns.campaigns }); -} - export default function Index() { - const { campaigns } = useLoaderData(); + const { opportunity, campaigns } = useOutletContext(); + console.log({ opportunity, campaigns }); return ( - + {/* */} {/* diff --git a/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx b/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx index 30997669..3865c783 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx @@ -1,5 +1,8 @@ -import { type LoaderFunctionArgs, json } from "@remix-run/node"; -import { useLoaderData } from "@remix-run/react"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json, useLoaderData } from "@remix-run/react"; +import { Group, Text } from "packages/dappkit/src"; +import Tooltip from "packages/dappkit/src/components/primitives/Tooltip"; +import { api } from "src/api/index.server"; import LeaderboardLibrary from "src/components/element/leaderboard/LeaderboardLibrary"; export type DummyLeaderboard = { @@ -15,13 +18,68 @@ export async function loader({ params }: LoaderFunctionArgs) { { rank: 2, address: "0x5678", rewards: 50, protocol: "Compound" }, { rank: 3, address: "0x9abc", rewards: 25, protocol: "Aave" }, { rank: 4, address: "0xdef0", rewards: 10, protocol: "Compound" }, - { rank: 5, address: "0x1234", rewards: 5, protocol: "Aave" }, + { rank: 5, address: "0x1235", rewards: 5, protocol: "Aave" }, ]; - return json(leaderboard); + const { data: chains } = await api.v4.chains.index.get({ + query: { search: params.chain }, + }); + + const chain = chains?.[0]; + if (!chain) throw "DSS"; + + console.log({ chain: chains?.[0]?.id, id: params.id, type: params.type }); + + const { data: campaigns } = await api.v4.campaigns.index.get({ + query: { + chainId: chains?.[0]?.id, + type: params.type as Parameters< + typeof api.v4.campaigns.index.get + >[0]["query"]["type"], + identifier: params.id, + }, + }); + + const campaignIdentifiers = campaigns?.map( + (campaign) => campaign?.identifier + ); + if (!campaignIdentifiers) throw "DSS"; + + const { data: rewards } = await api.v4.rewards.breakdown.get({ + query: { + campaignIdentifiers: [ + "0x32f1cc3a5a775f60eaaa9796a92bc356016ec574b6b470d141477261994c09ae", + ], + chainId: 100, + }, + }); + + return json({ leaderboard, rewards }); } export default function Index() { - const leaderboard = useLoaderData(); - return ; + // const { opportunity, campaigns } = useOutletContext(); + const { leaderboard, rewards } = useLoaderData(); + + return ( + <> + + + + Total rewarded users + + {/* Probably a count from api */} + {rewards?.length} + + + + Total reward distributed + + 400k + + + + + + ); } diff --git a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx index d8ef0c9d..da044e74 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx @@ -1,37 +1,124 @@ -import { type LoaderFunctionArgs, type MetaFunction, json } from "@remix-run/node"; -import { Meta, Outlet, isRouteErrorResponse, useLoaderData, useRouteError } from "@remix-run/react"; -import { api } from "src/api/index.server"; -import Heading from "src/components/composite/Heading"; - -import { Container } from "dappkit"; -import Tag from "src/components/element/Tag"; -import useOpportunity from "src/hooks/resources/useOpportunity"; import type { Campaign } from "@angleprotocol/merkl-api"; +import { type LoaderFunctionArgs, json } from "@remix-run/node"; +import { + Meta, + Outlet, + isRouteErrorResponse, + useLoaderData, + useRouteError, +} from "@remix-run/react"; import moment from "moment"; import { useMemo } from "react"; +import { api } from "src/api/index.server"; +import Tag from "src/components/element/Tag"; +import useOpportunity from "src/hooks/resources/useOpportunity"; +import Hero from "src/components/composite/Hero"; -export async function loader({ params: { id, type, chain: chainId } }: LoaderFunctionArgs) { +export async function loader({ + params: { id, type, chain: chainId }, +}: LoaderFunctionArgs) { if (!chainId || !id || !type) throw ""; - const { data: chains } = await api.v4.chains.get({ query: { search: chainId } }); - const chain = chains?.[0]; + const { data: chains } = await api.v4.chains.index.get({ + query: { search: chainId }, + }); - if (!chain) throw ""; + if (!chains?.[0]) throw ""; - const { data: opportunity } = await api.v4.opportunities({ id: `${chain.id}-${type}-${id}` }).get(); + // get Opportunities + const { data: opportunity } = await api.v4 + .opportunities({ id: `${chains?.[0]?.id}-${type}-${id}` }) + .get(); if (!opportunity) throw "Opportunity"; - const { data: campaigns } = await api.v4.opportunities({ id: `${chain.id}-${type}-${id}` }).campaigns.get(); + // get Campaigns + const { data: campaigns } = await api.v4.campaigns.index.get({ + query: { + chainId: chains?.[0]?.id, + type: type as Parameters< + typeof api.v4.campaigns.index.get + >[0]["query"]["type"], + identifier: id, + }, + }); + if (!campaigns || !opportunity) throw "DAZZ"; - return json({ opportunity, campaigns: campaigns.campaigns }); + return json({ opportunity, campaigns }); } -export const meta: MetaFunction = ({ data }) => { - if (data?.error) return [{ title: "404 on Merkl" }]; - return [{ title: `${data?.name} on Merkl` }]; -}; +// export const meta: MetaFunction = ({ data }) => { +// if (data?.error) return [{ title: "404 on Merkl" }]; +// return [{ title: `${data?.name} on Merkl` }]; +// }; + +export default function Index() { + const { opportunity, campaigns } = useLoaderData(); + const { tags, description, link } = useOpportunity(opportunity); + const styleName = useMemo(() => { + const spaced = opportunity?.name.split(" "); + + return spaced + .map((str, index) => { + if (!str.match(/[\p{Letter}\p{Mark}]+/gu)) + return [ + + {str} + , + ]; + if (str.includes("-")) + return str + .split("-") + .flatMap((s, i, arr) => [ + s, + i !== arr.length - 1 && -, + ]); + if (str.includes("/")) + return str + .split("/") + .flatMap((s, i, arr) => [ + s, + i !== arr.length - 1 && /, + ]); + // biome-ignore lint/suspicious/noArrayIndexKey: required + return [{str}]; + }) + .flatMap((str, index, arr) => [str, index !== arr.length - 1 && " "]); + }, [opportunity]); + + const filteredCampaigns = useMemo(() => { + const now = moment().unix(); + return campaigns.filter((c: Campaign) => Number(c.endTimestamp) > now); + }, [campaigns]); + + return ( + <> + + ({ src: t.icon }))} + navigation={{ label: "Back to opportunities", link: "/" }} + title={styleName} + description={description} + tabs={[ + { label: "Overview", link }, + { label: "Leaderboard", link: `${link}/leaderboard` }, + { label: "Analytics", link: `${link}/analytics` }, + ]} + tags={tags.map((tag) => ( + + ))} + campaigns={filteredCampaigns} + > + + + + ); +} export function ErrorBoundary() { const error = useRouteError(); @@ -58,35 +145,3 @@ export function ErrorBoundary() { } return

Unknown Error

; } - -export default function Index() { - const { opportunity, campaigns } = useLoaderData(); - - const { tags, description, link } = useOpportunity(opportunity); - - const filteredCampaigns = useMemo(() => { - const now = moment().unix(); - return campaigns.filter((c: Campaign) => Number(c.endTimestamp) > now); - }, [campaigns]); - - return ( - - - ({ src: t.icon }))} - navigation={{ label: "Back to opportunities", link: "/" }} - title={opportunity.name} - description={description} - tabs={[ - { label: "Overview", link }, - { label: "Leaderboard", link: `${link}/leaderboard` }, - ]} - tags={tags.map(tag => ( - - ))} - campaigns={filteredCampaigns}> - - - - ); -} From df05284870e6411986065ab504792c725e70fabd Mon Sep 17 00:00:00 2001 From: Hugo Lextrait Date: Mon, 2 Dec 2024 11:20:37 +0100 Subject: [PATCH 03/11] fixes --- src/routes/_merkl.(home).(opportunities).tsx | 10 ++++++++-- src/routes/_merkl.opportunity.$chain.$type.$id.tsx | 1 - 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/routes/_merkl.(home).(opportunities).tsx b/src/routes/_merkl.(home).(opportunities).tsx index 6a611ac3..bca1164d 100644 --- a/src/routes/_merkl.(home).(opportunities).tsx +++ b/src/routes/_merkl.(home).(opportunities).tsx @@ -7,7 +7,9 @@ import OpportunityLibrary from "src/components/element/opportunity/OpportunityLi import { ErrorContent } from "src/components/layout/ErrorContent"; export async function loader({ request }: LoaderFunctionArgs) { - const { opportunities, count } = await OpportunityService.getManyFromRequest(request); + const { opportunities, count } = await OpportunityService.getManyFromRequest( + request + ); const chains = await ChainService.getAll(); return json({ opportunities, chains, count }); @@ -19,7 +21,11 @@ export default function Index() { return ( - + ); } diff --git a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx index a80962aa..4f97c401 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx @@ -24,7 +24,6 @@ export async function loader({ const opportunity = await OpportunityService.get(opportunityId); const campaigns = await campaignService.get(); - console.log({ opportunity }); // get Campaigns // const { data: campaigns } = await api.v4.campaigns.index.get({ From 61a56817912ea192d971c2db999e343efa0a7760 Mon Sep 17 00:00:00 2001 From: Hugo Lextrait Date: Mon, 2 Dec 2024 11:55:48 +0100 Subject: [PATCH 04/11] services cleaning --- biome.json | 1 + src/api/services/campaign.service.ts | 11 +++-- src/api/services/chain.service.ts | 29 ++++++++---- src/api/services/opportunity.service.ts | 60 +++++++++++++++++-------- src/api/services/protocol.service.ts | 10 +++-- src/api/services/reward.service.ts | 10 +++-- src/api/services/token.service.ts | 21 ++++++--- 7 files changed, 93 insertions(+), 49 deletions(-) diff --git a/biome.json b/biome.json index d166d961..e6855c48 100644 --- a/biome.json +++ b/biome.json @@ -26,6 +26,7 @@ "noParameterAssign": "off" }, "complexity": { + "noStaticOnlyClass": "off", "noBannedTypes": "error", "noExcessiveCognitiveComplexity": "off", "noExtraBooleanCast": "off", diff --git a/src/api/services/campaign.service.ts b/src/api/services/campaign.service.ts index ef27dd65..c57021e0 100644 --- a/src/api/services/campaign.service.ts +++ b/src/api/services/campaign.service.ts @@ -1,17 +1,16 @@ import type { Campaign } from "@angleprotocol/merkl-api"; import { api } from "../index.server"; -class CampaignService { +export abstract class CampaignService { // ------ Fetch all campaigns - async get(): Promise { + static async get(): Promise { const { data } = await api.v4.campaigns.index.get({ query: {} }); + return data; } // ------ Fetch a campaign by ID - async getByID(Id: string): Promise { - return "To implements"; + static async getByID(Id: string): Promise { + return null; } } - -export const campaignService = new CampaignService(); diff --git a/src/api/services/chain.service.ts b/src/api/services/chain.service.ts index 289b0ef0..52991689 100644 --- a/src/api/services/chain.service.ts +++ b/src/api/services/chain.service.ts @@ -1,38 +1,49 @@ import type { Chain } from "@angleprotocol/merkl-api"; import { api } from "../index.server"; -// biome-ignore lint/complexity/noStaticOnlyClass: export abstract class ChainService { static async #fetch( call: () => Promise, - resource = "Chain", + resource = "Chain" ): Promise> { const { data, status } = await call(); if (status === 404) throw new Response(`${resource} not found`, { status }); - if (status === 500) throw new Response(`${resource} unavailable`, { status }); + if (status === 500) + throw new Response(`${resource} unavailable`, { status }); if (data == null) throw new Response(`${resource} unavailable`, { status }); return data; } static async getAll(): Promise { - const chains = await ChainService.#fetch(async () => api.v4.chains.index.get({ query: {} })); + const chains = await ChainService.#fetch(async () => + api.v4.chains.index.get({ query: {} }) + ); //TODO: add some cache here return chains; } - static async getMany(query: Parameters[0]["query"]): Promise { - const chains = await ChainService.#fetch(async () => api.v4.chains.index.get({ query })); + static async getMany( + query: Parameters[0]["query"] + ): Promise { + const chains = await ChainService.#fetch(async () => + api.v4.chains.index.get({ query }) + ); //TODO: add some cache here return chains; } - static async get(query: Parameters[0]["query"]): Promise { - const chains = await ChainService.#fetch(async () => api.v4.chains.index.get({ query })); + static async get( + query: Parameters[0]["query"] + ): Promise { + const chains = await ChainService.#fetch(async () => + api.v4.chains.index.get({ query }) + ); - if (chains.length === 0) throw new Response("Chain not found", { status: 404 }); + if (chains.length === 0) + throw new Response("Chain not found", { status: 404 }); //TODO: add some cache here return chains?.[0]; diff --git a/src/api/services/opportunity.service.ts b/src/api/services/opportunity.service.ts index 28e20af5..79e94de4 100644 --- a/src/api/services/opportunity.service.ts +++ b/src/api/services/opportunity.service.ts @@ -2,16 +2,16 @@ import type { Campaign, Opportunity } from "@angleprotocol/merkl-api"; import config from "merkl.config"; import { api } from "../index.server"; -// biome-ignore lint/complexity/noStaticOnlyClass: export abstract class OpportunityService { static async #fetch( call: () => Promise, - resource = "Opportunity", + resource = "Opportunity" ): Promise> { const { data, status } = await call(); if (status === 404) throw new Response(`${resource} not found`, { status }); - if (status === 500) throw new Response(`${resource} unavailable`, { status }); + if (status === 500) + throw new Response(`${resource} unavailable`, { status }); if (data == null) throw new Response(`${resource} unavailable`, { status }); return data; } @@ -24,7 +24,7 @@ export abstract class OpportunityService { */ static #getQueryFromRequest( request: Request, - override?: Parameters[0]["query"], + override?: Parameters[0]["query"] ) { const status = new URL(request.url).searchParams.get("status"); const action = new URL(request.url).searchParams.get("action"); @@ -33,57 +33,79 @@ export abstract class OpportunityService { const items = new URL(request.url).searchParams.get("items"); const search = new URL(request.url).searchParams.get("search"); - const [sort, order] = new URL(request.url).searchParams.get("sort")?.split("-") ?? []; + const [sort, order] = + new URL(request.url).searchParams.get("sort")?.split("-") ?? []; const filters = Object.assign( { status, action, chainId, items, sort, order, name: search, page }, override ?? {}, - page !== null && { page: Number(page) - 1 }, + page !== null && { page: Number(page) - 1 } ); const query = Object.entries(filters).reduce( - (_query, [key, filter]) => Object.assign(_query, filter == null ? {} : { [key]: filter }), - {}, + (_query, [key, filter]) => + Object.assign(_query, filter == null ? {} : { [key]: filter }), + {} ); return query; } static async getManyFromRequest(request: Request) { - return OpportunityService.getMany(OpportunityService.#getQueryFromRequest(request)); + return OpportunityService.getMany( + OpportunityService.#getQueryFromRequest(request) + ); } static async getMany( - query: Parameters[0]["query"], + query: Parameters[0]["query"] ): Promise<{ opportunities: Opportunity[]; count: number }> { //TODO: updates tags to take an array const opportunities = await OpportunityService.#fetch(async () => - api.v4.opportunities.index.get({ query: Object.assign({ ...query }, config.tags?.[0] ? { tags: config.tags?.[0] }: {}) }), + api.v4.opportunities.index.get({ + query: Object.assign( + { ...query }, + config.tags?.[0] ? { tags: config.tags?.[0] } : {} + ), + }) + ); + const count = await OpportunityService.#fetch(async () => + api.v4.opportunities.count.get({ query }) ); - const count = await OpportunityService.#fetch(async () => api.v4.opportunities.count.get({ query })); - return { opportunities: opportunities.filter(o => o !== null), count }; + return { opportunities: opportunities.filter((o) => o !== null), count }; } - static async getCampaigns(query: { chainId: number; type: string; identifier: string }): Promise { + static async getCampaigns(query: { + chainId: number; + type: string; + identifier: string; + }): Promise { const { chainId, type, identifier } = query; type T = Parameters[0]["query"]["type"]; const campaigns = await OpportunityService.#fetch(async () => - api.v4.campaigns.index.get({ query: { chainId, type: type as T, identifier } }), + api.v4.campaigns.index.get({ + query: { chainId, type: type as T, identifier }, + }) ); - return campaigns.filter(c => c !== null); + return campaigns.filter((c) => c !== null); } - static async get(query: { chainId: number; type: string; identifier: string }): Promise { + static async get(query: { + chainId: number; + type: string; + identifier: string; + }): Promise { const { chainId, type, identifier } = query; const opportunity = await OpportunityService.#fetch(async () => - api.v4.opportunities({ id: `${chainId}-${type}-${identifier}` }).get(), + api.v4.opportunities({ id: `${chainId}-${type}-${identifier}` }).get() ); //TODO: updates tags to take an array - if (config.tags && !opportunity.tags.includes(config.tags?.[0])) throw new Response("Opportunity inacessible", { status: 403 }); + if (config.tags && !opportunity.tags.includes(config.tags?.[0])) + throw new Response("Opportunity inacessible", { status: 403 }); return opportunity; } diff --git a/src/api/services/protocol.service.ts b/src/api/services/protocol.service.ts index 67e529bc..c41962be 100644 --- a/src/api/services/protocol.service.ts +++ b/src/api/services/protocol.service.ts @@ -1,22 +1,24 @@ import type { Protocol } from "@angleprotocol/merkl-api"; import { api } from "../index.server"; -// biome-ignore lint/complexity/noStaticOnlyClass: export abstract class ProtocolService { static async #fetch( call: () => Promise, - resource = "Protocol", + resource = "Protocol" ): Promise> { const { data, status } = await call(); if (status === 404) throw new Response(`${resource} not found`, { status }); - if (status === 500) throw new Response(`${resource} unavailable`, { status }); + if (status === 500) + throw new Response(`${resource} unavailable`, { status }); if (data == null) throw new Response(`${resource} unavailable`, { status }); return data; } static async get(query: { id: string }): Promise { - const protocol = await ProtocolService.#fetch(async () => api.v4.protocols(query).get({ query })); + const protocol = await ProtocolService.#fetch(async () => + api.v4.protocols(query).get({ query }) + ); return protocol; } diff --git a/src/api/services/reward.service.ts b/src/api/services/reward.service.ts index 523c0ddc..2c6096d2 100644 --- a/src/api/services/reward.service.ts +++ b/src/api/services/reward.service.ts @@ -1,22 +1,24 @@ import type { Reward } from "@angleprotocol/merkl-api"; import { api } from "../index.server"; -// biome-ignore lint/complexity/noStaticOnlyClass: export abstract class RewardService { static async #fetch( call: () => Promise, - resource = "Reward", + resource = "Reward" ): Promise> { const { data, status } = await call(); if (status === 404) throw new Response(`${resource} not found`, { status }); - if (status === 500) throw new Response(`${resource} unavailable`, { status }); + if (status === 500) + throw new Response(`${resource} unavailable`, { status }); if (data == null) throw new Response(`${resource} unavailable`, { status }); return data; } static async getForUser(address: string): Promise { - const rewards = await RewardService.#fetch(async () => api.v4.users({ address }).rewards.full.get()); + const rewards = await RewardService.#fetch(async () => + api.v4.users({ address }).rewards.full.get() + ); //TODO: add some cache here return rewards; diff --git a/src/api/services/token.service.ts b/src/api/services/token.service.ts index 22264fd8..cbe9b289 100644 --- a/src/api/services/token.service.ts +++ b/src/api/services/token.service.ts @@ -1,22 +1,26 @@ import type { Token } from "@angleprotocol/merkl-api"; import { api } from "../index.server"; -// biome-ignore lint/complexity/noStaticOnlyClass: export abstract class TokenService { static async #fetch( call: () => Promise, - resource = "Chain", + resource = "Chain" ): Promise> { const { data, status } = await call(); if (status === 404) throw new Response(`${resource} not found`, { status }); - if (status === 500) throw new Response(`${resource} unavailable`, { status }); + if (status === 500) + throw new Response(`${resource} unavailable`, { status }); if (data == null) throw new Response(`${resource} unavailable`, { status }); return data; } - static async getMany(query: Parameters[0]["query"]): Promise { - const tokens = await TokenService.#fetch(async () => api.v4.tokens.index.get({ query })); + static async getMany( + query: Parameters[0]["query"] + ): Promise { + const tokens = await TokenService.#fetch(async () => + api.v4.tokens.index.get({ query }) + ); return tokens; } @@ -24,9 +28,12 @@ export abstract class TokenService { static async getSymbol(symbol: string | undefined): Promise { if (!symbol) throw new Response("Token not found"); - const tokens = await TokenService.#fetch(async () => api.v4.tokens.index.get({ query: { symbol } })); + const tokens = await TokenService.#fetch(async () => + api.v4.tokens.index.get({ query: { symbol } }) + ); - if (tokens.length === 0) throw new Response("Token not found", { status: 404 }); + if (tokens.length === 0) + throw new Response("Token not found", { status: 404 }); return tokens; } } From 6c4da0f598a326dc154a4b2cf6037a090eb723fc Mon Sep 17 00:00:00 2001 From: Hugo Lextrait Date: Mon, 2 Dec 2024 11:56:49 +0100 Subject: [PATCH 05/11] primitives tags and campaign sorting --- src/components/element/Tag.tsx | 444 +++++++++--------- .../element/campaign/CampaignLibrary.tsx | 36 +- .../_merkl.opportunity.$chain.$type.$id.tsx | 4 +- 3 files changed, 251 insertions(+), 233 deletions(-) diff --git a/src/components/element/Tag.tsx b/src/components/element/Tag.tsx index e096e831..6a367a64 100644 --- a/src/components/element/Tag.tsx +++ b/src/components/element/Tag.tsx @@ -6,6 +6,7 @@ import { Group, Hash, Icon, + PrimitiveTag, Text, Title, } from "dappkit"; @@ -37,226 +38,225 @@ export default function Tag({ value, ...props }: TagProps) { - // return null; - // switch (type) { - // case "status": { - // const status = statuses[value as TagTypes["status"]] ?? statuses.LIVE; - // return ( - // - // - // - // Status - // - // - // - // {status?.label} - // - // - // - // {status?.description} - // - // - // } - // > - // - // - // {status?.label} - // - // - // ); - // } - // case "chain": { - // const chain = value as TagTypes["chain"]; - // return ( - // - // - // - // Chain - // id: {chain?.id} - // - // - // - // {chain?.name} - // - // - // - // - // - // } - // > - // - // - // {chain?.name} - // - // - // ); - // } - // case "action": { - // const action = actions[value as TagTypes["action"]]; - // if (!action) return ; - // return ( - // - // - // - // Action - // - // - // - // {action?.label} - // - // - // - // {action?.description} - // - // - // } - // > - // - // - // {action?.label} - // - // - // ); - // } - // case "token": { - // const token = value as TagTypes["token"]; - // if (!token) return ; - // return ( - // - // - // - // Token - // - // {token.address} - // - // - // - // - // {token?.name} - // - // - // - // {/* {token?.description} */} - // - // - // - // - // - // } - // > - // - // - // {token?.symbol} - // - // - // ); - // } - // case "tokenChain": { - // const token = value as TagTypes["tokenChain"]; - // if (!token) return ; - // return ( - // - // - // - // Token - // - // {token.address} - // - // - // - // - // {token?.name} - // - // - // - // {/* {token?.description} */} - // - // - // - // - // - // - // } - // > - // - // - // {token.chain.name} - // - // - // ); - // } - // case "protocol": { - // const protocol = value; - // if (!protocol) return ; - // return ( - // - // - // - // Protocol - // - // - // - // {value?.name} - // - // - // - // {/* {token?.description} */} - // - // - // - // - // - // } - // > - // - // - // {value?.name} - // - // - // ); - // } - // default: - // return {value}; - // } + switch (type) { + case "status": { + const status = statuses[value as TagTypes["status"]] ?? statuses.LIVE; + return ( + + + + Status + + + + {status?.label} + + + + {status?.description} + + + } + > + + + {status?.label} + + + ); + } + case "chain": { + const chain = value as TagTypes["chain"]; + return ( + + + + Chain + id: {chain?.id} + + + + {chain?.name} + + + + + + } + > + + + {chain?.name} + + + ); + } + case "action": { + const action = actions[value as TagTypes["action"]]; + if (!action) return ; + return ( + + + + Action + + + + {action?.label} + + + + {action?.description} + + + } + > + + + {action?.label} + + + ); + } + case "token": { + const token = value as TagTypes["token"]; + if (!token) return ; + return ( + + + + Token + + {token.address} + + + + + {token?.name} + + + + {/* {token?.description} */} + + + + + + } + > + + + {token?.symbol} + + + ); + } + case "tokenChain": { + const token = value as TagTypes["tokenChain"]; + if (!token) return ; + return ( + + + + Token + + {token.address} + + + + + {token?.name} + + + + {/* {token?.description} */} + + + + + + + } + > + + + {token.chain.name} + + + ); + } + case "protocol": { + const protocol = value; + if (!protocol) return ; + return ( + + + + Protocol + + + + {value?.name} + + + + {/* {token?.description} */} + + + + + + } + > + + + {value?.name} + + + ); + } + default: + return {value}; + } } diff --git a/src/components/element/campaign/CampaignLibrary.tsx b/src/components/element/campaign/CampaignLibrary.tsx index fa3ec79a..6b368bde 100644 --- a/src/components/element/campaign/CampaignLibrary.tsx +++ b/src/components/element/campaign/CampaignLibrary.tsx @@ -14,10 +14,17 @@ export default function CampaignLibrary({ campaigns }: CampaignProps) { const rows = useMemo(() => { const now = moment().unix(); - const shownCampaigns = campaigns.filter(c => showInactive || Number(c.endTimestamp) > now); + const shownCampaigns = campaigns.filter( + (c) => showInactive || Number(c.endTimestamp) > now + ); const startsOpen = shownCampaigns.length < 3; - return shownCampaigns?.map(c => ); + const campaignsSorted = shownCampaigns.sort( + (a, b) => Number(b.endTimestamp) - Number(a.endTimestamp) + ); + return campaignsSorted?.map((c) => ( + + )); }, [campaigns, showInactive]); return ( @@ -26,23 +33,34 @@ export default function CampaignLibrary({ campaigns }: CampaignProps) { Campaigns - } - footer={"Something"}> + footer={"Something"} + > {!!rows.length ? ( rows ) : ( No active campaign
-
diff --git a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx index 4f97c401..e46d088a 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx @@ -11,8 +11,8 @@ import { OpportunityService } from "src/api/services/opportunity.service"; import Tag from "src/components/element/Tag"; import { ErrorHeading } from "src/components/layout/ErrorHeading"; import useOpportunity from "src/hooks/resources/useOpportunity"; -import { campaignService } from "src/api/services/campaign.service"; import type { Campaign } from "@angleprotocol/merkl-api"; +import { CampaignService } from "src/api/services/campaign.service"; export async function loader({ params: { id, type, chain: chainId }, @@ -23,7 +23,7 @@ export async function loader({ const opportunityId = { chainId: chain.id, type, identifier: id }; const opportunity = await OpportunityService.get(opportunityId); - const campaigns = await campaignService.get(); + const campaigns = await CampaignService.get(); // get Campaigns // const { data: campaigns } = await api.v4.campaigns.index.get({ From a49681a4da86e9e5e78a152ca57dcab072264290 Mon Sep 17 00:00:00 2001 From: Hugo Lextrait Date: Mon, 2 Dec 2024 15:37:52 +0100 Subject: [PATCH 06/11] refactor: improve query parameter handling and enhance CampaignService methods --- src/api/opportunity/opportunity.ts | 19 +++++---- src/api/services/campaign.service.ts | 42 +++++++++++++++++++ src/components/composite/Hero.tsx | 28 +++++++------ ...portunity.$chain.$type.$id.leaderboard.tsx | 23 +++++----- .../_merkl.opportunity.$chain.$type.$id.tsx | 1 - 5 files changed, 80 insertions(+), 33 deletions(-) diff --git a/src/api/opportunity/opportunity.ts b/src/api/opportunity/opportunity.ts index bb329eee..a0bd6e4a 100644 --- a/src/api/opportunity/opportunity.ts +++ b/src/api/opportunity/opportunity.ts @@ -2,27 +2,28 @@ import { api } from "../index.server"; function getQueryParams( request: Request, - overrideQuery?: Parameters[0]["query"], + overrideQuery?: Parameters[0]["query"] ) { const status = new URL(request.url).searchParams.get("status"); const action = new URL(request.url).searchParams.get("action"); const chainId = new URL(request.url).searchParams.get("chain"); const page = new URL(request.url).searchParams.get("page"); - console.log("PAGE", page); const items = new URL(request.url).searchParams.get("items"); const search = new URL(request.url).searchParams.get("search"); - const [sort, order] = new URL(request.url).searchParams.get("sort")?.split("-") ?? []; + const [sort, order] = + new URL(request.url).searchParams.get("sort")?.split("-") ?? []; const filters = Object.assign( { status, action, chainId, items, sort, order, name: search, page }, overrideQuery ?? {}, - page !== null && { page: Number(page) - 1 }, + page !== null && { page: Number(page) - 1 } ); const query = Object.entries(filters).reduce( - (_query, [key, filter]) => Object.assign(_query, filter == null ? {} : { [key]: filter }), - {}, + (_query, [key, filter]) => + Object.assign(_query, filter == null ? {} : { [key]: filter }), + {} ); return query; @@ -30,12 +31,14 @@ function getQueryParams( export async function fetchOpportunities( request: Request, - overrideQuery?: Parameters[0]["query"], + overrideQuery?: Parameters[0]["query"] ) { const query = getQueryParams(request, overrideQuery); const { data: count } = await api.v4.opportunities.count.get({ query }); - const { data: opportunities } = await api.v4.opportunities.index.get({ query }); + const { data: opportunities } = await api.v4.opportunities.index.get({ + query, + }); if (count === null || !opportunities) throw "Cannot fetch opportunities"; return { opportunities, count }; diff --git a/src/api/services/campaign.service.ts b/src/api/services/campaign.service.ts index c57021e0..44fbe589 100644 --- a/src/api/services/campaign.service.ts +++ b/src/api/services/campaign.service.ts @@ -2,6 +2,41 @@ import type { Campaign } from "@angleprotocol/merkl-api"; import { api } from "../index.server"; export abstract class CampaignService { + /** + * 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 status = new URL(request.url).searchParams.get("status"); + const action = new URL(request.url).searchParams.get("action"); + const chainId = new URL(request.url).searchParams.get("chain"); + const page = new URL(request.url).searchParams.get("page"); + + const items = new URL(request.url).searchParams.get("items"); + const search = new URL(request.url).searchParams.get("search"); + const [sort, order] = + new URL(request.url).searchParams.get("sort")?.split("-") ?? []; + + const filters = Object.assign( + { status, action, chainId, items, sort, order, name: search, 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; + } + // ------ Fetch all campaigns static async get(): Promise { const { data } = await api.v4.campaigns.index.get({ query: {} }); @@ -9,6 +44,13 @@ export abstract class CampaignService { return data; } + static async getByParams( + query: Parameters[0]["query"] + ): Promise { + const { data } = await api.v4.campaigns.index.get({ query }); + return data; + } + // ------ Fetch a campaign by ID static async getByID(Id: string): Promise { return null; diff --git a/src/components/composite/Hero.tsx b/src/components/composite/Hero.tsx index aefc5546..ffe50bd1 100644 --- a/src/components/composite/Hero.tsx +++ b/src/components/composite/Hero.tsx @@ -1,6 +1,7 @@ import type { Campaign, Opportunity } from "@angleprotocol/merkl-api"; import { useLocation } from "@remix-run/react"; import { + Box, Container, Divider, Group, @@ -37,6 +38,7 @@ export default function Hero({ children, campaigns, opportunity, + tabs, }: HeroProps) { const location = useLocation(); @@ -182,23 +184,23 @@ export default function Hero({
)} - {/* {!!tabs && ( - - {tabs?.map((tab) => ( - - ))} - - )} */}
+ {!!tabs && ( + + {tabs?.map((tab) => ( + + ))} + + )}
{children}
); diff --git a/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx b/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx index 3865c783..b6b94e5c 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx @@ -3,6 +3,7 @@ import { json, useLoaderData } from "@remix-run/react"; import { Group, Text } from "packages/dappkit/src"; import Tooltip from "packages/dappkit/src/components/primitives/Tooltip"; import { api } from "src/api/index.server"; +import { CampaignService } from "src/api/services/campaign.service"; import LeaderboardLibrary from "src/components/element/leaderboard/LeaderboardLibrary"; export type DummyLeaderboard = { @@ -28,23 +29,24 @@ export async function loader({ params }: LoaderFunctionArgs) { const chain = chains?.[0]; if (!chain) throw "DSS"; - console.log({ chain: chains?.[0]?.id, id: params.id, type: params.type }); - - const { data: campaigns } = await api.v4.campaigns.index.get({ - query: { - chainId: chains?.[0]?.id, - type: params.type as Parameters< - typeof api.v4.campaigns.index.get - >[0]["query"]["type"], - identifier: params.id, - }, + const campaigns = await CampaignService.getByParams({ + chainId: chain.id, + type: params.type as Parameters< + typeof api.v4.campaigns.index.get + >[0]["query"]["type"], + identifier: params.id, }); + console.log({ campaigns }); + const campaignIdentifiers = campaigns?.map( (campaign) => campaign?.identifier ); + if (!campaignIdentifiers) throw "DSS"; + // const rewards = await RewardService.getByCampaignsId(); + const { data: rewards } = await api.v4.rewards.breakdown.get({ query: { campaignIdentifiers: [ @@ -58,7 +60,6 @@ export async function loader({ params }: LoaderFunctionArgs) { } export default function Index() { - // const { opportunity, campaigns } = useOutletContext(); const { leaderboard, rewards } = useLoaderData(); return ( diff --git a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx index e46d088a..2e003b55 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx @@ -96,7 +96,6 @@ export default function Index() { tabs={[ { label: "Overview", link }, { label: "Leaderboard", link: `${link}/leaderboard` }, - { label: "Analytics", link: `${link}/analytics` }, ]} tags={tags.map((tag) => ( Date: Mon, 2 Dec 2024 15:53:12 +0100 Subject: [PATCH 07/11] refactor: remove unused getCampaigns method and update campaign retrieval in opportunity loader --- src/api/services/opportunity.service.ts | 17 ----------------- .../_merkl.opportunity.$chain.$type.$id.tsx | 15 ++++----------- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/src/api/services/opportunity.service.ts b/src/api/services/opportunity.service.ts index 79e94de4..c650253e 100644 --- a/src/api/services/opportunity.service.ts +++ b/src/api/services/opportunity.service.ts @@ -76,23 +76,6 @@ export abstract class OpportunityService { return { opportunities: opportunities.filter((o) => o !== null), count }; } - static async getCampaigns(query: { - chainId: number; - type: string; - identifier: string; - }): Promise { - const { chainId, type, identifier } = query; - - type T = Parameters[0]["query"]["type"]; - const campaigns = await OpportunityService.#fetch(async () => - api.v4.campaigns.index.get({ - query: { chainId, type: type as T, identifier }, - }) - ); - - return campaigns.filter((c) => c !== null); - } - static async get(query: { chainId: number; type: string; diff --git a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx index 2e003b55..e109db8d 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx @@ -23,18 +23,11 @@ export async function loader({ const opportunityId = { chainId: chain.id, type, identifier: id }; const opportunity = await OpportunityService.get(opportunityId); - const campaigns = await CampaignService.get(); - // get Campaigns - // const { data: campaigns } = await api.v4.campaigns.index.get({ - // query: { - // chainId: chains?.[0]?.id, - // type: type as Parameters< - // typeof api.v4.campaigns.index.get - // >[0]["query"]["type"], - // identifier: id, - // }, - // }); + // get opportunity and populate campaigns + const campaigns = await CampaignService.getByParams({ + opportunityId: opportunity.id, + }); if (!campaigns || !opportunity) throw "DAZZ"; From d8d37d8b53298051b3c47ad0b48da1d659a7c1a1 Mon Sep 17 00:00:00 2001 From: Hugo Lextrait Date: Tue, 3 Dec 2024 11:15:46 +0100 Subject: [PATCH 08/11] refactor: update campaign handling in various components and services --- src/api/services/opportunity.service.ts | 24 +++++++- src/api/services/reward.service.ts | 16 +++++ src/components/composite/Hero.tsx | 9 +-- .../element/campaign/CampaignLibrary.tsx | 27 ++++----- .../element/campaign/CampaignTable.tsx | 6 +- .../element/campaign/CampaignTableRow.tsx | 9 ++- ...pportunity.$chain.$type.$id.(overview).tsx | 10 +--- ...portunity.$chain.$type.$id.leaderboard.tsx | 60 ++++++++----------- .../_merkl.opportunity.$chain.$type.$id.tsx | 24 ++++---- 9 files changed, 103 insertions(+), 82 deletions(-) diff --git a/src/api/services/opportunity.service.ts b/src/api/services/opportunity.service.ts index c650253e..81a74bff 100644 --- a/src/api/services/opportunity.service.ts +++ b/src/api/services/opportunity.service.ts @@ -1,4 +1,4 @@ -import type { Campaign, Opportunity } from "@angleprotocol/merkl-api"; +import type { Opportunity } from "@angleprotocol/merkl-api"; import config from "merkl.config"; import { api } from "../index.server"; @@ -92,4 +92,26 @@ export abstract class OpportunityService { return opportunity; } + + static async getCampaignsByParams(query: { + chainId: number; + type: string; + identifier: string; + }) { + const { chainId, type, identifier } = query; + const opportunityWithCampaigns = await OpportunityService.#fetch(async () => + api.v4 + .opportunities({ id: `${chainId}-${type}-${identifier}` }) + .campaigns.get() + ); + + //TODO: updates tags to take an array + if ( + config.tags && + !opportunityWithCampaigns.tags.includes(config.tags?.[0]) + ) + throw new Response("Opportunity inacessible", { status: 403 }); + + return opportunityWithCampaigns; + } } diff --git a/src/api/services/reward.service.ts b/src/api/services/reward.service.ts index 2c6096d2..76fa6204 100644 --- a/src/api/services/reward.service.ts +++ b/src/api/services/reward.service.ts @@ -23,4 +23,20 @@ export abstract class RewardService { //TODO: add some cache here return rewards; } + + static async getByParams(query: { + items?: number; + page?: number; + chainId: number; + campaignIdentifiers: string[]; + }) { + return RewardService.#fetch(async () => + api.v4.rewards.index.get({ + query: { + ...query, + campaignIdentifiers: query.campaignIdentifiers.join(","), + }, + }) + ); + } } diff --git a/src/components/composite/Hero.tsx b/src/components/composite/Hero.tsx index ffe50bd1..81ab50db 100644 --- a/src/components/composite/Hero.tsx +++ b/src/components/composite/Hero.tsx @@ -25,7 +25,6 @@ export type HeroProps = PropsWithChildren<{ description: ReactNode; tags?: ReactNode[]; tabs?: { label: ReactNode; link: string }[]; - campaigns?: Campaign[]; opportunity?: Opportunity; }>; @@ -36,16 +35,18 @@ export default function Hero({ description, tags, children, - campaigns, opportunity, tabs, }: HeroProps) { const location = useLocation(); const filteredCampaigns = useMemo(() => { + if (!opportunity?.campaigns) return null; const now = moment().unix(); - return campaigns?.filter((c: Campaign) => Number(c.endTimestamp) > now); - }, [campaigns]); + return opportunity.campaigns?.filter( + (c: Campaign) => Number(c.endTimestamp) > now + ); + }, [opportunity]); const totalRewards = useMemo(() => { const amounts = filteredCampaigns?.map((campaign) => { diff --git a/src/components/element/campaign/CampaignLibrary.tsx b/src/components/element/campaign/CampaignLibrary.tsx index 6b368bde..eac68530 100644 --- a/src/components/element/campaign/CampaignLibrary.tsx +++ b/src/components/element/campaign/CampaignLibrary.tsx @@ -1,20 +1,22 @@ -import type { Campaign } from "@angleprotocol/merkl-api"; +import type { Opportunity } from "@angleprotocol/merkl-api"; import { Button, Group, Icon, Text } from "dappkit"; import moment from "moment"; import { useMemo, useState } from "react"; import { CampaignTable } from "./CampaignTable"; import CampaignTableRow from "./CampaignTableRow"; -export type CampaignProps = { - campaigns: Campaign[]; +export type IProps = { + opportunity: Opportunity; }; -export default function CampaignLibrary({ campaigns }: CampaignProps) { +export default function CampaignLibrary(props: IProps) { + const { opportunity } = props; const [showInactive, setShowInactive] = useState(false); const rows = useMemo(() => { + if (!opportunity?.campaigns) return null; const now = moment().unix(); - const shownCampaigns = campaigns.filter( + const shownCampaigns = opportunity.campaigns.filter( (c) => showInactive || Number(c.endTimestamp) > now ); const startsOpen = shownCampaigns.length < 3; @@ -25,7 +27,7 @@ export default function CampaignLibrary({ campaigns }: CampaignProps) { return campaignsSorted?.map((c) => ( )); - }, [campaigns, showInactive]); + }, [opportunity, showInactive]); return ( Campaigns } - footer={"Something"} > - {!!rows.length ? ( + {!!rows?.length ? ( rows ) : ( @@ -56,10 +54,7 @@ export default function CampaignLibrary({ campaigns }: CampaignProps) { look="soft" className="m-auto" > - {" "} + {!showInactive ? "Show" : "Hide"} Inactive diff --git a/src/components/element/campaign/CampaignTable.tsx b/src/components/element/campaign/CampaignTable.tsx index 3dbd74f3..050c4043 100644 --- a/src/components/element/campaign/CampaignTable.tsx +++ b/src/components/element/campaign/CampaignTable.tsx @@ -26,9 +26,9 @@ export const [CampaignTable, CampaignRow, CampaignColumns] = createTable({ compactSize: "minmax(20px,1fr)", className: "justify-center", }, - profile: { - name: "Incentivized Liquidity", - size: "minmax(100px,200px)", + identifier: { + name: "ID", + size: "minmax(100px,150px)", compactSize: "minmax(100px,1fr)", className: "justify-start", }, diff --git a/src/components/element/campaign/CampaignTableRow.tsx b/src/components/element/campaign/CampaignTableRow.tsx index 6738256c..c691fc8b 100644 --- a/src/components/element/campaign/CampaignTableRow.tsx +++ b/src/components/element/campaign/CampaignTableRow.tsx @@ -17,6 +17,7 @@ import { formatUnits, parseUnits } from "viem"; import moment from "moment"; import RestrictionsCollumn from "./tableCollumns/RestrictionsCollumn"; import Tooltip from "packages/dappkit/src/components/primitives/Tooltip"; +import Chain from "../chain/Chain"; export type CampaignTableRowProps = Component<{ campaign: Campaign; @@ -48,8 +49,8 @@ export default function CampaignTableRow({ {...props} className={mergeClass("cursor-pointer", className)} onClick={toggleIsOpen} - chainColumn={null} - profileColumn={profile} + chainColumn={} + identifierColumn={{campaign.identifier}} restrictionsColumn={} dailyRewardsColumn={ @@ -114,6 +115,10 @@ export default function CampaignTableRow({ Conditions + + Incentivized Liquidity + {profile} + Blacklisted for (); + const { opportunity } = useOutletContext(); return ( - + {/* */} {/* diff --git a/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx b/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx index b6b94e5c..d4a0e4f9 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx @@ -2,9 +2,10 @@ import type { LoaderFunctionArgs } from "@remix-run/node"; import { json, useLoaderData } from "@remix-run/react"; import { Group, Text } from "packages/dappkit/src"; import Tooltip from "packages/dappkit/src/components/primitives/Tooltip"; -import { api } from "src/api/index.server"; -import { CampaignService } from "src/api/services/campaign.service"; import LeaderboardLibrary from "src/components/element/leaderboard/LeaderboardLibrary"; +// import { ChainService } from "src/api/services/chain.service"; +// import { OpportunityService } from "src/api/services/opportunity.service"; +// import { RewardService } from "src/api/services/reward.service"; export type DummyLeaderboard = { rank: number; @@ -13,7 +14,9 @@ export type DummyLeaderboard = { protocol: string; }; -export async function loader({ params }: LoaderFunctionArgs) { +export async function loader({ + params: { id, type, chain: chainId }, +}: LoaderFunctionArgs) { const leaderboard: DummyLeaderboard[] = [ { rank: 1, address: "0x1234", rewards: 100, protocol: "Aave" }, { rank: 2, address: "0x5678", rewards: 50, protocol: "Compound" }, @@ -22,45 +25,32 @@ export async function loader({ params }: LoaderFunctionArgs) { { rank: 5, address: "0x1235", rewards: 5, protocol: "Aave" }, ]; - const { data: chains } = await api.v4.chains.index.get({ - query: { search: params.chain }, - }); + // ----------- Need to implement this part @Hugo ------------ + // if (!chainId || !id || !type) throw ""; + // const chain = await ChainService.get({ search: chainId }); - const chain = chains?.[0]; - if (!chain) throw "DSS"; + // const opportunity = await OpportunityService.getCampaignsByParams({ + // chainId: chain.id, + // type: type, + // identifier: id, + // }); - const campaigns = await CampaignService.getByParams({ - chainId: chain.id, - type: params.type as Parameters< - typeof api.v4.campaigns.index.get - >[0]["query"]["type"], - identifier: params.id, - }); + // const campaignIdentifiers = opportunity?.campaigns?.map((c) => c.identifier); + // if (!campaignIdentifiers) throw new Error("No campaign identifiers found"); - console.log({ campaigns }); + // console.log({ campaignIdentifiers, chain: chain.id }); - const campaignIdentifiers = campaigns?.map( - (campaign) => campaign?.identifier - ); - - if (!campaignIdentifiers) throw "DSS"; - - // const rewards = await RewardService.getByCampaignsId(); - - const { data: rewards } = await api.v4.rewards.breakdown.get({ - query: { - campaignIdentifiers: [ - "0x32f1cc3a5a775f60eaaa9796a92bc356016ec574b6b470d141477261994c09ae", - ], - chainId: 100, - }, - }); + // const rewards = await RewardService.getByParams({ + // campaignIdentifiers, + // chainId: chain.id, + // }); + // ----------- Need to implement this part @Hugo ------------ - return json({ leaderboard, rewards }); + return json({ leaderboard }); } export default function Index() { - const { leaderboard, rewards } = useLoaderData(); + const { leaderboard } = useLoaderData(); return ( <> @@ -70,7 +60,7 @@ export default function Index() { Total rewarded users {/* Probably a count from api */} - {rewards?.length} + {leaderboard?.length} diff --git a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx index e109db8d..10885b4d 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx @@ -11,8 +11,7 @@ import { OpportunityService } from "src/api/services/opportunity.service"; import Tag from "src/components/element/Tag"; import { ErrorHeading } from "src/components/layout/ErrorHeading"; import useOpportunity from "src/hooks/resources/useOpportunity"; -import type { Campaign } from "@angleprotocol/merkl-api"; -import { CampaignService } from "src/api/services/campaign.service"; +import type { Opportunity } from "@angleprotocol/merkl-api"; export async function loader({ params: { id, type, chain: chainId }, @@ -20,18 +19,16 @@ export async function loader({ if (!chainId || !id || !type) throw ""; const chain = await ChainService.get({ search: chainId }); - const opportunityId = { chainId: chain.id, type, identifier: id }; - const opportunity = await OpportunityService.get(opportunityId); - - // get opportunity and populate campaigns - const campaigns = await CampaignService.getByParams({ - opportunityId: opportunity.id, + const opportunity = await OpportunityService.getCampaignsByParams({ + chainId: chain.id, + type: type, + identifier: id, }); - if (!campaigns || !opportunity) throw "DAZZ"; + if (!opportunity) throw "DAZZ"; - return json({ opportunity, campaigns }); + return json({ opportunity }); } export const meta: MetaFunction = ({ data, error }) => { @@ -40,11 +37,11 @@ export const meta: MetaFunction = ({ data, error }) => { }; export type OutletContextOpportunity = { - campaigns: Campaign[]; + opportunity: Opportunity; }; export default function Index() { - const { opportunity, campaigns } = useLoaderData(); + const { opportunity } = useLoaderData(); const { tags, description, link } = useOpportunity(opportunity); const styleName = useMemo(() => { @@ -97,10 +94,9 @@ export default function Index() { size="md" /> ))} - campaigns={campaigns} opportunity={opportunity} > - + ); From cfa2115f1139e5b14ab468031951d4a81af455d6 Mon Sep 17 00:00:00 2001 From: Hugo Lextrait Date: Tue, 3 Dec 2024 11:17:00 +0100 Subject: [PATCH 09/11] lint --- src/api/opportunity/opportunity.ts | 14 ++-- src/api/services/campaign.service.ts | 16 ++-- src/api/services/chain.service.ts | 28 ++----- src/api/services/opportunity.service.ts | 47 ++++------- src/api/services/protocol.service.ts | 9 +- src/api/services/reward.service.ts | 11 +-- src/api/services/token.service.ts | 20 ++--- src/components/composite/Hero.tsx | 64 +++----------- src/components/element/Tag.tsx | 48 +++-------- .../element/campaign/CampaignLibrary.tsx | 23 ++--- .../element/campaign/CampaignTableRow.tsx | 84 +++++-------------- .../leaderboard/LeaderboardLibrary.tsx | 2 +- .../leaderboard/LeaderboardTableRow.tsx | 4 +- src/routes/_merkl.(home).(opportunities).tsx | 10 +-- ...pportunity.$chain.$type.$id.(overview).tsx | 2 +- ...portunity.$chain.$type.$id.leaderboard.tsx | 4 +- .../_merkl.opportunity.$chain.$type.$id.tsx | 37 ++------ 17 files changed, 114 insertions(+), 309 deletions(-) diff --git a/src/api/opportunity/opportunity.ts b/src/api/opportunity/opportunity.ts index a0bd6e4a..5917d41b 100644 --- a/src/api/opportunity/opportunity.ts +++ b/src/api/opportunity/opportunity.ts @@ -2,7 +2,7 @@ import { api } from "../index.server"; function getQueryParams( request: Request, - overrideQuery?: Parameters[0]["query"] + overrideQuery?: Parameters[0]["query"], ) { const status = new URL(request.url).searchParams.get("status"); const action = new URL(request.url).searchParams.get("action"); @@ -11,19 +11,17 @@ function getQueryParams( const items = new URL(request.url).searchParams.get("items"); const search = new URL(request.url).searchParams.get("search"); - const [sort, order] = - new URL(request.url).searchParams.get("sort")?.split("-") ?? []; + const [sort, order] = new URL(request.url).searchParams.get("sort")?.split("-") ?? []; const filters = Object.assign( { status, action, chainId, items, sort, order, name: search, page }, overrideQuery ?? {}, - page !== null && { page: Number(page) - 1 } + page !== null && { page: Number(page) - 1 }, ); const query = Object.entries(filters).reduce( - (_query, [key, filter]) => - Object.assign(_query, filter == null ? {} : { [key]: filter }), - {} + (_query, [key, filter]) => Object.assign(_query, filter == null ? {} : { [key]: filter }), + {}, ); return query; @@ -31,7 +29,7 @@ function getQueryParams( export async function fetchOpportunities( request: Request, - overrideQuery?: Parameters[0]["query"] + overrideQuery?: Parameters[0]["query"], ) { const query = getQueryParams(request, overrideQuery); diff --git a/src/api/services/campaign.service.ts b/src/api/services/campaign.service.ts index 44fbe589..00604e36 100644 --- a/src/api/services/campaign.service.ts +++ b/src/api/services/campaign.service.ts @@ -10,7 +10,7 @@ export abstract class CampaignService { */ static #getQueryFromRequest( request: Request, - override?: Parameters[0]["query"] + override?: Parameters[0]["query"], ) { const status = new URL(request.url).searchParams.get("status"); const action = new URL(request.url).searchParams.get("action"); @@ -19,19 +19,17 @@ export abstract class CampaignService { const items = new URL(request.url).searchParams.get("items"); const search = new URL(request.url).searchParams.get("search"); - const [sort, order] = - new URL(request.url).searchParams.get("sort")?.split("-") ?? []; + const [sort, order] = new URL(request.url).searchParams.get("sort")?.split("-") ?? []; const filters = Object.assign( { status, action, chainId, items, sort, order, name: search, page }, override ?? {}, - page !== null && { page: Number(page) - 1 } + page !== null && { page: Number(page) - 1 }, ); const query = Object.entries(filters).reduce( - (_query, [key, filter]) => - Object.assign(_query, filter == null ? {} : { [key]: filter }), - {} + (_query, [key, filter]) => Object.assign(_query, filter == null ? {} : { [key]: filter }), + {}, ); return query; @@ -44,9 +42,7 @@ export abstract class CampaignService { return data; } - static async getByParams( - query: Parameters[0]["query"] - ): Promise { + static async getByParams(query: Parameters[0]["query"]): Promise { const { data } = await api.v4.campaigns.index.get({ query }); return data; } diff --git a/src/api/services/chain.service.ts b/src/api/services/chain.service.ts index 52991689..0980468b 100644 --- a/src/api/services/chain.service.ts +++ b/src/api/services/chain.service.ts @@ -4,46 +4,34 @@ import { api } from "../index.server"; export abstract class ChainService { static async #fetch( call: () => Promise, - resource = "Chain" + resource = "Chain", ): Promise> { const { data, status } = await call(); if (status === 404) throw new Response(`${resource} not found`, { status }); - if (status === 500) - throw new Response(`${resource} unavailable`, { status }); + if (status === 500) throw new Response(`${resource} unavailable`, { status }); if (data == null) throw new Response(`${resource} unavailable`, { status }); return data; } static async getAll(): Promise { - const chains = await ChainService.#fetch(async () => - api.v4.chains.index.get({ query: {} }) - ); + const chains = await ChainService.#fetch(async () => api.v4.chains.index.get({ query: {} })); //TODO: add some cache here return chains; } - static async getMany( - query: Parameters[0]["query"] - ): Promise { - const chains = await ChainService.#fetch(async () => - api.v4.chains.index.get({ query }) - ); + static async getMany(query: Parameters[0]["query"]): Promise { + const chains = await ChainService.#fetch(async () => api.v4.chains.index.get({ query })); //TODO: add some cache here return chains; } - static async get( - query: Parameters[0]["query"] - ): Promise { - const chains = await ChainService.#fetch(async () => - api.v4.chains.index.get({ query }) - ); + static async get(query: Parameters[0]["query"]): Promise { + const chains = await ChainService.#fetch(async () => api.v4.chains.index.get({ query })); - if (chains.length === 0) - throw new Response("Chain not found", { status: 404 }); + if (chains.length === 0) throw new Response("Chain not found", { status: 404 }); //TODO: add some cache here return chains?.[0]; diff --git a/src/api/services/opportunity.service.ts b/src/api/services/opportunity.service.ts index 81a74bff..2b45abbf 100644 --- a/src/api/services/opportunity.service.ts +++ b/src/api/services/opportunity.service.ts @@ -5,13 +5,12 @@ import { api } from "../index.server"; export abstract class OpportunityService { static async #fetch( call: () => Promise, - resource = "Opportunity" + resource = "Opportunity", ): Promise> { const { data, status } = await call(); if (status === 404) throw new Response(`${resource} not found`, { status }); - if (status === 500) - throw new Response(`${resource} unavailable`, { status }); + if (status === 500) throw new Response(`${resource} unavailable`, { status }); if (data == null) throw new Response(`${resource} unavailable`, { status }); return data; } @@ -24,7 +23,7 @@ export abstract class OpportunityService { */ static #getQueryFromRequest( request: Request, - override?: Parameters[0]["query"] + override?: Parameters[0]["query"], ) { const status = new URL(request.url).searchParams.get("status"); const action = new URL(request.url).searchParams.get("action"); @@ -33,47 +32,38 @@ export abstract class OpportunityService { const items = new URL(request.url).searchParams.get("items"); const search = new URL(request.url).searchParams.get("search"); - const [sort, order] = - new URL(request.url).searchParams.get("sort")?.split("-") ?? []; + const [sort, order] = new URL(request.url).searchParams.get("sort")?.split("-") ?? []; const filters = Object.assign( { status, action, chainId, items, sort, order, name: search, page }, override ?? {}, - page !== null && { page: Number(page) - 1 } + page !== null && { page: Number(page) - 1 }, ); const query = Object.entries(filters).reduce( - (_query, [key, filter]) => - Object.assign(_query, filter == null ? {} : { [key]: filter }), - {} + (_query, [key, filter]) => Object.assign(_query, filter == null ? {} : { [key]: filter }), + {}, ); return query; } static async getManyFromRequest(request: Request) { - return OpportunityService.getMany( - OpportunityService.#getQueryFromRequest(request) - ); + return OpportunityService.getMany(OpportunityService.#getQueryFromRequest(request)); } static async getMany( - query: Parameters[0]["query"] + query: Parameters[0]["query"], ): Promise<{ opportunities: Opportunity[]; count: number }> { //TODO: updates tags to take an array const opportunities = await OpportunityService.#fetch(async () => api.v4.opportunities.index.get({ - query: Object.assign( - { ...query }, - config.tags?.[0] ? { tags: config.tags?.[0] } : {} - ), - }) - ); - const count = await OpportunityService.#fetch(async () => - api.v4.opportunities.count.get({ query }) + query: Object.assign({ ...query }, config.tags?.[0] ? { tags: config.tags?.[0] } : {}), + }), ); + const count = await OpportunityService.#fetch(async () => api.v4.opportunities.count.get({ query })); - return { opportunities: opportunities.filter((o) => o !== null), count }; + return { opportunities: opportunities.filter(o => o !== null), count }; } static async get(query: { @@ -83,7 +73,7 @@ export abstract class OpportunityService { }): Promise { const { chainId, type, identifier } = query; const opportunity = await OpportunityService.#fetch(async () => - api.v4.opportunities({ id: `${chainId}-${type}-${identifier}` }).get() + api.v4.opportunities({ id: `${chainId}-${type}-${identifier}` }).get(), ); //TODO: updates tags to take an array @@ -100,16 +90,11 @@ export abstract class OpportunityService { }) { const { chainId, type, identifier } = query; const opportunityWithCampaigns = await OpportunityService.#fetch(async () => - api.v4 - .opportunities({ id: `${chainId}-${type}-${identifier}` }) - .campaigns.get() + api.v4.opportunities({ id: `${chainId}-${type}-${identifier}` }).campaigns.get(), ); //TODO: updates tags to take an array - if ( - config.tags && - !opportunityWithCampaigns.tags.includes(config.tags?.[0]) - ) + if (config.tags && !opportunityWithCampaigns.tags.includes(config.tags?.[0])) throw new Response("Opportunity inacessible", { status: 403 }); return opportunityWithCampaigns; diff --git a/src/api/services/protocol.service.ts b/src/api/services/protocol.service.ts index c41962be..a0664eef 100644 --- a/src/api/services/protocol.service.ts +++ b/src/api/services/protocol.service.ts @@ -4,21 +4,18 @@ import { api } from "../index.server"; export abstract class ProtocolService { static async #fetch( call: () => Promise, - resource = "Protocol" + resource = "Protocol", ): Promise> { const { data, status } = await call(); if (status === 404) throw new Response(`${resource} not found`, { status }); - if (status === 500) - throw new Response(`${resource} unavailable`, { status }); + if (status === 500) throw new Response(`${resource} unavailable`, { status }); if (data == null) throw new Response(`${resource} unavailable`, { status }); return data; } static async get(query: { id: string }): Promise { - const protocol = await ProtocolService.#fetch(async () => - api.v4.protocols(query).get({ query }) - ); + const protocol = await ProtocolService.#fetch(async () => api.v4.protocols(query).get({ query })); return protocol; } diff --git a/src/api/services/reward.service.ts b/src/api/services/reward.service.ts index 76fa6204..b081cef0 100644 --- a/src/api/services/reward.service.ts +++ b/src/api/services/reward.service.ts @@ -4,21 +4,18 @@ import { api } from "../index.server"; export abstract class RewardService { static async #fetch( call: () => Promise, - resource = "Reward" + resource = "Reward", ): Promise> { const { data, status } = await call(); if (status === 404) throw new Response(`${resource} not found`, { status }); - if (status === 500) - throw new Response(`${resource} unavailable`, { status }); + if (status === 500) throw new Response(`${resource} unavailable`, { status }); if (data == null) throw new Response(`${resource} unavailable`, { status }); return data; } static async getForUser(address: string): Promise { - const rewards = await RewardService.#fetch(async () => - api.v4.users({ address }).rewards.full.get() - ); + const rewards = await RewardService.#fetch(async () => api.v4.users({ address }).rewards.full.get()); //TODO: add some cache here return rewards; @@ -36,7 +33,7 @@ export abstract class RewardService { ...query, campaignIdentifiers: query.campaignIdentifiers.join(","), }, - }) + }), ); } } diff --git a/src/api/services/token.service.ts b/src/api/services/token.service.ts index cbe9b289..bbefb1fa 100644 --- a/src/api/services/token.service.ts +++ b/src/api/services/token.service.ts @@ -4,23 +4,18 @@ import { api } from "../index.server"; export abstract class TokenService { static async #fetch( call: () => Promise, - resource = "Chain" + resource = "Chain", ): Promise> { const { data, status } = await call(); if (status === 404) throw new Response(`${resource} not found`, { status }); - if (status === 500) - throw new Response(`${resource} unavailable`, { status }); + if (status === 500) throw new Response(`${resource} unavailable`, { status }); if (data == null) throw new Response(`${resource} unavailable`, { status }); return data; } - static async getMany( - query: Parameters[0]["query"] - ): Promise { - const tokens = await TokenService.#fetch(async () => - api.v4.tokens.index.get({ query }) - ); + static async getMany(query: Parameters[0]["query"]): Promise { + const tokens = await TokenService.#fetch(async () => api.v4.tokens.index.get({ query })); return tokens; } @@ -28,12 +23,9 @@ export abstract class TokenService { static async getSymbol(symbol: string | undefined): Promise { if (!symbol) throw new Response("Token not found"); - const tokens = await TokenService.#fetch(async () => - api.v4.tokens.index.get({ query: { symbol } }) - ); + const tokens = await TokenService.#fetch(async () => api.v4.tokens.index.get({ query: { symbol } })); - if (tokens.length === 0) - throw new Response("Token not found", { status: 404 }); + if (tokens.length === 0) throw new Response("Token not found", { status: 404 }); return tokens; } } diff --git a/src/components/composite/Hero.tsx b/src/components/composite/Hero.tsx index 81ab50db..17b4b6c1 100644 --- a/src/components/composite/Hero.tsx +++ b/src/components/composite/Hero.tsx @@ -1,17 +1,6 @@ import type { Campaign, Opportunity } from "@angleprotocol/merkl-api"; import { useLocation } from "@remix-run/react"; -import { - Box, - Container, - Divider, - Group, - Icon, - type IconProps, - Icons, - Text, - Title, - Value, -} from "dappkit"; +import { Box, Container, Divider, Group, Icon, type IconProps, Icons, Text, Title, Value } from "dappkit"; import { Button } from "dappkit"; import config from "merkl.config"; import moment from "moment"; @@ -28,38 +17,24 @@ export type HeroProps = PropsWithChildren<{ opportunity?: Opportunity; }>; -export default function Hero({ - navigation, - icons, - title, - description, - tags, - children, - opportunity, - tabs, -}: HeroProps) { +export default function Hero({ navigation, icons, title, description, tags, children, opportunity, tabs }: HeroProps) { const location = useLocation(); const filteredCampaigns = useMemo(() => { if (!opportunity?.campaigns) return null; const now = moment().unix(); - return opportunity.campaigns?.filter( - (c: Campaign) => Number(c.endTimestamp) > now - ); + return opportunity.campaigns?.filter((c: Campaign) => Number(c.endTimestamp) > now); }, [opportunity]); const totalRewards = useMemo(() => { - const amounts = filteredCampaigns?.map((campaign) => { + const amounts = filteredCampaigns?.map(campaign => { const duration = campaign.endTimestamp - campaign.startTimestamp; const dayspan = BigInt(duration) / BigInt(3600 * 24); return parseUnits(campaign.amount, 0) / BigInt(dayspan); }); - const sum = amounts?.reduce( - (accumulator, currentValue) => accumulator + currentValue, - 0n - ); + const sum = amounts?.reduce((accumulator, currentValue) => accumulator + currentValue, 0n); if (!sum) return "0.0"; return formatUnits(sum, 18); }, [filteredCampaigns]); @@ -68,8 +43,7 @@ export default function Hero({ <> + style={{ backgroundImage: `url('${config.images.hero}')` }}> @@ -80,8 +54,7 @@ export default function Hero({ disabled={!navigation?.link} to={navigation?.link} look="soft" - size="xs" - > + size="xs"> Home @@ -112,14 +85,14 @@ export default function Hero({ {!!icons && ( {icons?.length > 1 - ? icons?.map((icon) => ( + ? icons?.map(icon => ( )) - : icons?.map((icon) => ( + : icons?.map(icon => ( {/* TODO: Show "Opportunities" or "Campaigns" according to the page */} {!location?.pathname.includes("user") && ( - + - + {totalRewards} @@ -191,12 +157,8 @@ export default function Hero({ {!!tabs && ( - {tabs?.map((tab) => ( - ))} diff --git a/src/components/element/Tag.tsx b/src/components/element/Tag.tsx index 6a367a64..a76048a3 100644 --- a/src/components/element/Tag.tsx +++ b/src/components/element/Tag.tsx @@ -1,15 +1,5 @@ import type { Chain, Opportunity, Token } from "@angleprotocol/merkl-api"; -import { - Button, - Divider, - Dropdown, - Group, - Hash, - Icon, - PrimitiveTag, - Text, - Title, -} from "dappkit"; +import { Button, Divider, Dropdown, Group, Hash, Icon, PrimitiveTag, Text, Title } from "dappkit"; import type { ButtonProps } from "dappkit"; import { type Action, actions } from "src/config/actions"; import type { Protocol } from "src/config/protocols"; @@ -33,11 +23,7 @@ export type TagProps = ButtonProps & { value: TagTypes[T]; }; -export default function Tag({ - type, - value, - ...props -}: TagProps) { +export default function Tag({ type, value, ...props }: TagProps) { switch (type) { case "status": { const status = statuses[value as TagTypes["status"]] ?? statuses.LIVE; @@ -60,8 +46,7 @@ export default function Tag({ Open - } - > + }> {status?.label} @@ -90,8 +75,7 @@ export default function Tag({ Open - } - > + }> {chain?.name} @@ -121,8 +105,7 @@ export default function Tag({ Open - } - > + }> {action?.label} @@ -160,8 +143,7 @@ export default function Tag({ - } - > + }> {token?.symbol} @@ -191,11 +173,7 @@ export default function Tag({ {/* {token?.description} */} - @@ -207,8 +185,7 @@ export default function Tag({ - } - > + }> {token.chain.name} @@ -235,11 +212,7 @@ export default function Tag({ {/* {token?.description} */} - - } - > + }> {value?.name} diff --git a/src/components/element/campaign/CampaignLibrary.tsx b/src/components/element/campaign/CampaignLibrary.tsx index eac68530..dd6b8d70 100644 --- a/src/components/element/campaign/CampaignLibrary.tsx +++ b/src/components/element/campaign/CampaignLibrary.tsx @@ -16,17 +16,11 @@ export default function CampaignLibrary(props: IProps) { const rows = useMemo(() => { if (!opportunity?.campaigns) return null; const now = moment().unix(); - const shownCampaigns = opportunity.campaigns.filter( - (c) => showInactive || Number(c.endTimestamp) > now - ); + const shownCampaigns = opportunity.campaigns.filter(c => showInactive || Number(c.endTimestamp) > now); const startsOpen = shownCampaigns.length < 3; - const campaignsSorted = shownCampaigns.sort( - (a, b) => Number(b.endTimestamp) - Number(a.endTimestamp) - ); - return campaignsSorted?.map((c) => ( - - )); + const campaignsSorted = shownCampaigns.sort((a, b) => Number(b.endTimestamp) - Number(a.endTimestamp)); + return campaignsSorted?.map(c => ); }, [opportunity, showInactive]); return ( @@ -35,25 +29,20 @@ export default function CampaignLibrary(props: IProps) { Campaigns - - } - > + }> {!!rows?.length ? ( rows ) : ( No active campaign
- diff --git a/src/components/element/campaign/CampaignTableRow.tsx b/src/components/element/campaign/CampaignTableRow.tsx index c691fc8b..4c55cf01 100644 --- a/src/components/element/campaign/CampaignTableRow.tsx +++ b/src/components/element/campaign/CampaignTableRow.tsx @@ -1,47 +1,29 @@ import type { Campaign } from "@angleprotocol/merkl-api"; -import { - type Component, - Group, - Hash, - Icon, - OverrideTheme, - Text, - Value, - mergeClass, -} from "dappkit"; +import { type Component, Group, Hash, Icon, OverrideTheme, Text, Value, mergeClass } from "dappkit"; +import moment from "moment"; +import Tooltip from "packages/dappkit/src/components/primitives/Tooltip"; import { useCallback, useMemo, 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 { formatUnits, parseUnits } from "viem"; -import moment from "moment"; import RestrictionsCollumn from "./tableCollumns/RestrictionsCollumn"; -import Tooltip from "packages/dappkit/src/components/primitives/Tooltip"; -import Chain from "../chain/Chain"; export type CampaignTableRowProps = Component<{ campaign: Campaign; startsOpen?: boolean; }>; -export default function CampaignTableRow({ - campaign, - startsOpen, - className, - ...props -}: CampaignTableRowProps) { +export default function CampaignTableRow({ campaign, startsOpen, className, ...props }: CampaignTableRowProps) { const { time, profile, dailyRewards, active } = useCampaign(campaign); const [isOpen, setIsOpen] = useState(startsOpen); - const toggleIsOpen = useCallback(() => setIsOpen((o) => !o), []); + const toggleIsOpen = useCallback(() => setIsOpen(o => !o), []); const campaignAmount = useMemo( - () => - formatUnits( - parseUnits(campaign.amount, 0), - campaign.rewardToken.decimals - ), - [campaign] + () => formatUnits(parseUnits(campaign.amount, 0), campaign.rewardToken.decimals), + [campaign], ); return ( @@ -55,11 +37,7 @@ export default function CampaignTableRow({ dailyRewardsColumn={ - + @@ -69,10 +47,7 @@ export default function CampaignTableRow({ {time} } - arrowColumn={ - - } - > + arrowColumn={}> {isOpen && (
@@ -80,11 +55,7 @@ export default function CampaignTableRow({ Campaign information
Total - + {campaignAmount}
@@ -92,13 +63,8 @@ export default function CampaignTableRow({ 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")}
@@ -125,16 +91,11 @@ export default function CampaignTableRow({ helper={
{campaign.params.blacklist.length > 0 - ? campaign.params.blacklist.map( - (blacklist: string) => blacklist - ) + ? campaign.params.blacklist.map((blacklist: string) => blacklist) : "No address"}
- } - > - - {campaign.params.blacklist.length} address - + }> + {campaign.params.blacklist.length} address @@ -143,16 +104,11 @@ export default function CampaignTableRow({ helper={
{campaign.params.whitelist.length > 0 - ? campaign.params.whitelist.map( - (blacklist: string) => blacklist - ) + ? campaign.params.whitelist.map((blacklist: string) => blacklist) : "No address"}
- } - > - - {campaign.params.whitelist.length} address - + }> + {campaign.params.whitelist.length} address
diff --git a/src/components/element/leaderboard/LeaderboardLibrary.tsx b/src/components/element/leaderboard/LeaderboardLibrary.tsx index e5713a1e..6c6ddd23 100644 --- a/src/components/element/leaderboard/LeaderboardLibrary.tsx +++ b/src/components/element/leaderboard/LeaderboardLibrary.tsx @@ -1,8 +1,8 @@ import { Text } from "dappkit"; import { useMemo } from "react"; +import type { DummyLeaderboard } from "src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard"; import { LeaderboardTable } from "./LeaderboardTable"; import LeaderboardTableRow from "./LeaderboardTableRow"; -import type { DummyLeaderboard } from "src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard"; export type IProps = { leaderboard: DummyLeaderboard[]; diff --git a/src/components/element/leaderboard/LeaderboardTableRow.tsx b/src/components/element/leaderboard/LeaderboardTableRow.tsx index 5ddde2af..b598b9df 100644 --- a/src/components/element/leaderboard/LeaderboardTableRow.tsx +++ b/src/components/element/leaderboard/LeaderboardTableRow.tsx @@ -1,6 +1,6 @@ -import { type Component, mergeClass, Text } from "dappkit"; -import { LeaderboardRow } from "./LeaderboardTable"; +import { type Component, Text, mergeClass } from "dappkit"; import type { DummyLeaderboard } from "src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard"; +import { LeaderboardRow } from "./LeaderboardTable"; export type CampaignTableRowProps = Component<{ row?: DummyLeaderboard; diff --git a/src/routes/_merkl.(home).(opportunities).tsx b/src/routes/_merkl.(home).(opportunities).tsx index bca1164d..b641a275 100644 --- a/src/routes/_merkl.(home).(opportunities).tsx +++ b/src/routes/_merkl.(home).(opportunities).tsx @@ -7,9 +7,7 @@ import OpportunityLibrary from "src/components/element/opportunity/OpportunityLi import { ErrorContent } from "src/components/layout/ErrorContent"; export async function loader({ request }: LoaderFunctionArgs) { - const { opportunities, count } = await OpportunityService.getManyFromRequest( - request - ); + const { opportunities, count } = await OpportunityService.getManyFromRequest(request); const chains = await ChainService.getAll(); return json({ opportunities, chains, count }); @@ -21,11 +19,7 @@ export default function Index() { return ( - + ); } diff --git a/src/routes/_merkl.opportunity.$chain.$type.$id.(overview).tsx b/src/routes/_merkl.opportunity.$chain.$type.$id.(overview).tsx index 089557c1..5992cb86 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.(overview).tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.(overview).tsx @@ -2,8 +2,8 @@ import { Group } from "@ariakit/react"; import { useOutletContext } from "@remix-run/react"; import { Space } from "packages/dappkit/src"; import CampaignLibrary from "src/components/element/campaign/CampaignLibrary"; -import type { OutletContextOpportunity } from "./_merkl.opportunity.$chain.$type.$id"; import { ErrorContent } from "src/components/layout/ErrorContent"; +import type { OutletContextOpportunity } from "./_merkl.opportunity.$chain.$type.$id"; export default function Index() { const { opportunity } = useOutletContext(); diff --git a/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx b/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx index d4a0e4f9..b4419141 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx @@ -14,9 +14,7 @@ export type DummyLeaderboard = { protocol: string; }; -export async function loader({ - params: { id, type, chain: chainId }, -}: LoaderFunctionArgs) { +export async function loader({ params: { id, type, chain: chainId } }: LoaderFunctionArgs) { const leaderboard: DummyLeaderboard[] = [ { rank: 1, address: "0x1234", rewards: 100, protocol: "Aave" }, { rank: 2, address: "0x5678", rewards: 50, protocol: "Compound" }, diff --git a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx index 10885b4d..b6e179cc 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx @@ -1,21 +1,15 @@ -import { - type LoaderFunctionArgs, - type MetaFunction, - json, -} from "@remix-run/node"; +import type { Opportunity } from "@angleprotocol/merkl-api"; +import { type LoaderFunctionArgs, type MetaFunction, json } from "@remix-run/node"; import { Meta, Outlet, useLoaderData } from "@remix-run/react"; -import Hero from "src/components/composite/Hero"; import { useMemo } from "react"; import { ChainService } from "src/api/services/chain.service"; import { OpportunityService } from "src/api/services/opportunity.service"; +import Hero from "src/components/composite/Hero"; import Tag from "src/components/element/Tag"; import { ErrorHeading } from "src/components/layout/ErrorHeading"; import useOpportunity from "src/hooks/resources/useOpportunity"; -import type { Opportunity } from "@angleprotocol/merkl-api"; -export async function loader({ - params: { id, type, chain: chainId }, -}: LoaderFunctionArgs) { +export async function loader({ params: { id, type, chain: chainId } }: LoaderFunctionArgs) { if (!chainId || !id || !type) throw ""; const chain = await ChainService.get({ search: chainId }); @@ -58,17 +52,11 @@ export default function Index() { if (str.includes("-")) return str .split("-") - .flatMap((s, i, arr) => [ - s, - i !== arr.length - 1 && -, - ]); + .flatMap((s, i, arr) => [s, i !== arr.length - 1 && -]); if (str.includes("/")) return str .split("/") - .flatMap((s, i, arr) => [ - s, - i !== arr.length - 1 && /, - ]); + .flatMap((s, i, arr) => [s, i !== arr.length - 1 && /]); // biome-ignore lint/suspicious/noArrayIndexKey: required return [{str}]; }) @@ -79,7 +67,7 @@ export default function Index() { <> ({ src: t.icon }))} + icons={opportunity.tokens.map(t => ({ src: t.icon }))} navigation={{ label: "Back to opportunities", link: "/" }} title={styleName} description={description} @@ -87,15 +75,8 @@ export default function Index() { { label: "Overview", link }, { label: "Leaderboard", link: `${link}/leaderboard` }, ]} - tags={tags.map((tag) => ( - - ))} - opportunity={opportunity} - > + tags={tags.map(tag => )} + opportunity={opportunity}> From 087bb8527c71539d491fa9aa4ba7c38f15358742 Mon Sep 17 00:00:00 2001 From: Hugo Lextrait Date: Tue, 3 Dec 2024 11:22:06 +0100 Subject: [PATCH 10/11] lint --- .../_merkl.opportunity.$chain.$type.$id.tsx | 33 ++++--------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx index f2f38b23..e08112a1 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx @@ -1,9 +1,5 @@ import type { Opportunity } from "@angleprotocol/merkl-api"; -import { - type LoaderFunctionArgs, - type MetaFunction, - json, -} from "@remix-run/node"; +import { type LoaderFunctionArgs, type MetaFunction, json } from "@remix-run/node"; import { Meta, Outlet, useLoaderData } from "@remix-run/react"; import { useMemo } from "react"; import { ChainService } from "src/api/services/chain.service"; @@ -13,9 +9,7 @@ import Tag from "src/components/element/Tag"; import { ErrorHeading } from "src/components/layout/ErrorHeading"; import useOpportunity from "src/hooks/resources/useOpportunity"; -export async function loader({ - params: { id, type, chain: chainId }, -}: LoaderFunctionArgs) { +export async function loader({ params: { id, type, chain: chainId } }: LoaderFunctionArgs) { if (!chainId || !id || !type) throw ""; const chain = await ChainService.get({ search: chainId }); @@ -59,17 +53,11 @@ export default function Index() { if (str.includes("-")) return str .split("-") - .flatMap((s, i, arr) => [ - s, - i !== arr.length - 1 && -, - ]); + .flatMap((s, i, arr) => [s, i !== arr.length - 1 && -]); if (str.includes("/")) return str .split("/") - .flatMap((s, i, arr) => [ - s, - i !== arr.length - 1 && /, - ]); + .flatMap((s, i, arr) => [s, i !== arr.length - 1 && /]); return [{str}]; }) .flatMap((str, index, arr) => [str, index !== arr.length - 1 && " "]); @@ -79,7 +67,7 @@ export default function Index() { <> ({ src: t.icon }))} + icons={opportunity.tokens.map(t => ({ src: t.icon }))} navigation={{ label: "Back to opportunities", link: "/" }} title={styleName} description={description} @@ -87,15 +75,8 @@ export default function Index() { { label: "Overview", link }, { label: "Leaderboard", link: `${link}/leaderboard` }, ]} - tags={tags.map((tag) => ( - - ))} - opportunity={opportunity} - > + tags={tags.map(tag => )} + opportunity={opportunity}> From 69bb0346a6eab160c527ca8644c600cf738cfeda Mon Sep 17 00:00:00 2001 From: Hugo Lextrait Date: Tue, 3 Dec 2024 11:24:53 +0100 Subject: [PATCH 11/11] update merkl api --- bun.lockb | Bin 827834 -> 827834 bytes package.json | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/bun.lockb b/bun.lockb index ab8cd7c70e09741e584724f621542714f5fb6ccf..340d0b502d6b790270b21e203b478fd21518269a 100755 GIT binary patch delta 457 zcmV;)0XF`+!!o+VGLSAHa4cH{b>;J1q#rye+9LqWiq{;P?ZF9lO%{Vm)KDP5R+tpD3?&a0SSj0QUZq=QUkXcQUr{R zK(Gl4mT8}5w?>Af*rxncDai-q(Fl*rsXF4>rL-AK~ zHutyRDU;KXJS>M$^&%-(FBK; zZv}^yZw9xOZwJc80ya5^VKxZ2VKxcWwHq}60KaPj01*HHfCmH`M{eMn*dhU5FfKDL zFqcs53C#mFE;NTA?FokgYVGBac{H)b$3H8f*5GC4P8F=An2H92Nu zG-WnpV`4R7WnnR8Gc#j0w|w~vr-GNmlMEu4oni|JmrZ;O9G8-A3?R3Od<-!qt^2`i delta 452 zcmV;#0XzP>!!o+VGLSAH$AB+eBkj=mI;Rv*#}?JX^((5V)!J1rC@7wm1ZxG-fljqf z0UGH6I+M|SCzp^60tvU7=>ZF9ljMjrm)KDP5R+)3FozgY0*4q<1GgAb1dWYANdD3n znU{8!mgs1yBQ<|Oa7D2cZM`x!geuNmS#k}IbFAqdYg_)u2g7_Lq0TwT=93wpy|5c{ zPhcLCp)Muux8>gi7>_{P8KD9G6d}#eiZhgOlfcxo_yN3OrNQnT&4SykOwvy^uSuE! z?yjLkhnH{-qeUZ$&+3sNf+vO-*?T*ri zPj5dZ*y2Lu{NZs3~OA^~17E;24Lmr(2p z%>yznFoz)R35Ou<3b!Eb3o#5UH8?M3H)3TsWo2VAF=1pmH#uc7WMnutF*9U0H#Ipo uW-u}|F=A#mWnpHweEAEff|nvH3L=-CVhaeDO?(U-mx3k=Ah(Hp3^66f?!$fn diff --git a/package.json b/package.json index e0c9fc55..97e1a2f7 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ ], "dependencies": { "@acab/ecsstatic": "^0.8.0", - "@angleprotocol/merkl-api": "0.10.58", + "@angleprotocol/merkl-api": "0.10.69", "@ariakit/react": "^0.4.12", "@elysiajs/eden": "^1.1.3", "@emotion/css": "^11.13.4",