From 874e60ab451daa726a908d5a8e1321e1d1cb9ede Mon Sep 17 00:00:00 2001 From: sheykei Date: Fri, 17 Jan 2025 15:11:22 +0100 Subject: [PATCH 1/4] clean: dir structure for opportunity & every type error --- packages/dappkit | 2 +- src/components/composite/Hero.tsx | 2 +- .../element/{opportunity => }/Pagination.tsx | 4 +- src/components/element/Tag.tsx | 3 +- .../element/campaign/CampaignLibrary.tsx | 6 +- .../element/campaign/CampaignTableRow.tsx | 4 +- .../element/chain/ChainTableRow.tsx | 2 +- .../element/functions/SearchBar.tsx | 8 +- .../leaderboard/LeaderboardLibrary.tsx | 2 +- .../element/participate/Participate.tsx | 6 +- .../element/position/PositionLibrary.tsx | 2 +- .../element/position/PositionTableRow.tsx | 2 +- .../element/protocol/ProtocolLibrary.tsx | 2 +- .../element/reinvest/ReinvestBanner.tsx | 2 +- .../rewards/ClaimRewardsChainTableRow.tsx | 2 +- .../rewards/ClaimRewardsTokenTableRow.tsx | 4 +- ...ClaimRewardsTokenTableRowByOpportunity.tsx | 2 +- src/components/element/token/TokenLibrary.tsx | 2 +- .../element/token/TokenTableRow.tsx | 2 +- src/hooks/resources/useCampaign.tsx | 2 +- src/hooks/resources/useOpportunity.tsx | 157 ---------- src/index.generated.ts | 296 ++++++++++-------- .../chain/routes/chain.$id.opportunities.tsx | 2 +- .../components}/OpportunityButton.tsx | 10 +- .../components}/OpportunityFilters.tsx | 12 +- .../element}/OpportunityParticipateModal.tsx | 5 +- .../components/items}/OpportunityCell.tsx | 75 +---- .../components/items/OpportunityShortCard.tsx | 51 +++ .../components/items}/OpportunityTableRow.tsx | 89 ++---- .../library}/OpportunityFeatured.tsx | 6 +- .../library}/OpportunityLibrary.tsx | 16 +- .../components/library}/OpportunityTable.tsx | 4 +- .../hooks/useOpportunityMetadata.tsx | 205 ++++++++++++ .../hooks/useOpportunityMetrics.tsx | 34 ++ .../hooks/useOpportunityRewards.tsx | 105 +++++++ src/modules/opportunity/opportunity.model.ts | 6 - .../opportunity/opportunity.service.ts | 8 +- .../routes/opportunities.header.tsx | 4 +- .../opportunity/routes/opportunities.list.tsx | 40 ++- ...opportunity.$chain.$type.$id.campaigns.tsx | 20 +- .../opportunity.$chain.$type.$id.header.tsx | 98 ++---- ...portunity.$chain.$type.$id.leaderboard.tsx | 14 +- .../routes/protocol.$id.opportunities.tsx | 2 +- .../routes/token.$symbol.opportunities.tsx | 2 +- tsconfig.json | 3 + vite.config.ts | 7 + 46 files changed, 737 insertions(+), 595 deletions(-) rename src/components/element/{opportunity => }/Pagination.tsx (92%) delete mode 100644 src/hooks/resources/useOpportunity.tsx rename src/{components/element/opportunity => modules/opportunity/components}/OpportunityButton.tsx (58%) rename src/{components/element/opportunity => modules/opportunity/components}/OpportunityFilters.tsx (96%) rename src/{components/element/opportunity => modules/opportunity/components/element}/OpportunityParticipateModal.tsx (92%) rename src/{components/element/opportunity => modules/opportunity/components/items}/OpportunityCell.tsx (53%) create mode 100644 src/modules/opportunity/components/items/OpportunityShortCard.tsx rename src/{components/element/opportunity => modules/opportunity/components/items}/OpportunityTableRow.tsx (62%) rename src/{components/element/opportunity => modules/opportunity/components/library}/OpportunityFeatured.tsx (80%) rename src/{components/element/opportunity => modules/opportunity/components/library}/OpportunityLibrary.tsx (89%) rename src/{components/element/opportunity => modules/opportunity/components/library}/OpportunityTable.tsx (95%) create mode 100644 src/modules/opportunity/hooks/useOpportunityMetadata.tsx create mode 100644 src/modules/opportunity/hooks/useOpportunityMetrics.tsx create mode 100644 src/modules/opportunity/hooks/useOpportunityRewards.tsx delete mode 100644 src/modules/opportunity/opportunity.model.ts diff --git a/packages/dappkit b/packages/dappkit index ce0b1e8..6e2901b 160000 --- a/packages/dappkit +++ b/packages/dappkit @@ -1 +1 @@ -Subproject commit ce0b1e863da730787460c49ee787eb6ad8db647a +Subproject commit 6e2901b88df7e12dd5eff68b55f5a7a35d8e68e2 diff --git a/src/components/composite/Hero.tsx b/src/components/composite/Hero.tsx index b972e37..fb94790 100644 --- a/src/components/composite/Hero.tsx +++ b/src/components/composite/Hero.tsx @@ -24,7 +24,7 @@ export type HeroProps = PropsWithChildren<{ breadcrumbs?: { name?: string; link: string; component?: ReactNode }[]; navigation?: { label: ReactNode; link: string }; description: ReactNode; - tags?: ReactNode[]; + tags?: ReactNode[] | ReactNode; sideDatas?: HeroInformations[]; tabs?: { label: ReactNode; link: string; key: string }[]; }>; diff --git a/src/components/element/opportunity/Pagination.tsx b/src/components/element/Pagination.tsx similarity index 92% rename from src/components/element/opportunity/Pagination.tsx rename to src/components/element/Pagination.tsx index 39245b6..71a8ea9 100644 --- a/src/components/element/opportunity/Pagination.tsx +++ b/src/components/element/Pagination.tsx @@ -1,7 +1,7 @@ import { Button, Group, Icon, Select } from "dappkit"; import { useMemo } from "react"; -import { DEFAULT_ITEMS_PER_PAGE } from "../../../constants/pagination"; -import useSearchParamState from "../../../hooks/filtering/useSearchParamState"; +import { DEFAULT_ITEMS_PER_PAGE } from "../../constants/pagination"; +import useSearchParamState from "../../hooks/filtering/useSearchParamState"; export type PaginationProps = { count?: number; diff --git a/src/components/element/Tag.tsx b/src/components/element/Tag.tsx index 0e0b50d..2ed30af 100644 --- a/src/components/element/Tag.tsx +++ b/src/components/element/Tag.tsx @@ -1,4 +1,5 @@ import type { Chain, Token } from "@merkl/api"; +import type { Opportunity } from "@merkl/api" import { useSearchParams } from "@remix-run/react"; import { Button, @@ -17,7 +18,6 @@ import { import merklConfig from "../../config"; import { actions } from "../../config/actions"; import { statuses } from "../../config/status"; -import type { Opportunity } from "../../modules/opportunity/opportunity.model"; export type TagTypes = { chain: Opportunity["chain"]; @@ -34,6 +34,7 @@ export type TagType = { }; export type TagProps = { type: T; + look?: PrimitiveTagProps["look"]; value: TagTypes[T]; filter?: boolean; size?: PrimitiveTagProps["size"]; diff --git a/src/components/element/campaign/CampaignLibrary.tsx b/src/components/element/campaign/CampaignLibrary.tsx index d2f1250..f3eb55d 100644 --- a/src/components/element/campaign/CampaignLibrary.tsx +++ b/src/components/element/campaign/CampaignLibrary.tsx @@ -1,13 +1,13 @@ -import type { Chain } from "@merkl/api"; +import type { Campaign, Chain } from "@merkl/api"; +import type { Opportunity } from "@merkl/api" import { Box, Button, Group, Icon, Text, Title } from "dappkit"; import moment from "moment"; import { useMemo, useState } from "react"; -import type { OpportunityWithCampaigns } from "../../../modules/opportunity/opportunity.model"; import { CampaignTable } from "./CampaignTable"; import CampaignTableRow from "./CampaignTableRow"; export type CampaignLibraryProps = { - opportunity: OpportunityWithCampaigns; + opportunity: Opportunity & { campaigns: Campaign[] }; chain: Chain; }; diff --git a/src/components/element/campaign/CampaignTableRow.tsx b/src/components/element/campaign/CampaignTableRow.tsx index 691d72d..2b8edc6 100644 --- a/src/components/element/campaign/CampaignTableRow.tsx +++ b/src/components/element/campaign/CampaignTableRow.tsx @@ -1,4 +1,6 @@ +import Tag from "@core/components/element/Tag"; import type { Campaign, Chain as ChainType } from "@merkl/api"; +import type { Opportunity } from "@merkl/api" import { Box, Button, @@ -23,8 +25,6 @@ import { type ReactNode, useCallback, useMemo, useState } from "react"; import { v4 as uuidv4 } from "uuid"; import EtherScan from "../../../assets/images/etherscan.svg"; import useCampaign from "../../../hooks/resources/useCampaign"; -import type { Opportunity } from "../../../modules/opportunity/opportunity.model"; -import Tag from "../Tag"; import Token from "../token/Token"; import { CampaignRow } from "./CampaignTable"; import CampaignTooltipDates from "./CampaignTooltipDates"; diff --git a/src/components/element/chain/ChainTableRow.tsx b/src/components/element/chain/ChainTableRow.tsx index 3d916b6..365e5b4 100644 --- a/src/components/element/chain/ChainTableRow.tsx +++ b/src/components/element/chain/ChainTableRow.tsx @@ -1,7 +1,7 @@ +import type { TagTypes } from "@core/components/element/Tag"; import type { Chain } from "@merkl/api"; import { Link } from "@remix-run/react"; import { type BoxProps, Group, Icon, Title, mergeClass } from "dappkit"; -import type { TagTypes } from "../Tag"; import { ChainRow } from "./ChainTable"; export type ChainTableRowProps = { diff --git a/src/components/element/functions/SearchBar.tsx b/src/components/element/functions/SearchBar.tsx index d8db907..0fb6552 100644 --- a/src/components/element/functions/SearchBar.tsx +++ b/src/components/element/functions/SearchBar.tsx @@ -1,11 +1,11 @@ import type { Opportunity } from "@merkl/api"; import { Form, useLocation } from "@remix-run/react"; -import { Divider, Group, Icon, Icons, Input, Modal, Title, useShortcut } from "dappkit"; +import { Divider, Group, Icon, Input, Modal, Title, useShortcut } from "dappkit"; import { Button } from "dappkit"; import { Scroll } from "dappkit"; import { type ReactNode, useEffect, useMemo, useState } from "react"; -import useOpportunity from "../../../hooks/resources/useOpportunity"; import { type Results, type Searchable, useMerklSearch } from "../../../hooks/useMerklSearch"; +import useOpportunityData from "../../../modules/opportunity/hooks/useOpportunityMetadata"; const titles: { [S in Searchable]: ReactNode } = { chain: "Chains", @@ -14,12 +14,12 @@ const titles: { [S in Searchable]: ReactNode } = { }; function OpportunityResult({ opportunity }: { opportunity: Opportunity }) { - const { link, icons } = useOpportunity(opportunity); + const { link, Icons } = useOpportunityData(opportunity); return ( <> diff --git a/src/components/element/leaderboard/LeaderboardLibrary.tsx b/src/components/element/leaderboard/LeaderboardLibrary.tsx index 177c301..38f808f 100644 --- a/src/components/element/leaderboard/LeaderboardLibrary.tsx +++ b/src/components/element/leaderboard/LeaderboardLibrary.tsx @@ -5,7 +5,7 @@ import { useMemo } from "react"; import { v4 as uuidv4 } from "uuid"; import { DEFAULT_ITEMS_PER_PAGE } from "../../../constants/pagination"; import type { RewardService } from "../../../modules/reward/reward.service"; -import Pagination from "../opportunity/Pagination"; +import Pagination from "../Pagination"; import { LeaderboardTable, LeaderboardTableWithoutReason } from "./LeaderboardTable"; import LeaderboardTableRow from "./LeaderboardTableRow"; diff --git a/src/components/element/participate/Participate.tsx b/src/components/element/participate/Participate.tsx index 1cdd7bc..8fec17d 100644 --- a/src/components/element/participate/Participate.tsx +++ b/src/components/element/participate/Participate.tsx @@ -1,3 +1,4 @@ +import OpportunityShortCard from "@core/modules/opportunity/components/items/OpportunityShortCard"; import type { Opportunity } from "@merkl/api"; import { useLocation } from "@remix-run/react"; import { Button, Group, Icon, Input, PrimitiveTag, Text, Value } from "dappkit"; @@ -7,10 +8,9 @@ import { Fmt } from "dappkit"; import { Suspense, useEffect, useMemo, useState } from "react"; import { I18n } from "../../../I18n"; import merklConfig from "../../../config"; -import useOpportunity from "../../../hooks/resources/useOpportunity"; import useParticipate from "../../../hooks/useParticipate"; +import useOpportunityData from "../../../modules/opportunity/hooks/useOpportunityMetadata"; import { TokenService } from "../../../modules/token/token.service"; -import OpportunityShortCard from "../opportunity/OpportunityShortCard"; import TokenSelect from "../token/TokenSelect"; import Interact from "./Interact.client"; @@ -42,7 +42,7 @@ export default function Participate({ loading, } = useParticipate(opportunity.chainId, opportunity.protocol?.id, opportunity.identifier, tokenAddress); - const { link } = useOpportunity(opportunity); + const { link } = useOpportunityData(opportunity); const location = useLocation(); const isOnOpportunityPage = location.pathname.includes("/opportunities/"); const [success, setSuccess] = useState(false); diff --git a/src/components/element/position/PositionLibrary.tsx b/src/components/element/position/PositionLibrary.tsx index 114dc36..a130dae 100644 --- a/src/components/element/position/PositionLibrary.tsx +++ b/src/components/element/position/PositionLibrary.tsx @@ -1,7 +1,7 @@ import type { PositionT } from "@merkl/api/dist/src/modules/v4/liquidity"; import { Text, Title } from "dappkit"; import { useMemo } from "react"; -import Pagination from "../opportunity/Pagination"; +import Pagination from "../Pagination"; import { PositionTable } from "./PositionTable"; import PositionTableRow from "./PositionTableRow"; diff --git a/src/components/element/position/PositionTableRow.tsx b/src/components/element/position/PositionTableRow.tsx index 934f857..f152d9f 100644 --- a/src/components/element/position/PositionTableRow.tsx +++ b/src/components/element/position/PositionTableRow.tsx @@ -1,9 +1,9 @@ +import OpportunityButton from "@core/modules/opportunity/components/OpportunityButton"; import type { PositionT } from "@merkl/api/dist/src/modules/v4/liquidity"; import { type Component, PrimitiveTag, Value, sizeScale } from "dappkit"; import { useMemo } from "react"; import { parseUnits } from "viem"; import merklConfig from "../../../config"; -import OpportunityButton from "../opportunity/OpportunityButton"; import Token from "../token/Token"; import { PositionRow } from "./PositionTable"; diff --git a/src/components/element/protocol/ProtocolLibrary.tsx b/src/components/element/protocol/ProtocolLibrary.tsx index 1cc43b7..f485612 100644 --- a/src/components/element/protocol/ProtocolLibrary.tsx +++ b/src/components/element/protocol/ProtocolLibrary.tsx @@ -1,7 +1,7 @@ import type { Protocol } from "@merkl/api"; import { Group } from "dappkit"; import { useMemo } from "react"; -import Pagination from "../opportunity/Pagination"; +import Pagination from "../Pagination"; import ProtocolCell from "./ProtocolCell"; import ProtocolFilters from "./ProtocolFilters"; diff --git a/src/components/element/reinvest/ReinvestBanner.tsx b/src/components/element/reinvest/ReinvestBanner.tsx index 140d432..95dbeaf 100644 --- a/src/components/element/reinvest/ReinvestBanner.tsx +++ b/src/components/element/reinvest/ReinvestBanner.tsx @@ -1,8 +1,8 @@ +import OpportunityCell from "@core/modules/opportunity/components/items/OpportunityCell"; import type { Opportunity } from "@merkl/api"; import { Collapsible, EventBlocker, Group, Icon, Space, Text, mergeClass } from "dappkit"; import { useEffect, useMemo, useState } from "react"; import { I18n } from "../../../I18n"; -import OpportunityCell from "../../../components/element/opportunity/OpportunityCell"; import merklConfig from "../../../config"; import { OpportunityService } from "../../../modules/opportunity/opportunity.service"; diff --git a/src/components/element/rewards/ClaimRewardsChainTableRow.tsx b/src/components/element/rewards/ClaimRewardsChainTableRow.tsx index 81df2f7..3ce75c4 100644 --- a/src/components/element/rewards/ClaimRewardsChainTableRow.tsx +++ b/src/components/element/rewards/ClaimRewardsChainTableRow.tsx @@ -1,3 +1,4 @@ +import Tag from "@core/components/element/Tag"; import type { Reward } from "@merkl/api"; import { Button, type Component, Icon, Space, Value, mergeClass } from "dappkit"; import { TransactionButton, type TransactionButtonProps } from "dappkit"; @@ -9,7 +10,6 @@ import { useMemo, useState } from "react"; import merklConfig from "../../../config"; import useReward from "../../../hooks/resources/useReward"; import { UserService } from "../../../modules/user/user.service"; -import Tag from "../Tag"; import { ClaimRewardsChainRow } from "./ClaimRewardsChainTable"; import { ClaimRewardsTokenTable } from "./ClaimRewardsTokenTable"; import ClaimRewardsTokenTableRow from "./ClaimRewardsTokenTableRow"; diff --git a/src/components/element/rewards/ClaimRewardsTokenTableRow.tsx b/src/components/element/rewards/ClaimRewardsTokenTableRow.tsx index a3abed5..289732e 100644 --- a/src/components/element/rewards/ClaimRewardsTokenTableRow.tsx +++ b/src/components/element/rewards/ClaimRewardsTokenTableRow.tsx @@ -1,10 +1,10 @@ +import Tag from "@core/components/element/Tag"; +import OpportunityButton from "@core/modules/opportunity/components/OpportunityButton"; import type { Reward } from "@merkl/api"; import { Checkbox, type Component, Divider, type GetSet, Group, Icon, Space } from "dappkit"; import { Collapsible } from "dappkit"; import { Fmt } from "dappkit"; import { useMemo, useState } from "react"; -import Tag from "../Tag"; -import OpportunityButton from "../opportunity/OpportunityButton"; import { ClaimRewardsTokenRow } from "./ClaimRewardsTokenTable"; import ClaimRewardsTokenTablePrice from "./ClaimRewardsTokenTablePrice"; diff --git a/src/components/element/rewards/byOpportunity/ClaimRewardsTokenTableRowByOpportunity.tsx b/src/components/element/rewards/byOpportunity/ClaimRewardsTokenTableRowByOpportunity.tsx index 221865d..02cbeb6 100644 --- a/src/components/element/rewards/byOpportunity/ClaimRewardsTokenTableRowByOpportunity.tsx +++ b/src/components/element/rewards/byOpportunity/ClaimRewardsTokenTableRowByOpportunity.tsx @@ -1,6 +1,6 @@ +import OpportunityButton from "@core/modules/opportunity/components/OpportunityButton"; import type { Reward } from "@merkl/api"; import { type Component, Divider, type GetSet } from "dappkit"; -import OpportunityButton from "../../opportunity/OpportunityButton"; import Token from "../../token/Token"; import { ClaimRewardsByOpportunityRow } from "./ClaimRewardsTableByOpportunity"; diff --git a/src/components/element/token/TokenLibrary.tsx b/src/components/element/token/TokenLibrary.tsx index 1afac70..836c068 100644 --- a/src/components/element/token/TokenLibrary.tsx +++ b/src/components/element/token/TokenLibrary.tsx @@ -1,7 +1,7 @@ import type { Token } from "@merkl/api"; import { Group } from "dappkit"; import { useMemo } from "react"; -import Pagination from "../opportunity/Pagination"; +import Pagination from "../Pagination"; import TokenFilters from "./TokenFilters"; import { TokenTable } from "./TokenTable"; import TokenTableRow from "./TokenTableRow"; diff --git a/src/components/element/token/TokenTableRow.tsx b/src/components/element/token/TokenTableRow.tsx index 07d2a84..d33107a 100644 --- a/src/components/element/token/TokenTableRow.tsx +++ b/src/components/element/token/TokenTableRow.tsx @@ -1,3 +1,4 @@ +import type { TagTypes } from "@core/components/element/Tag"; import type { Token } from "@merkl/api"; import { Link } from "@remix-run/react"; import { Button, Group, Icon, Value } from "dappkit"; @@ -5,7 +6,6 @@ import type { BoxProps } from "dappkit"; import { Title } from "dappkit"; import { mergeClass } from "dappkit"; import merklConfig from "../../../config"; -import type { TagTypes } from "../Tag"; import { TokenRow } from "./TokenTable"; export type TokenTableRowProps = { diff --git a/src/hooks/resources/useCampaign.tsx b/src/hooks/resources/useCampaign.tsx index bbc3806..14cee32 100644 --- a/src/hooks/resources/useCampaign.tsx +++ b/src/hooks/resources/useCampaign.tsx @@ -1,4 +1,5 @@ import type { Campaign as CampaignFromApi } from "@merkl/api"; +import type { Opportunity } from "@merkl/api" import { Bar, Icon } from "dappkit"; import { Group, Text, Value } from "dappkit"; import { Time } from "dappkit"; @@ -8,7 +9,6 @@ import { parseUnits } from "viem"; import type { RuleType } from "../../components/element/campaign/rules/Rule"; import Token from "../../components/element/token/Token"; import type { Campaign } from "../../modules/campaigns/campaign.model"; -import type { Opportunity } from "../../modules/opportunity/opportunity.model"; export default function useCampaign(campaign: CampaignFromApi, opportunity?: Opportunity) { if (!campaign) diff --git a/src/hooks/resources/useOpportunity.tsx b/src/hooks/resources/useOpportunity.tsx deleted file mode 100644 index ba430c7..0000000 --- a/src/hooks/resources/useOpportunity.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import type { Token } from "@merkl/api"; -import { Icon, Value } from "dappkit"; -import { useMemo } from "react"; -import { v4 as uuidv4 } from "uuid"; -import type { TagType } from "../../components/element/Tag"; -import merklConfig from "../../config"; -import type { Opportunity } from "../../modules/opportunity/opportunity.model"; - -export default function useOpportunity(opportunity: Opportunity) { - const tags = useMemo(() => { - const tokens: TagType<"token">[] = opportunity.tokens.map(t => ({ - type: "token", - value: t, - })); - const action: TagType<"action"> = { - type: "action", - value: opportunity.action, - }; - const protocol: TagType<"protocol"> | undefined = opportunity?.protocol && { - type: "protocol", - value: opportunity.protocol, - }; - const chain: TagType<"chain"> = { - type: "chain", - value: opportunity?.chain, - }; - const status: TagType<"status"> = { - type: "status", - value: opportunity?.status, - }; - - return [protocol, chain, action, ...tokens, status].filter(a => a); - }, [opportunity]); - - const link = useMemo( - () => - `/opportunities/${opportunity.chain?.name?.toLowerCase?.().replace(" ", "-")}/${opportunity.type}/${opportunity.identifier}`, - [opportunity], - ); - - const iconTokens = useMemo(() => { - // If there is more than 1 icons, remove undefined ones - let tokens = opportunity.tokens; - if (tokens.length > 1) tokens = tokens.filter(token => !!token.icon); - if (tokens.length < 1) tokens = opportunity.tokens; - return tokens; - }, [opportunity]); - - const icons = useMemo( - () => iconTokens.map(({ icon, address }) => ), - [iconTokens], - ); - - const rewardIcons = useMemo( - () => - opportunity.rewardsRecord?.breakdowns?.map(({ token: { icon, address } }) => ( - - )) ?? [], - [opportunity], - ); - - const description = useMemo(() => { - const tokenSymbols = opportunity?.tokens?.reduce((str, token, index, tokens) => { - const noSeparator = index === tokens.length || index === 0; - const separator = index === tokens.length - 1 ? "-" : "-"; - - return str + (noSeparator ? "" : separator) + token.symbol; - }, ""); - - switch (opportunity.action) { - case "POOL": - return `Earn rewards by providing liquidity to the ${opportunity.protocol?.name} ${tokenSymbols} pool on ${opportunity.chain.name}, or through a liquidity manager supported by Merkl`; - case "HOLD": - return `Earn rewards by holding ${tokenSymbols} or by staking it in a supported contract`; - case "LEND": - return `Earn rewards by supplying liquidity to the ${opportunity.protocol?.name} ${tokenSymbols} on ${opportunity.chain.name}`; - case "BORROW": - return `Earn rewards by borrowing liquidity to the ${opportunity.protocol?.name} ${tokenSymbols} on ${opportunity.chain.name}`; - case "DROP": - return `Visit your dashboard to check if you've earned rewards from this airdrop`; - default: - break; - } - }, [opportunity]); - - const herosData = useMemo(() => { - let data = []; - switch (opportunity.action) { - default: - data = [ - !!opportunity.dailyRewards && { - label: "Daily rewards", - data: ( - - {opportunity.dailyRewards} - - ), - key: uuidv4(), - }, - !!opportunity.apr && { - label: "APR", - data: ( - - {opportunity.apr / 100} - - ), - key: uuidv4(), - }, - !!opportunity.tvl && { - label: "Total value locked", - data: ( - - {opportunity.tvl} - - ), - key: uuidv4(), - }, - ]; - } - return data.filter(data => !!data); - }, [opportunity]); - - const rewardsBreakdown = useMemo(() => { - if (!opportunity?.rewardsRecord?.breakdowns) return []; - - const tokenAddresses = opportunity.rewardsRecord.breakdowns.reduce((addresses, breakdown) => { - return addresses.add(breakdown.token.address); - }, new Set()); - - return Array.from(tokenAddresses).map(address => { - const breakdowns = opportunity.rewardsRecord.breakdowns.filter(({ token: t }) => t.address === address); - const amount = breakdowns?.reduce((sum, breakdown) => sum + BigInt(breakdown.amount), 0n); - - return { token: breakdowns?.[0]?.token, amount } satisfies { - token: Token; - amount: bigint; - }; - }); - }, [opportunity]); - - return { - link, - icons, - iconTokens, - rewardIcons, - description, - rewardsBreakdown, - opportunity: { - ...opportunity, - name: merklConfig.opportunityPercentage - ? opportunity.name - : opportunity.name.replace(/\s*\d+(\.\d+)?%$/, "").trim(), - }, - tags, - herosData, - }; -} diff --git a/src/index.generated.ts b/src/index.generated.ts index 964807b..2b584e1 100644 --- a/src/index.generated.ts +++ b/src/index.generated.ts @@ -1,5 +1,5 @@ /** - * + * */ export * from ".//root"; export { default as root } from ".//root"; @@ -34,6 +34,8 @@ export * from "./config/status"; export * from "./config/rewards"; export * from "./config/type"; export * from "./config/opportunity"; +export * from "./config/index"; +export { default as index } from "./config/index"; /** * api @@ -42,58 +44,6 @@ export * from "./api/utils"; export * from "./api/types"; export * from "./api/index"; -/** - * modules/token - */ -export * from "./modules/token/token.service"; - -/** - * modules/protocol - */ -export * from "./modules/protocol/protocol.service"; - -/** - * modules/cache - */ -export * from "./modules/cache/cache.service"; - -/** - * modules/chain - */ -export * from "./modules/chain/chain.service"; - -/** - * modules/zyfi - */ -export * from "./modules/zyfi/zyfi.service"; - -/** - * modules/liquidity - */ -export * from "./modules/liquidity/liquidity.service"; - -/** - * modules/claim - */ -export * from "./modules/claim/claim.service"; - -/** - * modules/reward - */ -export * from "./modules/reward/reward.service"; - -/** - * components/element - */ -export * from "./components/element/Tag"; -export { default as Tag } from "./components/element/Tag"; -export * from "./components/element/Socials"; -export { default as Socials } from "./components/element/Socials"; -export * from "./components/element/SwitchMode"; -export { default as SwitchMode } from "./components/element/SwitchMode"; -export * from "./components/element/AddressEdit"; -export { default as AddressEdit } from "./components/element/AddressEdit"; - /** * I18n */ @@ -116,63 +66,57 @@ export * from "./hooks/resources/useProtocols"; export { default as useProtocols } from "./hooks/resources/useProtocols"; export * from "./hooks/resources/useReward"; export { default as useReward } from "./hooks/resources/useReward"; -export * from "./hooks/resources/useOpportunity"; -export { default as useOpportunity } from "./hooks/resources/useOpportunity"; +export * from "./modules/opportunity/hooks/useOpportunityMetadata"; +export { default as useOpportunity } from "./modules/opportunity/hooks/useOpportunityMetadata"; export * from "./hooks/resources/useRewards"; export { default as useRewards } from "./hooks/resources/useRewards"; export * from "./hooks/resources/useChains"; export { default as useChains } from "./hooks/resources/useChains"; /** - * components/element/opportunity - */ -export * from "./components/element/opportunity/OpportunityTableRow"; -export { default as OpportunityTableRow } from "./components/element/opportunity/OpportunityTableRow"; -export * from "./components/element/opportunity/OpportunityLibrary"; -export { default as OpportunityLibrary } from "./components/element/opportunity/OpportunityLibrary"; -export * from "./components/element/opportunity/OpportunityCell"; -export { default as OpportunityCell } from "./components/element/opportunity/OpportunityCell"; -export * from "./components/element/opportunity/OpportunityParticipateModal"; -export { default as OpportunityParticipateModal } from "./components/element/opportunity/OpportunityParticipateModal"; -export * from "./components/element/opportunity/OpportunityButton"; -export { default as OpportunityButton } from "./components/element/opportunity/OpportunityButton"; -export * from "./components/element/opportunity/Pagination"; -export { default as Pagination } from "./components/element/opportunity/Pagination"; -export * from "./components/element/opportunity/OpportunityShortCard"; -export { default as OpportunityShortCard } from "./components/element/opportunity/OpportunityShortCard"; -export * from "./components/element/opportunity/OpportunityFilters"; -export { default as OpportunityFilters } from "./components/element/opportunity/OpportunityFilters"; -export * from "./components/element/opportunity/OpportunityTable"; -export * from "./components/element/opportunity/OpportunityFeatured"; -export { default as OpportunityFeatured } from "./components/element/opportunity/OpportunityFeatured"; + * components/element + */ +export * from "./components/element/Tag"; +export { default as Tag } from "./components/element/Tag"; +export * from "./components/element/Pagination"; +export { default as Pagination } from "./components/element/Pagination"; +export * from "./components/element/Socials"; +export { default as Socials } from "./components/element/Socials"; +export * from "./components/element/SwitchMode"; +export { default as SwitchMode } from "./components/element/SwitchMode"; +export * from "./components/element/AddressEdit"; +export { default as AddressEdit } from "./components/element/AddressEdit"; + +/** + * components/composite + */ +export * from "./components/composite/Hero"; +export { default as Hero } from "./components/composite/Hero"; +export * from "./components/composite/LiFiWidget.client"; /** - * components/element/historicalClaimsLibrary + * components/layout */ -export * from "./components/element/historicalClaimsLibrary/HistoricalClaimsRow"; -export { default as HistoricalClaimsRow } from "./components/element/historicalClaimsLibrary/HistoricalClaimsRow"; -export * from "./components/element/historicalClaimsLibrary/HistoricalClaimsTable"; -export * from "./components/element/historicalClaimsLibrary/HistoricalClaimsLibrary"; -export { default as HistoricalClaimsLibrary } from "./components/element/historicalClaimsLibrary/HistoricalClaimsLibrary"; +export * from "./components/layout/LayerMenu"; +export * from "./components/layout/LoadingIndicator"; +export { default as LoadingIndicator } from "./components/layout/LoadingIndicator"; +export * from "./components/layout/Header"; +export { default as Header } from "./components/layout/Header"; +export * from "./components/layout/Footer"; +export { default as Footer } from "./components/layout/Footer"; +export * from "./components/layout/ErrorContent"; +export * from "./components/layout/ErrorHeading"; /** - * components/element/functions + * modules/campaigns */ -export * from "./components/element/functions/SearchBar"; -export { default as SearchBar } from "./components/element/functions/SearchBar"; +export * from "./modules/campaigns/campaign.model"; +export * from "./modules/campaigns/campaign.service"; /** - * components/element/tvl + * modules/opportunity */ -export * from "./components/element/tvl/TvlRowAllocation"; -export { default as TvlRowAllocation } from "./components/element/tvl/TvlRowAllocation"; -export * from "./components/element/tvl/TvlLibrary"; -export { default as TvlLibrary } from "./components/element/tvl/TvlLibrary"; -export * from "./components/element/tvl/TvlTableRow"; -export { default as TvlTableRow } from "./components/element/tvl/TvlTableRow"; -export * from "./components/element/tvl/TvlSection"; -export { default as TvlSection } from "./components/element/tvl/TvlSection"; -export * from "./components/element/tvl/TvlTable"; +export * from "./modules/opportunity/opportunity.service"; /** * components/element/token @@ -246,12 +190,6 @@ export { default as ChainTableRow } from "./components/element/chain/ChainTableR export * from "./components/element/chain/ChainLibrary"; export { default as ChainLibrary } from "./components/element/chain/ChainLibrary"; -/** - * modules/campaigns - */ -export * from "./modules/campaigns/campaign.model"; -export * from "./modules/campaigns/campaign.service"; - /** * components/element/apr */ @@ -274,13 +212,6 @@ export * from "./components/element/rewards/ClaimRewardsTokenTableRow"; export { default as ClaimRewardsTokenTableRow } from "./components/element/rewards/ClaimRewardsTokenTableRow"; export * from "./components/element/rewards/ClaimRewardsChainTable"; -/** - * components/composite - */ -export * from "./components/composite/Hero"; -export { default as Hero } from "./components/composite/Hero"; -export * from "./components/composite/LiFiWidget.client"; - /** * components/element/position */ @@ -298,32 +229,18 @@ export * from "./components/element/participate/Participate"; export { default as Participate } from "./components/element/participate/Participate"; /** - * components/layout + * components/element/transaction */ -export * from "./components/layout/LayerMenu"; -export * from "./components/layout/LoadingIndicator"; -export { default as LoadingIndicator } from "./components/layout/LoadingIndicator"; -export * from "./components/layout/Header"; -export { default as Header } from "./components/layout/Header"; -export * from "./components/layout/Footer"; -export { default as Footer } from "./components/layout/Footer"; -export * from "./components/layout/ErrorContent"; -export * from "./components/layout/ErrorHeading"; +export * from "./components/element/transaction/TransactionOverview"; +export { default as TransactionOverview } from "./components/element/transaction/TransactionOverview"; /** - * components/element/campaign/tableCollumns - */ -export * from "./components/element/campaign/tableCollumns/RestrictionsCollumn"; -export { default as RestrictionsCollumn } from "./components/element/campaign/tableCollumns/RestrictionsCollumn"; - -/** - * components/element/rewards/byOpportunity + * modules/opportunity/components */ -export * from "./components/element/rewards/byOpportunity/ClaimRewardsTokenTableRowByOpportunity"; -export { default as ClaimRewardsTokenTableRowByOpportunity } from "./components/element/rewards/byOpportunity/ClaimRewardsTokenTableRowByOpportunity"; -export * from "./components/element/rewards/byOpportunity/ClaimRewardsTableByOpportunity"; -export * from "./components/element/rewards/byOpportunity/ClaimRewardsByOpportunity"; -export { default as ClaimRewardsByOpportunity } from "./components/element/rewards/byOpportunity/ClaimRewardsByOpportunity"; +export * from "./modules/opportunity/components/OpportunityButton"; +export { default as OpportunityButton } from "./modules/opportunity/components/OpportunityButton"; +export * from "./modules/opportunity/components/OpportunityFilters"; +export { default as OpportunityFilters } from "./modules/opportunity/components/OpportunityFilters"; /** * components/element/campaign/rules @@ -339,6 +256,21 @@ export { default as LiquidityRule } from "./components/element/campaign/rules/Li export * from "./components/element/campaign/rules/BooleanRule"; export { default as BooleanRule } from "./components/element/campaign/rules/BooleanRule"; +/** + * components/element/campaign/tableCollumns + */ +export * from "./components/element/campaign/tableCollumns/RestrictionsCollumn"; +export { default as RestrictionsCollumn } from "./components/element/campaign/tableCollumns/RestrictionsCollumn"; + +/** + * components/element/rewards/byOpportunity + */ +export * from "./components/element/rewards/byOpportunity/ClaimRewardsTokenTableRowByOpportunity"; +export { default as ClaimRewardsTokenTableRowByOpportunity } from "./components/element/rewards/byOpportunity/ClaimRewardsTokenTableRowByOpportunity"; +export * from "./components/element/rewards/byOpportunity/ClaimRewardsTableByOpportunity"; +export * from "./components/element/rewards/byOpportunity/ClaimRewardsByOpportunity"; +export { default as ClaimRewardsByOpportunity } from "./components/element/rewards/byOpportunity/ClaimRewardsByOpportunity"; + /** * components/element/position/subPosition */ @@ -347,12 +279,110 @@ export { default as SubPositionTableRow } from "./components/element/position/su export * from "./components/element/position/subPosition/SubPositionTable"; /** - * modules/opportunity + * modules/opportunity/components/element */ -export * from "./modules/opportunity/opportunity.model"; -export * from "./modules/opportunity/opportunity.service"; +export * from "./modules/opportunity/components/element/OpportunityParticipateModal"; +export { default as OpportunityParticipateModal } from "./modules/opportunity/components/element/OpportunityParticipateModal"; + +/** + * modules/opportunity/components/library + */ +export * from "./modules/opportunity/components/library/OpportunityLibrary"; +export { default as OpportunityLibrary } from "./modules/opportunity/components/library/OpportunityLibrary"; +export * from "./modules/opportunity/components/library/OpportunityTable"; +export * from "./modules/opportunity/components/library/OpportunityFeatured"; +export { default as OpportunityFeatured } from "./modules/opportunity/components/library/OpportunityFeatured"; + +/** + * modules/opportunity/components/items + */ +export * from "./modules/opportunity/components/items/OpportunityTableRow"; +export { default as OpportunityTableRow } from "./modules/opportunity/components/items/OpportunityTableRow"; +export * from "./modules/opportunity/components/items/OpportunityCell"; +export { default as OpportunityCell } from "./modules/opportunity/components/items/OpportunityCell"; +export * from "./modules/opportunity/components/items/OpportunityShortCard"; +export { default as OpportunityShortCard } from "./modules/opportunity/components/items/OpportunityShortCard"; + +/** + * modules/chain + */ +export * from "./modules/chain/chain.service"; /** * modules/interaction */ export * from "./modules/interaction/interaction.service"; + +/** + * modules/token + */ +export * from "./modules/token/token.service"; + +/** + * modules/protocol + */ +export * from "./modules/protocol/protocol.service"; + +/** + * modules/user + */ +export * from "./modules/user/user.service"; + +/** + * modules/cache + */ +export * from "./modules/cache/cache.service"; + +/** + * modules/liquidity + */ +export * from "./modules/liquidity/liquidity.service"; + +/** + * modules/zyfi + */ +export * from "./modules/zyfi/zyfi.service"; + +/** + * components/element/functions + */ +export * from "./components/element/functions/SearchBar"; +export { default as SearchBar } from "./components/element/functions/SearchBar"; + +/** + * components/element/historicalClaimsLibrary + */ +export * from "./components/element/historicalClaimsLibrary/HistoricalClaimsRow"; +export { default as HistoricalClaimsRow } from "./components/element/historicalClaimsLibrary/HistoricalClaimsRow"; +export * from "./components/element/historicalClaimsLibrary/HistoricalClaimsTable"; +export * from "./components/element/historicalClaimsLibrary/HistoricalClaimsLibrary"; +export { default as HistoricalClaimsLibrary } from "./components/element/historicalClaimsLibrary/HistoricalClaimsLibrary"; + +/** + * modules/claim + */ +export * from "./modules/claim/claim.service"; + +/** + * components/element/tvl + */ +export * from "./components/element/tvl/TvlRowAllocation"; +export { default as TvlRowAllocation } from "./components/element/tvl/TvlRowAllocation"; +export * from "./components/element/tvl/TvlLibrary"; +export { default as TvlLibrary } from "./components/element/tvl/TvlLibrary"; +export * from "./components/element/tvl/TvlTableRow"; +export { default as TvlTableRow } from "./components/element/tvl/TvlTableRow"; +export * from "./components/element/tvl/TvlSection"; +export { default as TvlSection } from "./components/element/tvl/TvlSection"; +export * from "./components/element/tvl/TvlTable"; + +/** + * components/element/reinvest + */ +export * from "./components/element/reinvest/ReinvestBanner"; +export { default as ReinvestBanner } from "./components/element/reinvest/ReinvestBanner"; + +/** + * modules/reward + */ +export * from "./modules/reward/reward.service"; diff --git a/src/modules/chain/routes/chain.$id.opportunities.tsx b/src/modules/chain/routes/chain.$id.opportunities.tsx index bf2c5b7..84f50ae 100644 --- a/src/modules/chain/routes/chain.$id.opportunities.tsx +++ b/src/modules/chain/routes/chain.$id.opportunities.tsx @@ -1,7 +1,7 @@ +import OpportunityLibrary from "@core/modules/opportunity/components/library/OpportunityLibrary"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { Container, Group, Space, Title } from "dappkit"; -import OpportunityLibrary from "../../../components/element/opportunity/OpportunityLibrary"; import merklConfig from "../../../config"; import { Cache } from "../../../modules/cache/cache.service"; import { ChainService } from "../../../modules/chain/chain.service"; diff --git a/src/components/element/opportunity/OpportunityButton.tsx b/src/modules/opportunity/components/OpportunityButton.tsx similarity index 58% rename from src/components/element/opportunity/OpportunityButton.tsx rename to src/modules/opportunity/components/OpportunityButton.tsx index 37d3fbb..0a7f0df 100644 --- a/src/components/element/opportunity/OpportunityButton.tsx +++ b/src/modules/opportunity/components/OpportunityButton.tsx @@ -1,19 +1,19 @@ +import useOpportunityData from "@core/modules/opportunity/hooks/useOpportunityMetadata"; import type { Opportunity } from "@merkl/api"; -import { Button, Icon, Icons } from "dappkit"; +import { Button, Icon } from "dappkit"; import { blockEvent } from "dappkit"; -import useOpportunity from "../../../hooks/resources/useOpportunity"; export type OpportuntiyButtonProps = { opportunity: Opportunity; }; export default function OpportunityButton({ opportunity }: OpportuntiyButtonProps) { - const { icons, link } = useOpportunity(opportunity); + const { name, Icons, link } = useOpportunityData(opportunity); return ( ); diff --git a/src/components/element/opportunity/OpportunityFilters.tsx b/src/modules/opportunity/components/OpportunityFilters.tsx similarity index 96% rename from src/components/element/opportunity/OpportunityFilters.tsx rename to src/modules/opportunity/components/OpportunityFilters.tsx index e0e989f..bcaff65 100644 --- a/src/components/element/opportunity/OpportunityFilters.tsx +++ b/src/modules/opportunity/components/OpportunityFilters.tsx @@ -1,13 +1,13 @@ +import merklConfig from "@core/config"; +import { actions } from "@core/config/actions"; +import type { OpportunityView } from "@core/config/opportunity"; +import useSearchParamState from "@core/hooks/filtering/useSearchParamState"; +import useChains from "@core/hooks/resources/useChains"; +import useProtocols from "@core/hooks/resources/useProtocols"; import type { Chain, Protocol } from "@merkl/api"; import { Form, useLocation, useNavigate, useNavigation, useSearchParams } from "@remix-run/react"; import { Button, Group, Icon, Input, Select } from "dappkit"; import { useCallback, useEffect, useMemo, useState } from "react"; -import merklConfig from "../../../config"; -import { actions } from "../../../config/actions"; -import type { OpportunityView } from "../../../config/opportunity"; -import useSearchParamState from "../../../hooks/filtering/useSearchParamState"; -import useChains from "../../../hooks/resources/useChains"; -import useProtocols from "../../../hooks/resources/useProtocols"; const filters = ["search", "action", "status", "chain", "protocol", "tvl", "sort"] as const; export type OpportunityFilter = (typeof filters)[number]; diff --git a/src/components/element/opportunity/OpportunityParticipateModal.tsx b/src/modules/opportunity/components/element/OpportunityParticipateModal.tsx similarity index 92% rename from src/components/element/opportunity/OpportunityParticipateModal.tsx rename to src/modules/opportunity/components/element/OpportunityParticipateModal.tsx index 487ba1f..81a94a3 100644 --- a/src/components/element/opportunity/OpportunityParticipateModal.tsx +++ b/src/modules/opportunity/components/element/OpportunityParticipateModal.tsx @@ -3,8 +3,9 @@ import { Button, Divider, Group, Image, Modal, Text, Title } from "dappkit"; import type { PropsWithChildren } from "react"; import React from "react"; -import merklConfig from "../../../config"; -import Participate from "../participate/Participate"; +import Participate from "@core/components/element/participate/Participate"; +import merklConfig from "@core/config"; + export type OpportunityParticipateModalProps = { opportunity: Opportunity; } & PropsWithChildren; diff --git a/src/components/element/opportunity/OpportunityCell.tsx b/src/modules/opportunity/components/items/OpportunityCell.tsx similarity index 53% rename from src/components/element/opportunity/OpportunityCell.tsx rename to src/modules/opportunity/components/items/OpportunityCell.tsx index efe5864..447a595 100644 --- a/src/components/element/opportunity/OpportunityCell.tsx +++ b/src/modules/opportunity/components/items/OpportunityCell.tsx @@ -1,29 +1,26 @@ +import type { TagTypes } from "@core/components/element/Tag"; +import AprModal from "@core/components/element/apr/AprModal"; +import type { OpportunityNavigationMode } from "@core/config/opportunity"; +import OpportunityParticipateModal from "@core/modules/opportunity/components/element/OpportunityParticipateModal"; +import useOpportunityData from "@core/modules/opportunity/hooks/useOpportunityMetadata"; +import useOpportunityRewards from "@core/modules/opportunity/hooks/useOpportunityRewards"; +import type { Opportunity } from "@merkl/api"; import { Link } from "@remix-run/react"; import type { BoxProps } from "dappkit"; import { Box, Button, Divider, - Dropdown, - Fmt, - Group, + Dropdown, Group, Icon, - Icons, PrimitiveTag, Text, Title, Value, mergeClass, - useOverflowingRef, + useOverflowingRef } from "dappkit"; import { useMemo } from "react"; -import merklConfig from "../../../config"; -import type { OpportunityNavigationMode } from "../../../config/opportunity"; -import useOpportunity from "../../../hooks/resources/useOpportunity"; -import type { Opportunity } from "../../../modules/opportunity/opportunity.model"; -import Tag, { type TagTypes } from "../Tag"; -import AprModal from "../apr/AprModal"; -import OpportunityParticipateModal from "./OpportunityParticipateModal"; export type OpportunityCellProps = { hideTags?: (keyof TagTypes)[]; @@ -33,53 +30,20 @@ export type OpportunityCellProps = { } & BoxProps; export default function OpportunityCell({ - opportunity: opportunityRaw, + opportunity, hideTags, navigationMode, }: OpportunityCellProps) { - const { tags, link, icons, opportunity } = useOpportunity(opportunityRaw); - + const { name, link, Tags, Icons } = useOpportunityData(opportunity); + const { formattedDailyRewards } = useOpportunityRewards(opportunity); const { ref, overflowing } = useOverflowingRef(); - const renderDailyRewards = useMemo(() => { - if (merklConfig.opportunity.library.dailyRewardsTokenAddress) { - const breakdowns = opportunity.rewardsRecord.breakdowns.filter(breakdown => { - return breakdown?.token.address === merklConfig.opportunity.library.dailyRewardsTokenAddress; - }); - const token = breakdowns?.[0]?.token; - const breakdownAmount = breakdowns.reduce((acc, breakdown) => { - return BigInt(acc) + BigInt(breakdown.amount); - }, 0n); - return ( - <> - - <Value value format={"0,0.##a"}> - {Fmt.toNumber(breakdownAmount.toString() ?? "0", token?.decimals).toString()} - </Value> - - {` ${token?.symbol ?? ""}`} - - - - - - ); - } - return ( - - <Value value format={merklConfig.decimalFormat.dollar}> - {opportunity.dailyRewards ?? 0} - </Value> - - ); - }, [opportunity]); - const cell = useMemo( () => ( - {renderDailyRewards} + {formattedDailyRewards} Total daily rewards @@ -99,7 +63,7 @@ export default function OpportunityCell({ - {icons} + - {opportunity.name} + {name} - {tags - ?.filter(a => a !== undefined) - ?.filter(({ type }) => !hideTags || !hideTags.includes(type)) - .map(tag => ( - - ))} + + )} + + )} + + ); +} diff --git a/src/components/element/opportunity/OpportunityTableRow.tsx b/src/modules/opportunity/components/items/OpportunityTableRow.tsx similarity index 62% rename from src/components/element/opportunity/OpportunityTableRow.tsx rename to src/modules/opportunity/components/items/OpportunityTableRow.tsx index 579d311..c739475 100644 --- a/src/components/element/opportunity/OpportunityTableRow.tsx +++ b/src/modules/opportunity/components/items/OpportunityTableRow.tsx @@ -1,18 +1,19 @@ +import type { TagTypes } from "@core/components/element/Tag"; +import AprModal from "@core/components/element/apr/AprModal"; +import TokenAmountModal from "@core/components/element/token/TokenAmountModal"; +import merklConfig from "@core/config"; +import type { OpportunityNavigationMode } from "@core/config/opportunity"; +import OpportunityParticipateModal from "@core/modules/opportunity/components/element/OpportunityParticipateModal"; +import { OpportunityRow } from "@core/modules/opportunity/components/library/OpportunityTable"; +import useOpportunityData from "@core/modules/opportunity/hooks/useOpportunityMetadata"; +import useOpportunityRewards from "@core/modules/opportunity/hooks/useOpportunityRewards"; +import type { Opportunity } from "@merkl/api"; import { Link } from "@remix-run/react"; import type { BoxProps } from "dappkit"; -import { Dropdown, Fmt, Group, Icon, Icons, PrimitiveTag, Text, Title, Value, mergeClass } from "dappkit"; +import { Dropdown, Group, Icon, Icons as IconGroup, PrimitiveTag, Text, Title, Value, mergeClass } from "dappkit"; import { EventBlocker } from "dappkit"; import { useOverflowingRef } from "dappkit"; import { useMemo } from "react"; -import merklConfig from "../../../config"; -import type { OpportunityNavigationMode } from "../../../config/opportunity"; -import useOpportunity from "../../../hooks/resources/useOpportunity"; -import type { Opportunity } from "../../../modules/opportunity/opportunity.model"; -import Tag, { type TagTypes } from "../Tag"; -import AprModal from "../apr/AprModal"; -import TokenAmountModal from "../token/TokenAmountModal"; -import OpportunityParticipateModal from "./OpportunityParticipateModal"; -import { OpportunityRow } from "./OpportunityTable"; export type OpportunityTableRowProps = { hideTags?: (keyof TagTypes)[]; @@ -22,12 +23,13 @@ export type OpportunityTableRowProps = { export default function OpportunityTableRow({ hideTags, - opportunity: opportunityRaw, + opportunity, className, navigationMode, ...props }: OpportunityTableRowProps) { - const { tags, link, icons, rewardsBreakdown, opportunity } = useOpportunity(opportunityRaw); + const { name, tags, link, icons, Tags, Icons } = useOpportunityData(opportunity); + const { rewardsBreakdown, formattedDailyRewards } = useOpportunityRewards(opportunity); const { ref, overflowing } = useOverflowingRef(); @@ -83,11 +85,11 @@ export default function OpportunityTableRow({ {opportunity.dailyRewards ?? 0} - + {rewardsBreakdown.map(({ token: { icon } }) => ( ))} - + @@ -95,39 +97,6 @@ export default function OpportunityTableRow({ [opportunity, rewardsBreakdown], ); - const renderDailyRewards = useMemo(() => { - if (merklConfig.opportunity.library.dailyRewardsTokenAddress) { - const breakdowns = opportunity.rewardsRecord.breakdowns.filter(breakdown => { - return breakdown?.token.address === merklConfig.opportunity.library.dailyRewardsTokenAddress; - }); - const token = breakdowns?.[0]?.token; - const breakdownAmount = breakdowns.reduce((acc, breakdown) => { - return BigInt(acc) + BigInt(breakdown.amount); - }, 0n); - return ( - <> - - <Value value format={"0,0.##a"}> - {Fmt.toNumber(breakdownAmount.toString() ?? "0", token?.decimals).toString()} - </Value> - - {` ${token?.symbol ?? ""}`} - - - - - - ); - } - return ( - - <Value value format={merklConfig.decimalFormat.dollar}> - {opportunity.dailyRewards ?? 0} - </Value> - - ); - }, [opportunity]); - // biome-ignore lint/correctness/useExhaustiveDependencies: cannot include props const row = useMemo(() => { switch (merklConfig.opportunityLibrary.rowView) { @@ -145,7 +114,7 @@ export default function OpportunityTableRow({ - {icons} + - {merklConfig.opportunityPercentage - ? opportunity.name - : opportunity.name.replace(/\s*\d+(\.\d+)?%$/, "").trim()} + {name} - {tags - ?.filter(a => a !== undefined) - ?.filter(({ type }) => !hideTags || !hideTags.includes(type)) - .map(tag => ( - - ))} + } @@ -187,7 +149,7 @@ export default function OpportunityTableRow({ opportunityColumn={ - {renderDailyRewards} + {formattedDailyRewards} @@ -198,20 +160,13 @@ export default function OpportunityTableRow({ className={mergeClass( overflowing && "hover:overflow-visible hover:animate-textScroll hover:text-clip", )}> - {merklConfig.opportunityPercentage - ? opportunity.name - : opportunity.name.replace(/\s*\d+(\.\d+)?%$/, "").trim()} + {name} - {tags - ?.filter(a => a !== undefined) - ?.filter(({ type }) => !hideTags || !hideTags.includes(type)) - .map(tag => ( - - ))} + } diff --git a/src/components/element/opportunity/OpportunityFeatured.tsx b/src/modules/opportunity/components/library/OpportunityFeatured.tsx similarity index 80% rename from src/components/element/opportunity/OpportunityFeatured.tsx rename to src/modules/opportunity/components/library/OpportunityFeatured.tsx index 7c1dd7a..2c3a68c 100644 --- a/src/components/element/opportunity/OpportunityFeatured.tsx +++ b/src/modules/opportunity/components/library/OpportunityFeatured.tsx @@ -1,7 +1,7 @@ +import type { Opportunity } from "@merkl/api" import { useMemo } from "react"; -import merklConfig from "../../../config"; -import type { Opportunity } from "../../../modules/opportunity/opportunity.model"; -import OpportunityCell from "./OpportunityCell"; +import merklConfig from "../../../../config"; +import OpportunityCell from "../items/OpportunityCell"; type OpportunityFeaturedProps = { opportunities: Opportunity[]; diff --git a/src/components/element/opportunity/OpportunityLibrary.tsx b/src/modules/opportunity/components/library/OpportunityLibrary.tsx similarity index 89% rename from src/components/element/opportunity/OpportunityLibrary.tsx rename to src/modules/opportunity/components/library/OpportunityLibrary.tsx index 623ac70..9c75322 100644 --- a/src/components/element/opportunity/OpportunityLibrary.tsx +++ b/src/modules/opportunity/components/library/OpportunityLibrary.tsx @@ -1,15 +1,15 @@ import type { Chain } from "@merkl/api"; +import type { Opportunity } from "@merkl/api"; import { Box, Group, type Order, Title } from "dappkit"; import { useCallback, useMemo, useState } from "react"; -import merklConfig from "../../../config"; -import type { OpportunityView } from "../../../config/opportunity"; -import useSearchParamState from "../../../hooks/filtering/useSearchParamState"; -import type { Opportunity } from "../../../modules/opportunity/opportunity.model"; -import OpportunityCell from "./OpportunityCell"; -import OpportunityFilters, { type OpportunityFilterProps } from "./OpportunityFilters"; +import Pagination from "../../../../components/element/Pagination"; +import merklConfig from "../../../../config"; +import type { OpportunityView } from "../../../../config/opportunity"; +import useSearchParamState from "../../../../hooks/filtering/useSearchParamState"; +import OpportunityFilters, { type OpportunityFilterProps } from "../OpportunityFilters"; +import OpportunityCell from "../items/OpportunityCell"; +import OpportunityTableRow from "../items/OpportunityTableRow"; import { OpportunityTable, type opportunityColumns } from "./OpportunityTable"; -import OpportunityTableRow from "./OpportunityTableRow"; -import Pagination from "./Pagination"; export type Displays = "grid" | "list"; diff --git a/src/components/element/opportunity/OpportunityTable.tsx b/src/modules/opportunity/components/library/OpportunityTable.tsx similarity index 95% rename from src/components/element/opportunity/OpportunityTable.tsx rename to src/modules/opportunity/components/library/OpportunityTable.tsx index b86b9ab..0e45bd7 100644 --- a/src/components/element/opportunity/OpportunityTable.tsx +++ b/src/modules/opportunity/components/library/OpportunityTable.tsx @@ -1,5 +1,5 @@ import { Title, createTable } from "dappkit"; -import merklConfig from "../../../config"; +import merklConfig from "../../../../config"; // biome-ignore lint/suspicious/noExplicitAny: TODO export function filterColumns>(columns: T): T { @@ -16,7 +16,7 @@ export function filterColumns>(columns: T): T { return filteredColumns; } -const opportunityColumns = { +export const opportunityColumns = { opportunity: { name: ( diff --git a/src/modules/opportunity/hooks/useOpportunityMetadata.tsx b/src/modules/opportunity/hooks/useOpportunityMetadata.tsx new file mode 100644 index 0000000..7e52852 --- /dev/null +++ b/src/modules/opportunity/hooks/useOpportunityMetadata.tsx @@ -0,0 +1,205 @@ +import type { TagProps, TagType, TagTypes } from "@core/components/element/Tag"; +import Tag from "@core/components/element/Tag"; +import merklConfig from "@core/config"; +import type { Opportunity } from "@merkl/api"; +import { type Component, Icon, Icons as IconGroup, type IconProps, type IconsProps } from "dappkit"; +import { useCallback, useMemo } from "react"; +import { v4 as uuidv4 } from "uuid"; + +const metadata = [ + "name", + "identifier", + "action", + "status", + "type", + "protocol", + "depositUrl", + "chain", + "tokens", + "rewardsRecord", +] satisfies (keyof Opportunity)[]; + +/** + * Formats basic metadata for a given opportunity + */ +export default function useOpportunityMetadata({ + name, + identifier, + type, + action, + status, + chain, + tokens, + protocol, + depositUrl, + rewardsRecord, + ...opportunity +}: Pick<Opportunity, (typeof metadata)[number]>) { + /** + * Formatted name + */ + const configuredName = useMemo(() => { + if (!merklConfig.opportunityPercentage) return name.replace(/\s*\d+(\.\d+)?%$/, "").trim(); + return name; + }, [name]); + + /** + * Formatted name split into multiple spans to be used in page header titles + */ + const title = useMemo(() => { + const spaced = configuredName.split(" "); + + return spaced + .map(str => { + const key = str + uuidv4(); + if (!str.match(/[\p{Letter}\p{Mark}]+/gu)) + return [ + <span key={key} className="text-main-11"> + {str} + </span>, + ]; + if (str.includes("-")) + return str + .split("-") + .flatMap((s, i, arr) => [s, i !== arr.length - 1 && <span className="text-main-11">-</span>]); + if (str.includes("/")) + return str + .split("/") + .flatMap((s, i, arr) => [s, i !== arr.length - 1 && <span className="text-main-11">/</span>]); + return [<span key={key}>{str}</span>]; + }) + .flatMap((str, index, arr) => [str, index !== arr.length - 1 && " "]); + }, [configuredName]); + + /** + * TagProps for each metadata that can be represented as a tag + */ + const tags = useMemo(() => { + const tag = <T extends keyof TagTypes>(tagType: T, value: TagType<T>["value"]) => + !!value + ? { + type: tagType, + value, + key: `${tagType}_${ + // biome-ignore lint/suspicious/noExplicitAny: <explanation> + (value as any)?.address ?? (value as any)?.name ?? value + }`, + } + : undefined; + + return [ + tag("protocol", protocol), + tag("chain", chain), + tag("action", action), + ...tokens.map(token => tag("token", token)), + tag("status", status), + ].filter(a => a !== undefined); + }, [protocol, action, status, tokens, chain]); + + /** + * Extensible tags components that can be filtered + * @param hide which tags to filers out + * @param props tag item props + */ + const Tags = useCallback( + function TagsComponent({ + hide, + only, + ...props + }: { hide?: (keyof TagTypes)[]; only?: (keyof TagTypes)[] } & Omit< + Component<TagProps<keyof TagTypes>>, + "value" | "type" + >) { + return tags + ?.filter(a => a !== undefined) + ?.filter(({ type }) => !hide || !hide.includes(type)) + ?.filter(({ type }) => !only || only.includes(type)) + .map(tag => <Tag {...tag} key={tag.key ?? uuidv4()} size="sm" {...props} />); + }, + [tags], + ); + + /** + * Internal link to the opportunity on this app + */ + const link = useMemo( + () => `/opportunities/${chain?.name?.toLowerCase?.().replace(" ", "-")}/${type}/${identifier}`, + [type, identifier, chain], + ); + + /** + * External link to the opportunity + */ + const url = useMemo(() => { + if (!!depositUrl) return depositUrl; + if (!!protocol?.url) return protocol?.url; + }, [depositUrl, protocol]); + + /** + * Tokens that are used to define an opportunity's main icons + */ + const iconTokens = useMemo(() => { + if (tokens.length > 1) return tokens.filter(token => !!token.icon); + return tokens; + }, [tokens]); + + /** + * Main icons that define an opportunity + */ + const icons = useMemo(() => iconTokens.map(t => ({ src: t.icon }) satisfies IconProps), [iconTokens]); + + /** + * Extensible icons group component + */ + const Icons = useCallback( + function IconsComponent({ groupProps, ...props }: { groupProps?: Omit<IconsProps, "children"> } & IconProps) { + return ( + <IconGroup {...groupProps}> + {icons.map(icon => ( + <Icon key={uuidv4()} {...icon} {...props} /> + ))} + </IconGroup> + ); + }, + [icons], + ); + + /** + * Explainer for the opportunity + */ + const description = useMemo(() => { + const symbols = tokens?.map(t => t.symbol).join("-"); + + switch (action) { + case "POOL": + return `Earn rewards by providing liquidity to the ${protocol?.name} ${symbols} pool on ${chain.name}, or through a liquidity manager supported by Merkl`; + case "HOLD": + return `Earn rewards by holding ${symbols} or by staking it in a supported contract`; + case "LEND": + return `Earn rewards by supplying liquidity to the ${protocol?.name} ${symbols} on ${chain.name}`; + case "BORROW": + return `Earn rewards by borrowing liquidity to the ${protocol?.name} ${symbols} on ${chain.name}`; + case "DROP": + return `Visit your dashboard to check if you've earned rewards from this airdrop`; + default: + break; + } + }, [tokens, protocol, chain, action]); + + return { + name: configuredName, + title, + link, + url, + icons, + Icons, + iconTokens, + description, + opportunity: { + ...opportunity, + name: configuredName, + }, + tags, + Tags, + }; +} diff --git a/src/modules/opportunity/hooks/useOpportunityMetrics.tsx b/src/modules/opportunity/hooks/useOpportunityMetrics.tsx new file mode 100644 index 0000000..399e370 --- /dev/null +++ b/src/modules/opportunity/hooks/useOpportunityMetrics.tsx @@ -0,0 +1,34 @@ +import merklConfig from "@core/config"; +import type { Opportunity } from "@merkl/api"; +import { Value } from "dappkit"; +import type { ValueProps } from "dappkit"; +import { useMemo } from "react"; +import { v4 as uuidv4 } from "uuid"; + +const metrics = ["dailyRewards", "apr", "tvl"] satisfies (keyof Opportunity)[]; + +/** + * Formats metrics for a given opportunity + */ +export default function useOpportunityMetrics({ dailyRewards, apr, tvl }: Pick<Opportunity, (typeof metrics)[number]>) { + /** + * Main metrics formatted for page headers + */ + const headerMetrics = useMemo(() => { + const metricsDefinition = { + "Daily Rewards": [dailyRewards, { format: merklConfig.decimalFormat.dollar }], + APR: [apr / 100, { format: "0.00%" }], + "Total value locked": [tvl / 100, { format: "0.00%" }], + } satisfies { [label: string]: [number | string, ValueProps] }; + + return Object.entries(metricsDefinition satisfies { [label: string]: [number | string, ValueProps] }) + .filter(([, [value]]) => !!value) + .map(([label, [value, props]]) => ({ + label, + data: <Value children={value} size={4} className="!text-main-12" {...props} />, + key: uuidv4(), + })); + }, [dailyRewards, apr, tvl]); + + return { headerMetrics }; +} diff --git a/src/modules/opportunity/hooks/useOpportunityRewards.tsx b/src/modules/opportunity/hooks/useOpportunityRewards.tsx new file mode 100644 index 0000000..12350ca --- /dev/null +++ b/src/modules/opportunity/hooks/useOpportunityRewards.tsx @@ -0,0 +1,105 @@ +import merklConfig from "@core/config"; +import type { Token } from "@merkl/api"; +import type { Opportunity } from "@merkl/api"; +import { Fmt, Icon, Icons, Text, Title, Value } from "dappkit"; +import { useMemo } from "react"; + +const rewards = [ + "name", + "identifier", + "action", + "status", + "type", + "protocol", + "dailyRewards", + "depositUrl", + "chain", + "tokens", + "rewardsRecord", +] satisfies (keyof Opportunity)[]; + +/** + * Formats rewards for a given opportunity + */ +export default function useOpportunityRewards({ + dailyRewards, + rewardsRecord, +}: Pick<Opportunity, (typeof rewards)[number]>) { + /** + * Icons for each rewarded tokens of the opportunity + */ + const rewardIcons = useMemo( + () => + rewardsRecord?.breakdowns?.map(({ token: { icon, address } }) => { + return <Icon key={address} rounded src={icon} />; + }) ?? [], + [rewardsRecord], + ); + + /** + * Picks tokens and amounts from the rewards breakdown + */ + const rewardsBreakdown = useMemo(() => { + if (!rewardsRecord?.breakdowns) return []; + + const tokenAddresses = rewardsRecord.breakdowns.reduce((addresses, breakdown) => { + return addresses.add(breakdown.token.address); + }, new Set<string>()); + + return Array.from(tokenAddresses).map(address => { + const breakdowns = rewardsRecord.breakdowns.filter(({ token: t }) => t.address === address); + const amount = breakdowns?.reduce((sum, breakdown) => sum + BigInt(breakdown.amount), 0n); + + return { token: breakdowns?.[0]?.token, amount } satisfies { + token: Token; + amount: bigint; + }; + }); + }, [rewardsRecord]); + + /** + * Formatted daily rewards displayed + */ + const formattedDailyRewards = useMemo(() => { + if (merklConfig.opportunity.library.dailyRewardsTokenAddress) { + const breakdowns = rewardsRecord.breakdowns.filter( + ({ token }) => token?.address === merklConfig.opportunity.library.dailyRewardsTokenAddress, + ); + const token = breakdowns?.[0]?.token; + const breakdownAmount = breakdowns.reduce((acc, breakdown) => acc + breakdown.amount, 0n); + + return ( + <> + <Title h={3} size={3} look="soft"> + <Value value format={"0,0.##a"}> + {Fmt.toNumber(breakdownAmount.toString() ?? "0", token?.decimals).toString()} + </Value> + + {token?.symbol && ` ${token?.symbol}`} + + + + + + ); + } + return ( + <> + + <Value value format={merklConfig.decimalFormat.dollar}> + {dailyRewards ?? 0} + </Value> + + + <Icons>{rewardIcons}</Icons> + + + ); + }, [rewardsRecord, dailyRewards, rewardIcons]); + + return { + rewardIcons, + rewardsBreakdown, + formattedDailyRewards + }; +} diff --git a/src/modules/opportunity/opportunity.model.ts b/src/modules/opportunity/opportunity.model.ts deleted file mode 100644 index e2c09a2..0000000 --- a/src/modules/opportunity/opportunity.model.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Opportunity as OpportunityFromApi } from "@merkl/api"; -import type { Fetched } from "../../api/types"; -import type { Campaign } from "../campaigns/campaign.model"; - -export type Opportunity = Fetched; -export type OpportunityWithCampaigns = Fetched; diff --git a/src/modules/opportunity/opportunity.service.ts b/src/modules/opportunity/opportunity.service.ts index 83f4758..83527d2 100644 --- a/src/modules/opportunity/opportunity.service.ts +++ b/src/modules/opportunity/opportunity.service.ts @@ -1,8 +1,8 @@ +import { api } from "@core/api"; +import { fetchWithLogs } from "@core/api/utils"; +import merklConfig from "@core/config"; +import { DEFAULT_ITEMS_PER_PAGE } from "@core/constants/pagination"; import type { Opportunity } from "@merkl/api"; -import { api } from "../../api"; -import { fetchWithLogs } from "../../api/utils"; -import merklConfig from "../../config"; -import { DEFAULT_ITEMS_PER_PAGE } from "../../constants/pagination"; export abstract class OpportunityService { static async getManyFromRequest( diff --git a/src/modules/opportunity/routes/opportunities.header.tsx b/src/modules/opportunity/routes/opportunities.header.tsx index 19cc87e..8f91dd7 100644 --- a/src/modules/opportunity/routes/opportunities.header.tsx +++ b/src/modules/opportunity/routes/opportunities.header.tsx @@ -1,7 +1,7 @@ +import { I18n } from "@core/I18n"; +import Hero from "@core/components/composite/Hero"; import type { MetaFunction } from "@remix-run/node"; import { Outlet } from "@remix-run/react"; -import { I18n } from "../../../I18n"; -import Hero from "../../../components/composite/Hero"; export const meta: MetaFunction = () => { return [{ title: I18n.trad.get.pages.home.headTitle }]; diff --git a/src/modules/opportunity/routes/opportunities.list.tsx b/src/modules/opportunity/routes/opportunities.list.tsx index 457895e..dd7e13d 100644 --- a/src/modules/opportunity/routes/opportunities.list.tsx +++ b/src/modules/opportunity/routes/opportunities.list.tsx @@ -1,14 +1,14 @@ +import CustomBanner from "@core/components/element/CustomBanner"; +import { ErrorContent } from "@core/components/layout/ErrorContent"; +import merklConfig from "@core/config"; +import { Cache } from "@core/modules/cache/cache.service"; +import { ChainService } from "@core/modules/chain/chain.service"; +import OpportunityLibrary from "@core/modules/opportunity/components/library/OpportunityLibrary"; +import { OpportunityService } from "@core/modules/opportunity/opportunity.service"; +import { ProtocolService } from "@core/modules/protocol/protocol.service"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; -import { Container, Group, Space, Title } from "dappkit"; -import CustomBanner from "src/components/element/CustomBanner"; -import OpportunityLibrary from "../../../components/element/opportunity/OpportunityLibrary"; -import { ErrorContent } from "../../../components/layout/ErrorContent"; -import merklConfig from "../../../config"; -import { Cache } from "../../../modules/cache/cache.service"; -import { ChainService } from "../../../modules/chain/chain.service"; -import { OpportunityService } from "../../../modules/opportunity/opportunity.service"; -import { ProtocolService } from "../../../modules/protocol/protocol.service"; +import { Container, Group, Show, Space, Title } from "dappkit"; export async function loader({ request }: LoaderFunctionArgs) { const { opportunities, count } = await OpportunityService.getManyFromRequest(request); @@ -31,18 +31,16 @@ export default function Index() { - {merklConfig.opportunity.featured.enabled && ( - <> - - BEST OPPORTUNITIES - - - - - ALL OPPORTUNITIES - - - )} + + + BEST OPPORTUNITIES + + + + + ALL OPPORTUNITIES + + diff --git a/src/modules/opportunity/routes/opportunity.$chain.$type.$id.campaigns.tsx b/src/modules/opportunity/routes/opportunity.$chain.$type.$id.campaigns.tsx index a1c31e2..76633a0 100644 --- a/src/modules/opportunity/routes/opportunity.$chain.$type.$id.campaigns.tsx +++ b/src/modules/opportunity/routes/opportunity.$chain.$type.$id.campaigns.tsx @@ -1,8 +1,8 @@ import { useOutletContext } from "@remix-run/react"; -import { Container, Group, Space } from "dappkit"; -import CampaignLibrary from "../../../components/element/campaign/CampaignLibrary"; -import { ErrorContent } from "../../../components/layout/ErrorContent"; -import type { OutletContextOpportunity } from "./opportunity.$chain.$type.$id.header"; +import { Container, Space } from "dappkit"; +import CampaignLibrary from "@core/components/element/campaign/CampaignLibrary"; +import { ErrorContent } from "@core/components/layout/ErrorContent"; +import type { OutletContextOpportunity } from "@core/modules/opportunity/routes/opportunity.$chain.$type.$id.header"; export default function Index() { const { opportunity, chain } = useOutletContext(); @@ -10,17 +10,7 @@ export default function Index() { return ( - - - - {/* {merklConfig.deposit && ( - - - - - - )} */} - + ); } diff --git a/src/modules/opportunity/routes/opportunity.$chain.$type.$id.header.tsx b/src/modules/opportunity/routes/opportunity.$chain.$type.$id.header.tsx index a623e20..bc638b9 100644 --- a/src/modules/opportunity/routes/opportunity.$chain.$type.$id.header.tsx +++ b/src/modules/opportunity/routes/opportunity.$chain.$type.$id.header.tsx @@ -1,32 +1,35 @@ -import type { Chain } from "@merkl/api"; +import Hero from "@core/components/composite/Hero"; +import { ErrorHeading } from "@core/components/layout/ErrorHeading"; +import merklConfig from "@core/config"; +import { Cache } from "@core/modules/cache/cache.service"; +import { ChainService } from "@core/modules/chain/chain.service"; +import OpportunityParticipateModal from "@core/modules/opportunity/components/element/OpportunityParticipateModal"; +import useOpportunityData from "@core/modules/opportunity/hooks/useOpportunityMetadata"; +import useOpportunityMetrics from "@core/modules/opportunity/hooks/useOpportunityMetrics"; +import { OpportunityService } from "@core/modules/opportunity/opportunity.service"; +import type { Campaign, Chain } from "@merkl/api"; +import type { Opportunity } from "@merkl/api"; import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { Meta, Outlet, useLoaderData } from "@remix-run/react"; import { Button, Group, Icon } from "dappkit"; import { useClipboard } from "dappkit"; -import { useMemo } from "react"; -import { v4 as uuidv4 } from "uuid"; -import Hero from "../../../components/composite/Hero"; -import Tag from "../../../components/element/Tag"; -import OpportunityParticipateModal from "../../../components/element/opportunity/OpportunityParticipateModal"; -import { ErrorHeading } from "../../../components/layout/ErrorHeading"; -import merklConfig from "../../../config"; -import useOpportunity from "../../../hooks/resources/useOpportunity"; -import { Cache } from "../../../modules/cache/cache.service"; -import { ChainService } from "../../../modules/chain/chain.service"; -import type { OpportunityWithCampaigns } from "../../../modules/opportunity/opportunity.model"; -import { OpportunityService } from "../../../modules/opportunity/opportunity.service"; export async function loader({ params: { id, type, chain: chainId } }: LoaderFunctionArgs) { if (!chainId || !id || !type) throw ""; const chain = await ChainService.get({ name: chainId }); - const rawOpportunity = await OpportunityService.getCampaignsByParams({ + const opportunity = await OpportunityService.getCampaignsByParams({ chainId: chain.id, type: type, identifier: id, }); - return { rawOpportunity, chain }; + + return { + //TODO: remove workaroung by either calling opportunity + campaigns or uniformizing api return types + opportunity: opportunity as typeof opportunity & Opportunity, + chain, + }; } export const clientLoader = Cache.wrap("opportunity", 300); @@ -35,59 +38,31 @@ export const meta: MetaFunction = ({ data, error }) => { if (error) return [{ title: error }]; return [ { - title: `${data?.rawOpportunity.name}`, + title: `${data?.opportunity.name}`, }, ]; }; export type OutletContextOpportunity = { - opportunity: OpportunityWithCampaigns; + opportunity: Opportunity & { campaigns: Campaign[] }; chain: Chain; }; export default function Index() { - const { rawOpportunity, chain } = useLoaderData(); - const { tags, description, link, herosData, opportunity, iconTokens } = useOpportunity(rawOpportunity); - - const { copy: copyCall, isCopied } = useClipboard(); + const { opportunity, chain } = useLoaderData(); - const styleName = useMemo(() => { - const spaced = opportunity.name.split(" "); + const { headerMetrics } = useOpportunityMetrics(opportunity); + const { title, Tags, description, link, url, icons } = useOpportunityData(opportunity); - return spaced - .map(str => { - const key = str + uuidv4(); - 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 && /]); - return [{str}]; - }) - .flatMap((str, index, arr) => [str, index !== arr.length - 1 && " "]); - }, [opportunity]); + const { copy: copyCall, isCopied } = useClipboard(); const currentLiveCampaign = opportunity.campaigns?.[0]; - const visitUrl = useMemo(() => { - if (!!opportunity.depositUrl) return opportunity.depositUrl; - if (!!opportunity.protocol?.url) return opportunity.protocol?.url; - }, [opportunity]); - return ( <> ({ src: t.icon }))} + icons={icons} breadcrumbs={[ { link: merklConfig.routes.opportunities?.route ?? "/", name: "Opportunities" }, { @@ -97,11 +72,11 @@ export default function Index() { ]} title={ - {styleName} + {title} {merklConfig.deposit && ( <> - {!!visitUrl && ( - )} @@ -112,8 +87,8 @@ export default function Index() { )} - {!merklConfig.deposit && !!visitUrl && ( - @@ -134,17 +109,8 @@ export default function Index() { key: "leaderboard", }, ]} - tags={tags.map(tag => ( - - ))} - sideDatas={herosData}> + tags={} + sideDatas={headerMetrics}> diff --git a/src/modules/opportunity/routes/opportunity.$chain.$type.$id.leaderboard.tsx b/src/modules/opportunity/routes/opportunity.$chain.$type.$id.leaderboard.tsx index 94ce13d..c0f01a0 100644 --- a/src/modules/opportunity/routes/opportunity.$chain.$type.$id.leaderboard.tsx +++ b/src/modules/opportunity/routes/opportunity.$chain.$type.$id.leaderboard.tsx @@ -1,3 +1,10 @@ +import LeaderboardLibrary from "@core/components/element/leaderboard/LeaderboardLibrary"; +import Token from "@core/components/element/token/Token"; +import merklConfig from "@core/config"; +import useSearchParamState from "@core/hooks/filtering/useSearchParamState"; +import { CampaignService } from "@core/modules/campaigns/campaign.service"; +import { ChainService } from "@core/modules/chain/chain.service"; +import { RewardService } from "@core/modules/reward/reward.service"; import type { Campaign } from "@merkl/api"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; @@ -18,13 +25,6 @@ import { import moment from "moment"; import { useCallback, useMemo } from "react"; import { formatUnits, parseUnits } from "viem"; -import LeaderboardLibrary from "../../../components/element/leaderboard/LeaderboardLibrary"; -import Token from "../../../components/element/token/Token"; -import merklConfig from "../../../config"; -import useSearchParamState from "../../../hooks/filtering/useSearchParamState"; -import { CampaignService } from "../../../modules/campaigns/campaign.service"; -import { ChainService } from "../../../modules/chain/chain.service"; -import { RewardService } from "../../../modules/reward/reward.service"; export async function loader({ params: { id, type, chain: chainId }, request }: LoaderFunctionArgs) { if (!chainId || !id || !type) throw ""; diff --git a/src/modules/protocol/routes/protocol.$id.opportunities.tsx b/src/modules/protocol/routes/protocol.$id.opportunities.tsx index bbbf0cc..3d25c84 100644 --- a/src/modules/protocol/routes/protocol.$id.opportunities.tsx +++ b/src/modules/protocol/routes/protocol.$id.opportunities.tsx @@ -1,8 +1,8 @@ +import OpportunityLibrary from "@core/modules/opportunity/components/library/OpportunityLibrary"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { Container, Group, Space, Title } from "dappkit"; import { useWalletContext } from "dappkit"; -import OpportunityLibrary from "../../../components/element/opportunity/OpportunityLibrary"; import merklConfig from "../../../config"; import { OpportunityService } from "../../../modules/opportunity/opportunity.service"; import { ProtocolService } from "../../../modules/protocol/protocol.service"; diff --git a/src/modules/token/routes/token.$symbol.opportunities.tsx b/src/modules/token/routes/token.$symbol.opportunities.tsx index 1b00dd8..9fab68f 100644 --- a/src/modules/token/routes/token.$symbol.opportunities.tsx +++ b/src/modules/token/routes/token.$symbol.opportunities.tsx @@ -1,7 +1,7 @@ +import OpportunityLibrary from "@core/modules/opportunity/components/library/OpportunityLibrary"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { Container, Group, Space, Title } from "dappkit"; -import OpportunityLibrary from "../../../components/element/opportunity/OpportunityLibrary"; import merklConfig from "../../../config"; import { Cache } from "../../../modules/cache/cache.service"; import { ChainService } from "../../../modules/chain/chain.service"; diff --git a/tsconfig.json b/tsconfig.json index 82709e3..bfbee69 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,9 @@ "forceConsistentCasingInFileNames": true, "baseUrl": ".", "sourceMap": true, + "paths": { + "@core/*": ["./src/*"] + }, // Vite takes care of building everything, not tsc. "noEmit": true diff --git a/vite.config.ts b/vite.config.ts index 8d73b55..3d6d1c7 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,6 +3,13 @@ import svgr from "@svgr/rollup"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; +declare module "@remix-run/server-runtime" { + // or cloudflare, deno, etc. + interface Future { + v3_singleFetch: true; + } +} + export default defineConfig({ plugins: [ svgr(), From 78a28a7ee03f33720bb9ab82f37a0f23011cc383 Mon Sep 17 00:00:00 2001 From: sheykei Date: Fri, 17 Jan 2025 15:25:01 +0100 Subject: [PATCH 2/4] fix: tags --- src/components/element/Tag.tsx | 2 +- .../element/campaign/CampaignLibrary.tsx | 2 +- .../element/campaign/CampaignTableRow.tsx | 2 +- src/hooks/resources/useCampaign.tsx | 2 +- src/index.generated.ts | 2 +- .../components/items/OpportunityCell.tsx | 11 ++++------- .../components/items/OpportunityShortCard.tsx | 14 +++++--------- .../components/library/OpportunityFeatured.tsx | 2 +- .../opportunity/hooks/useOpportunityRewards.tsx | 2 +- .../opportunity.$chain.$type.$id.campaigns.tsx | 4 ++-- 10 files changed, 18 insertions(+), 25 deletions(-) diff --git a/src/components/element/Tag.tsx b/src/components/element/Tag.tsx index 2ed30af..70b8ba2 100644 --- a/src/components/element/Tag.tsx +++ b/src/components/element/Tag.tsx @@ -1,5 +1,5 @@ import type { Chain, Token } from "@merkl/api"; -import type { Opportunity } from "@merkl/api" +import type { Opportunity } from "@merkl/api"; import { useSearchParams } from "@remix-run/react"; import { Button, diff --git a/src/components/element/campaign/CampaignLibrary.tsx b/src/components/element/campaign/CampaignLibrary.tsx index f3eb55d..ff62a87 100644 --- a/src/components/element/campaign/CampaignLibrary.tsx +++ b/src/components/element/campaign/CampaignLibrary.tsx @@ -1,5 +1,5 @@ import type { Campaign, Chain } from "@merkl/api"; -import type { Opportunity } from "@merkl/api" +import type { Opportunity } from "@merkl/api"; import { Box, Button, Group, Icon, Text, Title } from "dappkit"; import moment from "moment"; import { useMemo, useState } from "react"; diff --git a/src/components/element/campaign/CampaignTableRow.tsx b/src/components/element/campaign/CampaignTableRow.tsx index 2b8edc6..6d4c441 100644 --- a/src/components/element/campaign/CampaignTableRow.tsx +++ b/src/components/element/campaign/CampaignTableRow.tsx @@ -1,6 +1,6 @@ import Tag from "@core/components/element/Tag"; import type { Campaign, Chain as ChainType } from "@merkl/api"; -import type { Opportunity } from "@merkl/api" +import type { Opportunity } from "@merkl/api"; import { Box, Button, diff --git a/src/hooks/resources/useCampaign.tsx b/src/hooks/resources/useCampaign.tsx index 14cee32..07c9e13 100644 --- a/src/hooks/resources/useCampaign.tsx +++ b/src/hooks/resources/useCampaign.tsx @@ -1,5 +1,5 @@ import type { Campaign as CampaignFromApi } from "@merkl/api"; -import type { Opportunity } from "@merkl/api" +import type { Opportunity } from "@merkl/api"; import { Bar, Icon } from "dappkit"; import { Group, Text, Value } from "dappkit"; import { Time } from "dappkit"; diff --git a/src/index.generated.ts b/src/index.generated.ts index 2b584e1..e8feaa5 100644 --- a/src/index.generated.ts +++ b/src/index.generated.ts @@ -1,5 +1,5 @@ /** - * + * */ export * from ".//root"; export { default as root } from ".//root"; diff --git a/src/modules/opportunity/components/items/OpportunityCell.tsx b/src/modules/opportunity/components/items/OpportunityCell.tsx index 447a595..4134ce5 100644 --- a/src/modules/opportunity/components/items/OpportunityCell.tsx +++ b/src/modules/opportunity/components/items/OpportunityCell.tsx @@ -11,14 +11,15 @@ import { Box, Button, Divider, - Dropdown, Group, + Dropdown, + Group, Icon, PrimitiveTag, Text, Title, Value, mergeClass, - useOverflowingRef + useOverflowingRef, } from "dappkit"; import { useMemo } from "react"; @@ -29,11 +30,7 @@ export type OpportunityCellProps = { navigationMode?: OpportunityNavigationMode; } & BoxProps; -export default function OpportunityCell({ - opportunity, - hideTags, - navigationMode, -}: OpportunityCellProps) { +export default function OpportunityCell({ opportunity, hideTags, navigationMode }: OpportunityCellProps) { const { name, link, Tags, Icons } = useOpportunityData(opportunity); const { formattedDailyRewards } = useOpportunityRewards(opportunity); const { ref, overflowing } = useOverflowingRef(); diff --git a/src/modules/opportunity/components/items/OpportunityShortCard.tsx b/src/modules/opportunity/components/items/OpportunityShortCard.tsx index 564371e..0078d93 100644 --- a/src/modules/opportunity/components/items/OpportunityShortCard.tsx +++ b/src/modules/opportunity/components/items/OpportunityShortCard.tsx @@ -1,20 +1,20 @@ import useOpportunityData from "@core/modules/opportunity/hooks/useOpportunityMetadata"; import useOpportunityRewards from "@core/modules/opportunity/hooks/useOpportunityRewards"; import type { Opportunity } from "@merkl/api"; -import { Box, Button, Group, Icon, Icons, Text } from "dappkit"; -import { v4 as uuidv4 } from "uuid"; +import { Box, Button, Group, Icon, Text } from "dappkit"; export type OpportunityShortCardProps = { opportunity: Opportunity; displayLinks?: boolean }; export default function OpportunityShortCard({ opportunity, displayLinks }: OpportunityShortCardProps) { - const { url, Tags } = useOpportunityData(opportunity); - const { rewardIcons, formattedDailyRewards } = useOpportunityRewards(opportunity); + const { url, Tags, Icons } = useOpportunityData(opportunity); + const { formattedDailyRewards } = useOpportunityRewards(opportunity); return ( {formattedDailyRewards} - - {rewardIcons.map(icon => ( - - ))} - + {opportunity.name} diff --git a/src/modules/opportunity/components/library/OpportunityFeatured.tsx b/src/modules/opportunity/components/library/OpportunityFeatured.tsx index 2c3a68c..df71e47 100644 --- a/src/modules/opportunity/components/library/OpportunityFeatured.tsx +++ b/src/modules/opportunity/components/library/OpportunityFeatured.tsx @@ -1,4 +1,4 @@ -import type { Opportunity } from "@merkl/api" +import type { Opportunity } from "@merkl/api"; import { useMemo } from "react"; import merklConfig from "../../../../config"; import OpportunityCell from "../items/OpportunityCell"; diff --git a/src/modules/opportunity/hooks/useOpportunityRewards.tsx b/src/modules/opportunity/hooks/useOpportunityRewards.tsx index 12350ca..52a082f 100644 --- a/src/modules/opportunity/hooks/useOpportunityRewards.tsx +++ b/src/modules/opportunity/hooks/useOpportunityRewards.tsx @@ -100,6 +100,6 @@ export default function useOpportunityRewards({ return { rewardIcons, rewardsBreakdown, - formattedDailyRewards + formattedDailyRewards, }; } diff --git a/src/modules/opportunity/routes/opportunity.$chain.$type.$id.campaigns.tsx b/src/modules/opportunity/routes/opportunity.$chain.$type.$id.campaigns.tsx index 76633a0..d1911c5 100644 --- a/src/modules/opportunity/routes/opportunity.$chain.$type.$id.campaigns.tsx +++ b/src/modules/opportunity/routes/opportunity.$chain.$type.$id.campaigns.tsx @@ -1,8 +1,8 @@ -import { useOutletContext } from "@remix-run/react"; -import { Container, Space } from "dappkit"; import CampaignLibrary from "@core/components/element/campaign/CampaignLibrary"; import { ErrorContent } from "@core/components/layout/ErrorContent"; import type { OutletContextOpportunity } from "@core/modules/opportunity/routes/opportunity.$chain.$type.$id.header"; +import { useOutletContext } from "@remix-run/react"; +import { Container, Space } from "dappkit"; export default function Index() { const { opportunity, chain } = useOutletContext(); From 57d869632edc19e5f28890fccf8af384bbfcd5f1 Mon Sep 17 00:00:00 2001 From: sheykei Date: Fri, 17 Jan 2025 15:58:38 +0100 Subject: [PATCH 3/4] fix: tvl dollar format --- src/modules/opportunity/hooks/useOpportunityMetrics.tsx | 2 +- src/modules/opportunity/hooks/useOpportunityRewards.tsx | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/modules/opportunity/hooks/useOpportunityMetrics.tsx b/src/modules/opportunity/hooks/useOpportunityMetrics.tsx index 399e370..7b4ae5e 100644 --- a/src/modules/opportunity/hooks/useOpportunityMetrics.tsx +++ b/src/modules/opportunity/hooks/useOpportunityMetrics.tsx @@ -18,7 +18,7 @@ export default function useOpportunityMetrics({ dailyRewards, apr, tvl }: Pick Date: Fri, 17 Jan 2025 16:16:51 +0100 Subject: [PATCH 4/4] fix: bigints --- src/modules/opportunity/hooks/useOpportunityRewards.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/modules/opportunity/hooks/useOpportunityRewards.tsx b/src/modules/opportunity/hooks/useOpportunityRewards.tsx index 3e69992..b3ed871 100644 --- a/src/modules/opportunity/hooks/useOpportunityRewards.tsx +++ b/src/modules/opportunity/hooks/useOpportunityRewards.tsx @@ -4,10 +4,7 @@ import type { Opportunity } from "@merkl/api"; import { Fmt, Icon, Icons, Text, Title, Value } from "dappkit"; import { useMemo } from "react"; -const rewards = [ - "dailyRewards", - "rewardsRecord", -] satisfies (keyof Opportunity)[]; +const rewards = ["dailyRewards", "rewardsRecord"] satisfies (keyof Opportunity)[]; /** * Formats rewards for a given opportunity @@ -57,7 +54,7 @@ export default function useOpportunityRewards({ ({ token }) => token?.address === merklConfig.opportunity.library.dailyRewardsTokenAddress, ); const token = breakdowns?.[0]?.token; - const breakdownAmount = breakdowns.reduce((acc, breakdown) => acc + breakdown.amount, 0n); + const breakdownAmount = breakdowns.reduce((acc, breakdown) => BigInt(acc) + BigInt(breakdown.amount), 0n); return ( <>