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/bun.lockb b/bun.lockb index 7566537c..340d0b50 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/merkl.config.ts b/merkl.config.ts index 2e8b47d6..85a3eb38 100644 --- a/merkl.config.ts +++ b/merkl.config.ts @@ -173,5 +173,5 @@ export default createConfig({ [zksync.id]: http(), [optimism.id]: http(), }, - } + }, }); diff --git a/packages/dappkit b/packages/dappkit index cdf85fad..8ced032d 160000 --- a/packages/dappkit +++ b/packages/dappkit @@ -1 +1 @@ -Subproject commit cdf85fadd0f3f8303f9e044010deb2d8f401791e +Subproject commit 8ced032d34bbd5186fabb31ecf741bff89d0bab4 diff --git a/src/api/opportunity/opportunity.ts b/src/api/opportunity/opportunity.ts index bb329eee..5917d41b 100644 --- a/src/api/opportunity/opportunity.ts +++ b/src/api/opportunity/opportunity.ts @@ -8,7 +8,6 @@ function getQueryParams( 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"); @@ -35,7 +34,9 @@ export async function fetchOpportunities( 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 ef27dd65..00604e36 100644 --- a/src/api/services/campaign.service.ts +++ b/src/api/services/campaign.service.ts @@ -1,17 +1,54 @@ import type { Campaign } from "@angleprotocol/merkl-api"; import { api } from "../index.server"; -class CampaignService { +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 - async get(): Promise { + static async get(): Promise { const { data } = await api.v4.campaigns.index.get({ query: {} }); + + 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 - 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..0980468b 100644 --- a/src/api/services/chain.service.ts +++ b/src/api/services/chain.service.ts @@ -1,7 +1,6 @@ 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, diff --git a/src/api/services/opportunity.service.ts b/src/api/services/opportunity.service.ts index efa8a9fb..2b45abbf 100644 --- a/src/api/services/opportunity.service.ts +++ b/src/api/services/opportunity.service.ts @@ -1,8 +1,7 @@ -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"; -// biome-ignore lint/complexity/noStaticOnlyClass: export abstract class OpportunityService { static async #fetch( call: () => Promise, @@ -67,27 +66,37 @@ export abstract class OpportunityService { return { opportunities: opportunities.filter(o => o !== null), count }; } - static async getCampaigns(query: { chainId: number; type: string; identifier: string }): Promise { + static async get(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 } }), + const opportunity = await OpportunityService.#fetch(async () => + api.v4.opportunities({ id: `${chainId}-${type}-${identifier}` }).get(), ); - return campaigns.filter(c => c !== null); + //TODO: updates tags to take an array + if (config.tags && !opportunity.tags.includes(config.tags?.[0])) + throw new Response("Opportunity inacessible", { status: 403 }); + + return opportunity; } - static async get(query: { chainId: number; type: string; identifier: string }): Promise { + static async getCampaignsByParams(query: { + chainId: number; + type: string; + identifier: string; + }) { const { chainId, type, identifier } = query; - const opportunity = await OpportunityService.#fetch(async () => - api.v4.opportunities({ id: `${chainId}-${type}-${identifier}` }).get(), + 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 && !opportunity.tags.includes(config.tags?.[0])) + if (config.tags && !opportunityWithCampaigns.tags.includes(config.tags?.[0])) throw new Response("Opportunity inacessible", { status: 403 }); - return opportunity; + return opportunityWithCampaigns; } } diff --git a/src/api/services/protocol.service.ts b/src/api/services/protocol.service.ts index 67e529bc..a0664eef 100644 --- a/src/api/services/protocol.service.ts +++ b/src/api/services/protocol.service.ts @@ -1,7 +1,6 @@ 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, diff --git a/src/api/services/reward.service.ts b/src/api/services/reward.service.ts index 523c0ddc..b081cef0 100644 --- a/src/api/services/reward.service.ts +++ b/src/api/services/reward.service.ts @@ -1,7 +1,6 @@ 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, @@ -21,4 +20,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/api/services/token.service.ts b/src/api/services/token.service.ts index 22264fd8..bbefb1fa 100644 --- a/src/api/services/token.service.ts +++ b/src/api/services/token.service.ts @@ -1,7 +1,6 @@ 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, diff --git a/src/components/composite/Hero.tsx b/src/components/composite/Hero.tsx index fbd01da7..f72dede4 100644 --- a/src/components/composite/Hero.tsx +++ b/src/components/composite/Hero.tsx @@ -1,8 +1,9 @@ -import type { Campaign } from "@angleprotocol/merkl-api"; +import type { Campaign, Opportunity } from "@angleprotocol/merkl-api"; import { useLocation } from "@remix-run/react"; -import { 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"; import { type PropsWithChildren, type ReactNode, useMemo } from "react"; import { formatUnits, parseUnits } from "viem"; @@ -13,14 +14,20 @@ export type HeroProps = PropsWithChildren<{ description: ReactNode; tags?: ReactNode[]; tabs?: { label: ReactNode; link: string }[]; - campaigns?: Campaign[]; + opportunity?: Opportunity; }>; -export default function Hero({ navigation, icons, title, description, tags, tabs, children, campaigns }: 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); + }, [opportunity]); + const totalRewards = useMemo(() => { - const amounts = campaigns?.map(campaign => { + const amounts = filteredCampaigns?.map(campaign => { const duration = campaign.endTimestamp - campaign.startTimestamp; const dayspan = BigInt(duration) / BigInt(3600 * 24); @@ -30,7 +37,7 @@ export default function Hero({ navigation, icons, title, description, tags, tabs const sum = amounts?.reduce((accumulator, currentValue) => accumulator + currentValue, 0n); if (!sum) return "0.0"; return formatUnits(sum, 18); - }, [campaigns]); + }, [filteredCampaigns]); return ( <> @@ -116,45 +123,47 @@ export default function Hero({ navigation, icons, title, description, tags, tabs {!location?.pathname.includes("user") && ( - - {totalRewards} - + + + {totalRewards} + + Daily rewards - todo + + + {(opportunity?.apr ?? 0) / 100} + + APR - {campaigns?.length} + {filteredCampaigns?.length} Active campaigns )} - {/* {!!tabs && ( - - {tabs?.map((tab) => ( - - ))} - - )} */} + {!!tabs && ( + + {tabs?.map(tab => ( + + ))} + + )}
{children}
); diff --git a/src/components/element/Tag.tsx b/src/components/element/Tag.tsx index bb6b47d6..d940a69e 100644 --- a/src/components/element/Tag.tsx +++ b/src/components/element/Tag.tsx @@ -27,7 +27,6 @@ export default function Tag({ type, value, ...props }: switch (type) { case "status": { const status = statuses[value as TagTypes["status"]] ?? statuses.LIVE; - return ( ({ type, value, ...props }: } case "chain": { const chain = value as TagTypes["chain"]; - return ( ({ type, value, ...props }: } case "action": { const action = actions[value as TagTypes["action"]]; - if (!action) return ; - return ( ({ type, value, ...props }: ); } - case "token": { const token = value as TagTypes["token"]; - if (!token) return ; - return ( ({ type, value, ...props }: ); } - case "tokenChain": { const token = value as TagTypes["tokenChain"]; - if (!token) return ; - return ( ({ type, value, ...props }: ); } - case "protocol": { const protocol = value; - if (!protocol) return ; - return ( c.endTimestamp < moment().unix())); +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(c => showInactive || Number(c.endTimestamp) > now); + const shownCampaigns = opportunity.campaigns.filter(c => showInactive || Number(c.endTimestamp) > now); const startsOpen = shownCampaigns.length < 3; - return shownCampaigns?.map(c => ); - }, [campaigns, showInactive]); + const campaignsSorted = shownCampaigns.sort((a, b) => Number(b.endTimestamp) - Number(a.endTimestamp)); + return campaignsSorted?.map(c => ); + }, [opportunity, showInactive]); return ( Campaigns - } - footer={"Something"}> - {rows} + }> + {!!rows?.length ? ( + rows + ) : ( + + No active campaign +
+ +
+
+ )}
); } diff --git a/src/components/element/campaign/CampaignTable.tsx b/src/components/element/campaign/CampaignTable.tsx index a97a2f21..050c4043 100644 --- a/src/components/element/campaign/CampaignTable.tsx +++ b/src/components/element/campaign/CampaignTable.tsx @@ -2,27 +2,33 @@ 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", - 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 2be95f2f..4c55cf01 100644 --- a/src/components/element/campaign/CampaignTableRow.tsx +++ b/src/components/element/campaign/CampaignTableRow.tsx @@ -1,9 +1,14 @@ 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, 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 RestrictionsCollumn from "./tableCollumns/RestrictionsCollumn"; export type CampaignTableRowProps = Component<{ campaign: Campaign; @@ -11,14 +16,24 @@ 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} + chainColumn={} + identifierColumn={{campaign.identifier}} + restrictionsColumn={} dailyRewardsColumn={ @@ -32,29 +47,72 @@ export default function CampaignTableRow({ campaign, startsOpen, className, ...p {time} } - restrictionsColumn={[]} - profileColumn={profile} - arrowColumn={}> - {open && ( + 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 + + Incentivized Liquidity + {profile} + + + Blacklisted for + + {campaign.params.blacklist.length > 0 + ? campaign.params.blacklist.map((blacklist: string) => blacklist) + : "No address"} +
+ }> + {campaign.params.blacklist.length} address + + + + Whitelisted for + + {campaign.params.whitelist.length > 0 + ? campaign.params.whitelist.map((blacklist: string) => blacklist) + : "No address"} + + }> + {campaign.params.whitelist.length} address + + - - - 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..6c6ddd23 --- /dev/null +++ b/src/components/element/leaderboard/LeaderboardLibrary.tsx @@ -0,0 +1,23 @@ +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"; + +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..b598b9df --- /dev/null +++ b/src/components/element/leaderboard/LeaderboardTableRow.tsx @@ -0,0 +1,20 @@ +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; +}>; + +export default function LeaderboardTableRow({ row, className, ...props }: CampaignTableRowProps) { + return ( + #{row?.rank}} + addressColumn={{row?.address}} + rewardsColumn={{row?.rewards}} + protocolColumn={{row?.protocol}} + /> + ); +} diff --git a/src/config/type.ts b/src/config/type.ts index ae4b3b4c..b3eb4056 100644 --- a/src/config/type.ts +++ b/src/config/type.ts @@ -32,6 +32,9 @@ export type MerklConfig = { links: { [key: string]: string; }; + images: { + hero: string; + }; }; export function createConfig({ wagmi, ...config }: MerklConfig) { diff --git a/src/hooks/resources/useCampaign.tsx b/src/hooks/resources/useCampaign.tsx index 7a45b7d7..fef6c7b3 100644 --- a/src/hooks/resources/useCampaign.tsx +++ b/src/hooks/resources/useCampaign.tsx @@ -23,7 +23,7 @@ export default function useCampaign(campaign: Campaign) { 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.(home).(opportunities).tsx b/src/routes/_merkl.(home).(opportunities).tsx index 6a611ac3..b641a275 100644 --- a/src/routes/_merkl.(home).(opportunities).tsx +++ b/src/routes/_merkl.(home).(opportunities).tsx @@ -19,7 +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 166a054b..5992cb86 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.(overview).tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.(overview).tsx @@ -1,42 +1,23 @@ -import type { Campaign, Opportunity } from "@angleprotocol/merkl-api"; import { Group } from "@ariakit/react"; -import { type LoaderFunctionArgs, json } from "@remix-run/node"; -import { useLoaderData } from "@remix-run/react"; -import { Container, Space } from "packages/dappkit/src"; -import { ChainService } from "src/api/services/chain.service"; -import { OpportunityService } from "src/api/services/opportunity.service"; +import { useOutletContext } from "@remix-run/react"; +import { Space } from "packages/dappkit/src"; import CampaignLibrary from "src/components/element/campaign/CampaignLibrary"; -import Participate from "src/components/element/participate/Participate"; import { ErrorContent } from "src/components/layout/ErrorContent"; - -export async function loader({ params: { id, type, chain: chainId } }: LoaderFunctionArgs) { - //TODO: assess where to handle these - 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); - const campaigns = await OpportunityService.getCampaigns(opportunityId); - - return json({ opportunity, campaigns }); -} +import type { OutletContextOpportunity } from "./_merkl.opportunity.$chain.$type.$id"; export default function Index() { - const { opportunity, campaigns } = useLoaderData(); + const { opportunity } = useOutletContext(); 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..b4419141 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.leaderboard.tsx @@ -1,10 +1,74 @@ -import { type LoaderFunctionArgs, json } from "@remix-run/node"; -import { Text } from "dappkit/src"; +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 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 async function loader({ params }: LoaderFunctionArgs) { - return json({ chain: params.chain }); +export type DummyLeaderboard = { + rank: number; + address: string; + rewards: number; + protocol: string; +}; + +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" }, + { rank: 3, address: "0x9abc", rewards: 25, protocol: "Aave" }, + { rank: 4, address: "0xdef0", rewards: 10, protocol: "Compound" }, + { rank: 5, address: "0x1235", rewards: 5, protocol: "Aave" }, + ]; + + // ----------- Need to implement this part @Hugo ------------ + // if (!chainId || !id || !type) throw ""; + // const chain = await ChainService.get({ search: chainId }); + + // const opportunity = await OpportunityService.getCampaignsByParams({ + // chainId: chain.id, + // type: type, + // identifier: id, + // }); + + // const campaignIdentifiers = opportunity?.campaigns?.map((c) => c.identifier); + // if (!campaignIdentifiers) throw new Error("No campaign identifiers found"); + + // console.log({ campaignIdentifiers, chain: chain.id }); + + // const rewards = await RewardService.getByParams({ + // campaignIdentifiers, + // chainId: chain.id, + // }); + // ----------- Need to implement this part @Hugo ------------ + + return json({ leaderboard }); } export default function Index() { - return Leaderboard WIP; + const { leaderboard } = useLoaderData(); + + return ( + <> + + + + Total rewarded users + + {/* Probably a count from api */} + {leaderboard?.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 cb4e4f87..e08112a1 100644 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx +++ b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx @@ -1,10 +1,10 @@ +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"; @@ -13,11 +13,16 @@ export async function loader({ params: { id, type, chain: chainId } }: LoaderFun 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); + const opportunity = await OpportunityService.getCampaignsByParams({ + chainId: chain.id, + type: type, + identifier: id, + }); + + if (!opportunity) throw "DAZZ"; - return json(opportunity); + return json({ opportunity }); } export const meta: MetaFunction = ({ data, error }) => { @@ -25,16 +30,20 @@ export const meta: MetaFunction = ({ data, error }) => { return [{ title: `${data?.name} on Merkl` }]; }; +export type OutletContextOpportunity = { + opportunity: Opportunity; +}; + export default function Index() { - const opportunity = useLoaderData(); + const { opportunity } = useLoaderData(); const { tags, description, link } = useOpportunity(opportunity); + const styleName = useMemo(() => { const spaced = opportunity?.name.split(" "); return spaced .map((str, index) => { const key = str + crypto.randomUUID(); - // biome-ignore lint/suspicious/noArrayIndexKey: required if (!str.match(/[\p{Letter}\p{Mark}]+/gu)) return [ @@ -49,7 +58,6 @@ export default function Index() { 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 && " "]); @@ -66,10 +74,10 @@ export default function Index() { tabs={[ { label: "Overview", link }, { label: "Leaderboard", link: `${link}/leaderboard` }, - { label: "Analytics", link: `${link}/analytics` }, ]} - tags={tags.map(tag => )}> - + tags={tags.map(tag => )} + opportunity={opportunity}> + );