diff --git a/src/features/markets/components/market-indicators.tsx b/src/features/markets/components/market-indicators.tsx index 99ccdfad..86b476d1 100644 --- a/src/features/markets/components/market-indicators.tsx +++ b/src/features/markets/components/market-indicators.tsx @@ -5,7 +5,7 @@ import { LuUser } from 'react-icons/lu'; import { IoWarningOutline } from 'react-icons/io5'; import { AiOutlineFire } from 'react-icons/ai'; import { TooltipContent } from '@/components/shared/tooltip-content'; -import { useTrendingMarketKeys, getMetricsKey, useEverLiquidated } from '@/hooks/queries/useMarketMetricsQuery'; +import { useTrendingMarketKeys, useCustomSignalMarketKeys, getMetricsKey, useEverLiquidated, useMarketMetricsMap } from '@/hooks/queries/useMarketMetricsQuery'; import { computeMarketWarnings } from '@/hooks/useMarketWarnings'; import { useMarketPreferences } from '@/stores/useMarketPreferences'; import type { Market } from '@/utils/types'; @@ -23,8 +23,17 @@ type MarketIndicatorsProps = { export function MarketIndicators({ market, showRisk = false, isStared = false, hasUserPosition = false }: MarketIndicatorsProps) { const hasLiquidationProtection = useEverLiquidated(market.morphoBlue.chain.id, market.uniqueKey); const { trendingConfig } = useMarketPreferences(); + const { metricsMap } = useMarketMetricsMap(); + + // Backend-computed trending (official "hot markets") const trendingKeys = useTrendingMarketKeys(); - const isTrending = trendingConfig.enabled && trendingKeys.has(getMetricsKey(market.morphoBlue.chain.id, market.uniqueKey)); + const marketKey = getMetricsKey(market.morphoBlue.chain.id, market.uniqueKey); + const isTrending = trendingKeys.has(marketKey); + const trendingReason = metricsMap.get(marketKey)?.trendingReason; + + // User's custom tag + const customTagKeys = useCustomSignalMarketKeys(); + const hasCustomTag = trendingConfig.enabled && customTagKeys.has(marketKey); const warnings = showRisk ? computeMarketWarnings(market, true) : []; const hasWarnings = warnings.length > 0; const alertWarning = warnings.find((w) => w.level === 'alert'); @@ -101,6 +110,7 @@ export function MarketIndicators({ market, showRisk = false, isStared = false, h whitelisted={market.whitelisted} /> + {/* Backend-computed trending (official) */} {isTrending && ( } - detail="This market is trending based on flow metrics" + detail={trendingReason ?? 'This market is trending'} /> } > @@ -124,6 +134,22 @@ export function MarketIndicators({ market, showRisk = false, isStared = false, h )} + {/* User's custom tag */} + {hasCustomTag && ( + {trendingConfig.icon}} + detail="Matches your custom tag criteria" + /> + } + > +
+ {trendingConfig.icon} +
+
+ )} + {showRisk && hasWarnings && ( ; @@ -193,14 +196,16 @@ export const parseFlowAssets = (flowAssets: string, decimals: number): number => }; /** - * Determines if a market is trending based on flow thresholds. + * Determines if a market matches user's custom signal thresholds. * All non-empty thresholds must be met (AND logic). * Only positive flows (inflows) are considered. + * + * Note: This is for user-defined signals, separate from backend trending. */ -export const isMarketTrending = (metrics: MarketMetrics, trendingConfig: TrendingConfig): boolean => { - if (!trendingConfig.enabled) return false; +export const matchesCustomSignal = (metrics: MarketMetrics, signalConfig: TrendingConfig): boolean => { + if (!signalConfig.enabled) return false; - for (const [window, config] of Object.entries(trendingConfig.windows)) { + for (const [window, config] of Object.entries(signalConfig.windows)) { const supplyPct = config?.minSupplyFlowPct ?? ''; const supplyUsd = config?.minSupplyFlowUsd ?? ''; const borrowPct = config?.minBorrowFlowPct ?? ''; @@ -234,7 +239,7 @@ export const isMarketTrending = (metrics: MarketMetrics, trendingConfig: Trendin } } - const hasAnyThreshold = Object.values(trendingConfig.windows).some((c) => { + const hasAnyThreshold = Object.values(signalConfig.windows).some((c) => { const supplyPct = c?.minSupplyFlowPct ?? ''; const supplyUsd = c?.minSupplyFlowUsd ?? ''; const borrowPct = c?.minBorrowFlowPct ?? ''; @@ -245,12 +250,34 @@ export const isMarketTrending = (metrics: MarketMetrics, trendingConfig: Trendin return hasAnyThreshold; }; +// Legacy alias for backwards compatibility +export const isMarketTrending = matchesCustomSignal; + /** - * Returns a Set of market keys that are currently trending. - * Uses metricsMap for O(1) lookup and filters based on trending config from preferences. + * Returns a Set of market keys that are trending (backend-computed). + * Uses isTrending field from Monarch API - our curated "hot markets" signal. */ export const useTrendingMarketKeys = () => { const { metricsMap } = useMarketMetricsMap(); + + return useMemo(() => { + const keys = new Set(); + + for (const [key, metrics] of metricsMap) { + if (metrics.isTrending) { + keys.add(key); + } + } + return keys; + }, [metricsMap]); +}; + +/** + * Returns a Set of market keys matching user's custom signal config. + * Separate from backend trending - for user-defined filters. + */ +export const useCustomSignalMarketKeys = () => { + const { metricsMap } = useMarketMetricsMap(); const { trendingConfig } = useMarketPreferences(); return useMemo(() => { @@ -258,7 +285,7 @@ export const useTrendingMarketKeys = () => { if (!trendingConfig.enabled) return keys; for (const [key, metrics] of metricsMap) { - if (isMarketTrending(metrics, trendingConfig)) { + if (matchesCustomSignal(metrics, trendingConfig)) { keys.add(key); } } diff --git a/src/modals/settings/monarch-settings/details/TrendingDetail.tsx b/src/modals/settings/monarch-settings/details/TrendingDetail.tsx index ce8b1019..ceca7edb 100644 --- a/src/modals/settings/monarch-settings/details/TrendingDetail.tsx +++ b/src/modals/settings/monarch-settings/details/TrendingDetail.tsx @@ -4,8 +4,8 @@ import { useMemo } from 'react'; import { Input } from '@/components/ui/input'; import { IconSwitch } from '@/components/ui/icon-switch'; import { MarketIdentity, MarketIdentityMode } from '@/features/markets/components/market-identity'; -import { useMarketPreferences, type FlowTimeWindow, type TrendingWindowConfig } from '@/stores/useMarketPreferences'; -import { useMarketMetricsMap, isMarketTrending, getMetricsKey } from '@/hooks/queries/useMarketMetricsQuery'; +import { useMarketPreferences, type FlowTimeWindow, type TrendingWindowConfig, CUSTOM_TAG_ICONS, type CustomTagIcon } from '@/stores/useMarketPreferences'; +import { useMarketMetricsMap, matchesCustomSignal, getMetricsKey } from '@/hooks/queries/useMarketMetricsQuery'; import { useProcessedMarkets } from '@/hooks/useProcessedMarkets'; import { formatReadable } from '@/utils/balance'; import type { Market } from '@/utils/types'; @@ -17,8 +17,8 @@ const TIME_WINDOWS: { value: FlowTimeWindow; label: string }[] = [ { value: '30d', label: '30d' }, ]; -function generateFilterSummary(config: { enabled: boolean; windows: Record }): string { - if (!config.enabled) return 'Trending detection is disabled'; +function generateFilterSummary(config: { enabled: boolean; icon: CustomTagIcon; windows: Record }): string { + if (!config.enabled) return 'Custom tag is disabled'; const parts: string[] = []; @@ -91,7 +91,7 @@ function CompactInput({ } export function TrendingDetail() { - const { trendingConfig, setTrendingEnabled, setTrendingWindowConfig } = useMarketPreferences(); + const { trendingConfig, setTrendingEnabled, setTrendingIcon, setTrendingWindowConfig } = useMarketPreferences(); const { metricsMap } = useMarketMetricsMap(); const { allMarkets } = useProcessedMarkets(); const isEnabled = trendingConfig.enabled; @@ -102,7 +102,7 @@ export function TrendingDetail() { const matches: Array<{ market: Market; supplyFlowPct1h: number }> = []; for (const [key, metrics] of metricsMap) { - if (isMarketTrending(metrics, trendingConfig)) { + if (matchesCustomSignal(metrics, trendingConfig)) { const market = allMarkets.find((m) => getMetricsKey(m.morphoBlue.chain.id, m.uniqueKey) === key); if (market) { matches.push({ @@ -126,6 +126,27 @@ export function TrendingDetail() { return (
+ {/* Icon Picker */} +
+ Tag icon: +
+ {CUSTOM_TAG_ICONS.map((icon) => ( + + ))} +
+
+ {/* Toggle + Summary */}
diff --git a/src/stores/useMarketPreferences.ts b/src/stores/useMarketPreferences.ts index c960bb44..3de480b7 100644 --- a/src/stores/useMarketPreferences.ts +++ b/src/stores/useMarketPreferences.ts @@ -4,7 +4,7 @@ import { SortColumn } from '@/features/markets/components/constants'; import { DEFAULT_MIN_SUPPLY_USD } from '@/constants/markets'; import { DEFAULT_COLUMN_VISIBILITY, type ColumnVisibility } from '@/features/markets/components/column-visibility'; -// Trending feature types +// Custom Tags feature types (user-defined market filters) export type FlowTimeWindow = '1h' | '24h' | '7d' | '30d'; export type TrendingWindowConfig = { @@ -16,13 +16,19 @@ export type TrendingWindowConfig = { minBorrowFlowUsd: string; }; +// Available icons for custom tags +export const CUSTOM_TAG_ICONS = ['🏷️', '⭐', '🔥', '💎', '🚀', '📈', '💰', '⚡', '🎯', '👀'] as const; +export type CustomTagIcon = (typeof CUSTOM_TAG_ICONS)[number]; + export type TrendingConfig = { enabled: boolean; + icon: CustomTagIcon; // User-selected icon for their custom tag windows: Record; }; const DEFAULT_TRENDING_CONFIG: TrendingConfig = { enabled: false, + icon: '🏷️', windows: { '1h': { minSupplyFlowPct: '6', minSupplyFlowUsd: '', minBorrowFlowPct: '', minBorrowFlowUsd: '' }, '24h': { minSupplyFlowPct: '', minSupplyFlowUsd: '', minBorrowFlowPct: '', minBorrowFlowUsd: '' }, @@ -95,8 +101,9 @@ type MarketPreferencesActions = { setMinBorrowEnabled: (enabled: boolean) => void; setMinLiquidityEnabled: (enabled: boolean) => void; - // Trending Config (Beta) + // Custom Tags Config (user-defined market filters) setTrendingEnabled: (enabled: boolean) => void; + setTrendingIcon: (icon: CustomTagIcon) => void; setTrendingWindowConfig: (window: FlowTimeWindow, config: Partial) => void; // Bulk update for migration @@ -170,6 +177,10 @@ export const useMarketPreferences = create()( set((state) => ({ trendingConfig: { ...state.trendingConfig, enabled }, })), + setTrendingIcon: (icon) => + set((state) => ({ + trendingConfig: { ...state.trendingConfig, icon }, + })), setTrendingWindowConfig: (window, config) => set((state) => ({ trendingConfig: {