diff --git a/src/features/market-detail/components/campaign-modal.tsx b/src/features/market-detail/components/campaign-modal.tsx index 7d568a38..64241c1b 100644 --- a/src/features/market-detail/components/campaign-modal.tsx +++ b/src/features/market-detail/components/campaign-modal.tsx @@ -8,11 +8,11 @@ import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal'; import { getMerklCampaignURL } from '@/utils/external'; import type { SimplifiedCampaign, MerklCampaignType } from '@/utils/merklTypes'; -const CAMPAIGN_TYPE_CONFIG: Record = { - MORPHOSUPPLY: { badge: 'Lender Rewards', actionType: 'lenders', actionVerb: 'lend' }, - MORPHOSUPPLY_SINGLETOKEN: { badge: 'Lender Rewards', actionType: 'lenders', actionVerb: 'lend' }, - MULTILENDBORROW: { badge: 'Lend/Borrow Rewards', actionType: 'users', actionVerb: 'participate in' }, - MORPHOBORROW: { badge: 'Borrow Rewards', actionType: 'borrowers', actionVerb: 'borrow' }, +const CAMPAIGN_TYPE_CONFIG: Record = { + MORPHOSUPPLY: { badge: 'Lender Rewards' }, + MORPHOSUPPLY_SINGLETOKEN: { badge: 'Lender Rewards' }, + MULTILENDBORROW: { badge: 'Lend/Borrow Rewards' }, + MORPHOBORROW: { badge: 'Borrow Rewards' }, }; type CampaignModalProps = { @@ -22,30 +22,39 @@ type CampaignModalProps = { }; function getUrlIdentifier(campaign: SimplifiedCampaign): string { + // Always prefer opportunityIdentifier from the Opportunity object + if (campaign.opportunityIdentifier) { + return campaign.opportunityIdentifier; + } + // Fallback for legacy data switch (campaign.type) { case 'MORPHOSUPPLY_SINGLETOKEN': return campaign.targetToken?.address ?? campaign.campaignId; - case 'MULTILENDBORROW': - return campaign.opportunityIdentifier ?? campaign.campaignId; default: return campaign.marketId.slice(0, 42); } } +function formatCampaignDate(timestamp: number): string { + return new Date(timestamp * 1000).toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric', + }); +} + function CampaignRow({ campaign }: { campaign: SimplifiedCampaign }) { const urlIdentifier = getUrlIdentifier(campaign); const merklUrl = getMerklCampaignURL(campaign.chainId, campaign.type, urlIdentifier); - const { badge, actionType, actionVerb } = CAMPAIGN_TYPE_CONFIG[campaign.type] ?? CAMPAIGN_TYPE_CONFIG.MORPHOSUPPLY; + const { badge } = CAMPAIGN_TYPE_CONFIG[campaign.type] ?? CAMPAIGN_TYPE_CONFIG.MORPHOSUPPLY; return (
+ {/* Header: Badge + Reward Token + APR */}
- {/* Campaign Type Badge */} {badge} - - {/* Reward Token */}
{campaign.rewardToken.symbol}
- - {/* APR */} +{campaign.apr.toFixed(2)}% APR
-
- Extra incentives for all {actionType} who {actionVerb} to this market, earning {campaign.rewardToken.symbol} rewards. -
+ {/* Campaign Name */} + {campaign.name &&

{campaign.name}

} -
+ {/* Date Range + Link */} +
+ + {formatCampaignDate(campaign.startTimestamp)} — {formatCampaignDate(campaign.endTimestamp)} + = { '1d': '1D', '7d': '7D', '30d': '30D', + '3m': '3M', + '6m': '6M', }; type GradientConfig = { @@ -63,7 +65,14 @@ export function ChartTooltipContent({ active, payload, label, formatValue }: Cha if (!active || !payload) return null; return (
-

{new Date((label ?? 0) * 1000).toLocaleDateString()}

+

+ {new Date((label ?? 0) * 1000).toLocaleString(undefined, { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + })} +

{payload.map((entry: any) => (
{ + const handleTimeframeChange = (timeframe: '1d' | '7d' | '30d' | '3m' | '6m') => { setTimeframe(timeframe); }; @@ -124,7 +124,7 @@ function RateChart({ marketId, chainId, market }: RateChartProps) {
@@ -194,6 +196,16 @@ function RateChart({ marketId, chainId, market }: RateChartProps) { {...chartLegendStyle} {...legendHandlers} /> + - )} diff --git a/src/features/market-detail/components/charts/volume-chart.tsx b/src/features/market-detail/components/charts/volume-chart.tsx index 3169b4a1..8502b3a5 100644 --- a/src/features/market-detail/components/charts/volume-chart.tsx +++ b/src/features/market-detail/components/charts/volume-chart.tsx @@ -44,7 +44,7 @@ function VolumeChart({ marketId, chainId, market }: VolumeChartProps) { liquidity: true, }); - const handleTimeframeChange = (timeframe: '1d' | '7d' | '30d') => { + const handleTimeframeChange = (timeframe: '1d' | '7d' | '30d' | '3m' | '6m') => { setTimeframe(timeframe); }; @@ -180,7 +180,7 @@ function VolumeChart({ marketId, chainId, market }: VolumeChartProps) {
diff --git a/src/features/market-detail/components/market-header.tsx b/src/features/market-detail/components/market-header.tsx index 847763ea..bf2e6239 100644 --- a/src/features/market-detail/components/market-header.tsx +++ b/src/features/market-detail/components/market-header.tsx @@ -1,7 +1,9 @@ 'use client'; +import { useState } from 'react'; import Image from 'next/image'; import { formatUnits } from 'viem'; +import { motion, AnimatePresence } from 'framer-motion'; import { ChevronDownIcon } from '@radix-ui/react-icons'; import { GrStatusGood } from 'react-icons/gr'; import { IoWarningOutline, IoEllipsisVertical } from 'react-icons/io5'; @@ -129,6 +131,7 @@ export function MarketHeader({ onSupplyClick, onBorrowClick, }: MarketHeaderProps) { + const [isExpanded, setIsExpanded] = useState(false); const { short: rateLabel } = useRateLabel(); const { isAprDisplay } = useAppSettings(); const networkImg = getNetworkImg(network); @@ -221,13 +224,15 @@ export function MarketHeader({ width={40} height={40} /> - +
+ +
@@ -283,7 +288,7 @@ export function MarketHeader({ content={ } > @@ -307,7 +312,7 @@ export function MarketHeader({
{/* Advanced Details - Expandable */} -
- - - Advanced Details +
+
- -
- {/* Global Warnings (debt + general) at top */} - - - {/* Two-column grid */} -
- {/* LEFT: Market Configuration */} -
-
-

Market Configuration

- -
+
+ Advanced Details + +
+ + + + {isExpanded && ( + +
+ {/* Global Warnings (debt + general) at top */} + -
-
- Loan: - -
-
- Collateral: - -
-
- IRM: - + {/* Two-column grid */} +
+ {/* LEFT: Market Configuration */} +
+
+

Market Configuration

+ +
+ +
+
+ Loan: + +
+
+ Collateral: + +
+
+ IRM: + +
+
+ + {/* Asset warnings */} + +
+ + {/* RIGHT: Oracle */} +
+
+

Oracle

+ +
+ + + + {/* Oracle warnings */} + +
- - {/* Asset warnings */} - -
- - {/* RIGHT: Oracle */} -
-
-

Oracle

- -
- - - - {/* Oracle warnings */} - -
-
-
-
+ + )} + +
); diff --git a/src/hooks/useMarketCampaigns.ts b/src/hooks/useMarketCampaigns.ts index 9e00722b..114ad40a 100644 --- a/src/hooks/useMarketCampaigns.ts +++ b/src/hooks/useMarketCampaigns.ts @@ -2,6 +2,13 @@ import { useMemo } from 'react'; import type { SimplifiedCampaign } from '@/utils/merklTypes'; import { useMerklCampaignsQuery } from './queries/useMerklCampaignsQuery'; +// Blacklisted campaign IDs - these will be filtered out +const BLACKLISTED_CAMPAIGN_IDS: string[] = [ + // Seems to be reporting bad APY, not singleton for all market for sure + // https://app.merkl.xyz/opportunities/base/MORPHOSUPPLY_SINGLETOKEN/0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 + '0x4b5aa0f66eb6a63e3b761de8fbbcc8154d568086c1234ba58516f3263a79200a', +]; + type UseMarketCampaignsReturn = { campaigns: SimplifiedCampaign[]; activeCampaigns: SimplifiedCampaign[]; @@ -42,8 +49,13 @@ export function useMarketCampaigns(options: MarketCampaignsOptions): UseMarketCa ) : []; - // Combine both types of campaigns - const allMarketCampaigns = [...directMarketCampaigns, ...singleTokenCampaigns]; + // Combine both types of campaigns and filter out blacklisted ones + const allMarketCampaigns = [...directMarketCampaigns, ...singleTokenCampaigns].filter( + (campaign) => !BLACKLISTED_CAMPAIGN_IDS.includes(campaign.campaignId.toLowerCase()), + ); + + console.log(`[useMarketCampaigns] Active Campaigns for market ${normalizedMarketId.slice(0, 6)}`, allMarketCampaigns); + const activeCampaigns = allMarketCampaigns.filter((campaign) => campaign.isActive); return { diff --git a/src/stores/useMarketDetailChartState.ts b/src/stores/useMarketDetailChartState.ts index f7fec2c9..84c62d14 100644 --- a/src/stores/useMarketDetailChartState.ts +++ b/src/stores/useMarketDetailChartState.ts @@ -3,9 +3,10 @@ import type { TimeseriesOptions } from '@/utils/types'; const DAY_IN_SECONDS = 24 * 60 * 60; const WEEK_IN_SECONDS = 7 * DAY_IN_SECONDS; +const MONTH_IN_SECONDS = 30 * DAY_IN_SECONDS; // Helper to calculate time range based on timeframe string -const calculateTimeRange = (timeframe: '1d' | '7d' | '30d'): TimeseriesOptions => { +const calculateTimeRange = (timeframe: '1d' | '7d' | '30d' | '3m' | '6m'): TimeseriesOptions => { const endTimestamp = Math.floor(Date.now() / 1000); let startTimestamp; let interval: TimeseriesOptions['interval'] = 'HOUR'; @@ -15,7 +16,14 @@ const calculateTimeRange = (timeframe: '1d' | '7d' | '30d'): TimeseriesOptions = break; case '30d': startTimestamp = endTimestamp - 30 * DAY_IN_SECONDS; - // Use DAY interval for longer ranges if desired, adjust as needed + interval = 'DAY'; + break; + case '3m': + startTimestamp = endTimestamp - 3 * MONTH_IN_SECONDS; + interval = 'DAY'; + break; + case '6m': + startTimestamp = endTimestamp - 6 * MONTH_IN_SECONDS; interval = 'DAY'; break; default: @@ -26,13 +34,13 @@ const calculateTimeRange = (timeframe: '1d' | '7d' | '30d'): TimeseriesOptions = }; type ChartState = { - selectedTimeframe: '1d' | '7d' | '30d'; + selectedTimeframe: '1d' | '7d' | '30d' | '3m' | '6m'; selectedTimeRange: TimeseriesOptions; volumeView: 'USD' | 'Asset'; }; type ChartActions = { - setTimeframe: (timeframe: '1d' | '7d' | '30d') => void; + setTimeframe: (timeframe: '1d' | '7d' | '30d' | '3m' | '6m') => void; setVolumeView: (view: 'USD' | 'Asset') => void; }; diff --git a/src/utils/merklApi.ts b/src/utils/merklApi.ts index aa45dcce..3c3df17a 100644 --- a/src/utils/merklApi.ts +++ b/src/utils/merklApi.ts @@ -77,7 +77,19 @@ function isCampaignActive(campaign: MerklCampaign): boolean { // Helper to extract common campaign fields function getBaseCampaignFields( campaign: MerklCampaign, -): Pick { +): Pick< + SimplifiedCampaign, + | 'chainId' + | 'campaignId' + | 'type' + | 'apr' + | 'rewardToken' + | 'startTimestamp' + | 'endTimestamp' + | 'isActive' + | 'name' + | 'opportunityIdentifier' +> { return { chainId: campaign.computeChainId, campaignId: campaign.campaignId, @@ -91,6 +103,8 @@ function getBaseCampaignFields( startTimestamp: campaign.startTimestamp, endTimestamp: campaign.endTimestamp, isActive: isCampaignActive(campaign), + name: campaign.Opportunity?.name, + opportunityIdentifier: campaign.Opportunity?.identifier, }; } @@ -128,13 +142,11 @@ export function simplifyMerklCampaign(campaign: MerklCampaign): SimplifiedCampai export function expandMultiLendBorrowCampaign(campaign: MerklCampaign): SimplifiedCampaign[] { const baseFields = getBaseCampaignFields(campaign); const markets = campaign.params.markets ?? []; - const opportunityIdentifier = campaign.Opportunity?.identifier; return markets.map((m) => ({ ...baseFields, marketId: m.campaignParameters.market, collateralToken: { symbol: m.campaignParameters.symbolCollateralToken }, loanToken: { symbol: m.campaignParameters.symbolLoanToken }, - opportunityIdentifier, })); } diff --git a/src/utils/merklTypes.ts b/src/utils/merklTypes.ts index 28d5d92d..e32f6bd9 100644 --- a/src/utils/merklTypes.ts +++ b/src/utils/merklTypes.ts @@ -154,5 +154,6 @@ export type SimplifiedCampaign = { startTimestamp: number; endTimestamp: number; isActive: boolean; + name?: string; opportunityIdentifier?: string; };